Repository navigation
fix(turn): keep a resumed session stream live for a running turn - #237
Merged
Merged
Conversation
The agent-transcript integration case was red on CI about half the time: a queued message that drained while the page was reattaching never showed its answer, and the queue row it left behind never cleared. Two product defects, both about the page missing what it was not told. The runtime decided whether a resumed session stream is live or a replay from the session's persisted status. That status is an asynchronous projection of the runtime's status events, so it still read "idle" while a turn ran — for a queue-drained turn that is exactly what happens. The client had already asked whether a turn was running (`getActiveTurn`), the runtime said yes, and the stream it was then given replayed the history and closed, dropping the answer the drained turn committed a moment later. The runtime now reads the authoritative active turn as well, so the attach and the stream agree. A stream that waits for a terminal must not wait for one that can no longer arrive: the stream service records the turn of the last terminal it published, and a resume that follows that turn ends at its replay instead. The browser's event channel is a separate socket that can take seconds to open, and every event published before it opens is lost — the runtime publishes them, the page never sees them. The page therefore re-reads the two things those events would have told it when the channel becomes ready: the transcript head (a turn that ran and settled in that window is in no probe's answer) and the queue (an item that drained in it leaves a stale row). The guarantee is unchanged and strengthened: a queued message is delivered in order, and the queue empties.
The macOS CI leg of this gate failed at the first screen: `#webui-root` was still the empty mount div when the default 5s deadline expired, so the case reported a red about a shell that had not booted yet. The client boots by asking the host for its version and rendering the shell when the answer arrives, and that request carries the wire's own 30s deadline (`DEFAULT_REQUEST_TIMEOUT_MS`): past it the client renders the startup fallback instead of the shell, so no shell can arrive later than that. The wait therefore moves to the composer — the page's own input surface, which can only be on screen once the shell has rendered — and is given that same deadline. The mount node's own assertion stays. The macOS CI run of the whole 213-file webui group measured this case at 26.7s with the shell still not up when the 5s deadline expired, against 12.0s for the same case on ubuntu; the group itself measured 160.9s on macOS against 76.9s on ubuntu. The case's own 120s budget still has room for the rest of the case: about 21.6s of setup ran before that wait, and the rest of the case needs about 25s at the same ratio, so a wait that runs to its deadline still ends the case around 77s — the budget is never the thing that catches the failure. The reloaded page is a second cold start of the same client, so it waits on the same condition with the same deadline. No expectation changes: the queue drain this case is about is asserted exactly as before.
modacker
force-pushed
the
fix/agent-transcript-queue-drain
branch
from
October 23, 2026 05:41
7c26141 to
c4c0997
Compare
modacker
added a commit
that referenced
this pull request
Oct 23, 2026
…de packages/webui (#240) The webui branch is a downstream client of MiniMax-AI/minimax-code. Changes to the upstream packages do not flow back upstream and turn into unreviewed divergence at the next upstream sync, so they do not belong here. This removes the upstream-package half of the work merged today: - packages/local-runtime/src/turns/file-changes.ts (#232, #234) - packages/agent-tools/** (#238) - packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts (#238) - packages/local-runtime-v2/src/application/conversation/conversation-application.ts and .../stream/session-stream-service.ts (#237) Everything under packages/webui stays: the queue-drain client half, the regenerated visual baselines, the browser test budgets and the CJK font gate. The inventories follow the files: eight removed paths leave release/public-source.json and six removed suites leave test/vitest-suites.json. Co-authored-by: probe <probe@local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
packages/webui/test/integration/agent-transcript.test.tsasserts that a message queued behind an active turn is delivered afterwards, in order, and that the queue row disappears once it drains. On CI that case fails roughly half the time, and it fails on the unmodified base commit as well — it is not a regression of any open change, it is a product defect that Jiaxiaoyuan's97e33fd8("resolve queue drain race after turn terminal release") did not actually resolve.The browser ends up with no trace of the drained turn at all: the queued reply is never rendered and the queue row stays on screen. Two independent defects produce that.
1. The runtime decides "replay only" from a racy projection.
resumeSessionused the session's persistedstatusto decide whether a resumed stream should be live or replay-only. That status is an asynchronous projection of state events, so while a turn is running it still readsidle— and for a drained turn especially so. The runtime therefore served a replay-only stream and closed it before the queued reply was committed, so that reply never reached the browser. The runtime contradicts itself in the same breath: the client'sgetActiveTurnansweredbusyReason: "turn"(a turn is active) whileresumeSessionansweredstatus="idle".2. The browser loses every event published before its event channel opens. The event channel is a separate socket from the request channel. In the failing runs the server completed the handshake in 11ms while the browser's
openevent only fired 3.4s later; every event published in that window — includingsession.startfor the drained turn and threesession.queue.updatedevents — was lost. The page therefore never learned the turn had started, and the queue row it was already showing never got told to go away.What this changes
packages/local-runtime-v2/src/application/conversation/conversation-application.ts— the liveness of a resumed stream now comes from the authoritative active turn (turnInspection.activeTurnId, the same answer the client'sgetActiveTurngives) instead of the racy persisted status.packages/local-runtime-v2/src/service/session-system/stream/session-stream-service.ts— the service remembers which turn published the last terminal event, so a resumed stream that follows an already-finished turn ends at the replay instead of waiting forever for a terminal that will never come. This closes the hang window the first change would otherwise open.packages/webui/src/client/application/create-application.ts— when the event channel becomes ready the client converges on the authoritative state: it re-reads the transcript head (a turn that finished inside the window is in no probe result) and re-reads the queue (an item drained inside the window leaves no residual row).The guarantee only gets stronger: a queued message is delivered in order and the queue is emptied. The failing test file is not touched —
git diff 97e33fd8 -- packages/webui/test/is empty. No skip, no retry, no widened timeout, no weakened expectation.How it was checked
The defect was reproduced locally on the pristine base commit (the macOS form of the failure:
getByText('Queued follow-up delivered.')— element not found), and instrumented in three layers to separate "the runtime never published it" from "the browser never received it":session.queue.updatedx3,session.start(queued drain),session.finishopenfired 3.4s latergetActiveTurnvsresumeSessionbusyReason: "turn"vsstatus="idle"With the fix in place:
test:webuiwhole group, CI-equivalent (213 files): 213 files, 3264 passed, 34 skipped;test:capability(covers local-runtime-v2): 237 files, 5815 passed;check:source,check:webui-boundary,check:webui-dependency: green;session-stream-service.test.tsdisables the end-of-stream logic on purpose and fails withError: the resumed stream did not end— a condition failure, not a timeout — and passes again once restored.Not addressed here
activeTurnIdhalf has no unit test. The new unit test covers the stream-service end-of-stream semantics; the runtime's active-turn lookup is covered only end to end by the integration case.channelReadyrefreshes the transcript forstore.getSelectedSessionId(). If the view ever presents a session other than the selected one, that could in principle switch the presentation slot. The two agree on every path exercised here, and no counter-example was constructed.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable. —release/public-source.jsonwas regenerated withnode scripts/source-inventory.mjs --writeandtest/vitest-suites.jsondeclares the new test file.Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: not needed.