Skip to content

[Web UI] First user message bubble missing / messages misordered & duplicated in live rendering; server transcript is correct #3991

Open

Description

@aircolorX

Summary

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:

  1. 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.
  2. 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

  1. 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.
  2. Browser DevTools Console: zero errors, zero warnings.
  3. Server request log: all requests complete with code 0, including POST /prompts.
  4. 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.
  5. 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.
  6. 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

  1. Start the web UI, create a new session.
  2. Send a first prompt.
  3. Observe: user input bubble missing after the first assistant reply completes (variant 1), or first reply duplicated with the user bubble shifted (variant 2).
  4. 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).

Activity

  1. aircolorX commented on Sep 23, 2026

    @aircolorX
    Author

    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.

  2. aircolorX commented on Sep 28, 2026

    @aircolorX
    Author

    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).

    Verification

    • packages/transcript: 127/127 tests pass; packages/kap-server: 133/133 pass.
    • 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.

    Happy to share the patches if useful.

  3. aircolorX commented on Sep 28, 2026

    @aircolorX
    Author

    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).

    Verification

    • packages/transcript: 127/127 tests pass; packages/kap-server: 133/133 pass.
    • 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.

    Happy to share the patches if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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