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
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
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: open
|
||||
status: done
|
||||
created: 2026-08-25
|
||||
date_source: stated
|
||||
order: 4
|
||||
@@ -35,3 +35,47 @@ of exactly this class of bug in comments and tests), `TurnFold.tsx:88` (renders
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user