- load() no longer DELETEs the crash tail — it stays non-mutating w.r.t.
the event log and records a repair point (repairFrom). The next append
runs the DELETE inside its own transaction before inserting. This makes
the SQLite backend honor the SAME public contract as JSONL (load returns
the committed prefix; the subsequent append performs the one-time
physical truncation-repair), instead of mutating during load.
- All-tail load: when the discarded crash tail was the session's only
committed content (committed.length === 0), the metadata row still read
materialized = 1 from the prior append, so has()/list() reported a
session load() had just emptied. load() now flips the row's materialized
flag to 0 (metadata only — the orphaned event rows are still removed by
the deferred repair), so has()/list() are immediately consistent.
- Schema version: openDatabase now stores SCHEMA_VERSION in PRAGMA
user_version on a fresh database and rejects opening one whose
user_version is newer than this build supports, protecting against a
future incompatible layout.
Regression tests: load is non-mutating (tail rows survive until the next
append), all-tail load makes has()/list() false, and a newer-schema
database is rejected on open.
Add a SQLite SessionPersistence backend (node:sqlite), a SECOND
implementation built to prove the abstract seam + the shared
runPersistenceContract suite are genuinely backend-agnostic. Each
SessionEvent maps 1:1 onto an events row (session_id, seq, type, time,
data); append is an INSERT inside a transaction asserting the
contiguous-seq contract; the mutable SessionSummary lives in the
sessions metadata row.
It satisfies the SAME contract semantics as the JSONL backend, expressed
over rows instead of file bytes:
- Lazy materialization: create() records intent in memory; no row until
the first append (a never-appended session is absent from has()/list()
via a materialized flag set inside the first append transaction).
- Crash-tail-on-load: load() returns events only through the last
complete turn/end and deletes the uncommitted tail; a seq gap in the
committed region makes the session unloadable.
- Transactional append: a mid-batch failure (a UNIQUE seq collision from
a concurrent writer) rolls back entirely, keeping the cursor truthful.
Like the JSONL backend it is also the write-path plugin (session/event →
buffer → session/flush drain, onCreated seed/adopt/collision handling,
HMR seeding, dispose-to-quiescence). The package runs the shared
runPersistenceContract suite plus SQLite-specific tests (transaction
rollback, crash-tail cut, schema version, HMR adoption).
Docs flip every "SQLite is future/deferred" reference (ADR 0016,
architecture.md, the persistence module doc + README) to "implemented;
the contract holds both backends to identical semantics".