Skip to content

fix(vscode): restore streaming state when attaching to a live session - #2824

Open
matthiasgoergens wants to merge 3 commits into
MoonshotAI:mainfrom
matthiasgoergens:fix/vscode-reattach-streaming-state
Open

fix(vscode): restore streaming state when attaching to a live session#2824
matthiasgoergens wants to merge 3 commits into
MoonshotAI:mainfrom
matthiasgoergens:fix/vscode-reattach-streaming-state

Conversation

@matthiasgoergens

@matthiasgoergens matthiasgoergens commented Aug 11, 2026

Copy link
Copy Markdown

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

loadSession resets isStreaming: false unconditionally, 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

  • LoadKimiSessionHistory appends a synthetic turn_active marker to the history reply when the session's runtime is busy.
  • The webview store handles the marker and no longer forces isStreaming off 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.
  • Because the marker is sampled when the history is built, the turn may legitimately end while the replay is being applied (its terminal event consumed by the pre-load state). To converge in that ordering, the store revalidates through a new isSessionBusy bridge 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 isSessionBusy reports the runtime state; store level — a replay with turn_active keeps 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

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my fix works.
  • Ran gen-changesets skill, or this PR needs no changeset. (changeset included: kimi-code patch)
  • Ran gen-docs skill, or this PR needs no doc update.

…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-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7840384

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@moonshot-ai/kimi-code Patch
@moonshot-ai/kimi-code-sdk Patch
kimi-code Patch

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +175 to +179
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread packages/kosong/src/generate.ts Outdated
Comment on lines +192 to +194
const iterator = stream[Symbol.asyncIterator]();
for (;;) {
const next = await nextStreamPart(iterator, stream, stallTimeoutMs, requestSignal, stallAbort);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +166 to +170
.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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@matthiasgoergens
matthiasgoergens force-pushed the fix/vscode-reattach-streaming-state branch from ce42702 to 7840384 Compare August 11, 2026 13:53
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.
@matthiasgoergens
matthiasgoergens force-pushed the fix/vscode-reattach-streaming-state branch from 7840384 to 5195281 Compare August 11, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant