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.
5.2 KiB
RFC: Prune dead core-spine surface — SurfaceManager.invalidate(), the loop-internal exports, ToolExecutionResult.callId
Status: proposed
Problem
Three pieces of public spine surface share one defect class: their only possible role is to be ignored, or their trigger is unreachable.
SurfaceManager.invalidate()(packages/core/session/src/surface.ts). Its documented trigger — "the log has been replaced wholesale (e.g. after Session seed)" — is structurally unreachable: seeding happens inside theSessionconstructor,_surfaceis created lazily on first access, and the log reference is never reassigned afterward, so no constructedSurfaceManagerever observes a wholesale replacement. Sole caller: its own unit test. A rollback primitive protecting a scenario the implementation cannot produce.- The
runLoop,Inbox, andInboxMessageexports (packages/core/agent-loop/src/index.ts).runLoophas no importer outside the package — the only callers are the package's own internals (the agent constructs its loop with it), so the public re-export has zero consumers;Inbox/InboxMessagelikewise reach outside code only through the package's own inbox spec (switchable to the source module). The exports contradict the package's own docs — the inbox module doc says the public surface isAgent.send()/Agent.steer()— and the architecture dependency rule: nothing programs againstdsh-agent-loop; a replacement loop is a different bundle built ondsh-agent, not a consumer of this package's internals.ReactLoopAgentstays exported (cross-package tests construct it by package name). ToolExecutionResult.callId(packages/core/tools/src/index.ts; the inputToolExecution.callIdstays). Zero readers — and no listener can even construct a result:tools/pre-execute/tools/post-executelisteners return Decisions, the registry builds every result itself and always setscallIdto the inputexec.callId, and the post-execute dispatch snapshots the outcome before the waterfall precisely so a listener mutating the shared result reference cannot corrupt the id. The loop independently ignoresresult.callIdin favor of its owncall.id, and two regression tests exist solely to prove the field cannot matter (the loop's ignores-result-callId test and the registry's mutation guard). A field that is by construction a copy of its input, defended by snapshot machinery, and pinned by tests proving it is ignored is pure liability surface; the ACP bridge correlates via the session event'sdata.callId, never via the execution result.
Proposal
Delete the method and its test; delete the three export lines and their packages/core/agent-loop/README.md rows, pointing the inbox spec at the source module; drop the result field from the type, the registry's construction sites (the deny result, the dispatch result, toolErrorResult, and the post-execute snapshot's callId leg), the loop's ignore-comment, the proves-ignored regression test, and the mutation guard's callId assertions — the hazard they all pin disappears with the field, while the result's additionalContext ferry (a consumed post-execute channel) stays untouched. Update the ToolExecutionResult paste in tools.md (and its scripts/type-equiv.manifest.json row) and the result-shape row in packages/core/tools/README.md; for the invalidate() removal, amend the session-surface RFC's full-rebuild-after-wholesale-replacement sentence per implemented/AGENTS.md.
Sequencing: the in-flight surface-cache work (tool-pairing balance caching) neither uses nor touches invalidate, so that removal lands after or alongside it mechanically. The execute pipeline is tools/pre-execute → dispatch → tools/post-execute, and post-execute listeners receive the execution object alongside the result — nothing needs the result's own id.
Alternatives considered
Why not keep them?
A future consumer that swaps a session's log in place would want a reset primitive — it re-adds invalidate with itself. A replacement-loop author might want to reuse the inbox or the driver — the architecture already answers that a replacement loop is a different bundle. An isolated result-logging listener might want self-contained correlation on the result — the execution object is in scope at every listener, and a field that exists only to be ignored is worse than absent: it invites exactly the orphaned-pairing bug the loop comment warns about.
Acceptance criteria
invalidate()and the resultcallIdappear only in this RFC;runLoop/Inbox/InboxMessageremain package-internal only — no re-export from the package index and no outside-package importer; the agent-loop README lists only the consumed public surface; the inbox spec imports the source module.- The pre-/post-execute pipeline contract tests pass with the shrunk result type; the mutation-guard and proves-ignored tests shed their
callIdlegs with the hazard they pin.
Risks
All three are compile-visible removals with no runtime behavior change on any shipped path.