Files
platform/todo/known-bugs/refreshing-a-finished-chat-turn-loses-output.md
pastilhasandClaude Opus 5 0096392cde a task the agent woke up to starts a turn
Refreshing a chat lost the agent's answers: the turn folded to a seam and
printed "Turn finished · no closing message" over a turn that had plainly
answered. Reported against a commander, where it happened on most refreshes.

The durable log was innocent. A refresh renders from Claude's .jsonl, and the
harness delivers a finished background task as the agent's next user message —
a <task-notification> envelope. The parser dropped it so it would not replay as
a raw XML bubble you appear to have typed, and dropped a turn boundary with it,
because turn-rows derives turns from the message list. The woken turn merged
into the one before it, so that turn's closing prose became mid-turn work and
folded, and the merged segment ended on a tool call, which earned the marker.

Measured on the reported session: 30 messages, one user message, one fold
hiding seven answers, the marker reproduced at 21 of 30 refresh points.

The envelope now rebuilds into the same task row a live task:notification
produces, carrying startsTurn. buildRows makes that a boundary and buildTurn
exposes it as the turn's opener. Separately, the marker is withheld when a
segment ends on a tool call we never saw come back — still running, or cut off
holding it. A completed trailing tool call keeps its marker, so the test
pinning that live behaviour is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
2026-08-25 06:37:21 +00:00

5.5 KiB

id, title, theme, status, created, date_source, order
id title theme status created date_source order
known-bugs/refreshing-a-finished-chat-turn-loses-output Refreshing a finished chat turn loses output — replay collapses to a fold and the closing message vanishes known-bugs done 2026-08-25 stated 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 <task-notification> 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.