MoonshotAI / MoonshotAI/kimi-code
[BUG] Web UI: after interrupting a turn, the next question's thinking renders under the previous (interrupted) message
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
- In the web UI, send a question (Q1) that produces a long thinking stream.
- Click stop / interrupt while thinking is still streaming.
- Immediately send a new question (Q2).
- 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-728—const msgId = s.currentAssistantMsgId; if (!msgId) break;(same forassistant.deltaat :731-770)
So the race is:
- User aborts mid-thinking → daemon emits
turn.ended {reason:'cancelled'}(journaled, still in flight to the client). - User immediately sends Q2 → the new turn starts and its volatile thinking deltas are broadcast live, overtaking the still-undelivered durable frames.
- The client processes Q2's
thinking.deltawhilecurrentAssistantMsgIdstill points at Q1's aborted message M1 → the new thinking is appended into M1 (appendAssistantDelta, agentEventProjector.ts:338-360). messagesToTurnsthen 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 thegap → historyCompacted → snapshot resyncrecovery path (:716-719) never fires. turn.step.interrupted(:956-960) clears onlycurrentAssistantMsgId— unliketurn.ended(:909-950) it does not clearcurrentPromptId,turnTextLen, orturnThinkLen, so a subsequentturn.startedcan re-bind the stale promptId (s.currentPromptId ?? ulid('pr_'), :657-659), causing the new message to merge into Q1's turn group (continuesAssistantGroupmerges 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) onturn.step.interrupted, same asturn.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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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