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.21 source: script-error auto-recovery + errorShot, symbol_prims/set_models fix for AD 26.9.1, ENGINE_HALTING release lint, full verb docs + altium-a365 skill, install.sh ships all skills, internal material removed
20 files changed
+147−29
PLAN.md+6−6bridge-rs/Cargo.lock+1−1bridge-rs/Cargo.toml+1−1bridge-rs/src/a365.rs+2−2bridge-rs/src/agent.rs+18bridge-rs/src/dialogs.rs+32bridge-rs/src/main.rs+1−1bridge-rs/src/verbs.rs+1−1bridge.json+20−5dev-skills/altium-bridge-dev/SKILL.md+14−1install.shpage.json+1−1scripts/release.py+28src/agent/AdomAgent.pas.tmpl+16−4src/caller_identity.py+1−1src/handlers/png_stdlib.py+1−1src/handlers/progress.py+1−1src/handlers/uia.py+1−1src/server.py+2−2uninstall.shPLAN.md+6−6@@ -16,8 +16,8 @@ description: >- --- Parent skill: **adom-bridge-sdk-new-app** (the method) and **adom-bridge-sdk** (the contract).-Written 2026-08-29 from live probes of Altium Designer 26.9.1 on John's box (`galliaApril`-container → `AdomLapper` desktop) plus the vendor docs and the community MCP bridges.+Written 2026-08-29 from live probes of Altium Designer 26.9.1 on a Windows test machine+(driven from an Adom container) plus the vendor docs and the community MCP bridges. # altium-bridge — plan of record @@ -32,7 +32,7 @@ container → `AdomLapper` desktop) plus the vendor docs and the community MCP b | Listening ports | **none** while idle. No application COM automation server registered — only document ProgIDs (`AltiumLibrary`, `AltiumDraftsmanDocument`, …). | | UIA | The VCL/DevExpress shell **IS exposed**: 45 nodes on the licence screen — `MainMenu` MenuItems (File/View/Project/Window/Help/Preferences…), `TGUIDocBarButton`s, `TdxTabSheet` panes, all with `automationId`s and `invokable:true`. The **Home / License Management page content is a WebView2** (`ProgramData\...\WebView2Cache` exists) — its *Start Free Trial / Get License / Sign Out* buttons are **not** in the UIA tree. | | State dirs | `%APPDATA%\Altium\Altium Designer {ED62C102-…}\` (prefs), `%APPDATA%\Altium\AD26\`, `C:\ProgramData\Altium\Altium Designer {ED62C102-…}\Extensions\` (Altimade, Draftsman, IPC Footprint Generator, EXPORT3D, IPC2581, SchLibGenerationTool, …), `HKCU\Software\Altium\Altium Designer {ED62C102-…}`. |-| Auth state | Signed in as *John John* (Altium account), **no license**; a free *Altium Designer Viewer* seat is offered and a **15-day trial** button exists. Vendor docs: **Viewer mode prohibits scripting** → licence is a hard prerequisite for every in-app lane. |+| Auth state | Signed in to an Altium account, **no license**; a free *Altium Designer Viewer* seat is offered and a **15-day trial** button exists. Vendor docs: **Viewer mode prohibits scripting** → licence is a hard prerequisite for every in-app lane. | ## 1. The surfaces, ranked into lanes @@ -138,7 +138,7 @@ Costs a licensed AD + a build toolchain; do it only after L2 proves insufficient ## 2. Authentication & licensing — the gates, in order 1. **Altium account sign-in** — system browser OAuth (email / Google / SSO). Drive with- `nbrowser`/pup from the bridge; the user's Google session on this box is warm. Human step: password/2FA only.+ `nbrowser`/pup from the bridge; the user's browser is usually already signed in to their identity provider. Human step: password/2FA only. 2. **Licence** — On-Demand / Standalone / **Trial (15 days)** / **Viewer (no scripting)**. The trial button lives in the WebView2 licence page → CDP or image click, one toast to the user because it is a contractual click. `altium_readiness` must report `licensed:false` with the@@ -211,11 +211,11 @@ heartbeat and recover. Keep `handlers/dialog_classify.py` with title → categor `altium-libraries`, `altium-a365`, `altium-signin` sub-skills), 30–40 discovery triggers, hero, `make release`, audit checklist. Then (optional) Phase 5: C# SDK extension. -## 6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial, AdomLapper) — the facts the code is built on+## 6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial) — the facts the code is built on | Experiment | Result | |---|---|-| Trial licence | `Start Free Trial` (WebView2, image click) → Edge opens `altium.com/365-intro/activate-add-eval` → workspace form (name, region, terms, **Activate**) → workspace `Adom` provisioned at `john-john-3.365.altium.com` in ~60 s → back in AD: **Refresh**, then **Use License** on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to `… - Trial`. |+| Trial licence | `Start Free Trial` (WebView2, image click) → Edge opens `altium.com/365-intro/activate-add-eval` → workspace form (name, region, terms, **Activate**) → a workspace is provisioned at `<name>.365.altium.com` in ~60 s → back in AD: **Refresh**, then **Use License** on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to `… - Trial`. | | `-R` forwarding | A second `X2.EXE -R<Server>:<Cmd>(…)` forwards to the running instance and exits in ~2–8 s. **If the instance is busy (any modal, or a script running) the helper hangs and shows its own "There is another instance … busy" Information dialog** — kill it. | | `RunScriptText` | Works from the shell: `-RScriptingSystem:RunScriptText(Text="Begin RunApplication('…'); End;")`. **Statements only** — a `Var` block is "Unknown identifier: Var". | | `RunScript` | `-RScriptingSystem:RunScript(ProjectName="…\X.PrjScr"\|ProcName="Main")` works with a plain `\|` when the arg is passed VERBATIM (PowerShell `Start-Process`, or Python `Popen("<one string>")`). `Popen([exe, arg])` re-quotes (the path has a space) and the helper never forwards. `^\|` is only for cmd.exe. |
bridge-rs/Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "altium-bridge"-version = "0.2.8"+version = "0.2.21" dependencies = [ "getrandom", "serde",
bridge-rs/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.20"+version = "0.2.21" 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/a365.rs+2−2@@ -184,7 +184,7 @@ pub fn configure(args: &Value) -> Value { } } if cfg.get("workspace").and_then(|w| w.as_str()).unwrap_or("").is_empty() {- return json!({"success": false, "error": "workspace is required, e.g. 'john-john-3.365.altium.com'"});+ return json!({"success": false, "error": "workspace is required, e.g. 'acme.365.altium.com'"}); } save(&cfg); json!({"success": true, "workspace": cfg.get("workspace"), "hasToken": cfg.get("token").map(|t| !t.is_null()).unwrap_or(false),@@ -247,7 +247,7 @@ pub fn oauth_begin(args: &Value) -> Value { let workspace = args.get("workspace").or_else(|| cfg.get("workspace")).and_then(|x| x.as_str()).unwrap_or("").to_string(); if workspace.is_empty() { return json!({"success": false, "errorCode": "a365_workspace_required",- "error": "workspace is required, e.g. john-john-3.365.altium.com"});+ "error": "workspace is required, e.g. acme.365.altium.com"}); } if client_id.is_empty() { return json!({"success": false, "errorCode": "a365_client_id_required",
bridge-rs/src/agent.rs+18@@ -167,6 +167,8 @@ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], p return json!({"success": false, "error": format!("cannot post request: {e}")}); } let t_post = t0.elapsed().as_millis() as u64;+ let posted_unix_ms = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map(|x| x.as_millis() as u64).unwrap_or(0);+ let mut next_halt_check = Duration::from_millis(400); let resp = ws.join(format!("response_{id}.json")); let mut t_seen: Option<u64> = None; let mut parse_retries = 0u32;@@ -189,6 +191,22 @@ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], p std::thread::sleep(Duration::from_millis(5)); continue; }+ // The engine halted on a script runtime error: the reaper pressed the Error box, the+ // script now sits on a breakpoint and will NEVER answer. Do not burn the timeout and do+ // not leave Altium parked in its debugger: stop the script now and say what happened.+ if t0.elapsed() >= next_halt_check {+ next_halt_check = t0.elapsed() + Duration::from_millis(400);+ if let Some(ev) = crate::dialogs::script_error_since(posted_unix_ms) {+ let recovered = crate::lifecycle::recover(&format!("auto: script Error box during agent command '{command}'"));+ return json!({"success": false, "errorCode": "agent_script_error", "command": command,+ "error": format!("the agent script raised a runtime error while running '{command}' (Altium showed an 'Error' box; its text is in errorShot)"),+ "errorShot": ev.get("shot").cloned().unwrap_or(Value::Null),+ "autoRecovered": recovered.get("success").and_then(|b| b.as_bool()).unwrap_or(false),+ "recoverSteps": recovered.get("steps").cloned().unwrap_or(Value::Null),+ "agentAlive": is_alive(),+ "_hint": "READ errorShot (a PNG of Altium's error box, e.g. 'Undeclared identifier: X' = that API does not exist on this Altium build). The bridge already sent EditScript:Stop, so Altium is NOT parked on a breakpoint and the next verb restarts the agent. Do not retry the same command: it will halt again. Report it on adom/altium-bridge with the errorShot."});+ }+ } std::thread::sleep(Duration::from_millis(2)); } json!({"success": false, "errorCode": "agent_timeout", "error": format!("agent did not answer within {}s", timeout.as_secs()),
bridge-rs/src/dialogs.rs+32@@ -244,6 +244,22 @@ pub fn dismiss(button: &str, only_titles: Option<&[String]>) -> Value { static REAPED: Mutex<Vec<Value>> = Mutex::new(Vec::new()); static REAPED_HWNDS: Mutex<Vec<i64>> = Mutex::new(Vec::new()); static REAPER_RUNNING: AtomicBool = AtomicBool::new(false);+/// The last lone-OK box titled "Error" the reaper pressed: {atMs, title, shot}. A DelphiScript+/// runtime error ("Undeclared identifier ...") is exactly this box, and pressing OK leaves the+/// script STOPPED ON A BREAKPOINT in Altium's script debugger: the agent loop is dead, Altium+/// refuses to close, and the caller used to see nothing but a timeout (found on AD 26.9.1,+/// 2026-09-20). agent::call watches this so the halt is reported and cleared, never silent.+static LAST_SCRIPT_ERROR: Mutex<Option<Value>> = Mutex::new(None);++fn now_ms() -> u64 {+ std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|x| x.as_millis() as u64).unwrap_or(0)+}++/// The script Error box reaped at or after `since_ms` (unix ms), if any.+pub fn script_error_since(since_ms: u64) -> Option<Value> {+ LAST_SCRIPT_ERROR.lock().ok().and_then(|g| g.clone())+ .filter(|e| e.get("atMs").and_then(|a| a.as_u64()).unwrap_or(0) >= since_ms)+} /// Record a service event (close watchdog etc.) in the same ring `altium_dialog_log` shows. pub fn note_event(mut ev: Value) {@@ -300,7 +316,23 @@ pub fn reap_once() -> usize { if REAPED_HWNDS.lock().map(|g| g.contains(&hwnd)).unwrap_or(true) { continue; }+ // A lone-OK "Error" is how a script runtime error looks. Altium PAINTS the message, so+ // photograph it BEFORE pressing OK: the picture is the only copy of the error text.+ let title_str = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();+ let is_script_error = cat == "single_ok_acknowledgement" && title_str.trim().eq_ignore_ascii_case("error");+ let mut err_shot = Value::Null;+ if is_script_error {+ let r = ab::call("desktop_screenshot_base64", json!({"hwnd": hwnd}), &[], 10);+ if let Some(img) = r.get("image").or_else(|| r.get("base64")).and_then(|i| i.as_str()) {+ err_shot = json!(img);+ }+ } let ok = press(hwnd, &act).get("success").and_then(|b| b.as_bool()).unwrap_or(false);+ if is_script_error {+ if let Ok(mut g) = LAST_SCRIPT_ERROR.lock() {+ *g = Some(json!({"atMs": now_ms(), "title": title_str, "shot": err_shot}));+ }+ } let mut followup = Value::Null; if ok && cat == "licence_chooser" { // The chooser only opens the License Management page; the seat is claimed by the
bridge-rs/src/main.rs+1−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.20";+pub const BRIDGE_VERSION: &str = "0.2.21"; 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());
bridge-rs/src/verbs.rs+1−1@@ -122,7 +122,7 @@ fn describe() -> Value { "Run once per part right after assemble-altium / build_symbol_modes, then altium_save the SchLib and altium_show_symbol to check.", &["show_symbol", "symbol_prims", "save"], &["clear:true (default) also removes hand-drawn lines on the symbol; only the pins and the body rectangle survive.", "The KiCad symbol must already carry the outline (adom-symbol create bakes it; --outline-name none for side-etched chips)."], json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "kicadSym": "C:/lib/VL53L5CX.kicad_sym"})),- verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sized A4 or B for the part count, parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given.",+ verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sheet sized FROM the measured parts: 500 mil gaps, 600 mil margins, A4 minimum; parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given.", json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)", "title": "string (Title)", "number": "string (DocumentNumber)", "revision": "string", "drawnBy": "string", "description": "string (what the sheet shows, 2-3 sentences)"}), json!({"placed": "string", "count": "int", "designator": "string", "primitives": "int", "sch": "path"}), 90, "Three parts on one sheet: symbolName 'VL53L5CX;VL53L7CX;VL53L8CX' + schPath, then altium_show_schematic {filePath: schPath}.",
bridge.json+20−5@@ -2,20 +2,20 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.41",+ "version": "0.2.21", "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", "author": "Adom Inc.", "license": "MIT", "spawn": {- "kind": "python",- "entrypoint": "server.py",+ "kind": "exe",+ "entrypoint": "altium-bridge.exe", "port": 0, "persistent": true, "healthEndpoint": "/health", "stopMethod": "kill",- "killImageName": "python.exe"+ "killImageName": "altium-bridge.exe" }, "verbPrefixes": [ "altium_"@@ -35,17 +35,21 @@ "altium_activate_document", "altium_add_body", "altium_add_models",+ "altium_add_symbol_art", "altium_agent_recover", "altium_agent_start", "altium_agent_status", "altium_agent_stop",+ "altium_animate_view", "altium_board_shape",+ "altium_board_summary", "altium_build_footprint", "altium_build_symbol_modes", "altium_check_dialogs", "altium_cleanup_libs", "altium_clone_symbol", "altium_close",+ "altium_close_document", "altium_compile", "altium_compile_intlib", "altium_component_data",@@ -69,11 +73,13 @@ "altium_extension_ping", "altium_extract_intlib", "altium_footprint_primitives",+ "altium_foreground", "altium_import_kicad", "altium_import_part", "altium_install_extension", "altium_install_library", "altium_install_library_bundle",+ "altium_install_receipt", "altium_instances", "altium_launch", "altium_layers",@@ -86,6 +92,7 @@ "altium_list_nets", "altium_net_connections", "altium_normalize_bodies",+ "altium_plate_vias", "altium_open_document", "altium_open_project", "altium_pcb_report",@@ -102,6 +109,7 @@ "altium_rename_footprint", "altium_rename_symbol", "altium_review_board",+ "altium_rules_by_kind", "altium_run_command", "altium_run_drc", "altium_run_erc",@@ -110,6 +118,7 @@ "altium_save", "altium_schematic_data", "altium_set_layer_visibility",+ "altium_set_models", "altium_set_params", "altium_set_visibility", "altium_setup_trial",@@ -127,6 +136,7 @@ "altium_status", "altium_swap_footprint", "altium_symbol_mode",+ "altium_symbol_prims", "altium_test_board", "altium_uninstall", "altium_window_info",@@ -135,6 +145,7 @@ "statusVerb": "altium_status", "uninstall": "altium_uninstall", "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+ "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json", "timeouts": { "default": 60, "verbs": {@@ -151,7 +162,10 @@ "import_kicad": 300, "compile_intlib": 300, "extract_intlib": 300,- "install_extension": 200+ "install_extension": 200,+ "install_library_bundle": 330,+ "install_library": 150,+ "install_receipt": 10 }, "prefixes": [ {@@ -166,6 +180,7 @@ }, "verbDurations": { "altium_launch": 60,+ "altium_install_library_bundle": 30, "altium_agent_start": 8, "altium_run_outjob": 60, "altium_run_drc": 30
dev-skills/altium-bridge-dev/SKILL.md+14−1@@ -75,6 +75,19 @@ Warning. When you add a mutating command, wrap file reads and never leave a tran inside a brace comment (breaks compilation). An unknown identifier is a RUNTIME error that halts the debugger; `altium_agent_recover` clears it. +**From 0.2.21 a halt is never silent.** The standing reaper photographs a lone-OK `Error` box before+pressing it (`dialogs::script_error_since`); `agent::call` polls that every 400 ms while it waits, and+on a hit runs `lifecycle::recover` itself (`EditScript:Stop`) and returns `errorCode:+agent_script_error` with `errorShot` (the PNG: Altium paints the text, so the picture is the only+copy), `autoRecovered` and `recoverSteps`. Before that, the reaper pressed OK, the script sat on a+breakpoint, every verb timed out and Altium refused to close ("cannot close the current document+when a script is stopped on a breakpoint"). Also: `ISch_Component.ImplementationCount` /+`.Implementation[i]` do not exist on AD 26.9.1 (walk models with a `SchIterator_Create` ++`AddFilter_ObjectSet(MkSet(eImplementation))`). Every identifier in this section that a regex can+recognise is in `scripts/release.py` `ENGINE_HALTING`, and a build that contains one is refused:+add to that list whenever you find another. Test new agent commands on a SECOND Altium build+before promoting; one machine proved nothing about 26.9.1.+ ## The brace rule for JSON-building code Result strings build JSON with `'{' + ... + '}'`; that's fine in a STRING LITERAL. It is only@@ -120,7 +133,7 @@ locked Windows session (DirectX) — check `desktop_get_foreground` first. Decision (John): the bridge PROCESS moves to a Rust exe that (1) calls ab's verbs for everything desktop-shaped (no Win32 in the bridge), (2) absorbs the DelphiScript agent's shaping logic so the agent shrinks to generic read/apply primitives, (3) ships no MCP server (external agents use-`adom-bridge-cli`). Vertical slice VERIFIED live on AdomLapper: `/health` (Altium detected through+`adom-bridge-cli`). Vertical slice VERIFIED live on a Windows test machine: `/health` (Altium detected through `desktop_find_window`, title parsed incl. the ADDevelop shape), `/verbs`, `/command` for status, eval, run_command, open_document and the 11 Phase-1 introspection verbs — speaking the existing agent's file-IPC protocol unchanged. Build: cross-compile from the container (`rustup` + target
install.shpage.json+1−1@@ -140,4 +140,4 @@ "skills/altium-wiki-import/SKILL.md", "uninstall.sh" ]-}+}
scripts/release.py+28@@ -108,6 +108,32 @@ def lint_versions(ver): raise SystemExit(f"bridge.json insidersManifestUrl must be {INSIDERS_URL} (#9)") +# Identifiers MEASURED to halt Altium's DelphiScript engine (an unknown identifier is a RUNTIME+# error: an 'Error' box, then the script parks on a breakpoint and Altium will not even close).+# dev-skills/altium-bridge-dev lists how each was found. Add to this list whenever one is found;+# a build that contains one is refused, because the only other way to find it is a user's Altium.+ENGINE_HALTING = [+ r"\.ImplementationCount\b", r"\.Implementation\[", r"\bBottomXSize\b", r"\bMidXSize\b",+ r"\beProcessFreePrimitives\b", # (IPCB_Board.GroupIterator_Create also halts, but the same call is+ # correct on a component/footprint and a regex cannot see the receiver: review those by hand)+ r"\bRunProcessWithParameters\b", r"\.SuperClass\s*:=", r"\bCreateOleObject\b",+]+++def lint_agent_script():+ """Refuse an agent template that uses an identifier known to halt the engine."""+ src = (SRC / "agent" / "AdomAgent.pas.tmpl").read_text(encoding="utf-8", errors="replace")+ code = re.sub(r"\{[^}]*\}", "", src) # brace comments may NAME the identifier+ code = re.sub(r"//[^\n]*", "", code)+ code = re.sub(r"'[^'\n]*'", "''", code) # and so may string literals+ hits = []+ for pat in ENGINE_HALTING:+ for m in re.finditer(pat, code):+ hits.append(f"{pat} -> ...{code[max(0, m.start() - 40):m.end() + 20].strip()}...")+ if hits:+ raise SystemExit("agent template uses engine-halting identifiers (see ENGINE_HALTING):\n " + "\n ".join(hits))++ def build_exe(): subprocess.run(["cargo", "test", "--quiet"], cwd=RS, check=True) subprocess.run(["cargo", "build", "--release", "--target", TARGET], cwd=RS, check=True)@@ -167,6 +193,7 @@ def two_curl_split_check(): def ship_insiders(log_line): ver = json.loads((SRC / "bridge.json").read_text())["version"] lint_versions(ver)+ lint_agent_script() n = lint_verbs() print(f"verb lint ok: {n} verbs declared and dispatchable") build_exe()@@ -298,6 +325,7 @@ def main(): else: ver = json.loads((SRC / "bridge.json").read_text())["version"] lint_versions(ver)+ lint_agent_script() n = lint_verbs() print(f"verb lint ok: {n} verbs") build_exe()
src/agent/AdomAgent.pas.tmpl+16−4@@ -1638,12 +1638,18 @@ Begin Models := ''; If Param(Lines, 'models') = '1' Then Begin- For I := 0 To Comp.ImplementationCount - 1 Do+ { ISch_Component has no ImplementationCount/Implementation[] in DelphiScript on every AD+ build (26.9.1 halts the script there); the eImplementation child iterator works on all. }+ Iter := Comp.SchIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eImplementation));+ Impl := Iter.FirstSchObject;+ While Impl <> Nil Do Begin- Impl := Comp.Implementation[I]; If Models <> '' Then Models := Models + ','; Models := Models + '"' + Impl.ModelType + ':' + Impl.ModelName + '"';+ Impl := Iter.NextSchObject; End;+ Comp.SchIterator_Destroy(Iter); End; Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' + JRaw('pins', IntToStr(NPin)) + ',' + JRaw('rectangles', IntToStr(NRect)) + ',' + JRaw('lines', IntToStr(NLine)) + ',' +@@ -1691,9 +1697,15 @@ Begin Exit; End; Removed := 0;- While Comp.ImplementationCount > 0 Do+ { Same rule as CmdSymbolPrims: walk implementations with the eImplementation iterator. One+ iterator per removal, because removing under a live iterator invalidates it. }+ While Removed < 64 Do Begin- Impl := Comp.Implementation[0];+ Iter := Comp.SchIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eImplementation));+ Impl := Iter.FirstSchObject;+ Comp.SchIterator_Destroy(Iter);+ If Impl = Nil Then Break; Comp.RemoveSchImplementation(Impl); Removed := Removed + 1; End;
src/caller_identity.py+1−1@@ -32,7 +32,7 @@ def set_from_request(headers, args) -> dict: * INBOUND relay (Bridge -> bridge): the CLI's --ai-thread flag and any explicit `caller` in args arrive as `args["caller"] = {"aiThread", "containerName", "source"}`. This is what actually shows up in practice (verified on- AdomLapper 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers.+ ab 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers. * Header form (X-Adom-Caller-*): the shape Bridge uses on its OWN direct-API callbacks and what the SDK examples forward. Read it as a fallback so we also work if a future Bridge stamps the relay path with headers.
src/handlers/png_stdlib.py+1−1@@ -3,7 +3,7 @@ Why: ab's managed Python runtime (adom-runtimes/python-3.12.13, ab 2.0.44+) is stdlib-only — no PIL. On a fresh user box every screenshot fell back to .bmp, which kicad_state refuses ("capture failed (non-png fallback?)"), so a brand-new-install had NO working captures (found on ConfRoomROG, 2026-08-15). The bridge's+install had NO working captures (found on a second test machine, 2026-08-15). The bridge's own dependency policy is stdlib-only; the screenshot path now honors it. Input is what GetDIBits hands us: 32bpp BGRA, bottom-up. Output is an 8-bit RGB
src/handlers/progress.py+1−1@@ -40,7 +40,7 @@ _lock = threading.Lock() # Shipped defaults, used ONLY to seed a machine with no history of its own. # Every one of these is a measured figure from this fleet, not a guess. _DEFAULT_SECONDS = {- "cold_start": 60.0, # X2.exe from nothing (AD26, measured once on AdomLapper — refine)+ "cold_start": 60.0, # X2.exe from nothing (AD26, measured once — refine) "warm_start": 1.0, # Altium already running "agent_start": 8.0, # bootstrapping the resident DelphiScript agent "show_symbol": 12.0,
src/handlers/uia.py+1−1@@ -1,6 +1,6 @@ """UI Automation helpers — the QUIET way to drive Altium. -Measured 2026-09-01 on AD26.9.1 (John's box), after John asked "you keep turning on the cursor,+Measured 2026-09-01 on AD26.9.1 (test machine), after John asked "you keep turning on the cursor, but you could maybe use UIA to do those clicks": Altium exposes a real UIA tree, and far more of it than this bridge was using:
src/server.py+2−2@@ -1110,7 +1110,7 @@ def h_a365_configure(args): if args.get(k): cfg[k] = args[k] if not cfg.get("workspace"):- return {"success": False, "error": "workspace is required, e.g. 'john-john-3.365.altium.com'"}+ return {"success": False, "error": "workspace is required, e.g. 'acme.365.altium.com'"} _a365_save(cfg) return {"success": True, "workspace": cfg["workspace"], "hasToken": bool(cfg.get("token")), "_hint": "altium_a365_status to verify; altium_a365_query {query} to run GraphQL."}@@ -1194,7 +1194,7 @@ def h_a365_oauth_begin(args): workspace = args.get("workspace") or cfg.get("workspace") if not workspace: return {"success": False, "errorCode": "a365_workspace_required",- "error": "workspace is required, e.g. john-john-3.365.altium.com"}+ "error": "workspace is required, e.g. acme.365.altium.com"} with _a365_oauth_lock: if _a365_oauth.get("status") == "waiting": return {"success": False, "errorCode": "a365_oauth_in_progress",
uninstall.sh