Entry-write retirement: the tape is the only session log - #995
Open
ReganBell wants to merge 11 commits into
Open
Entry-write retirement: the tape is the only session log#995ReganBell wants to merge 11 commits into
ReganBell wants to merge 11 commits into
Conversation
Turns stop writing session_entries. The tape (messages + stamped meta + mirrors) is the single write path; model context, rendering, search, forks, pins, and deliveries all read tape projections. The entries table becomes a frozen archive with reads only via explicit archive paths. Deleted: the read-time coverage heal, replay.ts reconstruction fallback for model context, renderer/search/pins entries fallbacks, entries-vs- tape coverage cross-accounting, the replayPreamble, and the entries search-index writes. Fork seeding copies tape rows natively. clearSecurityTaint gains its tape-side counterpart. Outside-turn transcript writes go through a shared appendEntryOutsideTurn that allocates seqs against the projection and stamps coverage. Errored tool results render from a curated tape mirror so isError survives the cutover. Reconciled with the index-only chat search change: live turns no longer write entries, so the write-through trigger cannot see them; the tape-fed end-of-turn syncSearchIndex returns as the live indexer, with index-only reads and the store-level write-through (now serving archive writes and backfills) unchanged. The tape-index migration lands as sessions/store/0016 to leave the shipped 0012-0015 ids untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… ends With runtime entry writes retired, the write-through path cannot index a live turn, and a turn parked on an approval leaves its tape span open — its trigger would stay unsearchable until the turn completed. The end-of-turn sync moves to the lease-release sites every turn path flows through, and syncSearchIndex additionally indexes the contiguous run of stamp-final rows (stamped user carriers and entry mirrors) that directly extends the settled coverage. A gap occupied by unsettled drafts stops the run, so the index stays prefix-complete and a later settle still indexes the drafts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…luded The stamped-finals tail skipped overheard rows, so a trigger parked behind ambient channel traffic sat past a permanent gap and was never indexed until the turn settled. The tail now derives finals through userDraft — the same carrier shapes the settled projection renders — so overheard and delivery carriers advance the cursor and index the same text they later render with. Channel-shaped park test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es loud The flagged-input park writes a tainted user mirror and a settled turnEnd, then releases its lease on a path the main turn-exit sync never sees — a flagged first message in a fresh session stayed permanently unsearchable. All turn-exit sync sites now go through one helper that records a sync failure or an unservable projection to the error log instead of swallowing it silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A web-transcript delivery recorded outside any turn was invisible to search until the session's next turn exit, which may never come. The shared appendEntryOutsideTurn now runs the search sync after writing a searchable entry type; system records (turn failures, revisions) skip it. TranscriptAppendSessions widens to carry the two index methods the sync needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backfill's detector and verifier read only the frozen entries archive, so a session whose tape carried unindexed searchable messages reported covered and could never be repaired. The script now reads both sides: it indexes the archive rows and the tape projection's rows (store-level dedupe makes the union safe), verifies archive completeness and that indexed coverage reaches the projection's last searchable seq, and reports an unservable tape loudly instead of skipping it quietly. Smoke-verified against Postgres: a seeded coverage=-1 tape-only session reports '2 missing tape messages', indexes both, and re-runs as covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ojection The memorable provider's capture mined store.getEntries at turn end, so after the entry-write cutover it would capture nothing, silently (its tests stub the loader). The wiring now serves the capture window from projectedSessionHistory, which also covers pre-cutover sessions via the archive fallback. The AWS-cutover durable-redaction assertion had gone vacuous the same way — it now reads the projection and asserts the transcript is non-empty before checking it for sentinels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The forked session's history was invisible to search until the fork's first turn, whose exit sync then paid for indexing the whole copied history. The fork path syncs while it still holds the fork lease, so the copy is searchable the moment the fork exists. Abandoned-session catch-up (force-released leases) and fencing SessionStore.append as test-only are filed separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The open-span search tail required a top-level role on message rows, but claude and opencode carriers nest it (message.role / info.role), so a parked coarse-harness trigger was never indexed. Gate coarse rows the way the settled projection does: a stamped entrySeq plus a bareText or overheard meta marks a user carrier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The final step of the tape migration: turns stop writing
session_entries. The tape (messages + stamped meta + mirrors) is the single write path; model context, rendering, search, forks, pins, and deliveries all read tape projections. The entries table becomes a frozen archive (reads only via explicit archive paths); its export +DROPis a later, separate step.What this deletes: the read-time coverage heal, the replay.ts reconstruction fallback for model context, renderer/search/pins entries fallbacks, entries-vs-tape coverage cross-accounting, and the entries search-index writes. Fork seeding copies tape rows natively.
clearSecurityTaintgains its tape-side counterpart (columns and mirror payloads onsession_tape, in both stores). Outside-turn transcript writes (web deliveries, failure records) go through a sharedappendEntryOutsideTurnthat allocates seqs against the projection and stamps coverage. Also deliberately removed: the replay preamble — a session whose history cannot be reconstructed cold-starts with no synthetic context rather than a lossy preamble; that is intended, not an oversight.Design decisions baked in from review
Fork-race fixture and roster-change aborts. The fork-race test fixture hooked the retired
sessions.append, so it wedged once forks copy viaappendTape/appendRenderImport; it now hooksappendTapeon the first write to a session that is neither the source nor the known prior fork. Un-wedging it exposed a real masked regression: a turn aborted mid-flight by a project roster change returned refused without closing its tape span, leaving the trigger message permanently invisible (only bounded turns render). The terminal-failure recorder is extracted asrecordTurnFailureand runs on bothProjectRosterChangedexits — but only when the aborted turn actually appended tape rows, so a turn-start roster refusal leaves the tape byte-identical (asserted by the quarantine roster-epoch test).User-message loss on the terminal-failure path. The failure back-fill clear must not live at emit time: emit is a pure in-memory allocation, so a terminal failure between emit and the durable trigger write — including the trigger tape write itself failing — would skip the back-fill and lose the user's message. The clear lives in the turn's tape callback on the two durable user carriers: the bareText message branch, and annotation mirrors of non-overheard user entries (
mirrorsUserEntry, shared in session-store and reused by both stores). Tests cover the window; the trigger-tape-write-fails-terminally test fails against an emit-side clear.Archive-behind sessions: dedupe hardening + a hard sequencing gate, not an inline heal. Guarding
appendEntryOutsideTurnwith a heal would re-run the render-import machinery inside a delivery callback — heavy writes under a backfill lease, duplicatingscripts/lib/tape-retirement.ts— for a state the deploy sequencing gate exists to make unreachable. What is enforced in code forever is that durable records never double-write:tapeRecordedEntriesover mirrored annotation rows) in addition to the rendered view — a written record dedupes whether or not it renders. Mixed-state tests create the archive-ahead precondition and pin one durable record across repeated drains, decorator restarts, and repeatedrecordRunFailureEntrycalls.assessRenderImportrefuses a session whose tape carries rows stamped past the archive's last entry (skip reasontape-ahead,--forceincluded), so a backfill run can never write an anchor after live tape rows and cut live turns out of rendered history.latestEntrySeqserves without touching the entries archive; only the ambiguous window (mid-turn tails, genuinely archive-behind tapes) pays a one-row archive probe.On an archive-behind session, an outside-turn record is still written unservable (it dedupes but does not render) — accepted because the operator's sequencing gate makes that state unreachable in production.
Tool-error rendering survives the cutover. The SDK toolResult row deliberately carries no error flag (errors are agent inputs), so post-cutover every failed tool would render as a success and the curated extras (error, found:false, status, quarantined) would be gone.
recordResultstashes an errored result's curated payload by callId on the tool context ref; pi's message_end writer stamps the toolResult row with the emitted seq and follows it with an entry-mirror annotation carrying that payload; the projection treats the mirror as authoritative for stamped rows (draft suppressed, seq-slot arithmetic exact, model-facing replay payload untouched). Old unstamped rows keep drafting; foreign harnesses already mirror every entry. Pinned by a projection test (model-facing row says success, rendered entry says isError:true, byte-equal to the legacy transcript) and an agent-tools transport test.Also fixed along the way: the PG stamped-user-turn EXISTS saw its own just-inserted row, so the live-turns counter never moved for stamped rows (fixed + tested); first PG test for the tape-side taint-clear SQL;
projectedSessionHistorylogs when it falls back to the entries archive despite non-empty tape rows (the silent-stale-context state).Reconciliation with index-only chat search (#993)
#993 moved live search indexing to the entries write path (write-through on
session_entrieswrites) and deleted the end-of-turnsyncSearchIndex. This PR removes runtime entries writes, so that write path can no longer see live turns — combined naively, new messages would never be indexed. The union keeps both designs' intent:syncSearchIndexreturns as the live indexer. Where it runs: the turn's lease-release sites (the main turn finally, the background-run early release, and the security-screen park's own release), searchable outside-turn writes (appendEntryOutsideTurn, so a web-transcript delivery is indexed at write time), and fork creation (the copied history is searchable at fork time, not at the fork's first turn). A sync failure or an unservable projection is recorded to the error log, not swallowed. The one exit that cannot sync is a force-released lease (crashed worker / reaper sweep) — the session catches up at its next turn exit or via the backfill; live catch-up is filed as Search index catch-up for force-released sessions (worker/reaper) #1001, and fencing the test-onlySessionStore.appendas Retire SessionStore.append or fence it as test-only #1002.syncSearchIndexadditionally indexes the contiguous run of stamp-final rows — every stamped user carrier (overheard and delivery carriers included, so ambient channel traffic cannot wedge the tail) and stamped entry mirrors — that directly extends settled coverage. A gap occupied by unsettled drafts stops the run, so the index stays prefix-complete and a later settle still indexes the drafts. This preserves Make chat search fast and reliable using only its dedicated index #993's pinned contract that a message parked on an approval is searchable immediately.sessions/store/0016-tape-seq-indexes-v1, leaving the shipped 0012–0015 ids and checksums untouched.MERGE GATES — do not merge yet
assessRenderImportrefuses it afterwards (tape-ahead). A parity run started before deploy must not be read after it (post-cutover turns change what the report means).Verification
885 tests green across the affected suites (orchestrator, projects, turn-bookkeeping, session-fork, replay, revisions, tape/search/delivery/mirror suites, context, exemplars, authz, memory-provider, aws-role-cutover) plus PG store 55/55 against a real Postgres 16 (0016 migration, taint-clear SQL). The reworked backfill was smoke-verified against Postgres: a seeded coverage=-1 tape-only session reports its missing tape messages, indexes them, and re-runs as covered. Typecheck, eslint, oxlint, knip, prettier all clean.
🤖 Generated with Claude Code