← Commit history

1.6.55: universal error-dialog guard (auto-detect Fusion Error dialog on every open/mutating verb)

John Lauer ·bc3a5e20ed ·3mo ago ·parent 1608530
6 files changed +104−12
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.54\ No newline at end of file+1.6.55\ No newline at end of file
CHANGELOG.md+14
@@ -3,6 +3,20 @@ 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.55 - 2026-07-07++- **Universal error-dialog guard, baked into every mutating/open verb (John: "you have to+  screenshot to get the parent/child array to constantly look for errors ... bake this into every+  command").** A malformed `.lbr` makes Fusion pop an owned "Error" dialog ("<file>.lbr has errors+  and cannot be opened") while `Document.newDesignFromLocal` still returns ok (it opened an EMPTY+  design behind the error), so `open_lbr` reported success on a library that never loaded. New+  `operation_error` dialog classification (title "Error" / "cannot be opened" / "has errors") + a+  shared `_apply_failure_dialogs()` now runs after every open/mutating verb: it screenshots the+  owned popup (hwnd-targeted, background), flips the result to `success:false` ++  `errorCode:fusion_operation_error` with the exact resolution, attaches the shot to+  `data.errorDialog`, and dismisses the pure-ack popup so it can't wedge the next command. A failed+  op can no longer be returned as success.+ ## 1.6.54 - 2026-07-07  - **Fixed 3D auto-orient laying TALL through-hole parts on their side** (John caught it: a machine
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.54",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.54/adom-bridge-fusion-v1.6.54.zip",-  "sha256": "e2920d0d79a89fcc53c808dc2f8456c068c8be3929952f2bb8fd4b096a85e5fe",-  "size": 337157,+  "version": "1.6.55",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.55/adom-bridge-fusion-v1.6.55.zip",+  "sha256": "f1cf50ad2e471dc9ba6bb1e9cf2184562b382c03d8efc10fcca3b7ead58770cf",+  "size": 339847,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",   "released_at": "2026-07-07T16:20:00Z"-}+}\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.54",+  "version": "1.6.55",   "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.",
handlers/dialog_classify.py+22
@@ -112,8 +112,30 @@ _RULES = [      "Launch error from an INCOMPLETE webdeploy folder (missing FusionLauncher.exe.ini). Safe to "      "dismiss (OK/close) - it is only an acknowledgement. Launch the COMPLETE production folder (the "      "hash dir that HAS FusionLauncher.exe.ini); detect_fusion now prefers it."),++    # ⚠️ OPERATION FAILED. A bare "Error"-titled owned popup means the LAST command did+    # NOT succeed even if the API call returned ok - e.g. an .lbr that "has errors and+    # cannot be opened" (Fusion opened an EMPTY design instead), a failed import/export,+    # a bad STEP. This is NOT a decision and NOT a benign launch ack: the bridge flips the+    # command's success to False when it sees this (see server._apply_failure_dialogs), so+    # a malformed library can never again be reported as "opened". Keep this rule LAST so+    # the specific "error launching..."/seat rules above win first. It is a pure OK+    # acknowledgement, so it is safe to close (WM_CLOSE) after the failure is surfaced.+    (("error", "cannot be opened", "has errors", "failed to", "could not", "invalid"),+     "operation_error",+     "The last operation FAILED - Fusion is showing an Error dialog (the API call may still have "+     "returned ok because it opened an EMPTY design). READ the dialog screenshot for the exact "+     "message. Common case: '<file>.lbr has errors and cannot be opened' = the library is malformed "+     "(re-lint/regenerate; a frequent cause is a single UNNAMED symbol pin - EAGLE <pin name=''> + "+     "<connect pin=''> - name the pin from the KiCad pin NUMBER). The bridge already flipped success "+     "to False and dismissed the empty design; FIX the source file and retry. Do NOT report success."), ] +# Categories that mean the OPERATION FAILED. The bridge downgrades a command's+# success to False when a popup of one of these categories is up after it runs, so+# a failure (e.g. a malformed .lbr) can never be reported as success. These are pure+# OK-acknowledgements, so the bridge may close them after surfacing the failure.+FAILURE_CATEGORIES = {"operation_error"} # Categories that require a USER decision — the launch code must NOT auto-dismiss # these; it surfaces them + fires a notify_user + asks. DECISION_CATEGORIES = {"session_conflict_decision", "session_suspend_confirm"}
server.py+61−5
@@ -641,7 +641,10 @@ def _post_open_screenshot(result: dict, settle_time: float = 2.0) -> dict:         result["data"] = {}     result["data"]["postOpenScreenshot"] = screenshot_info -    return result+    # UNIVERSAL: if an owned "Error" popup is up (e.g. malformed .lbr "has errors and+    # cannot be opened"), flip success -> False + surface it. An open that silently+    # errored must never be reported as ok.+    return _apply_failure_dialogs(result)   def _capture_dialog_array(settle: float = 0.6) -> dict | None:@@ -738,6 +741,59 @@ def _merge_dialog_array(result: dict) -> dict:         result["data"]["dialogArray"] = arr     existing = result.get("_hint")     result["_hint"] = arr["_hint"] + ((" | " + existing) if existing else "")+    return _apply_failure_dialogs(result)+++def _apply_failure_dialogs(result: dict) -> dict:+    """UNIVERSAL error-dialog guard — baked into every mutating/open verb so the AI+    can never again report a failed op as success.++    Right after an operation, an owned Fusion popup titled "Error" (e.g. "<file>.lbr+    has errors and cannot be opened") means the op FAILED even when the underlying API+    call returned ok (Fusion opened an EMPTY design behind the error). This classifies+    any up dialog; if one is in FAILURE_CATEGORIES it:+      1. SCREENSHOTS the popup (hwnd-targeted, background) so the AI SEES it,+      2. flips result.success -> False + sets errorCode/error/_hint,+      3. dismisses the pure-ack popup (WM_CLOSE) so it can't block the next command.+    Best-effort + cheap when nothing is up (title enumeration, no screenshot). Never raises."""+    try:+        from handlers.dialog_classify import classify_blocking_dialogs, FAILURE_CATEGORIES+        dialogs = classify_blocking_dialogs()+    except Exception:+        return result+    failures = [d for d in (dialogs or []) if d.get("category") in FAILURE_CATEGORIES]+    if not failures or not isinstance(result, dict):+        return result+    f = failures[0]+    shots = []+    for d in failures:+        try:+            ss = screenshot_hwnd(d.get("hwnd"), label="error-dialog")+            if ss.get("success"):+                shots.append(ss["savedTo"])+        except Exception:+            pass+    prev_err = result.get("error") or ""+    result["success"] = False+    result["errorCode"] = "fusion_operation_error"+    result["error"] = (+        f"Operation FAILED - Fusion '{f.get('title') or 'Error'}' dialog is up"+        + (f" (prior: {prev_err})" if prev_err else "")+    )+    result["_hint"] = (f.get("resolution") or+                       "Fusion shows an Error dialog; the operation failed. Read the screenshot.")+    result.setdefault("data", {})+    if isinstance(result.get("data"), dict):+        result["data"]["errorDialog"] = {+            "title": f.get("title"), "category": f.get("category"),+            "resolution": f.get("resolution"), "screenshots": shots,+        }+    # Pure OK-acknowledgement — dismiss so it doesn't wedge the next command.+    for d in failures:+        try:+            close_window(d.get("hwnd"))+        except Exception:+            pass     return result  @@ -3111,15 +3167,15 @@ def dispatch_command(command: str, args: dict) -> dict:     if command == "attach_3d_package":         return _merge_dialog_array(_orchestrate_attach_3d_package(args))     if command == "make_3d_package":-        return _orchestrate_make_3d_package(args)+        return _apply_failure_dialogs(_orchestrate_make_3d_package(args))     if command == "build_library_3d":-        return _orchestrate_build_library_3d(args)+        return _apply_failure_dialogs(_orchestrate_build_library_3d(args))     if command == "capture_library_views":-        return _orchestrate_capture_library_views(args)+        return _apply_failure_dialogs(_orchestrate_capture_library_views(args))     if command == "cleanup_cloud_files":         return _orchestrate_cleanup_cloud_files(args)     if command == "generate_package":-        return _orchestrate_generate_package(args)+        return _apply_failure_dialogs(_orchestrate_generate_package(args))      # Add-in proxy commands     if command in ADDIN_COMMANDS: