← Commit history

v1.9.61: etiquette coverage + zombie gate + stop hardening

John Lauer ·d001534537 ·1mo ago ·parent b63d3d9
5 files changed +90−19
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.59\ No newline at end of file+1.9.61\ No newline at end of file
CHANGELOG.md+13
@@ -3,6 +3,19 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.61 - 2026-08-18++- **windowEtiquette annotation now covers ORCHESTRATED verbs** (show_*, make_test_project, tour,+  bind_library_3d, ...): a thin dispatch wrapper guarantees every backgrounded window-raising+  result carries the marker + re-call shape - the inner closure only covered direct/add-in paths.+- **Zombie-window gate on the save-prompt sentinel**: an UNATTRIBUTED "Save" dialog is only+  escalated (Escape) when the add-in main thread is actually blocked; responsive-thread ghosts+  (ConfRoomROG's black no-UIA windows) are left alone instead of escape-spammed every sweep.+- **Graceful stop hardening**: the clean-close document pass retries once (3s + 10s budget)+  instead of silently skipping on a transiently-busy add-in - skipping was what let WM_CLOSE hit+  the unsaved-doc Save modal. On failure, `bridgeDocsOnly:true` + a kill-is-safe hint when every+  blocking doc is a bridge-created throwaway, so callers can escalate to fusion_kill confidently.+ ## 1.9.60 - 2026-08-18 (dashboard only; bridge zip stays 1.9.59)  - **Every interactive element now acknowledges the human (John: the UI must say "yes, you just
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.59",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.59/adom-bridge-fusion-v1.9.59.zip",-    "sha256": "44aa7bcc595a5b4aa944e34a5bcf7c8dabd3fb0a45a1a30a78d22e1bf1353fea",-    "size": 404015,+    "version": "1.9.61",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.61/adom-bridge-fusion-v1.9.61.zip",+    "sha256": "b511771ebbb10ce1e1570c4064ae23160c32c6da7a713ba9463a58c49e97f879",+    "size": 404984,     "verbPrefixes": [         "fusion_"     ],
handlers/close_fusion.py+21−3
@@ -125,7 +125,16 @@ def _close_all_documents_via_addin() -> dict:         with urllib.request.urlopen(req, timeout=5) as resp:             resp_data = json.loads(resp.read())     except Exception:-        return result  # Add-in not reachable, skip clean close+        # ONE retry with a longer budget before giving up: a transiently-busy main thread+        # here silently skipped the clean close, so WM_CLOSE later hit the unsaved-doc+        # Save modal and the whole graceful stop failed (live, 2026-08-17).+        try:+            import time as _t+            _t.sleep(3)+            with urllib.request.urlopen(req, timeout=10) as resp:+                resp_data = json.loads(resp.read())+        except Exception:+            return result  # Add-in truly unreachable, skip clean close      if not resp_data.get("success"):         return result@@ -284,12 +293,21 @@ def handle_fusion_stop(fusion_info: dict, args: dict) -> dict:         (closed if _close_and_wait(hwnd, title) else failed).append(title)      if failed:+        # Bridge-created throwaway docs (keep the tuple in sync with server._BRIDGE_DOC_BUILTINS):+        # when EVERY failed window is one of ours, force-kill risks nothing of the user's, and+        # callers (tour cleanup, the test suite) can escalate automatically on bridgeDocsOnly.+        _bridge_marks = ("adom-test-", "adomdemosample", "adom-fusion-wrap", "ads8588sipm", "0603waf")+        bridge_only = all(any(m in t.lower() for m in _bridge_marks) for t in failed)         return {             "success": False,             "error": f"Graceful stop failed for: {', '.join(failed)}",             "output": f"Closed: {', '.join(closed)}" if closed else "",-            "_hint": "A window would not close gracefully (a modal dialog often blocks WM_CLOSE). "-                     "Use fusion_kill to force-kill, then retry.",+            "bridgeDocsOnly": bridge_only,+            "_hint": ("The unsaved document(s) blocking the close are BRIDGE-created throwaways "+                      "(sample/test docs) - fusion_kill is SAFE here, nothing of the user's is at "+                      "risk. Kill, then retry." if bridge_only else+                      "A window would not close gracefully (a modal dialog often blocks WM_CLOSE). "+                      "Use fusion_kill to force-kill, then retry."),         }      for _ in range(10):
server.py+51−11
@@ -4350,6 +4350,20 @@ def _dismiss_save_prompt_if_bridge_doc() -> dict:         # the doc stays open, nothing saved or discarded). A modal that blocks the add-in         # main thread for 2+ sweeps gets escaped rather than sat on; Don't Save is still         # NEVER clicked without attribution.+        # ZOMBIE GATE (2026-08-17, found live on ConfRoomROG): "Save"-titled windows can be+        # non-modal GHOSTS that block nothing (main thread fully responsive, PrintWindow+        # black, no UIA). Escaping those every other tick is pointless input spam - only+        # escalate on an unattributed prompt when the add-in main thread is ACTUALLY+        # blocked (status probe busy or unanswerable).+        try:+            _st = _check_addin_status(timeout=2.0)+            _blocked = (_st is None) or bool(_st.get("busy"))+        except Exception:+            _blocked = True+        if not _blocked:+            return {"handled": False,+                    "why": "unattributed dialog but the main thread is responsive - zombie window, leaving it alone",+                    "hwnd": prompt["hwnd"]}         n = _UNATTRIBUTED_PROMPT_STRIKES.get(prompt["hwnd"], 0) + 1         _UNATTRIBUTED_PROMPT_STRIKES[prompt["hwnd"]] = n         if n >= 2:@@ -7799,7 +7813,44 @@ def _assert_active_document(expect: str) -> dict | None:     return None  +_WINDOW_RAISING = {+    "open_cloud_file", "open_by_urn", "aps_open", "open_design", "open_lbr",+    "open_schematic", "open_board", "import_electronics", "import_step",+    "new_electronics_from_eagle", "show_schematic", "show_2d_board", "show_3d_board",+    "run_modeling_script", "execute_text_command", "electron_run",+    "attach_3d_package", "make_3d_package", "build_library_3d",+    "capture_library_views", "generate_package", "export_optimized_glb",+    "board_stackup", "save_to_cloud", "start",+    "show_symbol", "show_footprint", "show_3d_package", "make_test_project",+    "bind_library_3d", "show_library", "show_project",+}++ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> dict:+    """Thin wrapper: run the verb, then guarantee the windowEtiquette annotation lands on+    EVERY backgrounded window-raising result - the inner dispatcher's closure only covered+    direct-handler and add-in paths, so orchestrated verbs (show_*, make_test_project, the+    tour, ...) returned unannotated (the calling AI could not know no focus was taken).+    Idempotent with the inner annotation (the note is only appended once)."""+    _fg = bool((args or {}).get("foreground"))+    res = _dispatch_command_inner(command, args, caller_identity)+    try:+        cmd = command[7:] if command.startswith("fusion_") else command+        if cmd in _WINDOW_RAISING and not _fg and isinstance(res, dict):+            res.setdefault("windowEtiquette", "background")+            note = ("Window opened/changed in the BACKGROUND - the user's focus was NOT touched; "+                    "do NOT foreground anything to compensate. Render results via fusion_state. "+                    "If the USER asks to watch, re-call with {\"foreground\": true, "+                    "\"foregroundReason\": \"<why, shown to the user as a 3s caption>\"}.")+            h = res.get("_hint")+            if not (isinstance(h, str) and "focus was NOT touched" in h):+                res["_hint"] = (h + " " + note) if isinstance(h, str) else note+    except Exception:+        pass+    return res+++def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = None) -> dict:     """Dispatch a command to the appropriate handler.      caller_identity: {'thread','container','reason'} from the X-Adom-Caller-* headers AD stamped on@@ -7836,17 +7887,6 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->     # so a cloud open / view switch / import NEVER steals the user's foreground mid-typing. The     # deliberate-foreground verbs (prefs_open, mcp_enable, demo - which announce themselves with     # captions) are excluded on purpose. Refcounted, so overlapping verbs nest cleanly.-    _WINDOW_RAISING = {-        "open_cloud_file", "open_by_urn", "aps_open", "open_design", "open_lbr",-        "open_schematic", "open_board", "import_electronics", "import_step",-        "new_electronics_from_eagle", "show_schematic", "show_2d_board", "show_3d_board",-        "run_modeling_script", "execute_text_command", "electron_run",-        "attach_3d_package", "make_3d_package", "build_library_3d",-        "capture_library_views", "generate_package", "export_optimized_glb",-        "board_stackup", "save_to_cloud", "start",-        "show_symbol", "show_footprint", "show_3d_package", "make_test_project",-        "bind_library_3d", "show_library", "show_project",-    }     _ran_backgrounded = False     if command in _WINDOW_RAISING:         if (args or {}).get("foreground"):