SessionSummary (updatedAt/title/firstPrompt) and SessionPersistence.update() were dead state: zero production callers of update(), no production reader of updatedAt/firstPrompt, and ACP's title comes from a tool-call presenter, not storage. The live Session.header was already typed SessionHeader, so the summary only ever existed in the persistence layer, written and read by nothing but its own contract test. Delete it entirely (no SessionMeta alias — SessionMeta collapses to SessionHeader everywhere). This removes the JSONL .summary.json sidecar machinery, the SQLite title/first_prompt/updated_at columns and per-append updated_at bump, and the update() method from the abstract service and both backends. SQLite SCHEMA_VERSION goes 1->2 and openDatabase now rejects any non-current user_version (older or newer) — no migration, unreleased software. Net -400 lines, and it erases the JSONL-sidecar-vs-SQLite-column durability divergence that the upcoming write coordinator would otherwise have to model. Records the decision in docs/rfc/implemented/2026-06-19-drop-mutable-session-summary.md and migrates the 2026-06-14 session-persistence RFC's facts to current truth. Adds a standalone AGENTS.md section "Tests document behavior, not golden truth" (a passing test pins current behavior, not necessarily correct behavior) with the summary-drop as its worked example, and reinforces the no-migration pre-release stance.
@deepseek-ai/dsh-session-persistence
The abstract durable session-persistence seam (ctx.sessionPersistence). Defines WHAT a persistence backend does — durably store, reload, and list sessions — without saying HOW. Mirrors the dsh-bash capability-seam template (capability seams): an abstract service here, a concrete implementation in a sibling package, consumers that inject the interface.
The persisted unit IS the existing SessionEvent (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage) travels separately as SessionHeader, owned by dsh-session and re-exported here.
Service API (ctx.sessionPersistence)
| Method | Contract |
|---|---|
create(meta): Promise<void> |
Register a new session's metadata. MAY defer the physical write until the first append (lazy materialization). |
append(id, events): Promise<void> |
Durably persist a batch (from the session/flush drain). Append-only; first event seq == stored next-seq after any repair; rejects non-JSON-serializable data naming the offending type. |
load(id): Promise<{ meta; events }> |
Reload meta + log. Preserves an interrupted (unclosed) final turn and closes it with synthetic closers — an error tool/result per unanswered tool-call, then step/end?+turn/end {interrupted} (a turn can be huge — never truncated); only a torn tail fragment is dropped. Events contiguous (events[i].seq === i); rejects a committed-region gap/parse error or unknown version. |
list(): Promise<SessionHeader[]> |
Lightweight listing from metadata, no full-log parse. |
has(id) / delete(id) |
Existence / removal. A zero-event lazily-materialized session is absent from has/list. |
Invariants every backend must honor
- Append-only; a crashed turn is closed, not truncated. Committed events (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;loadpreserves them and durably appends synthetic closers (an errortool/resultper unansweredtool-call, thenstep/end?+turn/end {interrupted}) to balance the log and keep the rehydrated history a valid provider transcript. Only a never-fully-written torn tail fragment is discarded. - Contiguous seq.
loadrejects aseqgap/parse error in the MIDDLE of the log;append's firstseqmust equal the stored next-seq. - JSON-serializable data.
appendrejects non-serializableevent.data; backends snapshot each event when buffering (the livesession.eventsobject is mutable). - Durability.
appendreturns only once the batch is durable.
Testing backends
Import runPersistenceContract from tests/contract.ts and call it with a factory that yields a fresh, empty backend plus a teardown. Every backend is held to the same append-only / contiguous-seq / lazy-materialization / serializability semantics; a backend's own spec adds implementation-specific tests (crash repair, path sanitization) on top.
Two backends run this suite: dsh-session-persistence-jsonl (append-only file log) and dsh-session-persistence-sqlite (node:sqlite, each SessionEvent one row (session_id, seq, type, time, data)). Both passing the same contract is the proof that the seam is genuinely backend-agnostic — lazy materialization, crash-tail-on-load, and contiguous-seq hold identically over file bytes and over a transactional store.
Metadata types
Re-exported from dsh-session: SessionHeader (immutable session metadata: version, id, createdAt, cwd?, parentSession?).