Define the in-file RFC contract in docs/rfc/README.md § The file format: the header block (`# RFC: <title>` plus a dateless Status enum cross-checked against the lifecycle folder), the per-lifecycle body skeleton (a Problem opener everywhere; Proposal/Alternatives considered/ Acceptance criteria/Risks in proposed/; present-tense Decision/ Consequences with proposal-era headings banned in implemented/; the frozen proposal shape in rejected/), and a mandatory Alternatives considered section with a date-fenced grandfather comment for pre-format RFCs whose alternatives are not reconstructible from the record. Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the enum, 29 Context openers become Problem, the 39 legacy-format XXX debt markers are resolved and banned from reappearing, proposal-era sections in implemented RFCs are rewritten to shipped reality (including the web/fs/subagent seam RFCs' migration plans and test checklists, closing the doc-tiers deferred-work item on the web seam), every RFC gains an Alternatives considered section or the grandfather comment, and the bilingual pair is re-mirrored and re-recorded. Move the generated index tables out of README.md into a fully generated docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and verify-rfc-classification checks its freshness and rejects index-shaped rows in the curated README — which makes room for the format contract to live in the README front door instead of a separate FORMAT.md. The decision record, and the first RFC written in the new format, is docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
4.8 KiB
RFC: Drop the mutable session summary
Status: implemented
Problem
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.
Consequences
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.