refactor(session-persistence-sqlite): drop the materialized column; use row existence as the signal (review #35)

The `materialized` INTEGER column was redundant: create()/update() already
keep a lazy session in memory and write no row, so a `sessions` row is
written only by the first append. Its EXISTENCE is the materialization
signal — has()/list() now report exactly the sessions that have a row,
matching the JSONL backend's "file exists ⇔ materialized".

The column only existed to force has()/list() to FALSE for an all-tail
crash (a partial first turn, zero committed events). That actually
DIVERGED from the JSONL backend, whose file (and thus has()=true) survives
a first append that never reached turn/end. Removing the column drops that
special case: an all-tail session keeps its row and stays present, the
same as JSONL. The orphaned tail rows are still removed by the deferred
truncation-repair on the next append, and load() stays non-mutating.

Also: add a TODO to route through a cordis db service if one is adopted,
and correct the README's Node-version framing to the repo's engines (>=24).
This commit is contained in:
Tianyi Cui
2026-06-16 20:35:01 +08:00
parent c27b1bba6e
commit 3bef6b38c7
4 changed files with 41 additions and 44 deletions

View File

@@ -139,7 +139,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
await b2.dispose()
})
it('all-tail load: a session whose only content is a crash tail is absent from has()/list()', async () => {
it('all-tail load: a session materialized by a partial first turn stays present (JSONL parity), load returns zero committed events', async () => {
const path = await freshDbPath()
const m = meta('all-tail')
const b1 = await backend(path)
@@ -153,12 +153,15 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
await b1.dispose()
// A fresh backend loads it: the committed prefix is empty (no turn/end), so
// the session has no committed content. has()/list() must NOT report it.
// load returns zero events — but the session WAS materialized (its metadata
// row exists), so has()/list() still report it present, matching the JSONL
// backend whose file likewise survives a first append that never reached
// turn/end. The orphaned tail rows are removed by the next append's repair.
const b2 = await backend(path)
const loaded = await b2.ctx.sessionPersistence.load(m.id)
expect(loaded.events).toEqual([])
expect(await b2.ctx.sessionPersistence.has(m.id)).toBe(false)
expect((await b2.ctx.sessionPersistence.list()).map(x => x.id)).not.toContain(m.id)
expect(await b2.ctx.sessionPersistence.has(m.id)).toBe(true)
expect((await b2.ctx.sessionPersistence.list()).map(x => x.id)).toContain(m.id)
await b2.dispose()
})
@@ -269,7 +272,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
const path = await freshDbPath()
// Materialize a row with version 2 directly via the real schema.
const db = openDatabase(path)
db.prepare('INSERT INTO sessions (id, version, created_at, updated_at, materialized) VALUES (?, ?, ?, ?, 1)')
db.prepare('INSERT INTO sessions (id, version, created_at, updated_at) VALUES (?, ?, ?, ?)')
.run('v2', 2, 1, 1)
db.close()