Files
deepseek-harness/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
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.
2026-07-05 22:58:25 +08:00

6.1 KiB

RFC: Prune dead methods from the persistence seam

Status: implemented

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 bash get()/.list() removal was reverted before merge: each is a one-line accessor over the executor's already-tracked tasks map, and removing them forced dsh-tool-bash's tests onto a ~35-line onTaskDone-based completion-tracking harness to replace the one-line ctx.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), so get()/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.

Decision

The methods nothing consumes are removed — from the abstract seam, the implementation, and the contract/spec suites that existed only to exercise them:

  • SessionPersistence.has() / .delete() are gone: the abstract declarations, the coordinator's has/delete/deleteCore, and the PersistenceBackend.deleteStored hook (jsonl + sqlite each implemented deleteStored only to satisfy the hook — those implementations went too). The backends are the dual-backend design and otherwise out of scope; removing a hook they implemented for no consumer is part of removing the hook, not a backend redesign.
  • Every doc and source-comment reference is updated to the surviving four-method, list()-only contract — not only literal has(/delete(/deleteStored spellings but {@link has}/{@link delete} JSDoc links and "six public methods" counts — across the seam and backend READMEs, docs/architecture.md, the session-persistence and write-coordinator RFCs, and the coordinator/backends JSDoc.

Alternatives considered

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.

Verification

has/delete/deleteStored are gone from the persistence seam, impl, and contract suites with no new dead exports; the remaining operations (create/append/load/list) are untouched, with ACP session/list and crash-recovery behaving identically; and the seam README and docs/architecture.md list only the surviving methods.

Consequences

  • 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 a deleteStored impl, 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."