docs: trim generated prose

This commit is contained in:
Tianyi Cui
2026-07-12 03:36:43 +08:00
parent 3dca90261c
commit 75838e10b5
323 changed files with 2857 additions and 11833 deletions

View File

@@ -72,19 +72,10 @@ function isHeaderLine(value: unknown): value is HeaderLine {
}
/**
* Encode an arbitrary string as a single safe path segment, injectively over
* ALL JS (UTF-16) strings — including lone surrogates. A {@link SessionId} is
* an unvalidated branded string, so this neutralizes `../`, absolute paths,
* NUL, and separators before any filesystem use.
* Encode an arbitrary string as a single safe path segment, injectively over ALL JS (UTF-16)
* strings — including lone surrogates. A {@link SessionId} is an unvalidated branded string,
* so this neutralizes `../`, absolute paths, NUL, and separators before any filesystem use.
*
* Each UTF-16 code unit is either kept literal (the safe set `[A-Za-z0-9_-]`)
* or escaped as `~XXXX` (its 4-hex-digit code unit). `~` is itself escaped, so
* the mapping is injective and reversible: distinct inputs never collide. We
* iterate code UNITS (`charCodeAt`), not code points, so a lone surrogate
* escapes to a distinct `~XXXX` instead of being normalized to U+FFFD (which
* `Buffer.from(…, 'utf8')` would do, breaking injectivity). `.` is in the safe
* set for readability but the whole-segment tokens `.`/`..` are escaped so they
* can never traverse.
* @param raw - the string to encode; must be non-empty (throws on `''`).
* @returns the escaped single path segment, decodable back to `raw`.
*/
@@ -141,39 +132,19 @@ export function eventLine(event: SessionEvent): string {
}
/**
* Parse a JSONL log buffer into its preserved event prefix (the header is line
* 0). Returns the longest prefix of complete, seq-contiguous events plus the
* byte offset of the end of the last preserved line (`committedBytes`).
* Parse a JSONL log buffer into its preserved event prefix (the header is line 0). Returns the
* longest prefix of complete, seq-contiguous events plus the byte offset of the end of the
* last preserved line (`committedBytes`).
*
* A crash can leave a durable log whose final turn never closed: real,
* fully-written events sit after the last `turn/end`. Those are PRESERVED (a
* single turn can be huge in a long-horizon task — truncating it would destroy
* real work); the backend closes the orphaned open turn with a synthetic
* `turn/end {kind:'interrupted'}` on reload (the session-persistence RFC). Only a TORN trailing
* fragment — a final line never fully flushed (no newline, unparseable, or a
* seq gap) — is excluded; it bounds the preserved region. A parse error or seq
* gap AT OR BEFORE the last committed `turn/end` is committed-data corruption
* and makes the session unloadable (throws).
*
* This relies on the session-log invariant that every event lives inside a turn
* (`Session.append` enforces it): only the final turn can be open, so the
* preserved tail is at most one unclosed turn.
* @param buffer - the raw bytes of the log file (header line first).
* @returns the header, the preserved event prefix, and `committedBytes` — the
* byte offset the next append truncates any torn tail to.
*/
export function scanLog(buffer: Buffer): { meta: SessionHeader; events: SessionEvent[]; committedBytes: number } {
const text = buffer.toString('utf8')
// Split into complete (newline-terminated) lines, tracking the byte offset of
// each line's end so the truncation point is exact (multi-byte chars make the
// char offset differ from the byte offset). A trailing line with no newline is
// an uncommitted crash fragment and is ignored — it is below the last
// turn/end by construction (the loop only flushes whole lines).
//
// Track the byte offset with a RUNNING accumulator (`endByte`), adding each
// line's byte length as we go. Recomputing `Buffer.byteLength(text.slice(0, i))`
// per newline would rescan the whole prefix every time — O(n²) over a long
// log (one assistant/chunk line per token makes that pathological).
// Split into complete (newline-terminated) lines, tracking the byte offset of each line's end
// so the truncation point is exact (multi-byte chars make the char offset differ from the
// byte offset).
const lines: { text: string; endByte: number }[] = []
let start = 0
let byteOffset = 0
@@ -201,14 +172,8 @@ export function scanLog(buffer: Buffer): { meta: SessionHeader; events: SessionE
}
const headerLine = parsedHeader
// Find the committed region: the prefix up to and including the LAST complete
// `turn/end` in the WHOLE log. Two passes so a crash tail after the last
// turn/end is tolerated, but corruption/gaps AT OR BEFORE the last committed
// turn/end make the log unloadable (committed data must never be silently
// dropped).
//
// Pass 1: parse every line that parses, recording (parsedOk, seq, isTurnEnd,
// endByte) per line index. Lines that fail to parse are holes.
// Find the committed region: the prefix up to and including the LAST complete `turn/end` in
// the WHOLE log.
interface Parsed { ok: boolean; event?: SessionEvent; endByte: number }
const parsed: Parsed[] = eventEntries.map((entry) => {
try {
@@ -226,18 +191,8 @@ export function scanLog(buffer: Buffer): { meta: SessionHeader; events: SessionE
if (p?.ok && p.event?.type === 'turn/end') { lastTurnEnd = i; break }
}
// Walk the longest PREFIX of complete, seq-contiguous, parseable event lines
// (line i is a parsed event with seq === i). This is the preservable region:
// it includes any fully-written events of an interrupted final turn AFTER the
// last turn/end — those are real, durably-written work and must NOT be
// truncated (a single turn can be huge in a long-horizon task; the orphaned
// open turn is closed with a synthetic turn/end on reload, not discarded —
// the session-persistence RFC). The walk stops at the first hole (unparseable line or seq gap):
// - if that hole is AT OR BEFORE the last committed turn/end, committed data
// was damaged → the session is unloadable (throw);
// - if it is AFTER (or there is no committed turn/end yet), it is the
// tolerated crash boundary — a torn final line never fully flushed — and
// it simply bounds the preserved tail.
// Walk the longest PREFIX of complete, seq-contiguous, parseable event lines (line i is a
// parsed event with seq === i).
const preserved: SessionEvent[] = []
for (let i = 0; i < parsed.length; i++) {
const p = parsed[i]

View File

@@ -1,19 +1,5 @@
/**
* JSONL durable session-persistence backend (`@deepseek-ai/dsh-session-persistence-jsonl`).
*
* One append-only `.jsonl` event log per session (a header line then one
* `SessionEvent` per line, verbatim including `assistant/chunk` so `seq` stays
* contiguous), with lazy materialization (no file until the first `append`),
* atomic first write, and load-time repair of a never-committed crash tail.
*
* The backend supplies ONLY the file-bytes storage primitives (the
* {@link PersistenceBackend} hooks below); all the write-path orchestration
* (the `session/event` → buffer → `session/flush` drain, per-session
* serialization, write cursors, fork-seed persistence, HMR live-adoption,
* crash-repair sequencing, dispose quiescence) lives in the backend-agnostic
* {@link PersistenceCoordinator} this class composes. The four public
* {@link SessionPersistence} methods delegate to the coordinator.
*
* @module @deepseek-ai/dsh-session-persistence-jsonl
*/
@@ -80,10 +66,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
constructor(ctx: Context, public config: Config) {
super(ctx)
// Resolve the configured root to an ABSOLUTE path ONCE, here. A relative root
// would otherwise re-resolve against `process.cwd()` at every later
// readdir/open — so if any plugin or test changed cwd between create, append,
// and load, one session's files could split across directories.
// Resolve the configured root to an ABSOLUTE path ONCE, here.
this.root = resolve(config.root)
this.coordinator = new PersistenceCoordinator<number>(this.ctx, this)
}
@@ -102,11 +85,8 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
return this.coordinator.load(id)
}
// `list` is BOTH the public service method and the PersistenceBackend hook —
// one method, the bucket walk below. The coordinator adds no orchestration for
// listing (no per-id serialization, no cursor), so it would just call back into
// this same method; routing it through the coordinator would recurse. Defined
// once, in the "PersistenceBackend hooks" section.
// `list` is BOTH the public service method and the PersistenceBackend hook — one method, the
// bucket walk below.
/**
* The per-session init promises, exposed for white-box tests that await a
@@ -201,10 +181,8 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
await mkdir(dir, { recursive: true, mode: 0o700 })
await this.syncDir(this.root)
const finalPath = logPath(this.root, meta.cwd, meta.id)
// Never rename over an existing committed log: materialize is the FIRST write
// of a session the backend believes is new. A file here means a different
// session shares this id on disk — reject loudly. (createCore already guards
// the create path, so this is unreachable-in-practice TOCTOU defense.)
// Never rename over an existing committed log: materialize is the FIRST write of a session
// the backend believes is new.
/* v8 ignore next 3 -- createCore guards collisions before materialize; this is a TOCTOU backstop */
if (await this.exists(finalPath)) {
throw new Error(`refusing to materialize "${meta.id}": a log already exists on disk (load/resume it instead)`)
@@ -229,10 +207,8 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
await link(tmp, finalPath)
linked = true
} finally {
// If link FAILED, the temp is the only reference and must be removed before
// the original error propagates. If it SUCCEEDED, defer temp cleanup to
// AFTER the publish is durable (below) so a temp-rm failure can never reject
// a session whose log already published.
// If link failed, the temp is the only reference and must be removed before the original
// error propagates.
/* v8 ignore next -- link failure is the TOCTOU/IO race guarded above; not reachable in test */
if (!linked) await rm(tmp, { force: true })
}
@@ -240,9 +216,8 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
// entry survives a power loss: the new link is not crash-durable until the
// parent directory's metadata is synced.
await this.syncDir(dir)
// Best-effort temp cleanup: the log is already published and durable, so a
// failure to remove the (now-redundant) temp hard link must NOT reject the
// append. Swallow only the rm failure; nothing else of consequence runs here.
// Best-effort temp cleanup: the log is already published and durable, so a failure to
// remove the (now-redundant) temp hard link must not reject the append.
try {
await rm(tmp, { force: true })
} catch {
@@ -351,9 +326,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
const entries = await readdir(this.root, { withFileTypes: true })
return entries.filter(e => e.isDirectory()).map(e => `${this.root}/${e.name}`)
} catch (error) {
// ENOENT = the root has not been created yet → genuinely no sessions. Any
// other error (EACCES, ENOTDIR, transient I/O) must NOT be reported as "no
// sessions" — a durable backend cannot silently pretend state is absent.
// ENOENT = the root has not been created yet → genuinely no sessions.
if (isENOENT(error)) return []
throw error
}

View File

@@ -48,11 +48,6 @@ runPersistenceContract('jsonl', async () => {
})
// Run the shared coordinator orchestration suite against the real JSONL backend.
// One temp root is the shared storage scope (two mounted instances over the same
// root = HMR/reload). `corruptTail` appends a partial, newline-less fragment to
// the session's .jsonl past the committed region — a never-committed torn tail
// that drives the coordinator's commitRepair-with-tornMarker branch over real
// file bytes.
runCoordinatorContract('jsonl', async (): Promise<CoordinatorFixture> => {
const dir = await mkdtemp(join(tmpdir(), 'dsh-jsonl-coord-'))
return {
@@ -340,10 +335,9 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1
].join('\n') + '\n'
// No committed turn/end, so the gap is a tolerated crash boundary: scanLog
// PRESERVES the contiguous prefix (turn/start seq 0) — real interrupted-turn
// work, not discarded — and stops at the gap. The orphaned open turn is
// closed by loadCore's synthetic turn/end, not here.
// No committed turn/end, so the gap is a tolerated crash boundary: scanLog PRESERVES the
// contiguous prefix (turn/start seq 0) — real interrupted-turn work, not discarded — and
// stops at the gap.
expect(scanLog(Buffer.from(log)).events.map(e => e.seq)).toEqual([0])
})
@@ -463,9 +457,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
})
it('list reads a header line longer than the 8KB read chunk', async () => {
// readFirstLine accumulates across reads when the first line exceeds its
// buffer. Plant a valid header whose line is > 8192 bytes (a long extra
// field is tolerated by the header type guard) and confirm list() reads it.
// readFirstLine accumulates across reads when the first line exceeds its buffer.
const bucket = join(root, '_no-cwd')
await mkdir(bucket, { recursive: true })
const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, pad: 'x'.repeat(9000) })
@@ -485,10 +477,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
for (const s of ctx.sessions.list()) await ctx.parallel('session/flush', s)
await sessFiberA.dispose()
// A NEW live Session object reuses id "reuse". The init cache is keyed by
// the Session OBJECT, so this gets its OWN onCreated (not A's stale promise)
// — which detects the on-disk collision and rejects, rather than silently
// appending the new session's events onto A's log under a stale cursor.
// A NEW live Session object reuses id "reuse".
const backend = ctx.sessionPersistence as unknown as { inits: Map<Session, Promise<void>> }
let b!: Session
await ctx.plugin(Object.assign((inner: Context) => {
@@ -506,13 +495,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
await ctx.sessionPersistence.append(SessionId('x'), oneTurnLog())
await ctx.fiber.dispose()
// Backend 2 over the SAME root. A live no-cwd session reuses id "x". Because
// loadLive(id, undefined) is the DEFINITE no-cwd bucket (NOT an all-buckets
// scan), case-2 adoption does NOT match the "/w" log — so it would NOT
// silently graft the no-cwd events onto the "/w" log with a mismatched cwd
// (the bug a non-scope-exact loadLive caused). It falls through to the
// new-session path, where createCore's any-cwd collision probe (loadStored)
// catches the duplicate id and REJECTS — the id is taken in another bucket.
// Backend 2 over the same root.
const ctx2 = new Context()
await ctx2.plugin(SessionStore)
await ctx2.plugin(SessionPersistenceJsonl, { root })
@@ -580,9 +563,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
})
it('list surfaces a non-ENOENT root error (ENOTDIR) instead of reporting no sessions', async () => {
// A durable backend must NOT collapse a storage fault to "no sessions". Point
// the root at a regular FILE: readdir then fails with ENOTDIR, which must
// propagate rather than be swallowed as an empty listing.
// A durable backend must not collapse a storage fault to "no sessions".
const filePath = join(root, 'not-a-dir')
await writeFile(filePath, 'x')
const ctx2 = new Context()
@@ -593,11 +574,8 @@ describe('SessionPersistenceJsonl: edge cases', () => {
})
it('loadLive surfaces a non-ENOENT lookup error (ENOTDIR) instead of reporting absent', async () => {
// A non-ENOENT error from the per-id open() must surface, not be collapsed to
// "not found" (which would let live-adoption proceed under a false absence
// assumption). A live session's onCreated reaches loadLive(id, cwd) →
// exists(logPath). Make that cwd's bucket DIRECTORY a regular file: open()ing
// `bucket/<id>.jsonl` under it then fails ENOTDIR.
// A non-ENOENT error from the per-id open() must surface, not be collapsed to "not found"
// (which would let live-adoption proceed under a false absence assumption).
const cwd = '/x'
const ctx2 = new Context()
await ctx2.plugin(SessionStore)
@@ -716,10 +694,9 @@ describe('SessionPersistenceJsonl: edge cases', () => {
it('Session.append rejects a non-serializable event at the source (never enters the log)', () => {
const session = ctx.sessions.create(SessionId('reject-bad'))
// Serializability is enforced at the source: Session.append throws on a
// BigInt-bearing event BEFORE it enters session.events, so the durable log
// can never diverge from the live log. The throw surfaces at the caller's
// append site, not asynchronously in a backend flush.
// Serializability is enforced at the source: Session.append throws on a BigInt-bearing
// event before it enters session.events, so the durable log can never diverge from the live
// log.
expect(() => {
session.append('user/message', { content: [{ type: 'text', text: 'bad' }], source: { kind: 'user' }, bad: 1n } as never, { surfaceOp: 'append' })
}).toThrow(/non-JSON-serializable/)

View File

@@ -1,19 +1,5 @@
/**
* SQLite durable session-persistence backend (`@deepseek-ai/dsh-session-persistence-sqlite`).
*
* A SECOND {@link SessionPersistence} implementation, built to validate that the
* abstract seam + the shared `runPersistenceContract` suite are genuinely
* backend-agnostic: the same append-only / contiguous-seq / lazy-materialization
* / interrupted-turn-close-on-load semantics the JSONL backend expresses over
* file bytes, expressed here over `node:sqlite` rows. Each `SessionEvent` maps
* 1:1 onto a row `(session_id, seq, type, time, data, source_event_seqs, surface_op)`.
*
* Like the JSONL backend it supplies ONLY the storage primitives (the
* {@link PersistenceBackend} hooks below — INSERT/DELETE/SELECT inside
* transactions); all the write-path orchestration lives in the backend-agnostic
* {@link PersistenceCoordinator} this class composes. The four public
* {@link SessionPersistence} methods delegate to the coordinator.
*
* @module @deepseek-ai/dsh-session-persistence-sqlite
*/
@@ -89,10 +75,8 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
constructor(ctx: Context, public config: Config) {
super(ctx)
// Open the database asynchronously (the parent directory may need creating);
// every hook awaits `ready` first. Opening synchronously would force a sync
// mkdir and block plugin apply. schemastery (static Config) has already
// filled `journalMode`; the cast records that runtime fact.
// Open the database asynchronously (the parent directory may need creating); every hook
// awaits `ready` first.
this.ready = this.openDb(config.path, (config as Required<Config>).journalMode)
this.coordinator = new PersistenceCoordinator<number>(this.ctx, this)
}
@@ -121,10 +105,8 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
return this.coordinator.load(id)
}
// `list` is BOTH the public service method and the PersistenceBackend hook —
// one method (the SELECT below). The coordinator adds no orchestration for
// listing, so routing it through the coordinator would just recurse. Defined
// once, in the "PersistenceBackend hooks" section.
// `list` is BOTH the public service method and the PersistenceBackend hook — one method (the
// SELECT below).
/**
* The per-session init promises, exposed for white-box tests that await a

View File

@@ -56,35 +56,15 @@ export interface EventRow {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
/**
* Open the database at `path` and apply the schema + pragmas. `foreign_keys`
* makes `ON DELETE CASCADE` drop a session's events with its row; the
* `journal_mode` pragma is set from the plugin's `journalMode` config (`wal`
* default — the durability model the ADR records; the row shape maps 1:1
* onto `SessionEvent`; opencode runs this exact shape on SQLite/WAL).
*
* The table-layout version is persisted in SQLite's `PRAGMA user_version` and
* checked on open: a fresh database (user_version 0) is stamped with the
* current {@link SCHEMA_VERSION}; an existing database whose version is NOT the
* current one (written by a different, incompatible build — older or newer) is
* REJECTED rather than opened against a layout this build does not understand.
* There are no migrations: an earlier layout is not upgraded in place — it is
* rejected. v1 had a different `sessions` shape; v2 lacked all of
* `seed_length`/`source_event_seqs`/`surface_op`. v3 is SKIPPED: two unmerged
* branches each shipped a DISTINCT v3 (one adding only `seed_length`, the other
* adding only the surface columns), so an on-disk v3 is ambiguous — it could be
* either sibling layout, neither of which has all of this build's columns. v4
* is the merged layout carrying every column; bumping past the collided v3
* makes the version check reject both sibling v3 databases instead of opening
* one against columns it does not have.
* Open the database, validate its version, and apply schema and pragmas.
* @param path - the SQLite database file to open (created when absent).
* @param journalMode - the journal pragma to apply — a closed in-code union, validated by the plugin Config.
* @param journalMode - validated journal pragma.
* @returns the open handle with pragmas applied and both tables ensured.
*/
export function openDatabase(path: string, journalMode: JournalMode): DatabaseSync {
const db = new DatabaseSync(path)
db.exec('PRAGMA foreign_keys = ON')
// journalMode is a closed in-code union (validated by the plugin Config), not
// user-controlled SQL — safe to interpolate (PRAGMA takes no bound params).
// The validated union is safe to interpolate into a non-bindable PRAGMA.
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`)
// `PRAGMA user_version` always returns exactly one row { user_version }.
const { user_version: onDisk } = db.prepare('PRAGMA user_version').get() as { user_version: number }
@@ -93,9 +73,7 @@ export function openDatabase(path: string, journalMode: JournalMode): DatabaseSy
throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`)
}
if (onDisk === 0) {
// Fresh (or pre-versioning) database: stamp the current layout version.
// PRAGMA does not accept bound parameters, so interpolate the integer
// constant (SCHEMA_VERSION is a trusted in-code number, not user input).
// Stamp fresh or pre-versioning databases.
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
}
db.exec(`
@@ -162,28 +140,10 @@ export function rowToEvent(row: EventRow): SessionEvent {
}
/**
* The preserved prefix of an ordered event-row list (mirrors the JSONL
* backend's `scanLog`): the longest prefix of complete, seq-contiguous,
* parseable rows, PLUS the seq from which a never-committed torn tail must be
* deleted (or `undefined` if the whole list is intact).
* The preserved prefix of an ordered event-row list (mirrors the JSONL backend's `scanLog`):
* the longest prefix of complete, seq-contiguous, parseable rows, PLUS the seq from which a
* never-committed torn tail must be deleted (or `undefined` if the whole list is intact).
*
* A crash can leave a durable log whose final turn never closed: real,
* fully-written rows sit after the last `turn/end`. Those are PRESERVED — a
* single turn can be huge in a long-horizon task, so truncating it would
* destroy real work; the backend closes the orphaned open turn with a synthetic
* `turn/end {kind:'interrupted'}` on load (the session-persistence RFC). The ONLY thing excluded is
* a torn trailing fragment — a row whose `data` never parses, or a seq gap —
* AFTER the last committed `turn/end`; that bounds the preserved region and its
* seq is returned as `tornFrom` so `load` can physically delete it.
*
* The last `turn/end` is computed from the `type` COLUMN (never parsing tail
* `data`), so a malformed `data` in an uncommitted tail row is discarded rather
* than making the session unloadable. A parse error or seq gap AT OR BEFORE the
* last committed `turn/end` is committed-data corruption and throws.
*
* This relies on the session-log invariant that every event lives inside a turn
* (`Session.append` enforces it): only the final turn can be open, so the
* preserved tail is at most one unclosed turn.
* @param rows - one session's event rows, ordered by seq ascending.
* @returns the preserved event prefix, plus `tornFrom` — the seq the physical
* delete starts at — when a torn tail exists.
@@ -207,12 +167,7 @@ export function scanRows(rows: readonly EventRow[]): { preserved: SessionEvent[]
if (parsed[i]?.ok && rows[i]?.type === 'turn/end') { lastTurnEnd = i; break }
}
// Walk the longest PREFIX of complete, seq-contiguous, parseable rows
// (row i has seq === i). This includes the fully-written rows of an
// interrupted final turn AFTER the last turn/end — real work, never
// truncated. The walk stops at the first hole:
// - at or before the last committed turn/end → committed corruption (throw);
// - after it (or no committed turn/end) → tolerated torn tail (stop).
// Walk the longest PREFIX of complete, seq-contiguous, parseable rows (row i has seq === i).
const preserved: SessionEvent[] = []
for (let i = 0; i < rows.length; i++) {
const p = parsed[i]

View File

@@ -41,10 +41,6 @@ runPersistenceContract('sqlite', async () => {
})
// Run the shared coordinator orchestration suite against the real SQLite backend.
// A FILE-backed db (not :memory:) is the shared storage scope so two mounted
// instances see the same rows (HMR/reload). `corruptTail` INSERTs a row past the
// committed seq whose `data` is invalid JSON — a never-committed torn tail that
// drives the coordinator's commitRepair-with-tornMarker branch over real db rows.
runCoordinatorContract('sqlite', async (): Promise<CoordinatorFixture> => {
const dir = await mkdtemp(join(tmpdir(), 'dsh-sqlite-coord-'))
const path = join(dir, 'sessions.db')
@@ -66,9 +62,8 @@ runCoordinatorContract('sqlite', async (): Promise<CoordinatorFixture> => {
})
describe('scanRows', () => {
// scanRows works off EventRows (data is a JSON string column); build them from
// SessionEvents so the unit tests read in terms of the event vocabulary. Surface
// fields are serialized to their nullable columns so a round trip is faithful.
// scanRows works off EventRows (data is a JSON string column); build them from SessionEvents
// so the unit tests read in terms of the event vocabulary.
const rows = (events: SessionEvent[]): EventRow[] =>
events.map((e) => {
const se = e as SessionEvent<SurfaceEventType>
@@ -256,11 +251,8 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
})
it('rejects a sibling v3 database (the merge-collided version) rather than opening it against missing columns', async () => {
// Two unmerged branches each shipped a DISTINCT layout under user_version 3
// (one added only `seed_length`, the other only the surface columns). The
// merged build is v4; an on-disk v3 is ambiguous and is missing at least one
// of this build's columns, so it MUST be rejected, not opened. Stamp a v3
// database and confirm the version check refuses it.
// Two unmerged branches each shipped a DISTINCT layout under user_version 3 (one added only
// `seed_length`, the other only the surface columns).
const path = await freshDbPath()
openDatabase(path, 'wal').close() // creates + stamps user_version = SCHEMA_VERSION (4)
const db = openDatabase(path, 'wal')
@@ -277,11 +269,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
await b1.ctx.sessionPersistence.append(m.id, oneTurnLog()) // committed: seqs 0..5
await b1.dispose()
// Hand-insert a torn tail row (seq 6, no closing turn/end) whose `data` is
// invalid JSON. The contract: only a parse error in the COMMITTED region is
// unloadable; a torn tail must be discarded. scanRows finds the last
// turn/end on the seq+type columns (never parsing tail `data`), so the
// unparsable row after it bounds the preserved prefix and is deleted by load.
// Hand-insert a torn tail row (seq 6, no closing turn/end) whose `data` is invalid JSON.
const db = openDatabase(path, 'wal')
db.prepare('INSERT INTO events (session_id, seq, type, time, data) VALUES (?, 6, ?, 7, ?)')
.run(m.id, 'turn/start', '{not valid json')

View File

@@ -1,26 +1,6 @@
/**
* The backend-agnostic write-path orchestration shared by every first-party
* {@link SessionPersistence} backend.
*
* Every durable backend needs the same orchestration: the in-memory bookkeeping
* (the per-id state, the write-behind buffers, the per-id serialization chains,
* the per-session init promises), the `session/event` → buffer → `session/flush`
* drain, lazy materialization, crash-tail repair on load, the four
* `session/created` adoption cases (new / HMR-adopt / collision /
* ownerless-claim), and dispose-time quiescence. Only the STORAGE primitives are
* backend-specific (file bytes for `dsh-session-persistence-jsonl`, `node:sqlite`
* rows for `dsh-session-persistence-sqlite`). {@link PersistenceCoordinator} owns
* the orchestration; a backend supplies the storage primitives as a small
* {@link PersistenceBackend} hook object.
*
* The abstract {@link SessionPersistence} service's public API is independent of
* this: a backend IS a `SessionPersistence` (its four public methods delegate to
* a coordinator it composes), so a third-party backend MAY implement the service
* directly without using the coordinator at all.
*
* See the write-coordinator RFC (docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)
* for the design rationale (composition over inheritance, the opaque torn marker).
*
* The backend-agnostic write-path orchestration shared by every first-party {@link
* SessionPersistence} backend.
* @module @deepseek-ai/dsh-session-persistence/coordinator
*/
@@ -30,16 +10,9 @@ import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-
import { assertSerializable, seedCoversPrefix } from './index.ts'
/**
* A stored session's durable prefix as read back from a backend: its
* {@link SessionHeader}, the preserved (seq-contiguous, parseable) event prefix,
* and an OPAQUE `tornMarker` that is present iff a never-committed torn tail must
* be truncated before further writes.
*
* The coordinator NEVER inspects `tornMarker`'s value — it only tests
* `!== undefined` (is there a tail to repair?) and passes the value back to
* {@link PersistenceBackend.commitRepair}. Each backend chooses its own marker
* type: the JSONL backend uses the byte offset to truncate to, the SQLite
* backend uses the seq to delete from (both happen to be `number`).
* A stored session's durable prefix as read back from a backend: its {@link SessionHeader},
* the preserved (seq-contiguous, parseable) event prefix, and an OPAQUE `tornMarker` that is
* present iff a never-committed torn tail must be truncated before further writes.
*/
export interface StoredPrefix<TornMarker = unknown> {
meta: SessionHeader
@@ -112,16 +85,11 @@ interface SessionState {
/** The next seq the backend expects to append (the stored log length). */
cursor: number
/**
* Whether the backend has physically written this session (a JSONL file /
* SQLite row exists). `create()` registers state LAZILY — cursor 0,
* materialized false, nothing on disk — so an empty session leaves no
* artifact and the FIRST `appendBatch` writes the header + its events in ONE
* transaction (the "a row exists ⇔ it has events" invariant `list`
* relies on; a separate up-front materialize could crash leaving a row with
* zero events). The flag is the only signal that distinguishes a session
* registered-but-never-written from one durably present, which the reclaim
* path needs (an abandoned id with no artifact AND no buffered events is free
* to reuse; a materialized one is a real collision).
* Whether the backend has physically written this session (a JSONL file / SQLite row
* exists). `create()` registers state LAZILY — cursor 0, materialized false, nothing on disk
* — so an empty session leaves no artifact and the FIRST `appendBatch` writes the header +
* its events in one transaction (the "a row exists ⇔ it has events" invariant `list` relies
* on; a separate up-front materialize could crash leaving a row with zero events).
*/
materialized: boolean
/**
@@ -224,10 +192,9 @@ export class PersistenceCoordinator<TornMarker = unknown> {
// Validate serializability BEFORE cloning so a bad event surfaces the typed
// error rather than an opaque DataCloneError from structuredClone.
assertSerializable(events)
// Deep-snapshot the batch HERE, before the op waits behind the per-session
// chain: a caller that mutates a live array (e.g. session.events) — or an
// event inside it — before the op runs would otherwise have those changes
// persisted. The clone is taken synchronously (at call time).
// Deep-snapshot the batch HERE, before the op waits behind the per-session chain: a caller
// that mutates a live array (e.g. session.events) — or an event inside it — before the op
// runs would otherwise have those changes persisted.
const batch = events.map(e => structuredClone(e))
return this.serialize(id, () => this.appendCore(id, batch))
}
@@ -268,11 +235,9 @@ export class PersistenceCoordinator<TornMarker = unknown> {
const { meta, events, tornMarker } = stored
this.assertVersion(meta)
// Crash-recovery: if the log ended mid-turn (real, preserved events but no
// closing turn/end), close it durably DURING load so disk, the returned log,
// and the cursor all agree. The interrupted turn's real events are preserved,
// never truncated (a turn can be huge — the session-persistence RFC); only a
// never-fully-written torn tail fragment is discarded.
// Crash-recovery: if the log ended mid-turn (real, preserved events but no closing
// turn/end), close it durably DURING load so disk, the returned log, and the cursor all
// agree.
const closers = interruptedTurnClosers(events)
const balanced = [...events, ...closers]
@@ -288,12 +253,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
return { meta, events: balanced }
}
// NOTE: there is deliberately no coordinator `list()`. Listing needs none of
// the coordinator's orchestration (no per-id serialization, no cursor, no
// in-memory state) — it is a pure read of stored metadata. A backend's public
// `list()` IS the {@link PersistenceBackend.list} hook (one method); routing it
// through the coordinator would only forward to that same hook, so the
// coordinator stays out of the listing path entirely.
// NOTE: there is deliberately no coordinator `list()`.
// --- per-id serialization + adoption helpers ---
@@ -395,9 +355,8 @@ export class PersistenceCoordinator<TornMarker = unknown> {
// against later mutation of the live event objects.
const seed = session.events.map(e => structuredClone(e))
const p = this.onCreated(session, seed)
// Attach a no-op rejection handler so a failing init does not surface as an
// unhandled rejection if no flush observes `p` before it rejects. The REAL
// error is still delivered: flush/dispose await the same `p` from the map.
// Attach a no-op rejection handler so a failing init does not surface as an unhandled
// rejection if no flush observes `p` before it rejects.
p.catch(() => { /* observed by flush/dispose via the stored promise */ })
this.inits.set(session, p)
return p
@@ -418,15 +377,6 @@ export class PersistenceCoordinator<TornMarker = unknown> {
/**
* On session/created: sync the backend's in-memory state to a live Session.
*
* Cases, by whether this backend tracks the id and whether an artifact exists:
* 1. Already tracked → no-op (or claim ownerless state if the seed matches,
* or reclaim a truly-abandoned id, else reject as a collision).
* 2. Not tracked, an artifact EXISTS at this cwd and is a seq-aligned PREFIX
* of the live events → ADOPT it (HMR/reload), persisting any live suffix.
* 3. Not tracked, an artifact EXISTS but is NOT a prefix → REJECT (collision).
* 4. Not tracked and NO artifact → a genuinely new session: register meta
* (lazy) and persist its seed once.
*/
private async onCreated(session: Session, seed: readonly SessionEvent[]): Promise<void> {
const id = session.header.id
@@ -436,16 +386,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
/* v8 ignore next -- initFor dedupes per session object; same-object re-entry can't occur */
if (tracked.owner === session) return
if (tracked.owner === undefined) {
// Ownerless state from the public create()/load() API. The FIRST live
// session claims it — but ONLY if BOTH the cwd scope and the seed match.
// The cwd guard mirrors case-2's cwd-scoped loadLive(): a same-id
// ownerless artifact at a DIFFERENT cwd is a collision, not a claim
// (claiming it would append the live cwd's events under the stored
// header's cwd, the exact cross-cwd corruption the loadLive scope
// prevents). The seed guard then ensures the live events reproduce the
// persisted prefix (else a fresh, unrelated session reusing the id would
// have its seq 0..cursor-1 events filtered as already-written and
// grafted on).
// Ownerless state from the public create()/load() API.
if (tracked.meta.cwd !== session.header.cwd) {
throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(tracked.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`)
}
@@ -519,10 +460,8 @@ export class PersistenceCoordinator<TornMarker = unknown> {
}
private async flush(session: Session): Promise<void> {
// Wait for the session's init (onCreated) so the state/cursor and any
// fork-seed persistence are in place before draining. Awaiting the same
// promise initFor stored also surfaces an init failure (e.g. a collision)
// here, where the caller of session/flush observes it.
// Wait for the session's init (onCreated) so the state/cursor and any fork-seed persistence
// are in place before draining.
await this.inits.get(session)
// Serialize the WHOLE drain (read cursor → append → splice) on the per-session
// chain so two concurrent flushes cannot both read the same cursor and
@@ -534,10 +473,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
private async drain(session: Session): Promise<void> {
const buffer = this.buffers.get(session)
if (!buffer?.length) return
// Copy WITHOUT removing: the buffer is the only durable-pending copy of these
// events. Drain it only AFTER the append commits; events pushed during the
// await sit past batch.length and survive the prefix splice, so a
// retry/dispose re-drains the rest.
// Copy WITHOUT removing: the buffer is the only durable-pending copy of these events.
const batch = buffer.slice()
const state = this.states.get(session.header.id)
// Only append events at or beyond the write cursor (a resumed session's seed

View File

@@ -1,23 +1,7 @@
/**
* The durable session-persistence seam (`ctx.sessionPersistence`): an abstract
* service defining WHAT a persistence backend does — durably store, reload,
* and list sessions — without saying HOW. Implementations subclass
* {@link SessionPersistence} and register themselves as the
* `sessionPersistence` service; `@deepseek-ai/dsh-session-persistence-jsonl`
* (an append-only JSONL log per session) is the first and
* `@deepseek-ai/dsh-session-persistence-sqlite` (`node:sqlite`, one row per
* event) is a second that validates the seam is backend-agnostic by passing
* the same `runPersistenceContract` suite. Further backends swap in an object
* store or a remote service without touching the consumers (the write-path
* plugin, the agent-loop resume seam).
*
* The persisted unit IS the existing {@link SessionEvent} — there is no
* parallel "persisted message" type the log must be converted to and from
* (faithful to the event-sourced model: the log is the single source of
* truth). Metadata that is NOT replayable conversation state (format version,
* cwd, lineage, seed boundary) travels separately as {@link SessionHeader},
* which is owned by `dsh-session` and re-exported here.
*
* The durable session-persistence seam (`ctx.sessionPersistence`): an abstract service
* defining what a persistence backend does — durably store, reload, and list sessions —
* without saying how.
* @module @deepseek-ai/dsh-session-persistence
*/
@@ -39,12 +23,10 @@ declare module 'cordis' {
}
/**
* Whether a live session's seed reproduces a persisted prefix exactly. Backends
* use this collision check to distinguish a legitimate resume/HMR rebind from a
* different live session reusing an existing session id.
* Whether a live session's seed reproduces a persisted prefix exactly. Backends use this
* collision check to distinguish a legitimate resume/HMR rebind from a different live session
* reusing an existing session id.
*
* The comparison includes the full event payload, not just seq/type/time, so a
* mutated seed cannot be grafted onto a durable log with the same envelope.
* @param seed - the live session's creation-time event snapshot.
* @param prefix - the persisted prefix the seed must reproduce.
* @returns `true` when the prefix fits within the seed and every event matches by JSON text.
@@ -72,32 +54,9 @@ export function assertSerializable(events: readonly SessionEvent[]): void {
}
/**
* Abstract durable session-persistence service. Subclass, implement the
* abstract methods, and load the subclass as a plugin — it registers as
* `ctx.sessionPersistence` (one implementation per context; loading a second
* throws, cordis' standard duplicate-service behavior).
*
* Contracts every implementation MUST honor (a DB backend asserts them inside
* a transaction; a file backend appends at EOF):
*
* - **Append-only; a crashed turn is closed, not truncated.** Committed events
* — those at or below a flushed `turn/end` — are never rewritten. A crash can
* leave an unclosed final turn whose events are real (and possibly large);
* {@link load} preserves them and closes the orphaned turn with synthetic
* boundary events (see {@link load}). Only a never-fully-written torn tail
* fragment is discarded.
* - **Contiguous seq.** A persisted log is contiguous: `events[i].seq === i`.
* {@link load} rejects a parse error or a `seq` gap in the COMMITTED region
* (unloadable); {@link append}'s first event `seq` MUST equal the backend's
* stored next-seq (after `load` has balanced any interrupted turn).
* - **JSON-serializable data.** `SessionEventMap` is merge-extensible and
* `event.data` is typed only as `SessionEventMap[K]`, so {@link append}
* REJECTS non-JSON-serializable data with an error naming the offending
* event type. A backend snapshots (serializes/clones) each event when it
* buffers, since `session.events` hands out the live mutable object.
* - **Durability.** {@link append} returns only once the batch is durable
* (the file backend fsyncs; a DB commits). {@link create} MAY defer the
* physical write until the first {@link append} (lazy materialization).
* Abstract durable session-persistence service. Subclass, implement the abstract methods, and
* load the subclass as a plugin — it registers as `ctx.sessionPersistence` (one implementation
* per context; loading a second throws, cordis' standard duplicate-service behavior).
*/
export abstract class SessionPersistence extends Service {
constructor(ctx: Context) {
@@ -125,26 +84,10 @@ export abstract class SessionPersistence extends Service {
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
/**
* Reload a session: its {@link SessionHeader} plus the event log up to the last
* durable checkpoint. Returns `meta` AND `events` so the live session is
* reconstructed with its `cwd`/lineage, not just its log.
* Reload a session: its {@link SessionHeader} plus the event log up to the last durable
* checkpoint. Returns `meta` AND `events` so the live session is reconstructed with its
* `cwd`/lineage, not just its log.
*
* The loop only flushes at `turn/end`, so a crash can leave a durable log
* whose final turn never closed: real, fully-written events sit after the last
* `turn/end`. Those events are PRESERVED — a single turn can be huge in a
* long-horizon task, so truncating it would destroy real work — and `load`
* CLOSES the orphaned turn by durably appending the minimal synthetic boundary
* events: an error `tool/result` for every `tool-call` the crash left
* unanswered (so the rehydrated history is a valid provider transcript — a
* dangling assistant tool-call is otherwise rejected), then a `step/end` if a
* step was open, then a `turn/end` carrying the `{ kind: 'interrupted' }`
* reason. The returned `events` therefore end on a balanced `turn/end` and are
* immediately usable as a session seed. Only a never-fully-written TORN tail
* fragment (a half-written final record) is discarded. Returned events are
* contiguous (`events[i].seq === i`); a parse error or a `seq` gap in the
* COMMITTED region (at or before the last real `turn/end`) makes the session
* unloadable (reject). Rejects an unknown format `version`. See the session-persistence RFC for
* the crash-recovery contract.
* @param id - the persisted session to reload.
* @returns the header plus the event log, ending on a balanced `turn/end` —
* immediately usable as a session seed.

View File

@@ -43,15 +43,8 @@ export function oneTurnLog(): SessionEvent[] {
}
/**
* Append a whole event log to a LIVE session, event by event, forwarding the
* surface metadata each event already carries. A bare `append(e.type, e.data)`
* over a `SessionEvent[]` widens the type argument to the union, where the
* typed overload's mandatory-marker rule collapses to optional — and `append`'s
* runtime guard then rejects a surface-eligible event with no marker. This
* helper forwards the `surfaceOp`/`sourceEventSeqs` VERBATIM from the source
* event (it does not synthesize a default), so a well-formed recorded log
* round-trips through a live session intact and a fixture that forgot a marker
* still trips the guard.
* Append a whole event log to a LIVE session, event by event, forwarding the surface metadata
* each event already carries.
*/
export function appendLog(session: Session, events: readonly SessionEvent[]): void {
for (const e of events) {
@@ -222,10 +215,9 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
it('append rejects non-JSON-serializable event data, naming the event type', async () => {
const { persistence, dispose } = await make()
try {
// Every value `isJsonValue` rejects must be rejected by the backend, not
// just BigInt — otherwise a backend could pass this contract while still
// accepting values that corrupt the durable round-trip. Each is a
// plugin-added `extra` field on a single user/message (seq 0).
// Every value `isJsonValue` rejects must be rejected by the backend, not just BigInt —
// otherwise a backend could pass this contract while still accepting values that
// corrupt the durable round-trip.
const cyclic: Record<string, unknown> = { type: 'text', text: 'x' }
cyclic['self'] = cyclic
const badValues: unknown[] = [

View File

@@ -1,28 +1,5 @@
/**
* Reusable ORCHESTRATION suite for any backend that composes a
* {@link PersistenceCoordinator}. Where {@link runPersistenceContract} (in
* contract.ts) pins the public read/write SEMANTICS, this suite pins the
* coordinator's WRITE-PATH ORCHESTRATION — the behavior that is identical across
* every first-party backend because it lives in the shared coordinator, not in
* the storage primitives: the `session/created` → `session/event` →
* `session/flush` → dispose drain, lazy materialization, fork-seed persistence,
* the four `onCreated` adoption cases (new / HMR-adopt / collision /
* ownerless-claim), crash-tail repair on load, and dispose-time quiescence.
*
* A backend imports {@link runCoordinatorContract} and calls it with a
* {@link CoordinatorFixture} factory that knows how to (a) mount the REAL
* backend plugin on a {@link Context} over a SHARED storage scope (so HMR/reload
* tests can dispose one instance and mount another over the same bytes/rows),
* and (b) inject a never-committed torn tail for one session
* ({@link CoordinatorFixture.corruptTail}) so the through-coordinator torn-tail
* repair branch is exercised against real storage. The suite drives everything
* through the PUBLIC {@link SessionPersistence} API + the cordis SessionStore
* write path — never the storage primitives directly — so it runs unchanged for
* every backend (memory / jsonl / sqlite).
*
* Each scenario lives here once and runs once per backend through the fixture;
* the per-backend specs keep ONLY their storage-mechanics tests.
*
* Reusable ORCHESTRATION suite for any backend that composes a {@link PersistenceCoordinator}.
* @module @deepseek-ai/dsh-session-persistence/tests/coordinator-contract
*/
@@ -124,10 +101,9 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
})
it('round-trips the seed boundary (seedLength) through persistence', async () => {
// A forked child records how many leading events were inherited via the
// seed; the boundary must survive a reload (so a resume/replay can tell the
// inherited prefix from the child's own events). Both backends carry it on
// the header — JSONL on the header line, SQLite in the seed_length column.
// A forked child records how many leading events were inherited via the seed; the
// boundary must survive a reload (so a resume/replay can tell the inherited prefix from
// the child's own events).
const fix = await makeFixture()
const { ctx, fiber } = await freshCtx(fix)
try {
@@ -303,10 +279,8 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
await ctx.parallel('session/flush', session)
// Hot-reload: dispose instance 1, mount instance 2 over the SAME storage
// while the session stays live. Instance 2 has an empty states map but the
// log is materialized and is a prefix of the live events — it must ADOPT
// (not reject). A second turn appended after reload then persists.
// Hot-reload: dispose instance 1, mount instance 2 over the same storage while the
// session stays live.
await backend1.dispose()
await fix.mount(ctx)
session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
@@ -398,9 +372,8 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
await first.fiber.dispose()
}
// A FRESH backend + a NEW live session with the same id but NO explicit
// resume. onCreated treats it as new; create() rejects because a log already
// exists. The rejection surfaces via the init promise (flush awaits it).
// A fresh backend + a NEW live session with the same id but NO explicit resume. onCreated
// treats it as new; create() rejects because a log already exists.
const second = await freshCtx(fix)
try {
const s2 = second.ctx.sessions.create(SessionId('collide'), { meta: { cwd: WORK } })

View File

@@ -16,18 +16,8 @@ type MemoryStore = Map<string, { meta: SessionHeader; events: SessionEvent[] }>
interface MemoryConfig { store?: MemoryStore }
/**
* A trivial in-memory {@link SessionPersistence} that composes a
* {@link PersistenceCoordinator} over a dependency-free `Map`-backed
* {@link PersistenceBackend}. It is BOTH the coordinator's reference vehicle
* (the simplest possible storage — a `Map<id, {meta, events}>` with no torn
* tails, so `tornMarker` is always undefined) and the cover for the abstract
* base's constructor + service registration. The real durable backends are
* `@deepseek-ai/dsh-session-persistence-jsonl` / `-sqlite`.
*
* The store can be supplied via config so two backend instances share one Map —
* the in-RAM analogue of two backends over the same file/db, which the
* coordinator orchestration suite's HMR/reload tests need (a fresh instance with
* an empty in-memory states map adopting an already-materialized session).
* A trivial in-memory {@link SessionPersistence} that composes a {@link
* PersistenceCoordinator} over a dependency-free `Map`-backed {@link PersistenceBackend}.
*/
class MemoryPersistence extends SessionPersistence implements PersistenceBackend<never> {
static inject = ['sessions']
@@ -123,11 +113,6 @@ runPersistenceContract('memory', async () => {
})
// Run the shared coordinator orchestration suite against the in-memory backend.
// A per-fixture Map is the shared "storage", so two mounted instances see the
// same materialized sessions (HMR/reload). `corruptTail` is OMITTED: a Map store
// writes atomically in RAM and has no torn tails, so the suite's torn-tail test
// self-skips (and asserts the omission). The real torn-tail repair branch is
// covered by the jsonl/sqlite fixtures, which CAN inject one.
runCoordinatorContract('memory', async (): Promise<CoordinatorFixture> => {
const store: MemoryStore = new Map()
return {