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:
1 parent
9cc52beb09
commit
fd71d876e1
6 files changed
+215
-7
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user