--- id: known-bugs/refreshing-a-finished-chat-turn-loses-output title: 'Refreshing a finished chat turn loses output — replay collapses to a fold and the closing message vanishes' theme: known-bugs status: done created: 2026-08-25 date_source: stated order: 4 --- **Refreshing a finished chat turn loses output.** Reported by the owner 2026-08-25, with a screenshot: a Command HQ chat (the "Voice Memos" commander, first turn), refreshed after the turn completed, shows the prompt, then one collapsed seam reading `6 tool calls · 3 messages · 3 failed`, then `TURN FINISHED · NO CLOSING MESSAGE`. The turn's actual output — including AskUserQuestion activity the prompt explicitly requested — is gone from view. The header confirms how wrong the fold is: `0 turns · 0 tok · $0` on a session that plainly ran six tool calls. He reports this happens "a lot of times" on refresh, not once. Two distinct wrongs, and they may have different owners: 1. **The collapse itself is (he recalls) a deliberate decision** — fold past tool calls on replay. If so, the decision needs revisiting where it hides MESSAGES, not just tool chatter: "3 messages" inside a collapsed seam means prose the agent wrote is behind a click, and a first-time reader concludes the turn produced nothing. 2. **"No closing message" on a turn that had one.** The fold's own code says that banner was built for turns that genuinely ended without an answer, and its comments record it once mis-fired for streams (`turn-rows.ts:178`, `types.ts:38` — "printed 'Turn finished · no closing message' under work that was still going"). A replay reaching that state on a turn that DID close means the replay source is missing the final message — so look at what the fold is FED on refresh, not at the fold: the event log the page replays (`chat_session_events` → `turn-rows.ts` folding) versus what the live socket streamed. `3 failed` inside the seam is a second clue about what the stored events actually hold. Where to look, verified in the tree: `Chat/components/turn-rows.ts` (the folding, with its own history of exactly this class of bug in comments and tests), `TurnFold.tsx:88` (renders the banner), and whatever writes the durable events the replay reads — the claude sidecar has owned `chat_session_events` writes since the isolation work. Note `0 turns · 0 tok · $0` suggests the commander's turn ledger also saw none of it, which points the same direction: the durable record, not the renderer. --- **Fixed 2026-08-25. The durable record was innocent, and so was the fold.** A refresh never reads `chat_session_events` at all: `GET /chat/sessions/:id` parses Claude's `.jsonl`, and `attach` sets the replay cursor to the head precisely so the durable log is NOT re-delivered on top of it. Both symptoms came from one line in the transcript parser. The harness delivers a finished background task to the agent by writing it as the agent's next **user** message — a `` envelope. `claude-sessions.ts` dropped that message, so it would not replay as a raw XML bubble on your side of the conversation. Dropping it also dropped a turn boundary, because `turn-rows` derives turns from the message list and a turn starts at a user message. So the woken turn merged into the one before it, and everything that followed: - the earlier turn's closing prose stopped being the turn's trailing run and became mid-turn work, which **folds** — that is the "3 messages" behind a click, prose the agent wrote, hidden; - the merged segment then ended on a tool call, so the exposed tail was empty and `TURN FINISHED · NO CLOSING MESSAGE` was stamped over a turn that had answered. A commander wakes on these, so its whole life replayed as **one** turn. Measured on the reported session (`331fee09`, Voice Memos): 30 parsed messages, **one** user message, one fold hiding **seven** answers, and the banner reproduced at 21 of the 30 points a refresh could land on — which is the "a lot of times". The fix is in three parts. The parser rebuilds the envelope into the same `task` row a live `task:notification` produces, carrying `startsTurn: true` (entities decoded; a malformed envelope with no task id is still dropped). `buildRows` treats such a row as a turn boundary and `buildTurn` exposes it as the turn's opener, the way it exposes your question. And the marker is withheld when the segment ends on a tool call **we never saw come back** — that turn is still running or was cut off, and a reload renders with `isGenerating` false until the socket attaches. A COMPLETED trailing tool call keeps its marker; the existing test pinning that live behaviour passes unchanged. Verified by replaying the real transcript through the real parser and row builder: two turns, each ending with its own closing message on screen, no banner. 13 tests added (`turn-rows.test.ts`, `task-notification-replay.test.ts` — the latter drives the real loader, because the drop and the boundary live one call apart and that is how they came apart). **The two clues in the report above were both red herrings, worth recording.** The durable log is not the replay source. And `0 turns · 0 tok · $0` was transient: `command_hq_turns` now holds **2** turns and $0.92 for this commander — which independently confirms the session really is two turns while the replay was drawing one. **Still open, and a product decision rather than a defect:** point 1 above. Within a correctly-bounded turn, mid-turn prose still folds; only the trailing run is exposed. That is the documented intent ("question, summary, answer"), and it is untouched here.