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
v0.1.19: pre-empt script modals - cross-workspace residency check + in-flight watcher
7 files changed
+125−9
CHANGELOG.md+6@@ -1,5 +1,11 @@ # Changelog +## 0.1.19 — 2026-09-01+- **Pre-emption, properly.** John: *"you better pre-empt these from ever showing up"*. Two changes so a script modal cannot reach the user:+ 1. **A script lane now refuses when ANY resident loop is alive, in ANY workspace** — not just the current instance's. That gap is exactly how "Another script executing now" got on screen after per-instance workspaces landed: the pre-upgrade loop was still resident under the legacy path, `is_alive()` looked only at `agent/<key>`, saw nothing, and fired a bootstrap into an engine that was already running a script. The refusal names the workspace and heartbeat age so the fix is obvious.+ 2. **A watcher runs FOR THE DURATION of every `-R` lane** and presses OK the instant one of our own modals appears, so it exists for a few hundred milliseconds instead of sitting on screen until someone goes looking. Cleared modals come back as `modalsPreempted` rather than being hidden. The watcher only ever touches modals this bridge CAUSES (script-busy, another-instance, scripting-system); a save prompt or a licence question is the user's and is never pressed.+- Detection was always the weaker half: the engine is single-threaded, so the modal it raises is also what freezes it — which is precisely why watching during the operation works.+ ## 0.1.18 — 2026-09-01 - **Instances.** More than one Altium can run at once — John runs Altium Designer Develop beside Altium Designer so two AI threads can drive both — and the bridge assumed one in every place it mattered. New verb `altium_instances` lists each running Altium with the pid/key to address it; every other verb takes an optional `instance` (a pid, a key like `ADDevelop`, or a word from the edition/title). One Altium running: omit it, nothing changes. Several: verbs REFUSE with `ambiguous_instance` rather than guess, because guessing means acting inside the other thread's Altium. - **Each instance gets its own agent workspace** (`bridges-cache/altium/agent/<key>`), keyed by install folder so it survives a restart. Two resident loops sharing one `request.txt` is the failure that does not announce itself: whichever loop notices first eats the other's request and answers it from the wrong board.
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.18",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.18/adom-bridge-altium-v0.1.18.zip",- "sha256": "c3cdd963c5b5a872ae83885b69971abc212d1e2115a0299cddb4bd8144a501ce",- "size": 110023,+ "version": "0.1.19",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.19/adom-bridge-altium-v0.1.19.zip",+ "sha256": "21da53c635db6983af65ef53b0102e76edbd3c9090af348d070262b676ee8b9e",+ "size": 111520, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.19.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.18",+ "version": "0.1.19", "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",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.18",+ "version": "0.1.19", "type": "app", "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.18+0.1.19
src/handlers/agent.py+112−2@@ -302,9 +302,94 @@ def _reap_busy_notice(wait_sec: float = 4.0) -> list: return reaped ++# ---- pre-emption: never fire a script lane that can raise a modal (John, 2026-09-01) ---------++def _any_agent_alive(max_age_sec: float = ALIVE_SEC) -> dict | None:+ """Is ANY resident loop alive, in ANY workspace? Returns {workspace, ageSec, gen} or None.++ `is_alive()` only checks the CURRENT instance's workspace. That is exactly how the "Another+ script executing now" modal reached John's screen after per-instance workspaces landed: the+ pre-upgrade loop was still resident under the legacy path, this module was looking at+ `agent/AD26`, saw no heartbeat, and fired a -R bootstrap into an engine already running a+ script. A script lane must clear EVERY workspace, not just its own."""+ root = cache_dir() / "agent"+ candidates = [root / "hb.txt"]+ try:+ candidates += [d / "hb.txt" for d in root.iterdir() if d.is_dir()]+ except Exception:+ pass+ for hb in candidates:+ try:+ age = time.time() - hb.stat().st_mtime+ except Exception:+ continue+ if age <= max_age_sec:+ gen = None+ try:+ for line in hb.read_text(errors="replace").splitlines():+ if line.startswith("gen="):+ gen = line.split("=", 1)[1].strip()+ except Exception:+ pass+ return {"workspace": str(hb.parent), "ageSec": round(age, 2), "gen": gen}+ return None+++# Modals this bridge CAUSES. Only these are auto-cleared while an operation is in flight; anything+# else (a save prompt, a licence question) belongs to the user and must never be pressed.+_OURS = ("another script executing", "another instance of", "is busy at the moment",+ "scripting system is not able")+++def _watch_and_clear(stop_evt, found: list, poll_sec: float = 0.4) -> None:+ """Run WHILE a script lane is in flight: the instant one of OUR modals appears, press OK.++ Detection alone is not pre-emption. The engine is single-threaded, so the modal it raises is+ also what freezes it — watching during the operation means the dialog lives a few hundred+ milliseconds instead of sitting on the user's screen until someone goes looking."""+ from handlers import dialogs as _dl+ while not stop_evt.is_set():+ try:+ for d in _dl.scan():+ blob = f"{d.get('title', '')} {d.get('body', '')}".lower()+ if not any(k in blob for k in _OURS):+ continue+ caller_identity.ad_callback("desktop_ui_click",+ {"hwnd": d.get("hwnd"), "name": "OK", "role": "button"},+ on_behalf=False, timeout=8)+ found.append({"title": d.get("title"), "body": (d.get("body") or "")[:120],+ "clearedInFlight": True})+ except Exception:+ pass+ stop_evt.wait(poll_sec)+++def _guarded(fn):+ """Run `fn()` with the modal watcher live. Returns (result, cleared[])."""+ import threading+ evt = threading.Event()+ cleared: list = []+ th = threading.Thread(target=_watch_and_clear, args=(evt, cleared), daemon=True)+ th.start()+ try:+ return fn(), cleared+ finally:+ evt.set()+ th.join(timeout=2.0)++ def run_script_lane(proc_name: str, prjscr: str | None = None, timeout: float = 30.0) -> dict: """The -R lane: forward RunScript to the running instance. REFUSES while the agent is resident (one script at a time — see module docstring)."""+ other = _any_agent_alive()+ if other:+ return {"success": False, "errorCode": "agent_resident", "resident": other,+ "error": "a resident agent loop is already running in this Altium (workspace "+ f"{other['workspace']}, heartbeat {other['ageSec']}s ago) - a second script "+ "would raise Altium's 'Another script executing now' modal on the user's screen",+ "_hint": "altium_agent_stop, or altium_agent_recover if it is wedged, then retry. A workspace "+ "path that is not this instance's means a loop from before an upgrade: stopping it is safe."} if is_alive(): return {"success": False, "errorCode": "agent_resident", "error": "The Adom agent loop is resident; a second script would raise Altium's "@@ -320,19 +405,32 @@ def run_script_lane(proc_name: str, prjscr: str | None = None, timeout: float = p = _spawn_helper(exe, arg) except Exception as e: return {"success": False, "error": f"could not launch X2 helper: {e}"}+ # PRE-EMPT: watch for OUR OWN modals for as long as the helper is in flight and press OK the+ # instant one appears. The user should never have to find it. Anything not in _OURS (a save+ # prompt, a licence question) is left strictly alone.+ import threading as _th+ _evt = _th.Event()+ _cleared: list = []+ _w = _th.Thread(target=_watch_and_clear, args=(_evt, _cleared), daemon=True)+ _w.start() # The helper exits once the running instance accepted the request (or shows its # own "another instance is busy" dialog and hangs — we cap that). t0 = time.time() while time.time() - t0 < timeout: if p.poll() is not None:- return {"success": True, "helperExitCode": p.returncode, "helperSec": round(time.time() - t0, 2), "arg": arg}+ _evt.set(); _w.join(timeout=2.0)+ r = {"success": True, "helperExitCode": p.returncode, "helperSec": round(time.time() - t0, 2), "arg": arg}+ if _cleared:+ r["modalsPreempted"] = _cleared+ return r time.sleep(0.2) try: p.kill() except Exception: pass+ _evt.set(); _w.join(timeout=2.0) reaped = _reap_busy_notice()- return {"success": False, "errorCode": "instance_busy",+ return {"success": False, "errorCode": "instance_busy", "modalsPreempted": _cleared, "error": "The X2 helper never exited: the running instance refused the request as busy " "(a modal is open, or a script is already running).", "busyNoticeReaped": reaped,@@ -341,6 +439,14 @@ def run_script_lane(proc_name: str, prjscr: str | None = None, timeout: float = def run_script_text_lane(text: str, timeout: float = 30.0) -> dict: """-R RunScriptText: STATEMENTS ONLY (Begin ... End;). Refused while the agent is resident."""+ other = _any_agent_alive()+ if other:+ return {"success": False, "errorCode": "agent_resident", "resident": other,+ "error": "a resident agent loop is already running in this Altium (workspace "+ f"{other['workspace']}, heartbeat {other['ageSec']}s ago) - a second script "+ "would raise Altium's 'Another script executing now' modal on the user's screen",+ "_hint": "altium_agent_stop, or altium_agent_recover if it is wedged, then retry. A workspace "+ "path that is not this instance's means a loop from before an upgrade: stopping it is safe."} if is_alive(): return {"success": False, "errorCode": "agent_resident", "_hint": "Use altium_eval (goes through the agent) while the agent is up."}@@ -398,6 +504,10 @@ def start(wait_sec: float = 45.0) -> dict: r = run_script_lane("StartAgent") if killed: r["strayHelpersKilled"] = killed+ if r.get("modalsPreempted"):+ # the bridge caused these and the bridge cleared them, in flight — say so rather than+ # letting them look like something the user has to deal with+ r.setdefault("_note", f"{len(r['modalsPreempted'])} modal(s) pre-empted during the bootstrap") if not r.get("success") and r.get("errorCode") == "instance_busy" and not _no_recover: rec = recover("helper reported busy on start") r = run_script_lane("StartAgent")