fix(runtime-host): project queue changes that land while no root Turn is live - #5521
Conversation
… 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)
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)
|
The CI red was a real regression in my first push, thank you for catching it — diagnosed and fixed at What failed (3 tests, all Desktop-side consumers):
The fix: I reverted the seed hunk entirely ( Verified locally after the fix: |
…jection # Conflicts: # packages/runtime-host/src/adapter/session-projector.ts
jackwener
left a comment
There was a problem hiding this comment.
Finding
[P1][Normal user path] Reopening an idle Session still cannot clear a queue entry that drained while the Session was inactive.
The accept-path change correctly emits an empty queue_update when the drain arrives while the client remains subscribed. However, Desktop subscribes only to the active Session and unsubscribes when activeId changes (apps/desktop/src/renderer/app-shell-effects.ts). If the queued follow-up is consumed while that Session is inactive, there is no projector instance present to observe the drain.
When the user returns, the new projector is created from the current rootless snapshot, but RuntimeHostSessionProjector.seedActive() still returns [] as soon as rootTurn is null (packages/runtime-host/src/adapter/session-projector.ts). The renderer retains messageQueueBySession[sessionId] across navigation, and it removes that stale entry only after receiving an empty queue_update. Therefore the card remains visible and retracting it still returns not_found, which is one of the issue's documented normal reproduction paths. A direct probe against this exact head also returned an empty seed for a rootless snapshot with an authoritative empty queue.
Please make the rootless observation seed carry the authoritative queue state (or add an equivalent out-of-band queue snapshot) without violating the observer's no-content-event contract, and add a Desktop-level regression that switches away, drains the queue, then switches back. The PR description currently says that seedActive performs this rootless reseed, but that change was reverted from the final code.
Review summary
The continuously subscribed drain path and its focused projector regression are correct; the projector suite passed 27/27. Biome, ASF headers, diff checks, the hosted exact-head test check, and a clean synthetic merge onto current main all passed. Because the off-subscription production path still reproduces the reported phantom card, this exact head a5f8cb028957d8302f870beff409afb44e0d1ff3 is blocked. I did not approve or merge.
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
hqhq1025
left a comment
There was a problem hiding this comment.
Finding
[P1] The normal switch-away / switch-back path still leaves a phantom queued card. The change in packages/runtime-host/src/adapter/session-projector.ts:406-418 emits the authoritative empty queue when a subscriber stays connected through a rootless drain. Desktop instead unsubscribes from the old Session on navigation (apps/desktop/src/renderer/app-shell-effects.ts:400-458). If the queue drains while that Session is inactive, the new projector returns no events for its rootless snapshot (packages/runtime-host/src/adapter/session-projector.ts:161-168), while the renderer retains messageQueueBySession[sessionId] until it receives an empty queue_update (apps/desktop/src/renderer/app-shell-session-events.ts:303-323). The stale card therefore remains visible, and retract can return not_found. Please make resubscription convey the authoritative empty queue and cover switching away, draining, then switching back in a Desktop regression. I independently corroborate the same current-head issue already reported by jackwener, so I am not duplicating an inline comment.
Scope and readiness
This PR changes the projector drain emission and adds a focused continuously subscribed regression (2 files, +44/-2). I reviewed the projector, Desktop subscription lifecycle, queue state reducer, and the new test. I found no other substantiated P0–P3 issue in that scope. The current-head test check passed, and the diff and synthetic merge onto fetched main are clean; the remaining normal navigation path still blocks merging. I did not run the suite locally or exercise a real Desktop session switch. No schema or migration files changed.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
A Desktop that navigates away unsubscribes, so the projector fix that projects a rootless drain never reaches it: the drain lands while no subscriber is connected, and on navigating back the fresh projector's seedActive returned no events for a rootless snapshot. The client's stale queued card survived — retracting it fails with not_found — until an update that will never come. The rootless seed now emits the authoritative queue once, empty or not; the Desktop queue projection keys by Session and retires the card on the empty update. Carries the apache#5520 review finding; covered by a projector seed test and a Desktop queue-projection regression for switch away, drain, switch back. Generated-by: GLM-5.3-Flash (ZCode)
|
Fixed at Covered at both layers: a projector seed test (rootless seed → one empty |
Desktop test fixtures build rootless continuity snapshots without a queue field, and the seed's new authoritative-queue emission read straight into unplacedQueue. Project an empty queue when the snapshot carries none, matching the update path's tolerance; the observer seed expectation now reflects the queue_update the rootless seed carries. Generated-by: GLM-5.3-Flash (ZCode)
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed the current head eba044200976a9e72ca15b76033ea7a369a3cc98. I found no substantiated remaining P0–P3 issue in the changed queue-projection path.
The projector now emits queue_update when the queue changes even without a live root Turn, and seedActive() also sends an authoritative queue for a rootless snapshot (packages/runtime-host/src/adapter/session-projector.ts:161-175,416-429). This addresses the previous resubscription gap: when Desktop navigates away while the queue drains and then returns, the rootless seed reaches its existing empty-queue reducer, which deletes the stale card (apps/desktop/src/renderer/app-shell-session-events.ts:303-321). Added projector and Desktop tests cover the rootless drain, rootless seed, and stale-card clearing. No schema/migration files change.
At review time the current-head test check succeeds and the PR merges cleanly with freshly fetched main at the Git merge-tree level; GitHub still reports a blocked merge state, so this is not a merge-readiness claim. Diff-check is clean. I did not run tests locally or a real Desktop navigate-away/re-subscribe scenario. Human validation and merge decision remain outstanding.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Astro-Han
left a comment
There was a problem hiding this comment.
Approved at @Astro-Han's explicit request: a small, focused fix with no blocking findings in the automated review of this head and green CI.
A queue change that landed after the root Turn was gone projected nothing, and a rootless seed projected nothing either, so a queued card a client still held stayed on screen and its retract failed with not_found. The projector now emits the authoritative `queue_update` for every queue change and once on a rootless seed. The Desktop candidate harness's default snapshot gains the queue our protocol requires; upstream instead defaults a missing queue in the projector. Lead: apache#5521 (cc6dcca). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Watermark bfb315a. Done: apache#5573/apache#5600/apache#5601, apache#5521, apache#4875, apache#5723, apache#5738, apache#5742. Not applicable: apache#5737, apache#5593. Deferred: apache#5730. Consider: apache#5599, apache#5120, apache#5693. Diverged: apache#5740. Skipped: ACP, WorkHub, upstream renderer and packages/ui, one refactor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Fixes #5520
A queue that drains while no root Turn is live was never projected: the session projector gated
queue_updateonroot && queueChanged(...), andseedActivereturned nothing for a rootless snapshot. The Desktop's queue mirror is driven only byqueue_update, so the renderer kept a phantom queued card whose 删除/retract failed withnot_foundforever — 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 (#publishCanonicalfires on any canonical change); the drop was purely in the projector adapter.The fix stays at that shared root:
session-projector.tsnow pushesqueue_updatewhenever the queue changed, root or not, attributed toroot?.turnId ?? previousRoot?.turnId ?? ''. The steering synthesis stays root-gated (it attributes messages to the live Turn).seedActiveseeds 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 aqueue_updatecarries empty arrays, and the TUI mirrors the arrays verbatim.Verification
session-projector.test.ts(full file, rebuilt dist)message-coordinator.test.tsexecution-host-queue.test.tssession-subscription-client.test.tsupstream/mainworktree (Windows transportread side ended) — pre-existing, untouched by this PRnpm run check:asf-headersNot 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
queue_update