8.1 KiB
RFC: Prune dead methods from the persistence seam
Status: implemented (proposed and accepted 2026-06-20)
Implementation note (scope narrowed from the original proposal). This RFC proposed pruning dead methods from BOTH the persistence seam (
SessionPersistence.has()/.delete()) and the bash seam (BashExecutor.get()/.list()). Only the persistence removal shipped. The bashget()/.list()removal was reverted before merge: each is a one-line accessor over the executor's already-trackedtasksmap, and removing them forceddsh-tool-bash's tests onto a ~35-lineonTaskDone-based completion-tracking harness to replace the one-linectx.bash.get(id)lookup — the migration cost dwarfed the surface removed. Per the AGENTS.md "RFCs are proposals, not golden truth" principle, that friction is evidence the method earns its keep (a test harness IS a consumer that programs against the seam), soget()/list()stay. The bash-seam analysis below is retained for the record but was NOT acted on;BashTaskId-branding those methods lands in the branded-ids RFC instead. The persistence removal stands:has()/delete()had only contract-test callers and no test-ergonomics cost to remove.
Problem
A capability seam (interface / implementation / consumer) carries abstract methods that no consumer calls. The seam exists to let implementations and consumers evolve independently — but a method no consumer programs against is not a seam, it is speculative surface every implementation must still implement and test.
SessionPersistence.has() and .delete()
The abstract service declared its operations beyond create/append: load, list, has, delete. Production consumers of ctx.sessionPersistence use only two: the agent-loop resume path calls load() (packages/core/agent-loop/src/index.ts:176), and the ACP bridge calls list() for session/list (packages/ui/acp/src/index.ts:494). Grepping every sessionPersistence.* / persistence.* use across packages/*/src and examples/ finds no has( and no delete( on the service. The .has(/.delete( calls in packages/ui/acp/src/index.ts are on the in-memory SessionStore and a local Set of loading ids, not persistence. The only callers of has/delete were the contract suites and per-backend specs.
has() was not just unused — it was the most intricate branch in the shared coordinator: a tracked-vs-untracked dual-probe (loadLive(id, cwd) for a live-tracked session vs loadStored(id) for an untracked one) with a multi-line rationale. delete() dragged the deleteStored backend hook that every backend had to implement. This is the drop-mutable-session-summary pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one.
Proposal
Remove the methods nothing consumes, from the abstract seam, the implementation, and the contract/spec suites that exist only to exercise them:
SessionPersistence.has()/.delete(): delete the abstract declarations, the coordinator'shas/delete/deleteCore, and thePersistenceBackend.deleteStoredhook. Remove thehas/deleterows from the contract suite and the per-backend specs (jsonl + sqlite each implementeddeleteStoredonly to satisfy the hook — that implementation goes too). The backends are the dual-backend design and otherwise out of scope, but removing a hook they implement for no consumer is part of removing the hook, not a backend redesign.- Update every doc and source-comment reference to the removed methods — not only literal
has(/delete(/deleteStoredcall spellings, but also{@link has}/{@link delete}JSDoc links and prose that counts the methods (removing 2 of the persistence service's 6 public methods makes any "six public methods" phrasing wrong). The implementing PR grepshas/delete/deleteStored/{@link/sixacrossdocs/,packages/*/README.md, and source comments, and fixes each. The known doc sites: the seam README (packages/session-persistence/session-persistence/README.md'shas(id)/delete(id)API row and its "delegates its six public service methods" prose → four), the backend READMEs that describehas/listsemantics (packages/session-persistence/session-persistence-sqlite/README.md, packages/session-persistence/session-persistence-jsonl/README.md — reword "absent fromhas()/list()" to justlist()), the service-map / seam docs in docs/architecture.md, and the persistence prose in the session-persistence RFC and shared write-coordinator RFC. The known source-comment sites: the abstractcreate()JSDoc's{@link has}/{@link list}link (packages/session-persistence/session-persistence/src/index.ts — drop thehaslink), the coordinator's "six public methods"/"six public service methods" module + class JSDoc and its lazy-materialization JSDoc justifying thematerializedflag by "the signalhas/listrely on" (packages/session-persistence/session-persistence/src/coordinator.ts), the JSONL backend'sloadStored/deleteStoredcomment, and the SQLite backend'sschema.tsandindex.tscomments that mention "absent fromhas/list" — all reworded to the surviving four-method,list()-only contract.
Why not keep them as "the seam should be complete"?
The instinct that a persistence seam "should" offer delete is real — and it is exactly the speculative-completeness the pre-release stance warns against (AGENTS.md: optimize for the correct foundation, not for hypothetical callers you do not have). delete() is one method to re-add the day a consumer needs it: a session-management UI that deletes old sessions will want it — add it then, designed against that UI's real needs (soft-delete? cascade? confirmation?), not guessed now.
Re-adding a seam method with a live consumer is cheap and better-designed than the speculative version, because the consumer pins the contract. Carrying it unused means every implementation (and every future backend) must implement and test a method that does nothing.
Acceptance criteria
has/delete/deleteStoredare gone from the persistence seam, impl, and contract suites;pnpm run knipreports no new dead exports.- The remaining persistence operations (
create/append/load/list) are untouched; ACPsession/listand crash-recovery behave identically. pnpm run test:coveragestays 100% per-file (the contract/spec rows for the removed persistence methods are deleted with them).- The persistence seam README and
docs/architecture.mdno longer list the removedhas/deletemethods.
Risks
delete()is the kind of operation a product eventually wants. True — but "eventually" is the point. Deleting it now and re-adding it against a real consumer is strictly better than shipping a guessed contract. The dual backends each shed adeleteStoredimpl, which is a bounded edit in otherwise-out-of-scope packages.- Low coupling. The removal is confined to the persistence seam + impl + tests; no cross-package consumer references the removed methods, so there is no ripple beyond the docs.
Modest size, but it converts the seam from "what an implementation must provide for nobody" back to "exactly what a consumer uses."