Files
deepseek-harness/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
T
Tianyi Cui d6a2ab30c8 feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.

- Extract the `Branded<B>` primitive into a new standalone type-only package
  `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
  dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
  dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
  dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
  generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
  the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
  the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
  SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
  that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
  agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
  at the config boundary and the inner create()/resume casts disappear (only the
  genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
  Map keys and public params/exports (SessionStore, AgentRegistry + factory
  options, the ACP session-id surface + ToolPresenter CallId map, the
  persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
  the Branded type-equiv at dsh-brand, fix stale param types in the session/
  agent/bash READMEs, regenerate the cordis catalog + module graph.

Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
2026-06-21 07:19:59 +08:00

12 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 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

Two capability seams (interface / implementation / consumer) carry 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 declares four operations beyond create/append: load, list, has, delete (packages/session-persistence/session-persistence/src/index.ts:142-151). Production consumers of ctx.sessionPersistence use only two of them: the agent-loop resume path calls load() (packages/core/agent-loop/src/index.ts:176-194), and the ACP bridge calls list() for session/list (packages/ui/acp/src/index.ts). 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 are the contract suites and per-backend specs.

has() is not just unused — it is 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 (packages/session-persistence/session-persistence/src/coordinator.ts:298-310). delete() drags the deleteStored backend hook (coordinator.ts:99, coordinator.ts:313-319) that every backend must implement. This is the drop-mutable-session-summary pattern: a contract test exercises both, but no shipping code asks "is this session persisted?" or removes one.

BashExecutor.get() and .list()

The bash seam declares get(id) ("look up a background task by id") and list() ("all tracked background tasks") (packages/bash/bash/src/index.ts:88-107), both implemented by LocalBashExecutor (packages/bash/bash-local/src/index.ts:179-191). The sole production consumer — dsh-tool-bash — drives tasks via ownerOf, onTaskDone, start, readOutput, kill, resolve, run; it never calls get/list in shipping code, and there is no bash_list tool exposing a task roster to the model. So both are dead production seam surface. They are used by tests, more broadly than a single idiom: the bash seam/executor specs assert them directly (packages/bash/bash/tests/service.spec.ts, packages/bash/bash-local/tests/executor.spec.ts both call get()/list()), and several dsh-tool-bash tests reach through ctx.bash.get(id) to await a task's done, read its status, or inspect task fields (packages/bash/tool-bash/tests/tools.spec.ts, packages/bash/tool-bash/tests/integration.spec.ts). These are test-harness conveniences, not shipping consumers — but they are real test code an implementing PR must migrate or delete.

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's has/delete/deleteCore, and the PersistenceBackend.deleteStored hook. Remove the has/delete rows from the contract suite and the per-backend specs (jsonl + sqlite each implement deleteStored only 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.
  • BashExecutor.get() / .list(): delete the abstract declarations and the LocalBashExecutor impls. The seam/executor specs that assert get()/list() directly (bash/tests/service.spec.ts, bash-local/tests/executor.spec.ts) lose those assertions (the behavior is being removed). The dsh-tool-bash tests that reach through ctx.bash.get(id) to await done, read status, or inspect task fields switch to the public completion/status seam they should use — onTaskDone (or the done promise and status the start() return already exposes) — keeping their coverage without the removed lookup method.
  • Update every doc and source-comment reference to the removed methods — not only literal has(/delete(/get(/list(/deleteStored call 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 greps has/delete/get/list/deleteStored/{@link /six across docs/, packages/*/README.md, and source comments, and fixes each. The known doc sites: the seam READMEs (packages/session-persistence/session-persistence/README.md's has(id)/delete(id) API row and its "delegates its six public service methods" prose → four, packages/bash/bash/README.md's get(id)/list() row), the backend READMEs that describe has/list semantics (packages/session-persistence/session-persistence-sqlite/README.md, packages/session-persistence/session-persistence-jsonl/README.md — reword "absent from has()/list()" to just list()), 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 abstract create() JSDoc's {@link has}/{@link list} link (packages/session-persistence/session-persistence/src/index.ts — drop the has link), the coordinator's "six public methods"/"six public service methods" module + class JSDoc and its lazy-materialization JSDoc justifying the materialized flag by "the signal has/list rely on" (packages/session-persistence/session-persistence/src/coordinator.ts), the JSONL backend's loadStored/deleteStored comment, and the SQLite backend's schema.ts and index.ts comments that mention "absent from has/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, or a task executor "should" offer enumeration, 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). Each of these is one method to re-add the day a consumer needs it:

  • A session-management UI that deletes old sessions will want delete() — add it then, designed against that UI's real needs (soft-delete? cascade? confirmation?), not guessed now.
  • A bash_list tool that shows the model its running tasks will want list() — add it with the tool.

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/deleteStored are gone from the persistence seam, impl, and contract suites; pnpm run knip reports no new dead exports. (The bash get/list removal was reverted — see the implementation note above; those methods remain.)
  • The remaining seam operations (create/append/load/list for persistence; run/start/get/ownerOf/list/onTaskDone/readOutput/kill/resolve for bash) are untouched; ACP session/list, bash tool flows, and crash-recovery behave identically.
  • pnpm run test:coverage stays 100% per-file (the contract/spec rows for the removed persistence methods are deleted with them).
  • Persistence seam READMEs and docs/architecture.md no longer list the removed has/delete methods.

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 a deleteStored impl, which is a bounded edit in otherwise-out-of-scope packages.
  • list() on the bash seam is the natural seed for a future bash_list. Acknowledged in the pre-release foundation stance: add the seed when the tool lands. The executor still tracks tasks internally (the tasks map backs ownerOf/readOutput/kill); exposing an enumeration is a one-line re-add.
  • Low coupling. Both removals are confined to their seam + impl + tests; no cross-package consumer references the removed methods, so there is no ripple beyond the docs.

Modest size, but it converts two seams from "what an implementation must provide for nobody" back to "exactly what a consumer uses."