← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.5\ No newline at end of file+1.10.6\ 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.10.5",+  "version": "1.10.6",   "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.10.5"+version = "1.10.6" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.5"+version = "1.10.6" 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.10.5"+DASH_VERSION="1.10.6" 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.10.5",+  "version": "1.10.6",   "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−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.10.5",+  "version": "1.10.6",   "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,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.10.5"+version = "1.10.6" edition = "2021" license = "MIT" 
rust/bridge/src/chainexec.rs+9
@@ -147,6 +147,15 @@ pub fn addin_restart_info(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->         unames = Some(un);     }     let yours: Vec<String> = match &unames { Some(u) => { let y = user_unsaved_docs(&ctx.state_dir, u); info["userUnsaved"] = json!(y); info["autoRestart"] = json!(y.is_empty()); y } None => { info["autoRestart"] = json!(false); vec![] } };+    let policy = crate::lifecycle::restart_policy(&ctx.state_dir);+    info["restartPolicy"] = json!(policy);+    let yours = if policy == "auto" {+        // every open document is the AI's own on an Adom box: nothing to ask, the bridge restarts+        if !yours.is_empty() { info["userUnsavedIgnored"] = json!(yours); info["userUnsaved"] = json!([]); }+        info["autoRestart"] = json!(true);+        info["userMessage"] = json!("Fusion is restarting itself to load the Adom add-in (restartPolicy auto). Open documents are saved where Fusion can save them; never-saved scratch is discarded.");+        Vec::new()+    } else { yours };     if fire_toast && info["autoRestart"].as_bool() != Some(true) {         let f = ctx.state_dir.join("addin_restart_toast.json");         let _ = std::fs::create_dir_all(&ctx.state_dir);
rust/bridge/src/lifecycle.rs+84−3
@@ -141,6 +141,48 @@ pub fn kill(ctx: &Ctx, args: &Value) -> Value { }  /// `fusion_restart_for_addin`: save, stop (or kill when only bridge scratch is unsaved), start, wait.+/// Who owns a Fusion restart. "auto" (the default): the bridge restarts Fusion itself whenever+/// the add-in needs it, saving what can be saved and discarding the rest, because on an Adom box+/// the AI is the only thing driving Fusion and every open document is its own (John,+/// 2026-09-07: "you should literally know to just allow fully restarting fusion ... all the open+/// docs are just the ai's docs"). "ask" keeps the toast with the Save-and-restart choice for a+/// machine a person shares with the AI. Pref key `restartPolicy` in prefs.json, or the env+/// ADOM_FUSION_RESTART_POLICY.+pub fn restart_policy(state_dir: &std::path::Path) -> String {+    if let Ok(v) = std::env::var("ADOM_FUSION_RESTART_POLICY") { if !v.trim().is_empty() { return v.trim().to_lowercase(); } }+    crate::small_verbs::get_pref(state_dir, "restartPolicy").and_then(|v| v.as_str().map(|s| s.to_lowercase())).unwrap_or_else(|| "auto".into())+}++/// A plain informational toast on this machine (no buttons, nothing to answer).+pub fn notify_info(ctx: &Ctx, title: &str, body: &str) -> bool {+    match Loopback::from_env() {+        Some(ab) => { let r = ab.call("notify_user", json!({"title": title, "body": body, "level": "info"}), &ctx.caller, 15); r.is_ok() }+        None => false,+    }+}++/// When the add-in's main thread has been blocked by a modal since this instant (0 = not blocked).+static BLOCKED_SINCE: std::sync::Mutex<f64> = std::sync::Mutex::new(0.0);+/// How long a modal may hold the add-in before the bridge restarts Fusion itself (restartPolicy auto).+const BLOCKED_RESTART_AFTER_SECS: f64 = 90.0;++fn now_secs() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }++/// Restart Fusion in the background because a modal nobody answers has held the add-in+/// (restartPolicy auto). Save is impossible (the add-in cannot answer), so this is a kill ++/// start; the ledger and the reply carry what was on screen.+fn begin_blocked_restart(ctx: &Ctx, blocking: &[Value], held: f64) -> bool {+    crate::fusion_prefs::action_ledger(&ctx.state_dir, "auto_restart_blocked_modal", json!({"heldSec": (held * 10.0).round() / 10.0,+        "dialogs": blocking.iter().map(|d| json!({"title": d["title"], "category": d["category"]})).collect::<Vec<_>>()}));+    let url = crate::launch::SELF_URL.lock().unwrap().clone().or_else(|| crate::launch::SIDECAR_URL.lock().unwrap().clone());+    let Some(u) = url else { return false; };+    std::thread::spawn(move || {+        let r = ureq::post(&u).timeout(Duration::from_secs(900)).send_json(json!({"command": "fusion_restart_for_addin", "args": {"save": false, "auto": true, "wait": true, "why": "modal blocked the add-in"}})).ok().and_then(|r| r.into_json::<Value>().ok()).unwrap_or(json!({}));+        eprintln!("[fusion-bridge] auto restart after a blocking modal: {{success: {}, errorCode: {}}}", r["success"], r["errorCode"]);+    });+    true+}+ pub fn restart_for_addin(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     let sd = ctx.state_dir.clone();     if args["decline"].as_bool() == Some(true) {@@ -166,8 +208,10 @@ pub fn restart_for_addin(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->     }     let t0 = Instant::now();     let save = args["save"].as_bool().unwrap_or(true);+    let auto_policy = args["auto"].as_bool() == Some(true) || restart_policy(&sd) == "auto";     let mut steps: Vec<Value> = Vec::new();     let mut hard = false;+    let mut discarded: Vec<String> = Vec::new();     if save {         let sv = pass("close_all_documents", json!({"save": true}));         steps.push(json!({"step": "save", "ok": sv["success"].as_bool().unwrap_or(false), "closed": sv["data"]["closed"], "wouldPrompt": sv["data"]["wouldPrompt"]}));@@ -175,12 +219,25 @@ pub fn restart_for_addin(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->         if !wp.is_empty() {             let users: Vec<&String> = wp.iter().filter(|n| { let bare = n.replace('*', "").split(" (").next().unwrap_or("").trim().to_lowercase(); !bare.is_empty() && crate::etiquette::bridge_doc_in(&sd, &bare).is_none() && !bare.starts_with("untitled") && !bare.ends_with("_3d") }).collect();             if users.is_empty() { steps.push(json!({"step": "discard-bridge-scratch", "docs": wp})); hard = true; }+            else if auto_policy {+                // restartPolicy auto: every open document is the AI's own; a never-saved one is+                // scratch, so it is discarded rather than left to block the restart+                discarded = users.iter().map(|s| s.to_string()).collect();+                crate::fusion_prefs::action_ledger(&sd, "restart_discarded_unsaved", json!({"docs": discarded}));+                steps.push(json!({"step": "discard-under-auto-policy", "docs": discarded})); hard = true;+            }             else {                 return json!({"success": false, "errorCode": "unsaved_named_document", "error": format!("Cannot restart safely: {} has never been saved, so Fusion would raise its Save box and block the restart.", users.iter().map(|n| fusion_lbr::py_repr_str(n)).collect::<Vec<_>>().join(", ")), "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."});             }         }-        if sv["success"].as_bool() != Some(true) && sv["errorCode"].is_string() && sv["error"].as_str().map(|e| e.contains("Add-in request failed")).unwrap_or(false) {+        if sv["success"].as_bool() != Some(true) && auto_policy {+            // the add-in cannot save (it is the thing that is not answering): restart anyway,+            // hard, and say so; Fusion's own recovery keeps cloud documents' last autosave+            steps.push(json!({"step": "save-unavailable", "error": sv["error"].as_str().unwrap_or("").chars().take(160).collect::<String>()}));+            hard = true;+        }+        if sv["success"].as_bool() != Some(true) && !auto_policy && sv["errorCode"].is_string() && sv["error"].as_str().map(|e| e.contains("Add-in request failed")).unwrap_or(false) {             return json!({"success": false, "errorCode": "save_failed", "steps": steps, "error": format!("Refusing to restart because saving failed: {}", sv["error"].as_str().unwrap_or("").chars().take(200).collect::<String>()), "_hint": "Nothing was restarted; the user's documents are untouched."});         }     }@@ -190,7 +247,15 @@ pub fn restart_for_addin(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->     let lr = crate::launch::start(ctx, &json!({"waitForAddin": true}));     steps.push(json!({"step": "start", "ok": lr["success"].as_bool().unwrap_or(false)}));     let ready = crate::launch::round_trip_ready_gap(6, 5.0);-    json!({"success": ready, "addinReady": ready, "tookSec": (t0.elapsed().as_secs_f64() * 10.0).round() / 10.0, "steps": steps, "errorCode": if ready { Value::Null } else { json!("addin_still_not_responding") },+    if auto_policy {+        let why = args["why"].as_str().unwrap_or("to load the Adom add-in");+        let body = format!("The bridge restarted Fusion {why} (restartPolicy auto: every open document is the AI's own). {}{}",+            if steps.iter().any(|s| s["step"] == "save") { "Documents were saved first. " } else { "Nothing could be saved first. " },+            if discarded.is_empty() { String::new() } else { format!("Discarded never-saved scratch: {}. ", discarded.join(", ")) });+        crate::fusion_prefs::action_ledger(&sd, "auto_restart_done", json!({"ready": ready, "why": why, "steps": steps, "discarded": discarded}));+        let _ = notify_info(ctx, "Fusion restarted by the bridge", &body);+    }+    json!({"success": ready, "addinReady": ready, "restartPolicy": if auto_policy { "auto" } else { "ask" }, "discarded": discarded, "tookSec": (t0.elapsed().as_secs_f64() * 10.0).round() / 10.0, "steps": steps, "errorCode": if ready { Value::Null } else { json!("addin_still_not_responding") },         "_hint": if ready { "Fusion restarted and the add-in answered - re-run the verb that failed." } 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." }}) } @@ -249,15 +314,31 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val                     if !st["walkProgress"].is_null() { resp["progress"] = st["walkProgress"].clone(); }                     return resp;                 }+                // restartPolicy auto: a modal nobody answers does not get to hold the AI's Fusion+                // for ever. Seat/sign-in dialogs heal through their own flows and are excluded.+                let policy = restart_policy(&ctx.state_dir);+                let self_healing = blocking.iter().any(|d| matches!(d["category"].as_str().unwrap_or(""), "license_sessions_exceeded" | "session_conflict_decision" | "session_suspend_confirm" | "session_suspended_regrab" | "signin_required" | "upload_in_progress"));+                let held = { let mut s = BLOCKED_SINCE.lock().unwrap(); let n = now_secs(); if *s == 0.0 || n - *s > 900.0 { *s = n; } n - *s };+                if policy == "auto" && !self_healing && held >= BLOCKED_RESTART_AFTER_SECS {+                    *BLOCKED_SINCE.lock().unwrap() = 0.0;+                    let started = begin_blocked_restart(ctx, &blocking, held);+                    let titles = blocking.iter().map(|d| format!("'{}' ({})", d["title"].as_str().unwrap_or(""), d["category"].as_str().unwrap_or(""))).collect::<Vec<_>>().join(", ");+                    return json!({"success": false, "errorCode": "fusion_auto_restarting", "restartStarted": started, "heldSec": (held * 10.0).round() / 10.0, "blockingDialogs": blocking,+                        "error": format!("A modal has held Fusion's main thread for {:.0}s ({}), so the bridge is restarting Fusion itself (restartPolicy auto). The verb '{command}' did not run.", held, if titles.is_empty() { "no classified dialog; see postOpenScreenshot".to_string() } else { titles }),+                        "data": {"command": command, "health": h, "blockingDialogs": blocking, "postOpenScreenshot": shots},+                        "_hint": "Poll fusion_readiness until ready:true (60-120 s), then re-run the verb. Unsaved edits since the last autosave are gone; every open document was the AI's own. If the dialog in postOpenScreenshot is one the bridge should answer itself, file it on the fusion-bridge page with the screenshot so it gets a dialog rule."});+                }+                let auto_note = if policy == "auto" { format!(" The bridge restarts Fusion itself if this stays blocked for {:.0}s (restartPolicy auto; blocked {:.0}s so far); re-poll rather than restarting.", BLOCKED_RESTART_AFTER_SECS, held) } else { " This is NOT an add-in crash; with restartPolicy ask the user decides on a restart.".to_string() };                 let message = if !blocking.is_empty() {                     let titles = blocking.iter().map(|d| format!("'{}' ({})", d["title"].as_str().unwrap_or(""), d["category"].as_str().unwrap_or(""))).collect::<Vec<_>>().join(", ");                     let mut seen = Vec::new(); for d in &blocking { let r = d["resolution"].as_str().unwrap_or("").to_string(); if !seen.contains(&r) { seen.push(r); } }-                    format!("The AdomBridge add-in is alive but its main thread is blocked by a modal dialog: {titles}. This is NOT an add-in crash — do not restart Fusion. {}", seen.join(" "))+                    format!("The AdomBridge add-in is alive but its main thread is blocked by a modal dialog: {titles}.{auto_note} {}", seen.join(" "))                 } else {                     format!("The AdomBridge add-in is alive but its main thread is not responding (status: {main_thread}, pending: {pending}). Fusion 360 likely has a modal dialog open (Document Recovery, error, or update prompt) that is blocking execution. READ the screenshots to identify the dialog, then dismiss with fusion_send_key {{\"key\": \"escape\"}} or fusion_send_key {{\"key\": \"tab\"}} + {{\"key\": \"enter\"}}.")                 };                 return json!({"success": false, "error": "addin_main_thread_blocked", "message": message, "blockingDialogs": blocking, "data": {"command": command, "health": h, "blockingDialogs": blocking, "postOpenScreenshot": shots}});             }+            *BLOCKED_SINCE.lock().unwrap() = 0.0;             return json!({"success": false, "error": "addin_command_timeout", "message": format!("Command '{command}' timed out but the add-in reports main thread is {main_thread}. The command may be long-running or stuck."), "data": {"command": command, "health": h, "postOpenScreenshot": shots}});         }     }
skills/fusion-web-control/SKILL.md+2−1
@@ -79,7 +79,8 @@ the four questions a caller needs before starting: | `not_installed` | Fusion is not on this machine | Offer to install it; the plan includes the install stage (10-30 min) | | `installed_not_running` | Just needs launching | Proceed; launch is a stage, ~60s, folded into the ETA | | `needs_signin` | Running, but at the Autodesk sign-in screen (bridge 1.10.1+) | Drive the sign-in (fusion-autodesk-signin skill); board verbs return this at once, never a restart diagnosis |-| `addin_restart_required` | Running and signed in, but the add-in is not loaded | **Ask the user**, then `fusion_restart_for_addin` |+| `addin_restarting` | Running and signed in, add-in not loaded; the bridge is restarting Fusion itself (restartPolicy auto, the default since 1.10.6: every open document is the AI's own) | Poll `fusion_readiness`, then continue |+| `addin_restart_required` | Same, on a box whose prefs.json sets `restartPolicy: ask` | Show `userMessage`, then `fusion_restart_for_addin` on a yes | | `ready` | Good to go | Start |  It returns `plan[]` with a per-stage `estimatedSec` and `confidence`, plus `estimatedSec`