← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.276\ No newline at end of file+1.9.277\ 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.276",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.276/adom-bridge-fusion-v1.9.276.zip",-  "sha256": "f255b3ef27ee8a74ffcc95559b5555f760e347fb1aa50d9eea618ba7b1c4cd16",-  "size": 568530,+  "version": "1.9.277",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.277/adom-bridge-fusion-v1.9.277.zip",+  "sha256": "37a9a0017a7ac7beba3b48ffa7f9b87214e31b8b0910c6ecb3fc64079743a9aa",+  "size": 569762,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.276",+  "version": "1.9.277",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.276"+version = "1.9.277" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.276"+version = "1.9.277" edition = "2021"  [dependencies]
fusion_detect.py+65−2
@@ -321,8 +321,71 @@ def _is_fusion_running() -> bool:     return _has_fusion_process()  +_FUSION_RUNNING_MEMO = {"value": None, "at": 0.0}+++def _fusion_pids_toolhelp() -> list:+    """PIDs of Fusion360.exe via a Toolhelp32 snapshot: no child process, no console,+    milliseconds even when the CPU is pegged. Empty list on any failure."""+    try:+        import ctypes+        from ctypes import wintypes+        k32 = ctypes.windll.kernel32+        TH32CS_SNAPPROCESS = 0x2++        class PE32(ctypes.Structure):+            _fields_ = [("dwSize", wintypes.DWORD), ("cntUsage", wintypes.DWORD),+                        ("th32ProcessID", wintypes.DWORD),+                        ("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)),+                        ("th32ModuleID", wintypes.DWORD), ("cntThreads", wintypes.DWORD),+                        ("th32ParentProcessID", wintypes.DWORD), ("pcPriClassBase", ctypes.c_long),+                        ("dwFlags", wintypes.DWORD), ("szExeFile", ctypes.c_char * 260)]+        snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)+        if snap == wintypes.HANDLE(-1).value or not snap:+            return []+        try:+            pe = PE32(); pe.dwSize = ctypes.sizeof(PE32)+            out = []+            ok = k32.Process32First(snap, ctypes.byref(pe))+            while ok:+                if pe.szExeFile.decode(errors="ignore").lower() == "fusion360.exe":+                    out.append(int(pe.th32ProcessID))+                ok = k32.Process32Next(snap, ctypes.byref(pe))+            return out+        finally:+            k32.CloseHandle(snap)+    except Exception:+        return []++ def _has_fusion_process() -> bool:-    """Check if any Fusion 360 process is running via tasklist."""+    """Check if any Fusion 360 process is running.++    ⛔ A FALSE "NOT RUNNING" LAUNCHES A SECOND FUSION (2026-08-29, John saw Fusion's+    "Multiple instances are not supported" dialog mid-import): the tasklist probe below+    timed out while Fusion pegged the CPU importing a STEP, the except path answered+    False, and the dispatcher's auto-launch started another instance. So: a snapshot+    probe that cannot stall comes first, tasklist is the confirmation, and a probe that+    FAILS answers with the last known state, never False.+    """+    try:+        _pids = _fusion_pids_toolhelp()+        if _pids and any(_pid_alive(p) for p in _pids):+            _FUSION_RUNNING_MEMO.update(value=True, at=time.time())+            return True+    except Exception:+        pass+    try:+        r = _has_fusion_process_tasklist()+        _FUSION_RUNNING_MEMO.update(value=r, at=time.time())+        return r+    except Exception:+        last = _FUSION_RUNNING_MEMO.get("value")+        return bool(last) if last is not None else False+++def _has_fusion_process_tasklist() -> bool:+    """tasklist-based check (raises on timeout so the caller can fall back)."""     try:         # CREATE_NO_WINDOW (0x08000000) on Windows so the child cmd window doesn't         # flash on screen for every poll. The Tauri side spawns the Python bridge@@ -368,7 +431,7 @@ def _has_fusion_process() -> bool:             return "Fusion360.exe" in output     # cannot parse -> old behaviour         return any(_pid_alive(p) for p in pids)     except Exception:-        return False+        raise   # the caller answers with the last known state, never a guessed False   _STILL_ACTIVE = 259
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.276"+DASH_VERSION="1.9.277" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.276",+  "version": "1.9.277",   "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+11−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.276",+  "version": "1.9.277",   "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",@@ -75,7 +79,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.",@@ -104,9 +111,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+21−2
@@ -13043,13 +13043,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:             # save settled, so part 1 of 10 scored not_proven on library_not_active and the other             # nine passed. That is a timing miss, not a bad part: re-prove exactly those parts once,             # after the rest, when the library has long been active.+            # Also retried: add-in timeouts and "not responding" (run 6aeb6f853c17, 2026-08-29:+            # CH340G and IN-S63BTG missed on addin_command_timeout while Fusion was sluggish and+            # passed nothing else wrong). Those are timing misses too; a real geometry failure+            # says so in its error and is NOT retried.+            _transient = ("ACTIVE document", "addin_command_timeout", "not responding",+                          "main thread busy", "main_thread_busy")             _retry = [r for r in _prove_results if not r["proven"]-                      and "ACTIVE document" in str(r.get("error") or "")]+                      and any(k in str(r.get("error") or "") for k in _transient)]             for _rr in _retry:                 if _cancel_requested(run_id):                     break                 _dev = _rr["part"]-                state["stageLabel"] = "re-proving %s (the library was not active on its first pass)" % _dev+                state["stageLabel"] = "re-proving %s (its first pass hit a timing miss, not a part fault)" % _dev                 _save()                 def _frame_for_retry(entry, _d=_dev):                     entry = dict(entry); entry["part"] = _d; entry["kind"] = "proof"; entry["retry"] = True@@ -13752,6 +13758,19 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No                 "errorCode": "fusion_not_installed",                 "_hint": "Fusion 360 isn't installed. Do NOT tell the user to install it themselves - OFFER to install it FOR them and do it on a yes: the fusion-onboarding skill silent-installs Fusion + drives the Autodesk sign-in.",             }+        if not fusion_running:+            # SECOND OPINION BEFORE A LAUNCH (2026-08-29): a launch on a false "not running"+            # starts a SECOND Fusion ("Multiple instances are not supported" in the user's+            # face, mid-import). A probe can miss while Fusion pegs the CPU; the add-in+            # answering on its port, or a Fusion main window, proves it is up. Re-ask twice+            # over ~3s and only launch when every signal still says closed.+            import time as _sop+            from fusion_detect import _fusion_main_hwnd_quick as _hwnd_quick+            for _ in range(2):+                _sop.sleep(1.5)+                if _is_fusion_running() or _probe_addin(timeout=1.5) or _hwnd_quick():+                    fusion_running = True+                    break         if not fusion_running:             # AUTO-OPEN, DO NOT REFUSE (page issue #64, 2026-08-26).             # CLAUDE.md's automation doctrine: "If Fusion is closed, launch it ... Never