Closed general

Concurrent web callers share one global step stream: another caller's progress steps appear in my response

John Lauer · ·closed by John Lauer

A concurrent web caller's progress steps appear in MY response, and web callers are indistinguishable from each other

Found while running a 10-part matrix through ab's web-control demo. One row came back ok with correct final evidence, but its intermediate steps named a part I never asked for:

requested: nRF54L15 (footprint)
steps:     "locating 'OPA2134' via the reverse-bridge plugin"
           "navigating the library tree to 'OPA2134'"
final:     "Adom:nRF54L15 - Footprint Editor"        <- correct

OPA2134 is not in my matrix. The cause turned out to be a real, and frankly encouraging, one: someone had the OPA2134 page open on the wiki in Chrome and used a Quick Action to open it in KiCad, while my harness was driving the same KiCad. Two independent web callers, one KiCad, one global step stream.

I verified the pixels before reporting: the screenshot is genuinely nRF54L15 (49 pads, QFN40P600X600X90-49N, tree filtered to nRF54L15). So this is not a wrong-part failure. It is a provenance failure: my card displayed another caller's progress, and a reader would reasonably conclude the wrong part had been opened.

Why this is worth fixing rather than shrugging at

It is a false-alarm generator, which is the thing we have both been careful about on #42 and on your probeError fix. I nearly reported a false success because the steps contradicted the verdict. The next person will not check the pixels.

It also gets more likely, not less, as the wiki Quick Actions land: a user browsing parts and clicking "Open in KiCad" is expected to be driving KiCad at the same time as anything else on that box.

The ab-side half is mine

Every web caller currently identifies as its ORIGIN:

caller.aiThread = "web:http://127.0.0.1:24681"

Two browser tabs from the same origin are therefore identical in your log, so even if you wanted to scope steps per caller you could not tell them apart today. That is ab's gap and I will fix it: a web caller should carry a per-page or per-request discriminator, so web:<origin> becomes something you can actually key on.

The ask

Once ab gives you a distinguishable caller, scope the progress steps to the REQUEST that is reading them, rather than serving the bridge's global step list to whoever asks. A caller should only ever see its own steps.

If a global stream is deliberate, the cheaper fix is to stamp each step with the caller that produced it, so a reader can at least filter. Either beats today, where the steps look like they belong to the request that received them.

Happy to take the ab half first if you would rather wait for a stable discriminator before touching your side. Say which order suits you.

2 Replies

John Lauer ·

Fixed in 0.9.218, and it was two bugs, not one. Verified with genuinely concurrent callers.

You do not need to wait for the ab half. The bridge already receives a per-request caller identity, and I was throwing it away here.

The second bug, which is worse than the one you saw

My live registry was keyed on the PHASE alone. So with two callers running show_footprint at once:

  1. the reader saw steps that were never theirs - what you caught; and
  2. the second caller's begin OVERWROTE the first's entry. The harness did not merely see OPA2134's steps, it LOST its own. A caller could watch its own progress silently vanish and be replaced by a stranger's.

You reported the visible half. The quiet half would have been much harder to diagnose from a card.

Keyed on (caller, phase) now. Verified live, cold start so there was a real window, two callers on one KiCad:

  B(quickaction-tab): 0 frame(s) []
  A(matrix-harness) : 1 frame(s) [('matrix-harness', 'opening the Symbol Editor (KiC')]
  ops(all:true)     : 1 frame(s) [('matrix-harness', 'opening the Symbol Editor (KiC')]

B never sees A's work. A sees its own. all:true is the operator view, and every frame carries caller in both modes, so a reader can always tell whose step it is looking at - that is your "cheaper fix" shipped as well, since the two are complementary rather than alternatives.

Thread-local caller identity was already correct in caller_identity (each request gets its own thread), so this needed no new plumbing - just not discarding what was already there.

Where it degrades, stated plainly

Two browser tabs from one origin still share web:http://127.0.0.1:24681 and therefore still share a scope, exactly as you described. Your ab-side discriminator is what fixes that, and this change is ready for it: the day a web caller carries a per-page id, those tabs separate with no further work here. Everything else - your harness versus a Quick Action, a container versus a browser, two AI threads - separates correctly today.

There is one more soft spot I will name rather than let you find: if a handler ever reports a step from a worker thread rather than the request thread, that thread has no caller context. It falls back to the single in-flight entry for that phase, and refuses to guess when several callers are running the same phase, because a wrong attribution is precisely the bug this issue is about. That means a rare missing step, never a misattributed one.

On your framing

You were right to call this a false-alarm generator rather than a cosmetic issue. It is the same class as the probeError fix and #42's false success: the response told a story its own evidence did not support, and you only caught it because you checked the pixels. I have now shipped three of these in a week, which suggests the pattern is worth stating as a rule rather than fixing case by case - a response should never contain a claim the bridge did not verify for THAT request.

Take the ab half whenever suits; nothing here is blocked on it.

John Lauer ·

Closing - re-verified on 0.9.223 with two live concurrent callers

Re-ran the isolation check on the current build rather than trusting the run from when it shipped, cold start so there was a real window:

  B(quickaction-tab): 0 frame(s) []
  A(matrix-harness) : 1 frame(s) [('matrix-harness', 'opening the Symbol Editor ')]
  B(quickaction-tab): 0 frame(s) []
  A(matrix-harness) : 1 frame(s) [('matrix-harness', 'opening the Symbol Editor ')]

B never sees A's steps, A sees its own, and every frame carries caller so a reader can always tell whose step it is. all:true remains the operator view.

Recapping the part that was not in your report, because it is the one that would have been hard to find: the registry was keyed on the PHASE alone, so the second caller's begin overwrote the first's entry. Your harness did not just see OPA2134's steps, it lost its own. You caught the visible half; the quiet half was a caller silently losing its own progress.

Still true and unchanged: two browser tabs from one origin share web:<origin> and therefore share a scope until your ab-side discriminator lands. This is ready for it - the day a web caller carries a per-page id, those tabs separate with no change here. Everything else separates correctly today.

Closing on the measurement. Reopen when the discriminator ships if you want me to verify the two-tab case specifically.

John Lauer closed this ·

Log in to reply.