Add a second axis to every RFC — its class (feature, bug-fix,
simplification, architecture, process, testing) — encoded in the path
as docs/rfc/{lifecycle}/{class}/file.md. The folder is the label, so
the closed set is enforced by structure rather than a parsed field.
Two new doc-sync gates back it:
- verify-rfc-classification: every RFC sits in a valid class folder and
the README index lists it under the matching lifecycle→class heading.
- verify-doc-refs: every docs/*.md path cited in a packages|examples TS
comment resolves — closes a drift class verify-md-links can't see, and
catches the four comment refs this reorg moved.
The README gains a Classification section explaining the taxonomy and
per-class index sub-sections. A self-referential process RFC records why
the scheme is path-encoded and gated.
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).
Codex's converge pass on PR B found a cross-cwd adoption hole: the coordinator
calls loadLive(id, session.header.cwd) for HMR live-adoption, but JSONL's
loadLive delegated to findLog(id, cwd) which, for cwd === undefined, scanned
ALL cwd buckets. So a live NO-CWD session could adopt a same-id log from a real
cwd bucket, ending with a live cwd: undefined but a persisted meta.cwd: '/w'.
loadLive must treat `undefined` as the DEFINITE no-cwd bucket, not "unknown":
it now goes straight to logPath(cwd, id) (which maps undefined -> _no-cwd),
never the all-buckets scan. loadStored/deleteStored keep the any-cwd scan
(resume/removal identify by id alone), so findLog is now a pure scan-all and
loses its dead cwd-direct branch.
The coordinator's has() relied on loadLive(id, undefined) meaning "any scope"
for an untracked id — fixed to use loadStored for the untracked (unknown-cwd)
case and loadLive only for a tracked session's known cwd.
Adds a regression test: a no-cwd live session reusing an id persisted in a real
cwd bucket no longer cross-cwd-adopts — it falls through to createCore's
any-cwd collision probe and REJECTS, leaving the original log untouched. Also
fixes the README to say `tornMarker !== undefined` (a marker may be falsy, 0).
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.