Never let one unreadable line take a transcript down

Removing `Event::TaskNote` hours after adding it made every transcript that
had recorded one unreadable. `Transcript::open` parses every line, so `launch`
failed for those sessions and `SessionManager::new` logged
"couldn't relaunch session <id>" and skipped them -- and a skipped session has
no pump and no driver. On the phone that is no status, no history and nothing
sendable, for every live session that had run a background task. One
unfamiliar word took down every conversation it appeared in.

A transcript is append-only and permanent, so the set of kinds one can hold
only ever grows: what this build writes is not what it may have to read. A
line can come from a newer server, or from an older one that wrote a kind
since dropped, and neither may be able to end the file.

`Indexed::parse_at` degrades a line it cannot make sense of to
`Event::Unreadable { kind }` instead of failing the whole read. It keeps the
line's seq -- the cursors, the page bisection and the next-seq counter are all
addressed by it, and dropping the line would hand out a seq the file already
contains -- and carries the word the line called itself, so the phone can say
what is missing rather than that something is. A line with no readable seq is
still an error: that one cannot be placed at all.

`Event::TaskNote` comes back retired rather than deleted: deserializable,
never constructed, dated, with the reason on it. The phone folds it to no row,
which is the point -- an unreadable line correctly draws a placeholder, and
one per background task is the wall the row was removed for in the first
place.

Found while diagnosing a report that live sessions had lost their status and
could not be sent to. 173 server tests pass, including the new one, which
fails on the old code within a second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-06 22:23:45 -04:00
1 parent 9cc52beb09
commit fd71d876e1
6 files changed
+215 -7

No files matched your search

+28
View File
@@ -706,6 +706,34 @@ those, and `open_tasks` covers the backgrounded command, which has no subagent
to be found in the registry at all. Both are needed and neither subsumes the
other.
### A transcript outlives this enum (2026-09-06)
**The set of event kinds a transcript can hold only ever grows.** It is
append-only and permanent, so what this build *writes* is not what it may have
to *read*: a line can come from a newer server, or from an older one that wrote
a kind since dropped.
That was learned the expensive way. `Event::TaskNote` was added and removed
again within hours, and every transcript that had recorded one became
unreadable — `Transcript::open` parses every line, so `launch` failed for those
sessions and `SessionManager::new` skipped them. On the phone that is a session
with no status, no history and nothing sendable: one unfamiliar word took down
every live conversation it appeared in.
Two rules now. `Indexed::parse_at` degrades a line it cannot make sense of to
`Event::Unreadable { kind }` rather than failing the file, keeping its seq —
which is what the cursors, the bisection and the next-seq counter are all
addressed by — and carrying the word the line called itself, so the reader is
told what they are missing rather than that something is. The seq is still
required: a line that cannot say where it sits is not one this file can hold,
and dropping it silently would hand out a seq the file already contains.
And a variant is **retired, not deleted**: kept deserializable, never
constructed, with the date and the reason on it. `Event::TaskNote` is the
example, and the phone folds it to no row — which is the point, since an
unreadable line correctly draws a placeholder and one per background task is
the wall the row was removed for.
### A limit a subagent hits is the session's (2026-09-06)
A background Task runs on long after its parent's turn ended, so **the account