Repository navigation
fix(webui): reconcile a turn that outlives the client's own response stream - #249
Merged
Merged
Conversation
…stream A response stream that ends says the runtime closed it — not that it finished the turn. The runtime may close the stream early, and the queue may move on to the next turn while this one is ending, and the client had no way to tell the two apart: it took the end of its own stream for the end of the turn, showed the turn as done, and never followed the turn that was still running. The answer to "is a turn still live?" was available the whole time — the runtime's active-turn read — and the command set already used it for a `session.start` it never saw. A local send simply never asked. A send now reconciles the session it landed on once its own stream is done: the same owner the event path uses asks the runtime and re-attaches to whatever is still running. The session to reconcile is the one the send landed on, not the one the command set was built for — a first message creates the session it is sent to and the record moves onto it — so the application resolves it from the completion. A queued submission is left alone. It joined somebody else's turn and never had a stream of its own to end, and the turn it joined is somebody else's stream, which the queue's own events speak for. The runtime's terminal frame is not the shortcut it looks like. It says that *this* turn is over, and it says nothing about the next one: measured against the real runtime, a send whose turn ended while the queue moved on still carried its own `session_status:finished` frame, and the runtime reported the *following* turn as live. So the frame settles this turn and nothing else, and the client's question — is anything still running on this session — is only ever answered by the runtime's read. The four cases that pin this are `packages/webui/test/unit/conversation-turn-commands.test.ts` (the send hands over the session it landed on; a first message hands over the session it created rather than the home slot; a queued submission hands over nothing) and `packages/webui/test/unit/webui-application.test.ts` (the whole shape end to end: the send's own stream ends, the runtime says a turn is still running, and the client attaches to it). The three that assert the reconciliation fail on the trunk — the owner is never asked and the runtime's read is never made — and the queued one holds on the trunk and holds here.
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.
One product bug, reproduced against the real runtime before anything was changed, with four unit cases — three of which fail on the trunk and pass here.
The bug: a response stream that ends was taken for the end of the turn.
A stream that ends says the runtime closed it — not that it finished the turn. The runtime may close the stream early, and the queue may move on to the next turn while this one is ending. The client had no way to tell the two apart: the end of its own stream became
phase: "done", the sending flag came down, no refusal was raised, and the turn that was still running was never followed. The answer to "is a turn still live?" was available the whole time — the runtime's active-turn read — and the command set already used it for asession.startit never saw. A local send simply never asked; only the attach path reconciled.The fix: a send now reconciles the session it landed on once its own stream is done. The same owner the event path uses asks the runtime and re-attaches to whatever is still running. The session to reconcile is the one the send landed on, not the one the command set was built for — a first message creates the session it is sent to and the record moves onto it — so the application resolves it from the completion.
A queued submission is left alone: it joined somebody else's turn and never had a stream of its own to end, and the turn it joined is somebody else's stream, which the queue's own events speak for.
The obvious shortcut was to probe only when the stream ended without the runtime's terminal frame, which would have kept the common case free of a round trip. Measured against the real runtime, that shortcut is wrong. A send whose turn ended while the queue moved on to the next turn still carried its own
session_status:finishedframe — every frame on the stream belonged to that one turn — while the runtime's active-turn read reported the following turn as live. The terminal frame says this turn is over and nothing about the next one, so "did the stream carry a terminal frame" and "is anything still running on this session" are two different questions, and guarding the probe on the first one loses the queued case entirely.Two further facts came out of the same measurement: the local send's
[DONE]arrives after the turn has settled in the database (8 of 8 rounds, the probe already answered "no active turn" by then), so the retirement window is closed before the client can observe it; and a normal round therefore pays one probe that answers "nothing is running". That cost is the price of the only question that can be asked of the runtime, and it is the form that was approved.The cases that pin this are
packages/webui/test/unit/conversation-turn-commands.test.ts— the send hands over the session it landed on; a first message hands over the session it created rather than the home slot; a queued submission hands over nothing — andpackages/webui/test/unit/webui-application.test.ts— the whole shape end to end: the send's own stream ends, the runtime says a turn is still running, and the client attaches to it. The three that assert the reconciliation fail on the trunk (the owner is never asked, the runtime's read is never made); the queued one holds on the trunk and holds here.