← Commit history

1.6.46: seat dialog owned-popup detection + screenshot-verified resolve + readiness self-heal

John Lauer ·6e3be8ddd6 ·3mo ago ·parent 081e2ca
6 files changed +164−21
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.45\ No newline at end of file+1.6.46\ No newline at end of file
CHANGELOG.md+21
@@ -3,6 +3,27 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.46 - 2026-07-06++- **Fixed the seat/licensing dialog going UNDETECTED (John caught it live on winvm).** The+  "Suspend Remote Session" seat-confirm variant is only **823x262**, but `find_licensing_dialog()`+  required height **320-820**, so it silently slipped through - readiness reported `licensingDialog`+  gone while the dialog sat there blocking the whole launch. Root cause: detecting a modal by a+  fragile SIZE window. Now detected DETERMINISTICALLY by the **owned-popup** signal: a seat/sign-in+  modal is always an OWNED POPUP of the main Fusion window (the splash/main app never are), found via+  `GetWindow(GW_OWNER)` in `family_windows()`. Size is only a fallback.+- **Resolution is now SCREENSHOT-VERIFIED, not heuristic-verified.** `_resolve_seat_dialog()` clicks+  'Continue' (background UIA) then confirms the dialog truly cleared by reading the parent window's+  `ownedPopupCount` via `desktop_screenshot_window` (parent/child screenshotting) - instead of trusting+  the same size filter that had false-'resolved' it. This is the check that would have caught the+  earlier "I solved it" that hadn't.+- **Closed the auto-resolve GAP.** The launch loop only auto-resolved the seat dialog during its ~50s+  window. But the dialog also appears LATER (minutes post-sign-in, when the license server notices too+  many sessions), long after that loop exited. `fusion_readiness` now SELF-HEALS: whenever it detects+  the dialog it auto-resolves it in the background and re-probes - so the seat is handled in CODE no+  matter when it shows up, and the AI never has to notice or act.+- ctypes `GetWindow` given explicit `restype/argtypes` (HWND) to avoid 64-bit handle truncation.+ ## 1.6.45 - 2026-07-06  - **Add-in changes now AUTO-DEPLOY - the bridge no longer only installs the add-in when it's MISSING.**
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.45",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.45/adom-bridge-fusion-v1.6.45.zip",-  "sha256": "950ef28e331e8ef278e020205d547fa732ce07973daa9b2fcbe992a441d49be1",-  "size": 342643,+  "version": "1.6.46",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.46/adom-bridge-fusion-v1.6.46.zip",+  "sha256": "806b9b6dc88b289aa23fc6dbebd7da33944045dfd150808f12a0de69b4152278",+  "size": 345325,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-06T14:10:08Z"+  "released_at": "2026-07-06T14:51:53Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.45",+  "version": "1.6.46",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
fusion_detect.py+32−6
@@ -362,6 +362,11 @@ def family_windows() -> list:         HWND = ctypes.wintypes.HWND         user32.GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(ctypes.wintypes.DWORD)]         user32.IsWindowVisible.argtypes = [HWND]+        # restype/argtypes MUST be set or the returned HWND is truncated to 32-bit on+        # 64-bit Windows and the owner handle comes back wrong (same class of bug that+        # silently broke the focus-guard, 2026-07-06).+        user32.GetWindow.restype = HWND+        user32.GetWindow.argtypes = [HWND, ctypes.c_uint]         fam = _fusion_family_pids()         if not fam:             return results@@ -390,10 +395,13 @@ def family_windows() -> list:             h = rect.bottom - rect.top             if w <= 0 or h <= 0:                 return True+            GW_OWNER = 4+            owner = user32.GetWindow(hwnd, GW_OWNER)             results.append({                 "hwnd": int(hwnd), "title": title, "pid": int(wpid.value),                 "image": pid_img.get(int(wpid.value), ""),                 "width": w, "height": h, "left": rect.left, "top": rect.top,+                "owner": int(owner) if owner else 0,             })         except Exception:             pass@@ -413,13 +421,31 @@ def family_windows() -> list:   def find_licensing_dialog():-    """The seat/licensing modal ("Active Sessions Exceeded", or a sign-in prompt): a-    family window that is DIALOG-SIZED (the maximized main app is far bigger; tooltips-    far smaller). Detected by owning process + size, so the "Fusion360" title never-    matters. Returns the window dict, or None. The seat dialog is ~1235x527."""-    for w in family_windows():+    """The seat/licensing or sign-in modal ("Active Sessions Exceeded" / "Suspend Remote+    Session" / a sign-in prompt). Detected DETERMINISTICALLY by the OWNED-POPUP signal,+    NOT a fragile height window:++      A seat/sign-in modal is ALWAYS an owned popup of the main Fusion window (confirmed+      via desktop_screenshot_window's ownedPopupCount). The splash and the maximized main+      app are NEVER owned popups. So "family window that has an owner" is the reliable+      tell - it does not care about the exact dialog size or the "Fusion360" title.++    This replaced a size-only filter whose 320px height floor SILENTLY MISSED the+    823x262 "Suspend Remote Session" confirm variant, so readiness reported the dialog+    gone while it sat there blocking (John caught it live, 2026-07-06). A size-based+    fallback is kept for safety in case owner detection ever returns 0.+    Returns the window dict (with 'owner'), or None."""+    wins = family_windows()+    # Primary: an OWNED popup (has an owner window) that is bigger than a tooltip.+    for w in wins:+        if w.get("owner") and w["width"] >= 300 and w["height"] >= 120:+            return w+    # Fallback (owner==0 for all): a dialog-sized family window that is NOT the+    # maximized main app (>1500 wide / >900 tall) and not a tooltip. Height floor 180+    # so the 262px confirm still matches.+    for w in wins:         ww, hh = w["width"], w["height"]-        if 640 <= ww <= 1500 and 320 <= hh <= 820:+        if 480 <= ww <= 1500 and 180 <= hh <= 900:             return w     return None 
server.py+104−8
@@ -137,6 +137,67 @@ def _resolve_seat_via_uia(dlg: dict) -> bool:         return False  +def _owned_popup_count(main_hwnd) -> int:+    """ownedPopupCount on a window via AD's desktop_screenshot_window - the SAME+    parent/child screenshot signal John kept pointing at (2026-07-06): a modal seat/+    error dialog shows up as an OWNED POPUP of the main Fusion window. This is the+    ground-truth 'is a dialog still up' check used to VERIFY a seat dialog actually+    cleared, instead of trusting a window-size heuristic (which false-'resolved' the+    823x262 'Suspend Remote Session' confirm). Returns the count, or -1 if unavailable."""+    try:+        if not main_hwnd or not ad_client.available():+            return -1+        res = ad_client.call("desktop_screenshot_window", {"hwnd": int(main_hwnd)})+        if isinstance(res, dict):+            c = res.get("ownedPopupCount")+            if c is None and isinstance(res.get("data"), dict):+                c = res["data"].get("ownedPopupCount")+            if c is not None:+                return int(c)+    except Exception:+        pass+    return -1+++def _resolve_seat_dialog(max_clicks: int = 8) -> dict:+    """Detect + resolve the seat/licensing modal deterministically, IN THE BACKGROUND,+    and VERIFY it truly cleared by SCREENSHOTTING the parent window's ownedPopupCount -+    not by re-running a size heuristic (that false-'resolved' bit John hard, 2026-07-06:+    the code clicked once, the size filter then failed to re-detect the short confirm+    variant, and readiness lied that the dialog was gone while it sat there blocking).++    Each pass: find the dialog -> UIA-invoke 'Continue' (background, Suspend pre-selected+    so it grabs the license) -> screenshot the parent's ownedPopupCount. Done only when+    BOTH find_licensing_dialog() sees nothing AND the parent shows 0 owned popups (or the+    screenshot signal is unavailable and the heuristic agrees). Returns+    {sawDialog, clicks, verified}. Never raises."""+    import time as _t+    clicks = 0+    saw = False+    verified = False+    for _ in range(max_clicks):+        try:+            dlg = find_licensing_dialog()+        except Exception:+            dlg = None+        if not dlg:+            verified = True+            break+        saw = True+        parent = dlg.get("owner")+        _resolve_seat_via_uia(dlg)+        clicks += 1+        _t.sleep(1.5)+        # Ground-truth confirm via parent/child screenshot (John's insisted-on check).+        cnt = _owned_popup_count(parent)+        if cnt == 0:+            verified = True+            break+        # cnt == -1 (screenshot unavailable): fall through, the next find_licensing_dialog+        # pass decides. cnt > 0: still up, loop and click again.+    return {"sawDialog": saw, "clicks": clicks, "verified": verified}++ def _handle_launch(fusion_info: dict, args: dict) -> dict:     """Launch Fusion 360 and optionally wait for the AdomBridge add-in."""     # LIVE detect (not the stale bridge-start fusion_info snapshot) so a launch right@@ -221,10 +282,12 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:         except Exception:             _lic = None         if _lic:+            # Resolve + SCREENSHOT-VERIFY it cleared (parent ownedPopupCount), not a+            # size heuristic that false-'resolved' the short confirm variant.             if _seat_clicks < 10:-                if _resolve_seat_via_uia(_lic):-                    _seat_clicks += 1-            _t.sleep(1.5)+                _r = _resolve_seat_dialog(max_clicks=3)+                _seat_clicks += _r.get("clicks", 0)+            _t.sleep(1.0)             continue         # No seat dialog -> is the add-in actually serving? (ground truth for drivable)         if _probe_addin(timeout=1.5):@@ -853,12 +916,39 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     # instance answers while the CURRENT Fusion sits blocked behind the seat dialog, so     # gating on `not addin_ok` false-negatived `licensingDialog` while the dialog sat     # right there on the user's screen.+    #+    # ⚠️ GAP FIX (John caught it live 2026-07-06): the launch loop only auto-resolves the+    # seat dialog during its ~50s window. But this dialog also appears LATER - minutes+    # after sign-in, when the license server notices too many sessions - long after that+    # loop exited, so it just sat there blocking while readiness passively reported+    # licensingDialog:true. Now readiness SELF-HEALS: whenever it detects the dialog it+    # AUTO-RESOLVES it in the background (UIA 'Continue' + screenshot-verify), so the seat+    # is handled deterministically in CODE no matter when it shows up - the AI never has+    # to notice or act. (John: "do all this tracking from your code so it's deterministic+    # ... the ai never follows skills.")+    seat_resolved = False     licensing = False     if running:         try:             licensing = find_licensing_dialog() is not None         except Exception:             licensing = False+        if licensing:+            try:+                _r = _resolve_seat_dialog()+                seat_resolved = _r.get("clicks", 0) > 0+                # Re-check: did it actually clear? (screenshot-verified inside.)+                licensing = find_licensing_dialog() is not None+            except Exception:+                pass+            if not licensing:+                # Cleared - re-probe the add-in; Fusion may be drivable now.+                try:+                    addin_status = _check_addin_status(timeout=1.5)+                    addin_ok = addin_status is not None+                    ready = installed and running and addin_ok and not needs_signin+                except Exception:+                    pass     if licensing:         ready = False  # a seat/licensing dialog blocking the UI is never "ready" @@ -900,11 +990,14 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     elif ready:         hint = "READY - drive Fusion (fusion_open_lbr / build_library_3d / etc.)."     elif installed and running and licensing:-        hint = ("Fusion is blocked by a SEAT/LICENSING dialog ('Active Sessions Exceeded') - detected "-                "by owning process + dialog size (its title is just 'Fusion360'). fusion_start "-                "AUTO-RESOLVES it in the BACKGROUND by UIA-invoking its 'Continue' button (no "-                "foreground, no user action) and retries until it clears. If you see this persist, just "-                "call fusion_start again - it is not a crash, and never coordinate-click or ask the user.")+        hint = ("Fusion is blocked by a SEAT/LICENSING dialog ('Active Sessions Exceeded' / 'Suspend "+                "Remote Session') - detected DETERMINISTICALLY as an OWNED POPUP of the main Fusion "+                "window (its title is just 'Fusion360' and it can be as short as 262px, so it is found "+                "by owner+parent-screenshot, NOT a size guess). This verb JUST TRIED to auto-resolve it "+                "in the BACKGROUND (UIA 'Continue', Suspend pre-selected so it reclaims the seat) but it "+                "is still up - it may be mid-render (the first UIA Invoke no-ops). Just POLL "+                "fusion_readiness again in a few seconds; it keeps auto-resolving. Never coordinate-click "+                "or ask the user, and never kill/relaunch (the server keeps the seat).")     elif installed and running:         hint = ("Fusion is RUNNING but its add-in is NOT responding yet - NOT ready to drive. Either it "                 "is still finishing launch / signing in (poll fusion_readiness a few more times), or the "@@ -939,6 +1032,9 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     if licensing:         result["licensingDialog"] = True         result["statusVerb"] = "fusion_readiness"+    if seat_resolved:+        # We acted on it this call (whether or not it fully cleared yet).+        result["seatDialogAutoResolved"] = True     return result