Skip to content

fix(runtime-host): project queue changes that land while no root Turn is live - #5521

Open
ggbdpq wants to merge 2 commits into
apache:mainfrom
ggbdpq:fix/queue-drain-projection
Open

ggbdpq wants to merge 2 commits into
apache:mainfrom
ggbdpq:fix/queue-drain-projection

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5520

A queue that drains while no root Turn is live was never projected: the session projector gated queue_update on root && queueChanged(...), and seedActive returned nothing for a rootless snapshot. The Desktop's queue mirror is driven only by queue_update, so the renderer kept a phantom queued card whose 删除/retract failed with not_found forever — and switching sessions back and forth did not clear it, because the reseed hit the same gate. The Host broadcast the projection frames all along (#publishCanonical fires on any canonical change); the drop was purely in the projector adapter.

The fix stays at that shared root:

  • session-projector.ts now pushes queue_update whenever the queue changed, root or not, attributed to root?.turnId ?? previousRoot?.turnId ?? ''. The steering synthesis stays root-gated (it attributes messages to the live Turn).
  • seedActive seeds the queue mirror for a rootless session too — the empty queue is exactly the signal that lets an observer clear a stale card on (re)subscribe.

No consumer change is needed: the Desktop already deletes messageQueueBySession[sessionId] when a queue_update carries empty arrays, and the TUI mirrors the arrays verbatim.

Verification

Check Result
New regression tests (drain without root / seed without root) red before the fix, green after
session-projector.test.ts (full file, rebuilt dist) 29/29
message-coordinator.test.ts 76/76
execution-host-queue.test.ts 16/16 — one "did not become ready" timeout on the first full-file run; passed single and on full-file rerun (spawn-heavy flake, not code)
session-subscription-client.test.ts 13 failures reproduce identically on a pristine upstream/main worktree (Windows transport read side ended) — pre-existing, untouched by this PR
biome format/lint on touched files clean
npm run check:asf-headers pass

Not run locally: the full runtime-host matrix (~40 min; CI covers it) and the issue's Desktop manual repro (reported on macOS; the fixed root cause and its regression tests are platform-independent).

AI use

Analysis, patch, and tests done with GLM-5.3-Flash (ZCode) under human review.

Checklist

  • The fix lives at the shared root (projector), not in an individual caller
  • Regression tests fail before the fix and pass after
  • No protocol/event contract change — only the emission gating of the existing queue_update
  • ASF headers intact; biome clean on touched files

… is live

The session projector pushed `queue_update` only when a root Turn existed
(`root && queueChanged(...)`), and `seedActive` returned no events at all
for a snapshot without a root Turn. A queue that drained inside that
window - the queued follow-up consumed as the Turn went terminal, or a
drain between subscriptions - was therefore never projected: the renderer
kept the queued card, and every retract of it failed with `not_found`
(`queue.entry.retract`) while the failure path deliberately left the
projection unchanged. Switching sessions back and forth did not clear it
either, because the reseed hit the same gate.

Project the authoritative queue whenever it changes, independent of a
live root Turn (attributing the event to the previous root when the drain
lands in the same frame the Turn disappears), and seed the queue mirror
on subscribe even for an idle session - the empty queue is exactly what
tells an observer to drop a stale card. The steering synthesis stays
root-gated: it attributes messages to the live Turn.

Fixes apache#5520

Generated-by: GLM-5.3-Flash (ZCode)
@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 20, 2026
The Desktop session observer pins an empty seed event list for a snapshot
without a root Turn ("projects root lifecycle without fabricating content
events"), and the projector snapshots it seeds from can carry no queue at
all - the unconditional rootless seed both fabricated a queue_update the
contract forbids and crashed on the missing field.

The connected path is where the apache#5520 phantom actually lives: the drain
lands while the client is subscribed (session events route per session,
not per visible view), so projecting it there clears the card. A client
that never observed the session has no stale card for a seed to clear,
which is why seeding stays silent.

Fixes the regression introduced in 7b4a974.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

The CI red was a real regression in my first push, thank you for catching it — diagnosed and fixed at 5dac0705a.

What failed (3 tests, all Desktop-side consumers):

  • runtime-host-desktop-candidate × 2 — TypeError: Cannot read properties of undefined (reading 'hostEpoch'): the observer constructs projectors from snapshots that can carry no queue at all, and my unconditional rootless seed fed that straight into projectQueueUpdate.
  • runtime-host-session-observer "projects root lifecycle without fabricating content events" — this test pins the contract that a rootless seed emits []; my seed hunk fabricated a queue_update against it.

The fix: I reverted the seed hunk entirely (seedActive is back to returning [] for rootless snapshots) and kept only the accept-path change — which is where the #5520 phantom actually lives. The drain lands while the client is subscribed (session events route per session, not per visible view), so projecting it there clears the card whether or not that session is on screen; a client that never observed the session has no stale card for a seed to clear, which is exactly why the observer contract is what it is. The surviving regression test now documents that reasoning.

Verified locally after the fix: session-projector 28/28, runtime-host-session-observer 55/55, runtime-host-desktop-candidate 25/25 — the last two are the suites CI caught, which my local matrix had missed because the projector's consumers live in the Desktop workspace, not runtime-host. That gap is on me; the affected-workspace set is now part of my run book for this area.

@github-actions github-actions Bot added effort/S Under 100 readable lines and removed effort/M Under 500 readable lines labels Sep 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/S Under 100 readable lines

Projects

None yet

1 participant