← Commit history

v0.1.31: unwrap ab reply envelopes (every callback read was silently None)

John Lauer ·e0de1312bc ·1mo ago ·parent 551b940
7 files changed +37−8
CHANGELOG.md+4
@@ -1,5 +1,9 @@ # Changelog +## 0.1.31 — 2026-09-01+- **Systemic fix: `ad_callback` now unwraps ab's reply envelope.** ab's direct API answers `{success, status, error, output}` with the verb's REAL result JSON-encoded as a STRING in `output`. The Python bridge never unwrapped it, so EVERY field read back from an ab callback — `hwnd`, `windows`, `processes`, `controls` — was silently `None`. Calls that only ACTED (click, kill) worked, which is why this hid for so long; anything that tried to READ a result quietly saw nothing.+- Found because 0.1.30's background-capture helper decided a minimized window was not minimized: it was reading `rect` out of an envelope it could not see into. The Rust bridge has always unwrapped (`ab.rs::unwrap_output`) — this brings Python in line.+ ## 0.1.30 — 2026-09-01 - **Proof captures work with Altium minimized, without ever moving the window.** Windows does not render a minimized window, so a capture of one returned a ~237x39 title-bar sliver — which the bridge handed back as "proof" of a successful import, and which the wiki front-end would have displayed as evidence. Now every capture first makes the window CAPTURABLE: restore it if minimized, push it to the BOTTOM of the z-order, and hand the user's foreground straight back. Measured end to end at ~0.6 s; the capture then reads the occluded window at full size. - **The rejected alternative, recorded so nobody rebuilds it:** parking the window off-screen. John, 2026-09-01 — *"i hate the idea of parking an altium window off screen cuz if the user wants to manually foreground, you fuck them over."* Never move a window the user might reach for.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.30",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.30/adom-bridge-altium-v0.1.30.zip",-  "sha256": "5c4909ca9c72037891762c97941b2ef57aaf9703684444176350cfe884dbdea7",-  "size": 129499,+  "version": "0.1.31",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.31/adom-bridge-altium-v0.1.31.zip",+  "sha256": "ff4f7b392d17e964d9cb1501edc45f85240a95928bf02f60209d7f749cda254c",+  "size": 129960,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.31.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.30",+  "version": "0.1.31",   "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.30",+  "version": "0.1.31",   "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.30+0.1.31
src/caller_identity.py+26−1
@@ -97,6 +97,31 @@ def self_caller() -> dict:     return {"aiThread": "altium bridge (self)", "containerName": "local"}  ++def _unwrap(v: dict) -> dict:+    """ab's direct API answers {success, status, error, output} with the verb's REAL result+    JSON-encoded as a STRING in `output`. Without unwrapping it, every field a caller reads back+    (`hwnd`, `windows`, `processes`, `controls`) is silently None — which is exactly how the+    background-capture helper decided a minimized window was fine, and why anything in this bridge+    that tried to READ an ab result quietly saw nothing (found 2026-09-01; the Rust bridge has+    always unwrapped). Outer status fields are kept when the inner object does not carry them."""+    if not isinstance(v, dict):+        return v+    out = v.get("output")+    if isinstance(out, str) and out.strip().startswith("{"):+        try:+            import json as _j+            inner = _j.loads(out)+            if isinstance(inner, dict):+                for k in ("success", "status", "error", "errorCode", "durationMs"):+                    if k in v and k not in inner:+                        inner[k] = v[k]+                return inner+        except Exception:+            pass+    return v++ def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout: int = 30):     """Call a Bridge verb back over the loopback direct API, forwarding the caller. @@ -127,6 +152,6 @@ def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout         req = urllib.request.Request(f"{base}/command", data=_json.dumps(body).encode(),                                      headers=headers, method="POST")         with urllib.request.urlopen(req, timeout=timeout) as r:-            return _json.loads(r.read().decode() or "{}")+            return _unwrap(_json.loads(r.read().decode() or "{}"))     except Exception as e:         return {"success": False, "error": f"Bridge callback failed: {e}"}