MoonshotAI / MoonshotAI/kimi-code

[BUG] Web UI: after interrupting a turn, the next question's thinking renders under the previous (interrupted) message

Open
#3,617 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

Title: Web UI: after interrupting a turn, the next question's thinking renders under the previous (interrupted) message

Summary

In the Kimi Code web UI, if I interrupt a response mid-generation and immediately send a new question, the new question's thinking stream is appended to the previous (interrupted) assistant message, so it renders under the previous question's turn instead of the new one.

Steps to reproduce

  1. In the web UI, send a question (Q1) that produces a long thinking stream.
  2. Click stop / interrupt while thinking is still streaming.
  3. Immediately send a new question (Q2).
  4. Observe: Q2's thinking content appears under Q1's message bubble.

Root cause analysis

Delta frames (thinking.delta / assistant.delta) are volatile — fanned out live, never journaled — while turn-boundary frames (turn.ended, prompt.submitted, turn.step.started) are durable and delivered through the journal/replay pipeline:

  • packages/protocol/src/events.ts:1281-1287

On the client, deltas are routed by a single mutable pointer with no turn/prompt validation:

  • apps/kimi-web/src/api/daemon/agentEventProjector.ts:700-728const msgId = s.currentAssistantMsgId; if (!msgId) break; (same for assistant.delta at :731-770)

So the race is:

  1. User aborts mid-thinking → daemon emits turn.ended {reason:'cancelled'} (journaled, still in flight to the client).
  2. User immediately sends Q2 → the new turn starts and its volatile thinking deltas are broadcast live, overtaking the still-undelivered durable frames.
  3. The client processes Q2's thinking.delta while currentAssistantMsgId still points at Q1's aborted message M1 → the new thinking is appended into M1 (appendAssistantDelta, agentEventProjector.ts:338-360).
  4. messagesToTurns then renders it inside Q1's turn — it is literally part of M1, so no promptId/turn-grouping logic can recover.

Two details make the corruption stick instead of self-healing:

  • The offset-0 self-heal (meta?.offset === 0 && s.turnThinkLen > 0 → reset, agentEventProjector.ts:712-714) makes the misdirected delta align as a clean append, so the gap → historyCompacted → snapshot resync recovery path (:716-719) never fires.
  • turn.step.interrupted (:956-960) clears only currentAssistantMsgId — unlike turn.ended (:909-950) it does not clear currentPromptId, turnTextLen, or turnThinkLen, so a subsequent turn.started can re-bind the stale promptId (s.currentPromptId ?? ulid('pr_'), :657-659), causing the new message to merge into Q1's turn group (continuesAssistantGroup merges when either side's promptId is undefined, apps/kimi-web/src/composables/messagesToTurns.ts:325-331).

Note on #1609

PR #1609 ("make mid-turn delta offsets step-relative") fixes a different defect (duplicated text after reconnect/refresh mid-turn). It does not address this race: even on branches containing it (e.g. spine-v2), delta routing still has no turnId/promptId guard, the offset-0 self-heal is unchanged, and turn.step.interrupted still clears only currentAssistantMsgId.

Suggested fix directions

  • Tag delta frames with turnId/stepId and drop (or buffer) them client-side when they don't match the currently open step; and/or
  • Clear full per-turn state (currentPromptId, stream offsets) on turn.step.interrupted, same as turn.ended; and/or
  • Treat "offset-0 delta arriving while a message from a previous prompt is open" as a turn boundary (open a new message / resync) rather than an append.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/protocol/src/events.ts:1281-1287 and the delta routing in apps/kimi-web/src/api/daemon/agentEventProjector.ts:657-770, 909-960. Trace turn.step.interrupted, turn.ended, and the next turn's volatile deltas, then inspect grouping in apps/kimi-web/src/composables/messagesToTurns.ts:325-331. Done means reproducing Q1 interruption followed by Q2 and confirming Q2 thinking renders in its own turn without breaking replay or resync behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.