← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.221\ No newline at end of file+1.9.222\ 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.221",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.221/adom-bridge-fusion-v1.9.221.zip",-  "sha256": "bfd566d96136bf0fa15a38c022f3504c2619e5f7580c5c94107ef87eded2b4ec",-  "size": 510907,+  "version": "1.9.222",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.222/adom-bridge-fusion-v1.9.222.zip",+  "sha256": "9efebec63b8355b56f0308792c807c631e6e6622861fcaebdbf74a1a2687cca8",+  "size": 514182,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.221",+  "version": "1.9.222",   "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.221"+version = "1.9.222" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.221"+version = "1.9.222" edition = "2021"  [dependencies]
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.221"+DASH_VERSION="1.9.222" 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.221",+  "version": "1.9.222",   "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.221",+  "version": "1.9.222",   "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+179−3
@@ -333,6 +333,86 @@ def _cli_notify_all(title: str, body: str, level: str) -> dict:         return {"delivered": False, "error": str(e)[:200]}  +def _addin_restart_info(fire_toast: bool = True) -> dict:+    """Everything a CALLER needs to tell a HUMAN that Fusion must restart, plus a toast.++    ⛔ THE ADONIS INCIDENT (John, 2026-08-27). A brand-new user installed ab, already had+    Fusion open, and drove the fusion bridge from the wiki through web-control. The bridge+    wrote a fresh add-in into Fusion's add-in dirs - Fusion only loads add-ins AT LAUNCH, so+    his running Fusion could not see it - and then said NOTHING. It printed the sync to a+    console nobody reads, returned a bare "add-in not responding", and the verbs failed. He+    had no way to know the product was fine and merely needed a relaunch; John and Colby had+    to tell him by hand. Once he restarted, the library import worked perfectly first time.+    Everything needed to warn him was already known at that moment.++    So this returns a payload a web-control caller can render as a QUESTION ("we need to+    restart Fusion; you have N documents open; shall we, and we will save them first"), and+    fires an ab toast so the human is told even if the caller ignores the payload.++    Document counts come from the add-in, which by definition is NOT answering here, so they+    are reported as unknown rather than guessed - `documentsKnown` says which. A count we+    cannot take is not a count we may invent.+    """+    import time as _t+    info = {+        "restartRequired": True,+        "reason": "addin_installed_while_fusion_running",+        "explanation": ("The AdomBridge add-in was installed or updated while Fusion was "+                        "already running. Fusion loads add-ins only at launch, so this "+                        "Fusion cannot see it until it restarts. Nothing is wrong with the "+                        "install and nothing is lost - it just needs a relaunch."),+        "userMessage": ("Fusion needs a quick restart to finish setting up the Adom add-in. "+                        "We can save your open documents first."),+        "remedyVerb": "fusion_restart_for_addin",+        "documentsKnown": False,+        "openDocuments": None,+        "unsavedDocuments": None,+    }+    # Best effort: if the add-in answers at all (an OLD copy still serving), we can count.+    try:+        st = _proxy_to_addin("get_app_state", {}, timeout=8) or {}+        docs = st.get("documents")+        if isinstance(docs, list):+            info["documentsKnown"] = True+            info["openDocuments"] = len(docs)+            info["unsavedDocuments"] = sum(1 for d in docs if not d.get("isSaved"))+            info["documentNames"] = [d.get("name") for d in docs][:20]+    except Exception:+        pass++    if fire_toast:+        # ONE toast per staged install, not per failed verb: Adonis would have had a dozen+        # verbs fail, and a dozen identical toasts is its own bad experience.+        try:+            import pathlib as _pl, json as _js, os as _os+            d = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge"+            d.mkdir(parents=True, exist_ok=True)+            f = d / "addin_restart_toast.json"+            last = 0.0+            if f.exists():+                try:+                    last = float((_js.loads(f.read_text()) or {}).get("at") or 0)+                except Exception:+                    last = 0.0+            if _t.time() - last > 900:      # at most once every 15 minutes+                body = info["userMessage"]+                if info.get("documentsKnown"):+                    body = ("Fusion needs a quick restart to finish setting up the Adom "+                            "add-in. You have %d document(s) open%s - we will save them "+                            "first." % (info["openDocuments"],+                                        (", %d unsaved" % info["unsavedDocuments"])+                                        if info.get("unsavedDocuments") else ""))+                r = _cli_notify_all("Fusion needs a restart", body, "info")+                info["toast"] = {"delivered": bool(r.get("delivered")),+                                 "error": r.get("error")}+                f.write_text(_js.dumps({"at": _t.time()}))+            else:+                info["toast"] = {"delivered": False, "suppressed": "already toasted recently"}+        except Exception as e:+            info["toast"] = {"delivered": False, "error": str(e)[:200]}+    return info++ def _handle_notify_owner(args: dict) -> dict:     """LAST-RESORT: toast the user's MAIN desktop (cross-AD) to ask for a human step. @@ -12071,12 +12151,36 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No                               "including a 20s probe - if that happens it restarts Fusion "                               "itself and says so."),                 }+            # TELL THE HUMAN WHY, AND OFFER THE FIX (the Adonis incident, John 2026-08-27).+            # This used to return a bare "add-in not responding" and a hint aimed at an AI+            # ("fix it YOURSELF - never ask the user"). That advice is right for an agent+            # driving headlessly and WRONG when a person is sitting in front of Fusion with+            # documents open: a brand-new user drove this from the wiki, every verb failed,+            # and nothing ever told him the product was fine and just needed a relaunch.+            # The payload now carries what a web-control caller needs to ASK him, and a+            # toast fires so he is told even if the caller drops it on the floor.+            _ri = _addin_restart_info()             return {                 "success": False,-                "error": "Fusion 360 is running but AdomBridge add-in not responding.",-                "errorCode": "fusion_addin_not_responding",+                "error": ("Fusion 360 is running but the AdomBridge add-in is not responding "+                          "- it was installed or updated while Fusion was already open, and "+                          "Fusion only loads add-ins at launch."),+                "errorCode": "addin_restart_required",                 "hostResponding": True,-                "_hint": "The add-in isn't responding. Fix it YOURSELF - never ask the user: the bridge auto-installs the add-in to ALL Fusion add-in dirs (2025+ Fusion scans %APPDATA%/Autodesk/FusionAddins; the legacy API/AddIns dirs are silently ignored - issue #63), so restart Fusion via fusion_stop + fusion_start and it loads (runOnStartup). Verify with fusion_addin_status.",+                "restartRequired": True,+                "userMessage": _ri["userMessage"],+                "openDocuments": _ri.get("openDocuments"),+                "unsavedDocuments": _ri.get("unsavedDocuments"),+                "documentsKnown": _ri.get("documentsKnown"),+                "remedyVerb": "fusion_restart_for_addin",+                "toast": _ri.get("toast"),+                "_hint": ("ASK THE USER, do not silently restart their Fusion: show "+                          "userMessage, and if documentsKnown say how many documents are "+                          "open. On yes, call fusion_restart_for_addin {save:true} - it "+                          "saves open documents, restarts Fusion, waits for the add-in and "+                          "reports back. An AI driving with no human present may call it "+                          "directly. (Legacy callers: this was errorCode "+                          "fusion_addin_not_responding.)"),             }      # ── Busy gate: reject add-in commands immediately if a long command is running ──@@ -12617,6 +12721,77 @@ result = {"units": "mm", "worldSpace": bool(_WORLD), '''  +def _handle_restart_for_addin(fusion_info: dict, args: dict) -> dict:+    """Save open documents, restart Fusion, wait for the add-in, report back.++    The remedy half of the Adonis incident (John, 2026-08-27): telling a user "restart+    Fusion" is still making them do our job. This does it, and keeps the promise the+    message makes - "we will save your open documents first" - because a restart that+    quietly discards someone's work would be far worse than the original silence.++    save:false skips saving (an AI on an unattended box that knows the docs are scratch).+    """+    import time as _t+    save = args.get("save", True)+    t0 = _t.time()+    steps = []++    if save:+        # Save FIRST, and treat a failure as a STOP, not a warning: the whole point is+        # that nobody loses work to our restart.+        try:+            sv = _proxy_to_addin("close_all_documents", {"save": True}, timeout=180) or {}+            steps.append({"step": "save", "ok": bool(sv.get("success")),+                          "closed": (sv.get("data") or {}).get("closed"),+                          "wouldPrompt": (sv.get("data") or {}).get("wouldPrompt")})+            _wp = (sv.get("data") or {}).get("wouldPrompt")+            if _wp:+                return {"success": False, "errorCode": "unsaved_named_document",+                        "error": ("Cannot restart safely: %s has never been saved, so Fusion "+                                  "would raise its Save box and block the restart."+                                  % ", ".join(repr(n) for n in _wp)),+                        "steps": steps,+                        "_hint": ("Ask the user where to save it (fusion_save_to_cloud "+                                  "{name, projectName}), then retry. Nothing was closed and "+                                  "no dialog was raised.")}+        except Exception as e:+            return {"success": False, "errorCode": "save_failed", "steps": steps,+                    "error": "Refusing to restart because saving failed: %s" % str(e)[:200],+                    "_hint": "Nothing was restarted; the user's documents are untouched."}++    try:+        # handle_fusion_stop, imported at module top from handlers.close_fusion. My first+        # draft called a name that does not exist (_handle_fusion_stop) behind a globals()+        # check that would have silently taken a fallback path instead of failing loudly.+        st = handle_fusion_stop(detect_fusion(), {})+        steps.append({"step": "stop", "ok": bool((st or {}).get("success", True))})+    except Exception as e:+        steps.append({"step": "stop", "ok": False, "error": str(e)[:200]})++    _t.sleep(3)+    try:+        lr = _handle_launch(detect_fusion(), {"waitForAddin": True}) or {}+        steps.append({"step": "start", "ok": bool(lr.get("success"))})+    except Exception as e:+        return {"success": False, "errorCode": "restart_failed", "steps": steps,+                "error": "Fusion was stopped but did not come back: %s" % str(e)[:200],+                "_hint": "Launch it with fusion_start and check fusion_readiness "+                         "(needsSignin true means Autodesk sign-in is blocking)."}++    ready = _addin_round_trip_ready(tries=6, gap=5.0)+    return {+        "success": bool(ready),+        "addinReady": bool(ready),+        "tookSec": round(_t.time() - t0, 1),+        "steps": steps,+        "errorCode": None if ready else "addin_still_not_responding",+        "_hint": ("Fusion restarted and the add-in answered - re-run the verb that failed."+                  if ready else+                  "Fusion restarted but the add-in still is not answering. That is a real "+                  "defect, not a user problem: check fusion_addin_status and file it."),+    }++ def _handle_inspect_bodies(fusion_info: dict, args: dict) -> dict:     """fusion_inspect_bodies - what actually got built, in millimetres."""     world = bool(args.get("worldSpace"))@@ -12653,6 +12828,7 @@ def _handle_inspect_bodies(fusion_info: dict, args: dict) -> dict:   COMMAND_HANDLERS["inspect_bodies"] = _handle_inspect_bodies+COMMAND_HANDLERS["restart_for_addin"] = _handle_restart_for_addin