Skip to content

[VSCode] Streamed output duplicated/interleaved when two views race to open the same session #2799

Description

@matthiasgoergens

Bug description

Sometimes all assistant output in a chat appears twice, strictly interleaved chunk-by-chunk:

TheThe roaring roaring run run died died with with the the session session close close;; I'll I'll restart restart it it …

Tool labels and "Thinking" headings are doubled as well. The strict alternation (delta1, delta1, delta2, delta2, …) indicates two consumers appending the same delta stream concurrently.

Root cause

KimiRuntime.openSession / attachResumedSession check this.sessions.get(id) and only then await several RPCs (resumeSession, metadata updates, detachView, …) before wrapSession registers the runtime in the map. Two concurrent calls for the same session id (sidebar + editor panel opening at once, or a window reload overlapping a reattach) both see an empty map, both resume, and both call wrapSession, which unconditionally sessions.set(session.id, runtime) — the later runtime overwrites the earlier one in the map, but the earlier SessionRuntime is never closed: its session.onEvent listener (and approval/question handler registrations, which are keyed by session id in the SDK rpc layer) stay live.

Both runtimes then adapt and broadcast every SDK event. When the same webviewId is subscribed in both (both racing calls subscribe it, or it re-attaches later), the webview's appendOrCreate concatenates both copies into the same item — producing the interleaved duplication. The webview store has no id-based dedup, so nothing downstream catches it.

Reproduction

Timing-dependent in production, but deterministic in a unit test: two concurrent openSession calls with the same sessionId create two SessionRuntimes (observable via the session's onEvent subscription count = 2), and one emitted assistant.delta produces two ContentPart broadcasts per view.

Suggested fix

wrapSession should return the existing runtime when this.sessions already has one for the session id. A resumed Session handle is inert until wrapped (its constructor registers nothing — listeners and approval handlers are only installed by the SessionRuntime constructor), so the loser's handle can simply be dropped; the shared engine session must NOT be closed (that would kill it for the winning runtime too).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions