← Commit history

v0.1.35: standing dialog reaper - nuisance modals never reach the user

John Lauer ·27247e8f11 ·1mo ago ·parent e10ffe0
8 files changed +125−8
CHANGELOG.md+6
@@ -1,5 +1,11 @@ # Changelog +## 0.1.35 — 2026-09-01+- **A standing dialog reaper, running from bridge start.** John: *"and always pre-empt these from ever showing up."* Clearing a dialog after a verb notices it is not pre-emption — the user has already seen it. The reaper polls every 1.5 s and presses the catalog's SAFE action the instant a KNOWN nuisance appears, whether or not any verb is in flight. That is the only thing that can catch Altium's STARTUP warnings (`Could not start Vault Explorer`, `File type not recognised`), which appear with nothing running.+- **Hard rules, so it can never answer for the user:** only dialogs whose catalog entry declares a safe action; never `unknown`; never a save prompt (those name documents that may belong to another AI thread and are surfaced for a human decision). Every press is recorded and readable via the new `altium_dialog_log`.+- **Catalog additions with safe actions:** `Could not start Vault Explorer` (Altium's own extension failing to load — NOT the bridge: the extension is correctly registered, its payload is complete at 115 files including VaultExplorer.dll, and Altium's private .NET 8.0.15 runtime loads fine), `File type not recognised` (at startup it is Altium restoring a document it can no longer open).+- 110 verbs.+ ## 0.1.34 — 2026-09-01 - **The force-close guard now fires on INTENT, not on a blocked dialog.** 0.1.33 put it inside the blocked-by-dialog branch, so `altium_close {force:true}` sailed straight through whenever nothing happened to block — exactly the path that strands the seat. It is now checked at the top of the verb: `force:true` on a licensed instance is refused with `would_strand_licence_seat` unless `acceptSeatLoss:true`. - **`altium_instances` applies the Develop licence rule too.** 0.1.33 fixed only `licence_state()`, so the instance list still reported Altium Designer Develop as unlicensed while its agent was running and importing.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.34",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.34/adom-bridge-altium-v0.1.34.zip",-  "sha256": "3dc30ed5d47d1a9dd95dda049ab0e3aa3884c11a938d9f79068a56e57ec6e613",-  "size": 132603,+  "version": "0.1.35",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.35/adom-bridge-altium-v0.1.35.zip",+  "sha256": "4f050195f6dac158b7ab893e6b9da02d09bc40a0e874f72255a96bc73179fcb8",+  "size": 134461,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.35.zipadded
⋯ 1 unchanged line ⋯
bridge.json+2−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.34",+  "version": "0.1.35",   "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",@@ -59,6 +59,7 @@     "altium_demo",     "altium_describe",     "altium_diagnostics",+    "altium_dialog_log",     "altium_dismiss_dialogs",     "altium_ensure_licensed",     "altium_eval",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.34",+  "version": "0.1.35",   "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.34+0.1.35
src/handlers/dialogs.py+86
@@ -40,6 +40,16 @@ CATALOG = [      "A helper X2 could not hand its -R request to the running instance because that instance is busy (a modal is open)."),     ("do you want to save", "save_prompt", None, "Unsaved changes on close. Decide: Yes saves, No discards. Never default this."),     ("save changes", "save_prompt", None, "Unsaved changes. Decide explicitly."),+    ("could not start vault explorer", "altium_extension_broken", "OK",+     "Altium's own VaultExplorer extension fails to load at startup. NOT caused by the bridge: the "+     "extension is correctly registered and its payload is complete (VaultExplorer.dll + 114 files), "+     "and Altium's private .NET 8.0.15 runtime loads fine. Informational — dismissing it costs "+     "nothing, and the standing reaper does so automatically."),+    ("file type not recognised", "unsupported_document", "OK",+     "Altium was handed a path it does not open. From the bridge that means a bad open call (the "+     "bridge now refuses unsupported extensions up front); at STARTUP it means Altium is restoring a "+     "document from the last session that it can no longer open. Safe to dismiss."),+    ("file type not recognized", "unsupported_document", "OK", "Same as 'not recognised' (US spelling)."),     ("license maximum usage reached", "licence_seat_stranded", "OK",      "The seat is held by an instance that never released it — almost always an Altium that was "      "KILLED rather than closed. The licence page will still show Seats 0/1, which looks free and "@@ -192,3 +202,79 @@ def close(hwnd: int) -> dict:     WM_CLOSE = 0x0010     ctypes.windll.user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)     return {"success": True, "posted": "WM_CLOSE", "hwnd": hwnd}+++# ---- the standing reaper (John, 2026-09-01: "always pre-empt these from ever showing up") ------+#+# Clearing a dialog AFTER a verb notices it is not pre-emption — the user has already seen it. This+# runs continuously from bridge start and presses the catalog's SAFE action the moment one of our+# known nuisances appears, whether or not any verb is running. Altium's own startup warnings+# (VaultExplorer, "File type not recognised") are exactly the case: no verb is in flight when they+# appear, so nothing else would ever catch them.+#+# HARD RULES:+#   * only dialogs whose CATALOG entry declares a safeAction — never `unknown`, never a save prompt+#     (safeAction None), never anything naming a document+#   * every press is recorded, and surfaced by altium_dialog_log / altium_status+#   * one press per hwnd; a dialog that re-fires (License Renewal Failed) is logged once per hwnd so+#     the log stays readable++_REAPED: list = []+_REAPED_HWNDS: set = set()+_REAPER_STOP = None+++def reaped_log(limit: int = 25) -> list:+    return _REAPED[-limit:]+++def _reap_once() -> int:+    n = 0+    for d in scan():+        act = (d.get("safeAction") or d.get("classification", {}).get("safeAction")+               if isinstance(d.get("classification"), dict) else d.get("safeAction"))+        if not act:+            c = classify(d.get("title") or "", d.get("body") or "")+            act, cat = c.get("safeAction"), c.get("category")+        else:+            cat = d.get("category")+        if not act or cat in (None, "unknown", "save_prompt"):+            continue+        hw = d.get("hwnd")+        if hw in _REAPED_HWNDS:+            continue+        try:+            import caller_identity+            r = caller_identity.ad_callback("desktop_ui_click",+                                            {"hwnd": hw, "name": act, "role": "button"},+                                            on_behalf=False, timeout=8) or {}+            ok = bool(r.get("success"))+        except Exception:+            ok = False+        _REAPED_HWNDS.add(hw)+        _REAPED.append({"at": time.time(), "title": d.get("title"), "category": cat,+                        "pressed": act, "ok": ok,+                        "note": "pre-empted by the standing reaper — the user never had to deal with it"})+        del _REAPED[:-200]+        n += 1+    return n+++def start_reaper(poll_sec: float = 1.5):+    """Start the standing reaper. Idempotent."""+    global _REAPER_STOP+    if _REAPER_STOP is not None:+        return False+    import threading+    _REAPER_STOP = threading.Event()++    def _loop():+        while not _REAPER_STOP.is_set():+            try:+                _reap_once()+            except Exception:+                pass+            _REAPER_STOP.wait(poll_sec)++    threading.Thread(target=_loop, daemon=True, name="altium-dialog-reaper").start()+    return True
src/server.py+25−1
@@ -2104,6 +2104,21 @@ def h_read_board(args):             "data": d, "roundtripMs": r.get("roundtripMs")}  ++def h_dialog_log(args):+    """What the standing dialog reaper has pre-empted.++    The reaper runs from bridge start and presses the catalog's SAFE action the instant a KNOWN+    nuisance appears — Altium's VaultExplorer startup warning, "File type not recognised", the+    script-busy notice — so the user never has to deal with them. It never touches a save prompt or+    anything not in the catalog: those are surfaced by altium_check_dialogs for a human decision."""+    return {"success": True, "reaped": dialogs.reaped_log(int(args.get("limit", 25))),+            "running": True,+            "_hint": "Anything NOT listed here that reached the user is either a save prompt (never "+                     "auto-answered, by design) or a dialog missing from the catalog — report it so "+                     "it can be added with a safe action."}++ # ---- licence lifecycle: status, ensure-licensed, trial setup (John 2026-08-31) ------------  def h_license_status(args):@@ -2683,6 +2698,7 @@ _VERB_CATALOG = {     "close": "WM_CLOSE the main window; reports a save prompt instead of answering it; force:true taskkills.",     "activate_document": "Make an ALREADY-OPEN document current by invoking its document-bar tab through UIA — no cursor, no foreground.",     "layers_in_use": "How many layers the board ACTUALLY has: copper layers carrying primitives, densest first (a floor, not the stack).",+    "dialog_log": "What the standing dialog reaper has pre-empted, so nuisance modals never reach the user.",     "read_board": "The raw board read (components, nets, layers, copper, rules, occupancy, outline) in ONE round trip.",     "review_board": "Design review over one board read: placeholder values, single-pin nets, unused mechanical layers, off-grid parts, duplicate designators.",     "read_schematic": "Components + pins of the CURRENT schematic sheet (one round trip).",@@ -2751,7 +2767,8 @@ COMMAND_HANDLERS = {     "window_info": h_window_info, "state": h_state, "check_dialogs": h_check_dialogs,     "dismiss_dialogs": h_dismiss_dialogs, "launch": h_launch, "close": h_close,     "activate_document": h_activate_document, "layers_in_use": h_layers_in_use,-    "instances": h_instances, "review_board": h_review_board, "read_board": h_read_board, "read_schematic": h_read_schematic, "cross_check": h_cross_check,+    "instances": h_instances, "review_board": h_review_board, "read_board": h_read_board,+    "dialog_log": h_dialog_log, "read_schematic": h_read_schematic, "cross_check": h_cross_check,     "agent_start": h_agent_start, "agent_stop": h_agent_stop, "agent_status": h_agent_status,     "agent_recover": h_agent_recover,     "eval": h_eval, "run_command": h_run_command, "run_script": h_run_script,@@ -2924,6 +2941,13 @@ def main():     if "--port" in sys.argv:         port = int(sys.argv[sys.argv.index("--port") + 1])     host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")+    # Start the standing dialog reaper BEFORE serving: Altium's startup warnings (VaultExplorer,+    # "File type not recognised") appear with no verb in flight, so nothing else would catch them.+    try:+        dialogs.start_reaper()+        print("[altium] standing dialog reaper started (known nuisances are pre-empted, never save prompts)")+    except Exception as e:+        print(f"[altium] dialog reaper failed to start: {e}")     srv = ThreadingHTTPServer((host, port), Handler)     print(f"[altium] bridge {BRIDGE_VERSION} listening on {host}:{srv.server_address[1]}")     _install()