fix(vscode): restore streaming state when attaching to a live session - #2824
fix(vscode): restore streaming state when attaching to a live session#2824matthiasgoergens wants to merge 3 commits into
Conversation
…l error The busy rejections in SessionRuntime threw a plain Error, which emitError maps to code "internal" and the webview renders as "Internal error occurred." — alarming and wrong for what is simply "a response is already being generated". Throw KimiError with the existing turn.agent_busy code instead so the UI shows the mapped "A message is being sent. Please wait." with the real detail attached. Fixes MoonshotAI#2796
A prompt sent while the session was busy with a turn the webview lost track of (isStreaming resets to false on loadSession even when the session has a live turn — e.g. after a window reload) was rejected with a busy warning, and the text sat parked in pendingInput — invisible, not queued — until the other turn's terminal event finally restored it. The runtime now marks the busy rejection with reason "busy" and the bridge result carries bounced: true, so the webview can tell "this send never started a turn" apart from a normal turn end. On a bounce the message moves into the send queue (flushed by the live turn's terminal event) and the composer keeps streaming state; any other send that fails before its TurnBegin rolls the text straight back into the composer. A bounce during an exclusive operation (terminal rejection, no later terminal event) is left to the error path as before.
🦋 Changeset detectedLatest commit: 7840384 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce42702e73
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (runtime.isBusy) { | ||
| // The session has an in-flight turn (e.g. started by another view, or | ||
| // still running while this webview reloaded): tell the store, so the | ||
| // composer queues new input instead of bouncing off the busy runtime. | ||
| history.push({ type: "turn_active", payload: {}, _sessionId: runtime.id }); |
There was a problem hiding this comment.
Reconcile terminal events after emitting the active marker
If the live turn finishes after this isBusy check but before the history RPC is applied by the webview, stream_complete is either processed against the pre-load store or filtered by App.tsx, and then loadSession() replays this stale marker and sets isStreaming back to true. No later terminal event exists to clear it, so the composer remains locked and subsequent prompts queue indefinitely; buffer/reconcile events during loading or make the active-state snapshot versioned rather than returning a bare marker.
Useful? React with 👍 / 👎.
| const iterator = stream[Symbol.asyncIterator](); | ||
| for (;;) { | ||
| const next = await nextStreamPart(iterator, stream, stallTimeoutMs, requestSignal, stallAbort); |
There was a problem hiding this comment.
Close the iterator on every manual-loop exit
Replacing for await with this manual loop removes automatic AsyncIteratorClose: if onMessagePart throws, or the caller aborts while that callback is running, execution exits without calling iterator.return(). The production provider objects generally return a separate async-generator iterator, so cancelStream(stream) cannot close that iterator and the underlying HTTP stream may remain open; wrap the loop in try/finally and tear down the iterator on all exceptional exits. The mirrored v2 implementation has the same issue.
Useful? React with 👍 / 👎.
| .then((result) => { | ||
| const s = useChatStore.getState(); | ||
| // Only relevant while our send has not started a turn yet; once its | ||
| // TurnBegin arrived, the terminal events own the state transitions. | ||
| if (!s.awaitingTurnBegin) { |
There was a problem hiding this comment.
Bind each bridge reply to its originating send
When an earlier send emits a terminal/preflight error, the event unlocks the composer before that send's RPC reply necessarily arrives; if the user starts another send in that interval, this callback reads the new send's global awaitingTurnBegin and pendingInput. The stale reply can therefore unlock, enqueue, or retry the wrong prompt. Capture a per-send generation/request token and ignore the reply unless it still owns the current pending send.
Useful? React with 👍 / 👎.
| @@ -0,0 +1,208 @@ | |||
| import { generate, DEFAULT_STREAM_STALL_TIMEOUT_MS } from '#/generate'; | |||
There was a problem hiding this comment.
Consolidate the watchdog unit cases into generate.test.ts
This adds a new unit-test file for generate() even though packages/kosong/test/generate.test.ts already owns that component's tests; move these unit scenarios into the existing file and retain a separate file only for the genuinely e2e coverage. The repository explicitly requires preferring the existing corresponding test file rather than creating additional ones.
AGENTS.md reference: AGENTS.md:L59-L59
Useful? React with 👍 / 👎.
ce42702 to
7840384
Compare
loadSession reset isStreaming to false unconditionally, so a view that attached to a session with an in-flight turn (started in another view, or still running when the window reloaded) believed the session was idle. New input then took the send path and bounced off the busy runtime instead of queueing. LoadKimiSessionHistory now appends a synthetic turn_active marker when the runtime is busy, and the store keeps isStreaming set when a replay contains it: new input enqueues and is flushed by the live turn's terminal event.
7840384 to
5195281
Compare
Related Issue
Follow-up to #2817 (stacked on #2818). That PR fixed the symptom (bounced prompts parked in limbo); this one removes the underlying state desync.
Problem
loadSessionresetsisStreaming: falseunconditionally, so a view that attaches to a session with an in-flight turn — started in another view, or still running when the window reloaded — believes the session is idle. New input then takes the send path and bounces off the busy runtime instead of queueing.What changed
LoadKimiSessionHistoryappends a syntheticturn_activemarker to the history reply when the session's runtime is busy.isStreamingoff when a replay contains it: the composer treats the session as streaming, so new input enqueues and is flushed when the live turn's terminal event arrives.isSessionBusybridge method: if the session is no longer busy, the composer unlocks and any queued input flushes (review feedback).Tests: bridge-handler level — the marker is present iff the resumed runtime is busy, and
isSessionBusyreports the runtime state; store level — a replay withturn_activekeeps streaming state and routes new input to the queue, a replay without it resets as before, and a stale marker self-corrects when the revalidation reports idle.Checklist
gen-changesetsskill, or this PR needs no changeset. (changeset included:kimi-codepatch)gen-docsskill, or this PR needs no doc update.