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.
4.7 KiB
RFC: Drop the mutable session summary
Status: implemented (proposed and accepted 2026-06-19)
Context
The session-persistence seam split a session's out-of-log metadata into two types owned by dsh-session: an immutable SessionHeader (version, id, createdAt, cwd?, parentSession?) written once at creation, and a mutable SessionSummary (updatedAt, title?, firstPrompt?) "updateable without touching the append-only log". Their union was SessionMeta = SessionHeader & SessionSummary, and the abstract SessionPersistence service carried a seventh method — update(id, summary) — for rewriting the summary. Each backend implemented the mutable store its own way: JSONL wrote a separate atomic .summary.json sidecar beside the log (temp-write + rename, best-effort), SQLite kept updated_at/title/first_prompt columns bumped inside the append transaction.
The summary was designed for a future session picker (recency ordering via updatedAt, a title/firstPrompt preview). That picker was never built. An audit of the whole repo found the entire SessionSummary surface is dead state:
SessionPersistence.update()has zero production callers (every.update(hit iscreateHash().update()or a test).firstPromptis never read anywhere in production.titleis read in the ACP bridge — but from a tool-call presenter (present.title), never from stored session metadata.updatedAthas no consumer: the only production caller oflist()readsmeta.cwd(aSessionHeaderfield) to validate a workspace onsession/load; resume readscreatedAt/cwd/parentSession— all header fields.- Decisively: the live
Session.headerwas already typedSessionHeader, notSessionMeta— the summary never existed on the live session object; it lived only in the persistence layer, written and read by nothing but its own contract test.
Decision
Delete the mutable session summary entirely. SessionSummary and the SessionMeta name are removed; the metadata a backend stores and returns is just SessionHeader. SessionPersistence.update() is removed from the abstract service and every backend. JSONL loses the whole sidecar machinery (writeSidecar/readSidecar/touchSummary/removeSidecars/sidecarPath and the load/list overlays); SQLite drops the updated_at/title/first_prompt columns and the per-append updated_at bump, and its SCHEMA_VERSION goes 1 → 2.
Anything the summary was meant to provide is derivable from the append-only log when a consumer actually needs it (firstPrompt = first user/message; recency = the last event's time or the file mtime) or already lives in the immutable header (createdAt, cwd). The one thing not derivable — a user-edited title — had no implementation and is pure YAGNI; it can return as its own log event or header field if a real feature ever needs it.
This is recorded as a decision because it is durable (it narrows a public service contract and an on-disk format across two backends), contested (the summary was a deliberate forward-looking design, not an accident), and surprising (a future reader finding SessionHeader where the original RFC describes SessionMeta would otherwise ask why the summary vanished). It also unblocks the shared persistence write coordinator: with no mutable summary, the coordinator's hook interface needs no updateSummary hook and the JSONL-sidecar-vs-SQLite-column durability divergence disappears, so the two backends' write paths converge.
No migration
This is unreleased software (see root AGENTS.md § "Pre-release stance: foundation over blast radius"), so there are no on-disk databases or logs to preserve. SQLite does not migrate a v1 database: the openDatabase guard now rejects any non-current on-disk user_version (onDisk !== 0 && onDisk !== SCHEMA_VERSION) — older or newer — so a stale v1 DB is cleanly rejected rather than half-read against the new column set. A fresh database stamps the current version; that is the only path that needs to work.
What we gave up
A future session picker now has to derive its preview/ordering from the log (or reintroduce a typed field) rather than reading a ready-made summary row. That is the correct cost: a cache for a feature that does not exist is dead weight that every backend pays to maintain and every contract test pays to assert. The principle — a passing test pins current behavior, not necessarily correct behavior; behavior can be an artifact of a past compromise — is now recorded as a standalone convention in root AGENTS.md, with this change as its worked example.