← Commit history

The busy gate waits out the bridge's own quick reads instead of refusing

Drew Owens ·39377099be ·15d ago ·parent 70624dd
With ~90 documents open the post-verb document snapshot (document_info) can outlive the 3 s the etiquette waits for it, so the caller's next verb found it still running and was refused as main_thread_busy (2026-10-28, a mechanical inspect in the assembly campaign, behind a 4.9 s document_info). document_info and get_app_state end in seconds: the gate now waits up to 15 s for them, then judges the add-in as before. Co-Authored-By: Claude Opus 5.5 <[email protected]>
2 files changed +35−1
CHANGELOG.md+8
@@ -3,6 +3,14 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.13.3 (unreleased)++- The busy gate waits out the bridge's own quick reads (`document_info`, `get_app_state`) for up to 15 s instead+  of refusing the next verb. With about 90 documents open, the post-verb document snapshot can outlive the 3 s+  the window etiquette waits for it, and the caller's next verb then found it still running and got+  `main_thread_busy` (a mechanical `inspect` in the assembly campaign, behind a 4.9 s `document_info`). Real work+  in front of a verb is refused as before.+ ## 1.13.2 (2026-10-28, insiders)  The standard's shared writers, for files every bridge writes the same way.
rust/bridge/src/addin.rs+27−1
@@ -167,6 +167,18 @@ pub fn loaded_evidence(pids: &[u32]) -> Option<Value> {     loaded_evidence_from(None, Some(ago), port_refused()) } +/// The bridge's own quick reads. With ~90 documents open a document_info takes ~5 s, longer than+/// the 3 s the post-verb document snapshot waits, so it is still running when the caller's next+/// verb lands, and that verb was refused as main_thread_busy (2026-10-28, a mechanical inspect in+/// the assembly campaign). Such a read ends in seconds: the gate waits it out instead.+const SHORT_READS: &[&str] = &["document_info", "get_app_state"];+const SHORT_READ_WAIT: Duration = Duration::from_secs(15);++/// Is the add-in busy with one of the bridge's own quick reads, which the gate should wait out?+fn busy_with_short_read(st: &Value) -> bool {+    st["busy"].as_bool() == Some(true) && SHORT_READS.contains(&st["busyCommand"].as_str().unwrap_or(""))+}+ /// The two busy gates, in server.py's order: our own long command, then the add-in's /status. fn busy_gate(cmd: &str) -> Option<Value> {     if LONG_RUNNING.contains(&cmd) || BUSY_EXEMPT.contains(&cmd) { return None; }@@ -183,7 +195,12 @@ fn busy_gate(cmd: &str) -> Option<Value> {             "errorCode": "main_thread_busy", "busyCommand": bc, "elapsedSeconds": elapsed, "progress": progress,             "_hint": format!("A cloud search ({bc}) is in progress{}, running for {elapsed}s. Do NOT retry add-in commands (get_app_state, document_info, etc.) — they will all be rejected until the search finishes. Commands that still work right now: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window. Wait for the search to complete, then retry.", pct.map(|p| format!(" (~{p}% done)")).unwrap_or_default())}));     }-    if let Some(st) = addin_status_probe(Duration::from_secs(3)) {+    if let Some(mut st) = addin_status_probe(Duration::from_secs(3)) {+        let deadline = Instant::now() + SHORT_READ_WAIT;+        while busy_with_short_read(&st) && Instant::now() < deadline {+            std::thread::sleep(Duration::from_millis(250));+            match addin_status_probe(Duration::from_secs(3)) { Some(next) => st = next, None => break }+        }         // the palette (activity_banner) repaints in a fraction of a second and is advisory: a verb         // that lands while it paints waits its turn in the add-in instead of being refused         // a sub-second probe from another caller (the dashboard polls get_app_state every half@@ -471,6 +488,15 @@ mod tests {         assert!(loaded_evidence_from(None, None, false).is_none());     }     #[test]+    fn the_gate_waits_out_the_bridges_own_quick_reads() {+        // 2026-10-28: the post-verb document_info ran ~5 s with 94 documents open and the next+        // verb was refused; a quick read is waited out, real work still is not+        assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "document_info", "elapsedSeconds": 4.9})));+        assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "get_app_state", "elapsedSeconds": 2.5})));+        assert!(!busy_with_short_read(&json!({"busy": true, "busyCommand": "run_modeling_script", "elapsedSeconds": 4.9})));+        assert!(!busy_with_short_read(&json!({"busy": false, "busyCommand": "document_info"})));+    }+    #[test]     fn requests_leave_the_in_flight_list_however_they_end() {         assert!(IN_FLIGHT.lock().unwrap().iter().all(|(_, c, _)| c != "slow_one"));         let a = InFlight::begin("slow_one");