Commit Graph
2 Commits
Author SHA1 Message Date
Tianyi Cui f1dac1b1ed fix(session-persistence-sqlite): defer crash-tail repair to append; fix all-tail materialized flag; persist schema version (review #35)
- 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.
2026-06-16 00:05:25 +08:00
Tianyi Cui 9126697d87 feat(session-persistence-sqlite): second backend validating the abstraction
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".
2026-06-15 21:45:21 +08:00