← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.121\ No newline at end of file+1.9.122\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.121",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.136/adom-bridge-fusion-v1.9.121.zip",-  "sha256": "eec4f3ba279d3dd1a6aa349b3df8c8d459541fbe4d7e3a8f7081b19ea819fd3b",-  "size": 456629,+  "version": "1.9.122",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.137/adom-bridge-fusion-v1.9.122.zip",+  "sha256": "6c654c30b623b07ed26a5b388d969eebba4180f66bac205c30e467bc7c3a3847",+  "size": 458429,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.121",+  "version": "1.9.122",   "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/fusion-bridge",   "author": "Adom Inc.",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.136",+  "version": "1.9.137",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.136",+  "version": "1.9.137",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -70,7 +74,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -99,9 +106,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }+  ] }\ No newline at end of file
server.py+108−13
@@ -2623,6 +2623,49 @@ def _check_addin_status(timeout: float = 3.0) -> dict | None:         return None  +_HUNG_SINCE = [0.0]  # first time is_fusion_hung() reported hung; 0 = not currently hung+++def _confirmed_hung() -> dict:+    """A kill-grade hang verdict. IsHungAppWindow ALONE must never authorize a kill.++    Found the hard way (2026-08-21, three dead Fusions in one afternoon): a heavy+    main-thread modeling script (STEP import, saveAs) starves the add-in's HTTP thread+    via the GIL, so the 3s status probe AND the 5s health probe both time out, and the+    host window legitimately stops pumping messages - IsHungAppWindow goes true on a+    process that is WORKING. The old gate then taskkilled a healthy Fusion mid-bind.+    Same law as the busy guard (#41): a remediation that destroys state needs a+    STRONGER signal than one snapshot.++    Kill-grade means ALL of: (1) is_fusion_hung() true NOW, (2) a LONG status probe+    (20s) gets nothing - a merely GIL-starved add-in answers intermittently within+    that, (3) the condition has PERSISTED for 180s+ since first observed. Anything+    less returns hung:false with a `phase` explaining what to do (wait).+    """+    import time as _t+    try:+        from handlers.fusion_ui import is_fusion_hung+        _h = is_fusion_hung()+    except Exception:+        _h = {"hung": False, "signals": []}+    if not _h.get("hung"):+        _HUNG_SINCE[0] = 0.0+        return {"hung": False, "phase": "pumping"}+    if _check_addin_status(timeout=20.0) is not None:+        _HUNG_SINCE[0] = 0.0+        return {"hung": False, "phase": "busy-not-hung",+                "signals": _h.get("signals")}+    now = _t.time()+    if not _HUNG_SINCE[0]:+        _HUNG_SINCE[0] = now+    held = now - _HUNG_SINCE[0]+    if held < 180:+        return {"hung": False, "phase": "suspect", "heldSeconds": round(held, 1),+                "signals": _h.get("signals")}+    return {"hung": True, "phase": "confirmed", "heldSeconds": round(held, 1),+            "signals": _h.get("signals")}++ def _handle_addin_status() -> dict:     """Bridge-level handler for addin_status — wraps /status in standard format."""     status = _check_addin_status(timeout=3.0) or {"busy": False}@@ -2885,14 +2928,24 @@ def _diagnose_addin_timeout(command: str, original_result: dict) -> dict:     # responses (page issue #38): the add-in being down is recoverable over IPC, a HUNG     # HOST PROCESS is not. Reporting the second as the first sent a reader hunting a     # blocking dialog that did not exist.-    try:-        from handlers.fusion_ui import is_fusion_hung-        _hung = is_fusion_hung()-    except Exception:-        _hung = {"hung": False, "signals": []}+    _hung = _confirmed_hung()+    if _hung.get("phase") in ("suspect", "busy-not-hung"):+        return {+            "success": False,+            "error": "Fusion's main thread is not answering right now - most likely WORKING "+                     "on something heavy, not hung (the probes it starves are the very ones "+                     "this gate reads).",+            "errorCode": "fusion_addin_busy",+            "hostResponding": _hung.get("phase") == "busy-not-hung",+            "hungHeldSeconds": _hung.get("heldSeconds"),+            "_hint": "WAIT and re-poll. A real hang is only declared after 180s of continuous "+                     "silence including a 20s probe, and then the bridge restarts Fusion itself.",+        }     if _hung.get("hung"):         # DOCTRINE: this is mechanically fixable and the user should never have to do it.         # A hung process will not close politely, so go straight to the force path.+        # (_confirmed_hung: kill-grade evidence only - persistence + a refused 20s probe -+        # after this gate taskkilled three healthy, working Fusions in one day, 2026-08-21.)         _healed = False         try:             dispatch_command("kill", {})@@ -3475,7 +3528,13 @@ def _orchestrate_make_3d_package(args: dict) -> dict:                 "_hint": "Saved DIRECTLY to the cloud workspace - no upload step needed. "                          "Bind this wip_urn into the library's <packages3d>."}     if not f3d_path or not str(f3d_path).endswith(".f3d"):-        return {"success": False, "error": "f3d saveAs did not produce a .f3d path: " + str(res),+        # Carry the add-in's OWN error up: res={} with the script error dropped on the+        # floor cost a whole diagnosis round (2026-08-21 - the real story was Fusion being+        # killed mid-script, visible only in r['error']).+        return {"success": False,+                "error": "f3d saveAs did not produce a .f3d path: " + str(res)+                         + (" | addin: " + str(r.get("error"))[:200]+                            if isinstance(r, dict) and r.get("error") else ""),                 "before": before_shot, "after": after_shot,                 "_hint": "Confirm modelPath is a valid Windows .step path and Fusion is running."}     up = aps.handle_upload({"projectId": project_id, "folderId": folder_id,@@ -7237,6 +7296,27 @@ def _orchestrate_show_3d_package(args: dict) -> dict:     if step_path:         res = _merge_dialog_array(_orchestrate_attach_3d_package(             {"filePath": library_path, "modelPath": step_path, "packageName": name}))+        # THE ATTACH-FAILED BRANCH OF THE PLACEHOLDER GUARD (page issue #44): when the+        # attach we just attempted did not actually land a <package3d> in the library,+        # what renders next is Fusion's grey Placeholder - and this verb used to return+        # the attach result's success over it. Same offline check as no_3d_package_bound,+        # run on the branch where the bridge TRIED and failed rather than was never asked.+        _p3d_after = _library_artifact_names(library_path).get("p3d", [])+        if not _p3d_after:+            _up_err = str(res.get("error") or res.get("attachError") or "attach reported no error but bound nothing")+            return {"success": False,+                    "errorCode": "3d_attach_failed",+                    "error": "Attaching the STEP failed, so the library still has no 3D "+                             "package - refusing to show Fusion's Placeholder as if it "+                             "were the part.",+                    "attachError": _up_err[:400],+                    "libraryPath": library_path,+                    "_hint": ("The upstream failure is in attachError (a 409 here usually "+                              "means the f3d already exists in the cloud - the bind path "+                              "reuses it as of 1.9.11x, so retry fusion_bind_library_3d "+                              "and read ITS message). Nothing was rendered because a "+                              "Placeholder screenshot photographs exactly like a real "+                              "chip and must never count as one.")}         if wrapped:             res["wrappedLibrary"] = wrapped         res["view"] = "3d-package"@@ -10057,11 +10137,11 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No             pass  # Fall through to busy gate         elif not wait_for_addin(timeout=5):             # Hung HOST vs down ADD-IN (page issue #38) - opposite remedies, so say which.-            try:-                from handlers.fusion_ui import is_fusion_hung-                _h = is_fusion_hung()-            except Exception:-                _h = {"hung": False}+            # KILL-GRADE evidence only (_confirmed_hung): a busy main thread starves these+            # very probes and stops pumping messages, so one failed snapshot looks exactly+            # like a hang. Three healthy Fusions were taskkilled mid-work by this gate+            # before the persistence rule (2026-08-21).+            _h = _confirmed_hung()             if _h.get("hung"):                 _ok = False                 try:@@ -10073,8 +10153,9 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No                     pass                 return {                     "success": False,-                    "error": "Fusion 360 itself is NOT RESPONDING (hung host process); the "-                             "add-in cannot answer while it is.",+                    "error": "Fusion 360 itself is NOT RESPONDING (hung host process, held "+                             "%ss with the add-in refusing a 20s probe); the add-in cannot "+                             "answer while it is." % _h.get("heldSeconds"),                     "errorCode": "fusion_host_hung",                     "hungSignals": _h.get("signals"),                     "restartAttempted": _ok,@@ -10083,6 +10164,20 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No                               + ("The bridge force-restarted it; poll fusion_readiness."                                  if _ok else "Use fusion_kill then fusion_start.")),                 }+            if _h.get("phase") in ("suspect", "busy-not-hung"):+                return {+                    "success": False,+                    "error": "Fusion's main thread is not answering right now - most likely "+                             "WORKING on something heavy (a STEP import or save starves the "+                             "add-in's probes), not hung.",+                    "errorCode": "fusion_addin_busy",+                    "hostResponding": _h.get("phase") == "busy-not-hung",+                    "hungHeldSeconds": _h.get("heldSeconds"),+                    "_hint": ("WAIT and re-poll; do not restart anything. The bridge only "+                              "declares a real hang after 180s of continuous silence "+                              "including a 20s probe - if that happens it restarts Fusion "+                              "itself and says so."),+                }             return {                 "success": False,                 "error": "Fusion 360 is running but AdomBridge add-in not responding.",