You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Web UI] First user message bubble missing / messages misordered & duplicated in live rendering; server transcript is correct #3991
In the bundled Web UI (kimi web, local server on 127.0.0.1:10086), chat bubbles render incorrectly from the first turn of a session. Two symptom variants observed:
Nothing renders: neither the user's input bubble nor the assistant output appears. Only restarting the web server and refreshing makes the (complete) conversation visible.
Misordered / duplicated: the first assistant reply renders twice, and the user's first prompt bubble is shifted down one position (appears under the reply that answered it).
The CLI/TUI is unaffected. The server-side transcript (GET /api/v1/sessions/<id>/transcript) returns complete, correctly ordered data while the UI shows it misordered/duplicated at the same moment.
Environment
kimi-code versions: 0.43.x and also a fresh 0.39.1 install via official install.sh — bug reproduces on both, so it is not version-specific.
OS: Linux (WSL2), x86-64
Browser: Chrome, all extensions disabled
Web server banner: Kimi server ready 0.38.0
Evidence
Server data is complete and correctly ordered. Querying the REST API directly (.../transcript?agent_id=main&page_size=50) returns all turns in correct order (t0, t1, t2) while the UI simultaneously shows them misordered/duplicated.
Browser DevTools Console: zero errors, zero warnings.
Server request log: all requests complete with code 0, including POST /prompts.
Refresh alone does NOT recover. Only web-server restart + refresh restores correct rendering — suggests the corruption is not purely client-side React state; the per-session in-memory projection (or the frontend's merge baseline at session attach) appears stuck for the session's lifetime.
The UserPromptSubmit hook is NOT the trigger (earlier hypothesis disproven): with all UserPromptSubmit hooks fully disabled (verified against a fresh process launched after config change) and all Chrome extensions disabled, the "first user message bubble missing" variant still reproduces on a brand-new session's first turn. The bug follows the web frontend rendering layer, not the hook injection structure.
Server event stream (server/events/session_*.jsonl) for a session whose first user message never rendered shows context.spliced events only with deleteCount=0, append/insert-only, correctly ordered; hook-origin messages carry id: null. So the user message is present server-side — the loss happens purely in frontend rendering.
Reproduction
Start the web UI, create a new session.
Send a first prompt.
Observe: user input bubble missing after the first assistant reply completes (variant 1), or first reply duplicated with the user bubble shifted (variant 2).
Refresh → still broken. Restart server + refresh → renders correctly.
Impact
The web UI is unreliable for everyday use: rendered conversations are missing/misordered until the server is restarted. Downgrading is not a workaround (reproduces on 0.39.1).
Possibly related: #3806 (ghost user bubble, 0.43.0 regression), #3888 (hooks + pending call never renders in web clients).
Update (2026-09-23): additional findings across rounds 3–5
Round 3 — hook injection hypothesis invalidated. Reproduced on a fresh process (PID-verified) with all UserPromptSubmit hooks disabled and all browser extensions off (v0.39.1). The user bubble still disappears after the first assistant reply. The server-side transcript remains complete and correctly ordered, so this is a client-side rendering issue.
Round 4 — session list is a startup snapshot. Sessions created by another process (web ↔ TUI, same KIMI_CODE_HOME) don't appear until the receiving process is restarted; in-session refresh doesn't help. This suggests state is snapshotted at process start with no cross-process invalidation — a candidate mechanism for why "restart + refresh" is the only recovery. (Note: this alone doesn't explain the disappearing bubbles of the current session, so a pure rendering-layer fault is not excluded either.)
Round 5 — still reproduces on 2.0.2. After a full web-server restart on the new binary, the same "user bubbles gone after round 1" symptom appears in the browser. Affected range now 0.39.1 → 2.0.2.
Also noted: the web UI never displays hook-injected content (only the TUI does), which is consistent with the round-3 result.
Root cause found — two independent bugs stacking up. I traced this end-to-end (server event journal, WS frames, REST transcript paging, and the frontend projection path) and reproduced both symptom variants. Posting the analysis in case it helps an official fix.
Layer 1 — transcript_since catch-up can never match → silent full-reset storms (hook-independent)
In packages/kap-server, transcript frames were broadcast with the event-journal seq on the envelope, while clients resync with the ops-namespace seq carried in the payload. The two namespaces diverge, so transcript_since catch-up never matches and every WS reconnect falls back to a silent full reset. Additionally, when the transcript store detected an append gap, seeded clients were never told to resync — the server just diverged silently.
This layer needs no hooks at all, which explains evidence point 5 above: the "nothing renders until server restart" variant still reproduces with UserPromptSubmit hooks fully disabled, because the per-session projection baseline is wrecked by reset storms and never heals within the session's lifetime.
Fix (local): the envelope seq for transcript frames now uses the ops-namespace seq from the payload (transcriptService.ts, coreBinding.ts), and an append gap now broadcasts a transcript reset to seeded targets instead of silent divergence (sessionEventBroadcaster.ts).
Layer 2 — hook_result injections open pseudo turns in the snapshot projection → every turn shifts by one (hook-dependent)
In packages/transcript/src/history/groupTurns.ts, groupMessagesIntoSnapshot treated a user-role hook_result injection as a turn-opening user message (the hidden-user-origin set was missing hook_result). Snapshot turn ordinals shifted +1; then healTurnOps in kap-server's transcriptService.ts overwrote the live turn's correct origin with the misaligned snapshot origin. Net effect: user bubbles render one turn off and the first bubble is dropped — exactly variant 2 (first reply duplicated, first prompt bubble shifted under its own reply). With hooks enabled this compounds with Layer 1.
Fix (local): project hook_result user injections as hook markers (matching the live path) instead of opening a turn (+4 lines in groupTurns.ts), and healTurnOps now prefers the live turn's origin over the snapshot header (+1 line in transcriptService.ts). Regression tests added in packages/transcript/test/layers.test.ts (+64 lines).
End-to-end repro in an isolated environment confirms snapshot/live turn origins stay aligned.
Running a from-source build with both patches since yesterday across multiple new sessions: no missing bubbles, no duplication, no misordering, no server restarts needed.
Root cause found — two independent bugs stacking up. I traced this end-to-end (server event journal, WS frames, REST transcript paging, and the frontend projection path) and reproduced both symptom variants. Posting the analysis in case it helps an official fix.
Layer 1 — transcript_since catch-up can never match → silent full-reset storms (hook-independent)
In packages/kap-server, transcript frames were broadcast with the event-journal seq on the envelope, while clients resync with the ops-namespace seq carried in the payload. The two namespaces diverge, so transcript_since catch-up never matches and every WS reconnect falls back to a silent full reset. Additionally, when the transcript store detected an append gap, seeded clients were never told to resync — the server just diverged silently.
This layer needs no hooks at all, which explains evidence point 5 above: the "nothing renders until server restart" variant still reproduces with UserPromptSubmit hooks fully disabled, because the per-session projection baseline is wrecked by reset storms and never heals within the session's lifetime.
Fix (local): the envelope seq for transcript frames now uses the ops-namespace seq from the payload (transcriptService.ts, coreBinding.ts), and an append gap now broadcasts a transcript reset to seeded targets instead of silent divergence (sessionEventBroadcaster.ts).
Layer 2 — hook_result injections open pseudo turns in the snapshot projection → every turn shifts by one (hook-dependent)
In packages/transcript/src/history/groupTurns.ts, groupMessagesIntoSnapshot treated a user-role hook_result injection as a turn-opening user message (the hidden-user-origin set was missing hook_result). Snapshot turn ordinals shifted +1; then healTurnOps in kap-server's transcriptService.ts overwrote the live turn's correct origin with the misaligned snapshot origin. Net effect: user bubbles render one turn off and the first bubble is dropped — exactly variant 2 (first reply duplicated, first prompt bubble shifted under its own reply). With hooks enabled this compounds with Layer 1.
Fix (local): project hook_result user injections as hook markers (matching the live path) instead of opening a turn (+4 lines in groupTurns.ts), and healTurnOps now prefers the live turn's origin over the snapshot header (+1 line in transcriptService.ts). Regression tests added in packages/transcript/test/layers.test.ts (+64 lines).
End-to-end repro in an isolated environment confirms snapshot/live turn origins stay aligned.
Running a from-source build with both patches since yesterday across multiple new sessions: no missing bubbles, no duplication, no misordering, no server restarts needed.
Activity
aircolorX commentedon Sep 23, 2026
Update (2026-09-23): additional findings across rounds 3–5
KIMI_CODE_HOME) don't appear until the receiving process is restarted; in-session refresh doesn't help. This suggests state is snapshotted at process start with no cross-process invalidation — a candidate mechanism for why "restart + refresh" is the only recovery. (Note: this alone doesn't explain the disappearing bubbles of the current session, so a pure rendering-layer fault is not excluded either.)Also noted: the web UI never displays hook-injected content (only the TUI does), which is consistent with the round-3 result.
aircolorX commentedon Sep 28, 2026
Root cause found — two independent bugs stacking up. I traced this end-to-end (server event journal, WS frames, REST transcript paging, and the frontend projection path) and reproduced both symptom variants. Posting the analysis in case it helps an official fix.
Layer 1 —
transcript_sincecatch-up can never match → silent full-reset storms (hook-independent)In
packages/kap-server, transcript frames were broadcast with the event-journal seq on the envelope, while clients resync with the ops-namespace seq carried in the payload. The two namespaces diverge, sotranscript_sincecatch-up never matches and every WS reconnect falls back to a silent full reset. Additionally, when the transcript store detected an append gap, seeded clients were never told to resync — the server just diverged silently.This layer needs no hooks at all, which explains evidence point 5 above: the "nothing renders until server restart" variant still reproduces with
UserPromptSubmithooks fully disabled, because the per-session projection baseline is wrecked by reset storms and never heals within the session's lifetime.Fix (local): the envelope seq for transcript frames now uses the ops-namespace seq from the payload (
transcriptService.ts,coreBinding.ts), and an append gap now broadcasts a transcript reset to seeded targets instead of silent divergence (sessionEventBroadcaster.ts).Layer 2 —
hook_resultinjections open pseudo turns in the snapshot projection → every turn shifts by one (hook-dependent)In
packages/transcript/src/history/groupTurns.ts,groupMessagesIntoSnapshottreated a user-rolehook_resultinjection as a turn-opening user message (the hidden-user-origin set was missinghook_result). Snapshot turn ordinals shifted +1; thenhealTurnOpsin kap-server'stranscriptService.tsoverwrote the live turn's correct origin with the misaligned snapshot origin. Net effect: user bubbles render one turn off and the first bubble is dropped — exactly variant 2 (first reply duplicated, first prompt bubble shifted under its own reply). With hooks enabled this compounds with Layer 1.Fix (local): project
hook_resultuser injections as hook markers (matching the live path) instead of opening a turn (+4 lines ingroupTurns.ts), andhealTurnOpsnow prefers the live turn's origin over the snapshot header (+1 line intranscriptService.ts). Regression tests added inpackages/transcript/test/layers.test.ts(+64 lines).Verification
packages/transcript: 127/127 tests pass;packages/kap-server: 133/133 pass.Happy to share the patches if useful.
aircolorX commentedon Sep 28, 2026
Root cause found — two independent bugs stacking up. I traced this end-to-end (server event journal, WS frames, REST transcript paging, and the frontend projection path) and reproduced both symptom variants. Posting the analysis in case it helps an official fix.
Layer 1 —
transcript_sincecatch-up can never match → silent full-reset storms (hook-independent)In
packages/kap-server, transcript frames were broadcast with the event-journal seq on the envelope, while clients resync with the ops-namespace seq carried in the payload. The two namespaces diverge, sotranscript_sincecatch-up never matches and every WS reconnect falls back to a silent full reset. Additionally, when the transcript store detected an append gap, seeded clients were never told to resync — the server just diverged silently.This layer needs no hooks at all, which explains evidence point 5 above: the "nothing renders until server restart" variant still reproduces with
UserPromptSubmithooks fully disabled, because the per-session projection baseline is wrecked by reset storms and never heals within the session's lifetime.Fix (local): the envelope seq for transcript frames now uses the ops-namespace seq from the payload (
transcriptService.ts,coreBinding.ts), and an append gap now broadcasts a transcript reset to seeded targets instead of silent divergence (sessionEventBroadcaster.ts).Layer 2 —
hook_resultinjections open pseudo turns in the snapshot projection → every turn shifts by one (hook-dependent)In
packages/transcript/src/history/groupTurns.ts,groupMessagesIntoSnapshottreated a user-rolehook_resultinjection as a turn-opening user message (the hidden-user-origin set was missinghook_result). Snapshot turn ordinals shifted +1; thenhealTurnOpsin kap-server'stranscriptService.tsoverwrote the live turn's correct origin with the misaligned snapshot origin. Net effect: user bubbles render one turn off and the first bubble is dropped — exactly variant 2 (first reply duplicated, first prompt bubble shifted under its own reply). With hooks enabled this compounds with Layer 1.Fix (local): project
hook_resultuser injections as hook markers (matching the live path) instead of opening a turn (+4 lines ingroupTurns.ts), andhealTurnOpsnow prefers the live turn's origin over the snapshot header (+1 line intranscriptService.ts). Regression tests added inpackages/transcript/test/layers.test.ts(+64 lines).Verification
packages/transcript: 127/127 tests pass;packages/kap-server: 133/133 pass.Happy to share the patches if useful.