← Commit history

Update 5 file(s)

John Lauer ·f613ad928c ·1mo ago ·parent c87f60e
5 files changed +74−21
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.217\ No newline at end of file+0.9.218\ No newline at end of file
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.217",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.217/adom-bridge-kicad-v0.9.217.zip",-  "sha256": "3430f247cb8244f9cd8897bc993b07c78225ee3511766a360e910d94f72be932",-  "size": 484313,+  "version": "0.9.218",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.218/adom-bridge-kicad-v0.9.218.zip",+  "sha256": "b563d16f60a361cc777db1353b96df208297b7b0541fabd92a8d5569e0dc5375",+  "size": 485454,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-20T18:45:00Z",+  "released_at": "2026-08-20T19:10:00Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.217",+  "version": "0.9.218",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/progress.py+57−12
@@ -206,14 +206,41 @@ _live: dict = {} _live_seq = 0  -def begin(phase: str, start_kind: str = "warm", step_label: str = "starting") -> None:-    """Mark a phase as in flight. Safe to call for phases nobody watches."""+def _caller() -> str:+    """Who is asking. Used to keep concurrent callers out of each other's steps."""+    try:+        import caller_identity+        return str(caller_identity.thread_name() or "unknown")+    except Exception:+        return "unknown"+++def begin(phase: str, start_kind: str = "warm", step_label: str = "starting",+          caller: str = None) -> None:+    """Mark a phase as in flight, SCOPED TO THE CALLER.++    Wiki issue #46 (John): a second caller's steps appeared inside a first+    caller's response - "locating 'OPA2134'" in a response about nRF54L15,+    because someone used a wiki Quick Action on one KiCad while a matrix run+    drove the same box. Two independent web callers, one global step stream.++    Two distinct bugs, both from keying this registry on the phase alone:+      * a reader saw steps that were never theirs, which reads as a wrong-part+        failure to anyone who does not go and check the pixels;+      * worse and quieter, the second caller OVERWROTE the first's entry, so a+        caller could lose its own progress to someone else's call.++    Keying on (caller, phase) fixes both. It degrades honestly rather than+    perfectly: two browser tabs from one origin still share an identity until ab+    ships a per-page discriminator, but every other pair of callers separates+    correctly today, and every frame now says who produced it."""     global _live_seq+    who = caller or _caller()     with _lock:         _live_seq += 1-        _live[phase] = {"startedAt": time.time(), "startKind": start_kind,-                        "stepLabel": step_label, "percent": None,-                        "blockedBy": None, "seq": _live_seq}+        _live[(who, phase)] = {"startedAt": time.time(), "startKind": start_kind,+                               "stepLabel": step_label, "percent": None,+                               "blockedBy": None, "seq": _live_seq, "caller": who}   def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> None:@@ -222,10 +249,18 @@ def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> N     This is what turns one opaque 24-second bar into "launching KiCad" ->     "indexing symbol libraries" -> "navigating to IN-S42ATR", which is the     sub-phase breakdown Colby asked for on wiki issue #35."""+    who = _caller()     with _lock:-        cur = _live.get(phase)-        if not cur:-            return+        cur = _live.get((who, phase))+        if cur is None:+            # A handler thread may not carry the caller context; fall back to the+            # only in-flight entry for this phase, but never guess when several+            # callers are running the same phase - a wrong attribution is the+            # bug this issue is about.+            matches = [k for k in _live if k[1] == phase]+            if len(matches) != 1:+                return+            cur = _live[matches[0]]         cur["stepLabel"] = step_label         cur["blockedBy"] = blocked_by         if percent is not None:@@ -233,21 +268,30 @@ def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> N             cur["percent"] = max(percent, cur.get("percent") or 0)  -def end(phase: str) -> None:+def end(phase: str, caller: str = None) -> None:+    who = caller or _caller()     with _lock:-        _live.pop(phase, None)+        if _live.pop((who, phase), None) is None:+            for k in [k for k in _live if k[1] == phase and k[0] == who]:+                _live.pop(k, None)  -def live() -> list:+def live(mine_only: bool = True, asker: str = None) -> list:     """Every phase currently in flight, as full progress blocks. +    By default a caller sees ONLY ITS OWN phases (#46). Pass mine_only=False for+    an operator view of everything in flight; each frame carries `caller`.+     Percent/ETA are recomputed at READ time from this machine's own history, so     a poller gets a bar that keeps moving even if the verb has not reported a     new sub-step since its last one."""+    asker = asker or _caller()     out = []     with _lock:         snapshot = list(_live.items())-    for phase, cur in snapshot:+    for (who, phase), cur in snapshot:+        if mine_only and who != asker:+            continue         try:             # A step's explicit percent is a FLOOR, not a freeze. The first live             # test pinned the bar at 10% for 47s while the ETA counted to zero,@@ -264,6 +308,7 @@ def live() -> list:                       percent=_pct_now, start_kind=cur.get("startKind", "warm"),                       blocked_by=cur.get("blockedBy"))             b["seq"] = cur.get("seq")+            b["caller"] = who             out.append(b)         except Exception:             pass
server.py+10−2
@@ -610,7 +610,10 @@ def _handle_progress(kicad_info: dict, args: dict) -> dict:     is still happening. Read-only, cheap, and safe to poll every second."""     try:         from handlers import progress as _pg-        frames = _pg.live()+        # #46: a caller sees ONLY its own steps by default. `all:true` is the+        # operator view, and every frame carries `caller` either way so a reader+        # can always tell whose step it is looking at.+        frames = _pg.live(mine_only=not bool((args or {}).get("all")))     except Exception as e:         return {"success": False, "error": f"progress unavailable: {e}"[:140]} @@ -669,7 +672,12 @@ def _handle_progress(kicad_info: dict, args: dict) -> dict:         "busy": bool(frames),         "active": frames,         "history": _pg.history(),-        "_hint": ("Poll this while a long verb runs (1s is fine). `active` holds one "+        "callerScoped": True,+        "_hint": ("Scoped to YOU: `active` holds only the phases this caller started "+                  "(wiki #46 - another caller's steps used to appear in your response). "+                  "Pass all:true for every in-flight phase on the box; each frame carries "+                  "`caller` regardless. "+                  "Poll this while a long verb runs (1s is fine). `active` holds one "                   "progress block per in-flight phase: stepLabel is the human line for "                   "a status bar, percent/etaSec drive the bar, and confidence says "                   "whether the estimate is measured on THIS machine or still a default. "