app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
0.2.28 source: one thread per request, named agent_busy; battery aborts on a blocked bridge
7 files changed
+37−7
bridge-rs/Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "altium-bridge"-version = "0.2.27"+version = "0.2.28" dependencies = [ "getrandom", "serde",
bridge-rs/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.27"+version = "0.2.28" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/agent.rs+9−1@@ -154,6 +154,9 @@ pub fn call(command: &str, params: &[(&str, String)], payload: Option<&str>, tim /// diagnostics) have no selection, so `workspace_dir()` points at the legacy root and goes blind /// to a per-instance loop (adom/altium-bridge#8); they resolve a live workspace via /// `live_workspaces()` and call here directly.+/// The agent command currently posted (the agent serves one at a time): named in agent_busy.+static IN_FLIGHT: std::sync::Mutex<Option<(String, Instant)>> = std::sync::Mutex::new(None);+ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], payload: Option<&str>, timeout: Duration) -> Value { let ws = ws.to_path_buf(); let id = rid();@@ -171,8 +174,13 @@ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], p } let t_inbox = t0.elapsed().as_millis() as u64; if inbox.exists() {- return json!({"success": false, "errorCode": "agent_busy", "error": "previous request still in the inbox"});+ let holder = IN_FLIGHT.lock().ok().and_then(|g| g.clone());+ let (what, secs) = holder.map(|(c, at)| (c, at.elapsed().as_secs())).unwrap_or_else(|| ("(unknown)".into(), 0));+ return json!({"success": false, "errorCode": "agent_busy", "busyWith": what, "busyForSec": secs,+ "error": format!("the Altium agent is still running '{what}' ({secs} s); '{command}' was not started"),+ "_hint": "The agent runs ONE command at a time inside Altium. Wait and retry, or if busyForSec is far beyond what that command should take: altium_check_dialogs (a box may be blocking it), then altium_agent_recover."}); }+ if let Ok(mut g) = IN_FLIGHT.lock() { *g = Some((command.to_string(), Instant::now())); } if let Some(p) = payload { let tmp = ws.join("request.payload.tmp"); if std::fs::write(&tmp, p).is_ok() {
bridge-rs/src/main.rs+12−1@@ -25,7 +25,7 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.27";+pub const BRIDGE_VERSION: &str = "0.2.28"; fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> { let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());@@ -52,7 +52,14 @@ fn main() { dialogs::start_reaper(); taskbar::start_idle_watcher(); lifecycle::start_close_watchdog(); // adom/altium-bridge#4: exit the agent the instant a user-initiated close begins+ // ONE THREAD PER REQUEST. Until 0.2.28 requests ran inline in this loop, so a single slow verb+ // (a 25 s UIA search in signin, an agent call waiting on a parked script engine) blocked EVERY+ // request behind it, /health included; ab dropped its connection at its own budget while the+ // bridge kept grinding the abandoned request, and the queue only grew (the 30-verb cascades the+ // verb battery found, 2026-09-20). Agent-bound work still serialises where it must: the agent's+ // one-file inbox, which now answers a NAMED agent_busy instead of a dropped connection. for mut req in server.incoming_requests() {+ let spawned = std::thread::Builder::new().name("altium-request".into()).spawn(move || { let t0 = Instant::now(); let path = req.url().to_string(); let method = req.method().clone();@@ -85,5 +92,9 @@ fn main() { _ => json_response(404, &serde_json::json!({"error": "Not found"})), }; let _ = req.respond(resp);+ });+ if let Err(e) = spawned {+ eprintln!("[altium] could not spawn a request thread: {e}");+ } } }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.27",+ "version": "0.2.28", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",
dev-skills/altium-bridge-test/run_verb_tests.py+12−1@@ -262,6 +262,7 @@ def main(): only = [x for x in a.only.split(",") if x] rows = []+ transport_run, first_blocked_after, aborted = 0, "", False order = sorted(M.items(), key=lambda kv: (kv[1].get("phase", 9), list(M).index(kv[0]))) for verb, spec in order: if verb not in verbs or (only and verb not in only):@@ -292,10 +293,20 @@ def main(): outcome, detail = "FAIL", f"left dialog(s) on screen: {left} | was: {detail}" ab(T, TH, "altium_agent_recover", {}, timeout=90) rows.append(dict(verb=verb, outcome=outcome, detail=detail, sec=d.get("_sec"), note=spec.get("note", "")))+ # Three transport failures in a row = the bridge is blocked, not three bugs. Stop: firing+ # the rest of the matrix into a blocked bridge tests nothing and hammers the user's Altium.+ transport_run = transport_run + 1 if detail.startswith("transport/timeout") else 0+ if transport_run == 1:+ first_blocked_after = rows[-2]["verb"] if len(rows) > 1 else "(start)"+ if transport_run >= 3:+ print(f"\nABORT: 3 transport failures in a row. The bridge stopped answering right after '{first_blocked_after}'. "+ f"That verb (or the one that failed first, '{rows[-3]['verb']}') is the defect; the rest of the run is void.", flush=True)+ aborted = True+ break print(f"{outcome:5s} {verb:28s} {str(d.get('_sec')):>6}s {detail[:130]}", flush=True) n = {k: sum(1 for r in rows if r["outcome"] == k) for k in ("PASS", "FAIL", "SKIP")}- red = n["FAIL"] or uncovered+ red = n["FAIL"] or uncovered or aborted print("\n==== altium-bridge verb battery on", T, "| bridge", live.get("bridgeVersion"), "====") print(f"verbs in catalog {len(verbs)} | PASS {n['PASS']} | FAIL {n['FAIL']} | SKIP {n['SKIP']} (safety) | uncovered {len(uncovered)} | stale {len(stale)}") if uncovered: print("UNCOVERED (add to MATRIX):", uncovered)
page.json+1−1@@ -140,4 +140,4 @@ "skills/altium-wiki-import/SKILL.md", "uninstall.sh" ]-}+}