app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
Merge PR #30 and PR #31 (Drew Owens): hang gate spares a Fusion using CPU; golden cases for bodyMetrics, health, budgetSeconds
5 files changed
+48−6
diff truncated
CHANGELOG.md+8@@ -18,6 +18,14 @@ production assembly built from 23 referenced designs. above 200 components (`bodyMetrics:true`) and the health walk above 200 timeline items (`health:true`). - `efficiency_report` returns the full target. The add-in is 1.0.28; the exe embeds the new `schema.json`. +From Drew Owens (PR #30): the hang gate never kills a Fusion that is using CPU. On 2026-10-30 it killed a Fusion 37+minutes into a long build script whose request had timed out, closing 24 (saved) documents. `confirmed_hung` now also+needs the Fusion family under 15% of one core over 1.5 s, and its 180 s clock restarts whenever Fusion works. Busy+replies carry `keptBy` (`under-ceiling`, `fusion-cpu-active`) so a caller sees what keeps Fusion alive.++From Drew Owens (PR #31): `mech_validation.json` regenerated from master after #28, with the 12 validation cases for+`inspect`'s `bodyMetrics`/`health` and `bom`/`bom_export`'s `budgetSeconds` (2,805 cases).+ ## 1.13.26 (2026-10-30) - Engine 0.21.1 (standard 0.24.2): lint_library's duplicate_pad_number counts same-numbered pads that touch or overlap
rust/bridge/src/launch.rs+2−2@@ -351,8 +351,8 @@ pub fn gate(ctx: &Ctx, cmd: &str, args: &Value) -> Option<Value> { } if matches!(h["phase"].as_str(), Some("suspect") | Some("busy-not-hung") | Some("long-op-in-flight")) { return Some(json!({"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["phase"].as_str() == Some("busy-not-hung"), "hungHeldSeconds": h["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."}));+ "errorCode": "fusion_addin_busy", "hostResponding": h["phase"].as_str() == Some("busy-not-hung"), "hungHeldSeconds": h["heldSeconds"], "keptBy": h["keptBy"],+ "_hint": "WAIT and re-poll; do not restart anything. The bridge only declares a real hang after 180s of continuous silence with Fusion idle (not using CPU), including a 20s probe - if that happens it restarts Fusion itself and says so."})); } // A modal in front of the add-in is never a reason to restart (issue #87, John: "screenshot // itself using ab ... give the ai the parent/child array of screenshots so the ai can figure
rust/bridge/src/lifecycle.rs+2−2@@ -529,8 +529,8 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val } let hung = crate::readiness_exec::confirmed_hung(ctx); if matches!(hung["phase"].as_str(), Some("suspect") | Some("busy-not-hung") | Some("long-op-in-flight")) {- return json!({"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["phase"].as_str() == Some("busy-not-hung"), "hungHeldSeconds": hung["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."});+ return json!({"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["phase"].as_str() == Some("busy-not-hung"), "hungHeldSeconds": hung["heldSeconds"], "keptBy": hung["keptBy"],+ "_hint": "WAIT and re-poll. A real hang is only declared after 180s of continuous silence with Fusion idle (not using CPU), including a 20s probe, and then the bridge restarts Fusion itself."}); } if hung["hung"].as_bool() == Some(true) { let _ = kill(ctx, &json!({})); std::thread::sleep(Duration::from_secs(6)); let _ = crate::launch::start(ctx, &json!({}));
rust/bridge/src/readiness_exec.rs+36−2@@ -150,8 +150,24 @@ pub fn fusion_using_cpu(ctx: &Ctx) -> bool { t1.saturating_sub(t0) > 50_000 } -/// `_confirmed_hung`: kill-grade only when the OS says hung AND a 20s probe gets nothing AND it-/// has persisted 180s; a long op in flight (under 30 minutes, or with CPU still moving) protects.+/// CPU over a sample window that says Fusion is working: at least 15% of one core across the family+/// (GetProcessTimes counts 100 ns units). A hung host's background threads stay well under it.+pub fn working_from_ticks(delta: u64, window: Duration) -> bool {+ delta as f64 >= 0.15 * window.as_secs_f64() * 10_000_000.0+}++/// A Fusion still using CPU is working, whatever its window says: a script or native call that+/// outlived the request that started it burns a core on the main thread, where a real hang (a+/// deadlock, a wait on a lost modal) sits near idle.+pub fn fusion_working(ctx: &Ctx) -> bool {+ let window = Duration::from_millis(1500);+ let t0 = ctx.host.family_cpu_ticks(); std::thread::sleep(window); let t1 = ctx.host.family_cpu_ticks();+ working_from_ticks(t1.saturating_sub(t0), window)+}++/// `_confirmed_hung`: kill-grade only when the OS says hung AND a 20s probe gets nothing AND the host+/// is idle AND all of that has persisted 180s; a long op in flight (under 30 minutes, or with CPU+/// still moving) protects. pub fn confirmed_hung(ctx: &Ctx) -> Value { if let Some(lo) = crate::long_op_marker() { let el = now_f() - lo.1;@@ -161,6 +177,10 @@ pub fn confirmed_hung(ctx: &Ctx) -> Value { let h = is_fusion_hung(ctx); if h["hung"].as_bool() != Some(true) { *HUNG_SINCE.lock().unwrap() = 0.0; return json!({"hung": false, "phase": "pumping"}); } if crate::addin::addin_status_probe(Duration::from_secs(20)).is_some() { *HUNG_SINCE.lock().unwrap() = 0.0; return json!({"hung": false, "phase": "busy-not-hung", "signals": h["signals"]}); }+ // 2026-10-30: the gate killed a Fusion 37 minutes into a long build script, after the request+ // that started the script had timed out (so no long op was on record), and closed the user's+ // 24 documents. A kill now also needs the host to have gone idle.+ if fusion_working(ctx) { *HUNG_SINCE.lock().unwrap() = 0.0; return json!({"hung": false, "phase": "busy-not-hung", "keptBy": "fusion-cpu-active", "signals": h["signals"]}); } let now = now_f(); let mut since = HUNG_SINCE.lock().unwrap(); if *since == 0.0 { *since = now; }@@ -168,3 +188,17 @@ pub fn confirmed_hung(ctx: &Ctx) -> Value { if held < 180.0 { return json!({"hung": false, "phase": "suspect", "heldSeconds": (held * 10.0).round() / 10.0, "signals": h["signals"]}); } json!({"hung": true, "phase": "confirmed", "heldSeconds": (held * 10.0).round() / 10.0, "signals": h["signals"]}) }++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn a_fusion_burning_a_core_is_working_not_hung() {+ let window = Duration::from_millis(1500);+ assert!(working_from_ticks(15_000_000, window)); // a whole core: a script on the main thread+ assert!(working_from_ticks(2_250_000, window)); // 15%+ assert!(!working_from_ticks(300_000, window)); // 2%: a hung host's background threads+ assert!(!working_from_ticks(0, window));+ }+}
rust/core/tests/golden/mech_validation.json@@ -1 +1 @@