Files
platform/todo/known-bugs/refreshing-a-finished-chat-turn-loses-output.md
T
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

82 lines
5.5 KiB
Markdown

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