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
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:
- 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.
- "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.tsfolding) versus what the live socket streamed.3 failedinside 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 MESSAGEwas 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.