Reword the suite's module doc to describe what it IS — each scenario lives
here once and runs per backend through the fixture — rather than narrating
that the scenarios were previously duplicated in the per-backend specs. Per
the repo doc-current-state convention (no process/history in comments).
A reviewer found a cross-cwd hole: the ownerless-state claim path validated only
the seed prefix (via loadStored, any scope) and never compared the tracked
header's cwd to the live session's. So an ownerless `create(meta(id, "/a"))`
with cursor 0 (seed matches trivially) was claimed by a live session with the
same id at cwd "/b", and the "/b" events then appended under the "/a" header —
bypassing the cwd-scoped loadLive() guard that the HMR-adopt path (case 2) uses.
Add a cwd equality check before the seed check in the ownerless-claim branch: a
same-id ownerless artifact at a different cwd is a collision, not a claim. This
is a coordinator-level invariant (the live session's cwd must match the tracked
meta's cwd) and applies to both backends.
Tests (shared coordinator contract, run per backend): a live session at a
different cwd cannot claim cursor-0 ownerless state, cannot claim a
loaded-prefix even when the seed matches, and a no-cwd state cannot be claimed
by a cwd'd session. All fail without the guard.
Also documents WHY the `materialized` flag is needed (lazy create leaves no
artifact; it distinguishes registered-but-unwritten from durably-present for
has()/reclaim) and reframes the module doc to current-state, not the refactor
history (per the new AGENTS.md doc convention).
The JSONL and SQLite backends were byte-identical (or same-algorithm) for ALL
of their write-path orchestration — the four maps (states/buffers/chains/inits),
installWritePath, initFor, onCreated's four adoption cases, flush, drain,
serialize, adopt/adoptLivePrefix, assertVersion, and the create/append/load/
has/delete skeletons. Only the storage primitives (write bytes vs INSERT rows)
differed, so every fix landed twice.
Extract that orchestration into a PersistenceCoordinator in the seam package.
Each backend composes one (new PersistenceCoordinator(ctx, this)), implements a
small PersistenceBackend hook interface (loadStored, loadLive, appendBatch,
commitRepair, deleteStored, list, optional close), and delegates its six public
service methods to it. Composition, not inheritance — a backend exposes only the
hooks, can't reach the coordinator's private state, and the public
SessionPersistence API is unchanged so a third-party backend may still implement
it directly.
The crash-repair torn-tail token is OPAQUE: the coordinator computes the
synthetic closers (it owns interruptedTurnClosers) but only tests
`tornMarker !== undefined` and round-trips it to commitRepair, never inspecting
it (JSONL = byte offset, SQLite = seq). loadStored vs loadLive stay distinct so
HMR adoption is cwd-scoped (a same-id log at a different cwd is a collision, not
a resume). appendBatch carries meta so lazy-materialize + first-batch commit
atomically (no separate materialize hook).
Tests: the duplicated orchestration tests (adoption, HMR, collision,
dispose-drain, crash-tail) move into one runCoordinatorContract suite run once
per backend (memory + jsonl + sqlite) via hook fixtures; per-backend specs keep
only storage mechanics. A through-coordinator torn-tail test per real backend
keeps the commitRepair-with-marker branch covered under the 100% gate.
Net -112 lines (the dedup outweighs the new coordinator + shared suite); 100%
coverage; backends shrank ~1200 lines of duplicated churn. Migrates the
write-coordinator RFC proposed -> implemented.