docs: propose simplification RFCs

This commit is contained in:
Tianyi Cui
2026-06-20 16:33:03 +08:00
parent 0b0486796b
commit cc47f76cea
21 changed files with 546 additions and 0 deletions

View File

@@ -32,6 +32,25 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Optional Code Mode — model writes TypeScript against an SDK of all tools](proposed/2026-06-15-optional-code-mode.md) | 2026-06-15 |
| [Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)](proposed/2026-06-16-typed-event-schemas.md) | 2026-06-16 |
| [Unify the agent id and the session id](proposed/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 |
| [Retire mid-turn steering](proposed/2026-06-20-retire-mid-turn-steering.md) | 2026-06-20 |
| [Stop mirroring durable boundaries as agent events](proposed/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 |
| [Keep one public stop primitive](proposed/2026-06-20-public-agent-stop-surface.md) | 2026-06-20 |
| [Drop durable step boundary events](proposed/2026-06-20-drop-durable-step-boundaries.md) | 2026-06-20 |
| [Truncate interrupted final turns on load](proposed/2026-06-20-truncate-interrupted-turns.md) | 2026-06-20 |
| [Persist assembled assistant messages, not stream chunks](proposed/2026-06-20-assembled-assistant-messages-only.md) | 2026-06-20 |
| [Collapse trace-only session events](proposed/2026-06-20-collapse-trace-only-session-events.md) | 2026-06-20 |
| [Drop unused session lineage metadata](proposed/2026-06-20-drop-unused-session-lineage.md) | 2026-06-20 |
| [Make the bash tool foreground-only](proposed/2026-06-20-foreground-only-bash.md) | 2026-06-20 |
| [Drop bash full-output spill files](proposed/2026-06-20-drop-bash-output-spill-files.md) | 2026-06-20 |
| [Collapse tool-owned UI presentation](proposed/2026-06-20-generic-tool-rendering.md) | 2026-06-20 |
| [Drop ACP terminal `_meta` rendering](proposed/2026-06-20-drop-acp-terminal-meta.md) | 2026-06-20 |
| [Return the ACP bridge to one live session per connection](proposed/2026-06-20-single-session-acp-bridge.md) | 2026-06-20 |
| [Drop ACP session/load until resume has a product shape](proposed/2026-06-20-drop-acp-session-load.md) | 2026-06-20 |
| [Make the shared example base providerless](proposed/2026-06-20-providerless-example-base.md) | 2026-06-20 |
| [Classify product, integration, and support packages](proposed/2026-06-20-classify-support-packages.md) | 2026-06-20 |
| [Fold the persistence interface into dsh-session](proposed/2026-06-20-fold-session-persistence-interface.md) | 2026-06-20 |
| [Remove redundant recorded snapshot log goldens](proposed/2026-06-20-remove-redundant-snapshot-log-goldens.md) | 2026-06-20 |
| [Discover package inventories instead of maintaining static lists](proposed/2026-06-20-discover-package-inventory.md) | 2026-06-20 |
## Implemented

View File

@@ -0,0 +1,31 @@
# RFC: Persist assembled assistant messages, not stream chunks
Status: proposed
## Problem
The canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The persistence RFC chose this for token-level replay fidelity and contiguous `seq`, but the cost has grown: JSONL fixtures are dominated by tiny delta records, snapshot scenarios replay the model by grouping chunk events, ACP load reconstructs prior assistant output from chunks, and any future log reader must distinguish durable message history from token-level trace.
The loop already appends an assembled `assistant/message` for each step. That is the event `deriveMessages()` uses for the next model request. In other words, the resumable conversation state is already present without the chunks; chunks are a live rendering and deterministic-test artifact, not required conversation history.
## Proposal
Stop storing `assistant/chunk` in the canonical session log. The durable log keeps `assistant/message`, `tool/call`, `tool/result`, `usage` if retained, and turn boundaries. Live UIs can still receive token deltas through a deliberately transient stream event. Snapshot replay should move its model script into an explicit fixture sidecar or derive it from a recorded adapter artifact, rather than treating the canonical user session as a token tape.
ACP `session/load` can replay prior assistant messages as complete content blocks instead of simulating the original token stream. A loaded transcript need not reproduce every historical delta; it must show the same completed assistant content and resume with a valid provider history.
## Acceptance criteria
- `SessionEventMap` drops `assistant/chunk`, or marks it as non-persisted if a transitional live event is needed.
- Persistence docs no longer require every stream chunk to be stored verbatim.
- `llm-replay` and ACP snapshots use an explicit replay fixture format or sidecar for model chunks.
- `session/load` renders completed assistant messages from `assistant/message`.
- Stored logs get much smaller and remain `seq`-contiguous without chunk holes.
## What we give up
The canonical user session no longer reconstructs the exact token stream of an old turn. That is acceptable for resume and load, where completed message content is the user-visible state. Tests that need exact deterministic streams should own that fixture directly instead of smuggling it through the durable session format.
## Related
This supersedes the chunk-persistence choice in [session persistence](../implemented/2026-06-14-session-persistence.md) and affects [ACP snapshot tests](../implemented/2026-06-19-acp-snapshot-tests.md), whose current replay plugin derives its script from `assistant/chunk` events.

View File

@@ -0,0 +1,26 @@
# RFC: Classify product, integration, and support packages
Status: proposed
## Problem
`packages/` is flat. Core product packages, provider integrations, tool implementations, example UI support, and snapshot-only replay support all sit at the same level and look equally publishable. `packages/README.md` already has a `FIXME(package-hierarchy)` noting that `ui-stdio` and `llm-replay` were extracted from examples mostly for reuse and coverage. The flat layout makes support packages appear more foundational than they are and forces publish/lint/doc scripts to special-case intent in prose or static lists.
This is not just cosmetic. A package's location currently says little about whether it is core API, an integration, an example harness helper, or test infrastructure. That makes future removal harder because every top-level package looks like part of the same public surface.
## Proposal
Introduce an explicit package classification and move packages accordingly, for example `packages/core/`, `packages/integrations/`, `packages/tools/`, `packages/testing/`, and `packages/examples/`, or an equivalent structure decided in the implementing PR. The important part is that example/test support packages are not indistinguishable from product core.
This proposal does not delete `llm-replay` or `ui-stdio` by itself. It makes their status honest: either they graduate into product packages with documented consumers, or they live under a support/testing/example classification where release and compatibility expectations are lower.
## Acceptance criteria
- Each package has an explicit classification visible from path or package metadata.
- Scripts that publish, lint publishability, or generate module graphs use the classification instead of an ad hoc static list.
- Docs explain which package classes are part of the product API.
- YAML loader paths and TypeScript path aliases are updated in one coordinated move.
## What we give up
The restructure churns imports, workspace globs, docs links, and package paths. That churn is acceptable pre-release if it prevents the flat layout from fossilizing a support package as a product contract.

View File

@@ -0,0 +1,27 @@
# RFC: Collapse trace-only session events
Status: proposed
## Problem
The session event vocabulary includes first-class events that are not part of replayable conversation history and have little or no production consumption. `usage` is already present as a model stream chunk before the loop also appends a separate `usage` event. `error` duplicates the `turn/end { kind: 'error', message, code }` reason for loop failures; ACP settlement reads the turn-end reason, ACP rendering ignores the `error` event, and `deriveMessages()` skips it.
These events make the canonical transcript look more useful as telemetry than it currently is. They add event variants, invariants, tests, snapshots, and persistence cases, but they are not load-bearing for resume. The implemented [turn enclosure](../implemented/2026-06-15-turn-enclosure-invariant.md) already says post-turn operational diagnostics do not belong in the replayable session log.
## Proposal
Remove trace-only events from the canonical session log unless a production consumer needs them. Model usage can be derived from retained stream chunks, attached to `assistant/message`, or emitted on a separate telemetry channel. Loop errors should be represented by `turn/end.reason` for durable transcript semantics and `agent/error` or logging for operational diagnostics. Do not keep a parallel `error` event that consumers must reconcile with the final turn reason.
If analytics become real, add a projection helper or a dedicated telemetry store with its own retention policy. The user conversation log should contain what is needed to render, resume, and audit the interaction, not every metric-shaped detail the loop happened to observe.
## Acceptance criteria
- `SessionEventMap` drops `usage` and `error`, or folds their fields into nearby load-bearing events.
- The loop no longer appends a separate `usage` event for a usage chunk.
- The loop records durable failures only as `turn/end { kind: 'error' }` and reports live diagnostics through `agent/error`.
- ACP snapshots and persistence tests stop asserting trace-only lines.
- Documentation explains where token usage and operational errors are observed if they remain available.
## What we give up
A consumer can no longer filter the canonical log for `usage` or step-level `error` events. That is a real loss for future analytics and debugging, but there is no current production analytics consumer. Keeping a telemetry-shaped event in the replay log because it might matter later repeats the dead-summary pattern from [drop the mutable session summary](../implemented/2026-06-19-drop-mutable-session-summary.md).

View File

@@ -0,0 +1,26 @@
# RFC: Discover package inventories instead of maintaining static lists
Status: proposed
## Problem
Package and gate inventories are repeated by hand. `scripts/publint-all.ts` has a static list of publishable packages. The package cookbook tells authors to update several files. The package README carries a hand-written dependency graph. CI and development docs can drift from the actual `doc-sync` subcommands when new gates are added. These lists are small today, but every new package or gate creates another manual synchronization point.
Static lists are appropriate when they encode policy; they are needless friction when they duplicate manifest data that already exists in `package.json`, workspace globs, or package metadata.
## Proposal
Make package/gate inventories discoverable. Publishability should come from package metadata or classification, not from a static array in a script. Module graph generation should read package manifests. `doc-sync` should be the one command that defines and prints its sub-gates, with docs linking to that command rather than restating a second list.
This pairs well with [classifying support packages](2026-06-20-classify-support-packages.md), because discovery needs to know which packages are product-publishable, support-only, private, or examples.
## Acceptance criteria
- `publint-all` discovers publishable packages from manifests or a single classification source.
- Adding a package does not require editing a static package list for every gate.
- Docs describe the source of truth rather than repeating generated inventories.
- CI invokes the aggregate commands and lets those commands own their sub-gate lists.
## What we give up
Discovery scripts can become too clever. The implementation should stay boring: read manifests, filter on explicit fields, print the resolved list, and fail loud. The payoff is removing manual inventory drift, not inventing a build system.

View File

@@ -0,0 +1,25 @@
# RFC: Drop ACP session/load until resume has a product shape
Status: proposed
## Problem
ACP advertises `loadSession: true` and implements `session/load` by injecting persistence into the bridge, validating cwd against stored metadata, reconstructing an agent from the persisted log, and replaying prior transcript updates to the client. That path has its own race handling, loading-id guard, replay presenter logic, and tests. It also depends on the canonical log retaining enough UI data to reconstruct old chunks and tool presentations.
Durable persistence remains foundational, but editor-visible resume is not yet a designed product flow. There is no session picker, no title/preview metadata, and no clear UX for failed or partial loads. The bridge is paying complexity for a feature that is mostly exercised by tests and documentation.
## Proposal
For now, ACP starts fresh sessions only. `initialize` advertises `loadSession: false` or omits the capability, and `session/load` is unsupported. Persistence remains available to the agent loop and tests; resume can still exist as a lower-level factory if another consumer needs it. The editor bridge should reintroduce `session/load` alongside a real session-selection UX and a stable load transcript contract.
## Acceptance criteria
- ACP no longer injects `sessionPersistence` solely for `session/load`.
- `initialize` does not advertise load support.
- The `session/load` handler, loading-id tracking, cwd preflight for loaded sessions, and load replay tests are removed.
- Snapshot fixtures no longer rely on load replay presentation.
- ACP docs describe fresh-session support only.
## What we give up
An editor cannot reopen a prior persisted session through ACP. That is a real product feature, but the current implementation is ahead of the UX and ties the bridge to token-level log replay. Keeping persistence while dropping editor load narrows the bridge to the workflow it can currently present cleanly.

View File

@@ -0,0 +1,27 @@
# RFC: Drop ACP terminal `_meta` rendering
Status: proposed
## Problem
The ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented RFC deliberately avoided ACP's client-side `terminal/create` because bash execution belongs in the harness, but still adopted the reference agents' display-only `_meta` convention. That gives a nicer Zed card at the cost of bridge state, capability negotiation, terminal ids, special update mapping, text fallback tests, and exit-pill parsing in `dsh-tool-bash`.
The fallback path already exists: render the tool call and completed output as normal ACP content blocks. Non-Zed clients rely on that path anyway.
## Proposal
Ignore `clientCapabilities._meta.terminal_output` and render bash results through the plain ACP content path. Keep execution agent-side through `dsh-bash`; only the display-specific terminal metadata is removed. A terminal card can return later if ACP standardizes agent-executed terminals or if the product decides Zed-specific display is worth the maintenance cost.
This proposal is narrower than [collapsing tool-owned UI presentation](2026-06-20-generic-tool-rendering.md): it keeps generic `presentCall`/`presentResult` if those survive, but removes the terminal sub-shape and `_meta` mapping.
## Acceptance criteria
- ACP no longer reads or stores `_meta.terminal_output` capability state.
- `TerminalRendering`, terminal ids, terminal cwd resolution, and `_meta.terminal_*` update mapping disappear from `@deepseek-ai/dsh-acp`.
- `ToolTerminal` disappears from `@deepseek-ai/dsh-tools`, or is unused and deleted with the presentation cleanup.
- Bash result presentation no longer parses exit status for terminal pills.
- The implemented terminal-rendering RFC is superseded or moved to rejected with this proposal linked.
## What we give up
Zed users lose the dedicated terminal card: no cwd header, terminal display, or exit pill. They still see the command and output as plain content. That is a reasonable simplification while the ACP bridge is still unreleased and the `_meta` keys are a convention rather than a standard.

View File

@@ -0,0 +1,27 @@
# RFC: Drop bash full-output spill files
Status: proposed
## Problem
`dsh-bash-local` keeps bounded in-memory output and spills large stdout/stderr streams into private temp files. That requires a private directory, random owner-only file creation, close-failure handling, byte-offset incremental reads, lossy read reporting, path rendering in model-facing text, and cleanup discipline. The tool then tells the model to read a local spill path when output was truncated.
This solves a real problem, but in a narrow and leaky way. A spill path is a process-local filesystem artifact exposed to model output, not a durable harness artifact with scoped access, retention, or UI affordances. It also complicates background-task reads because a lossy incremental read has to point at one or two spill files.
## Proposal
Keep tail truncation, drop full-output spill files. A bash result contains the bounded tail plus a clear truncation marker; no path is emitted. If users need full-output recovery, add a generic artifact/blob service with explicit ownership, cleanup, and UI rendering, then let bash attach large outputs to that service.
This proposal can land independently of [foreground-only bash](2026-06-20-foreground-only-bash.md). If background tasks stay, `bash_output` should still report that output was dropped, but without advertising a spill path.
## Acceptance criteria
- `CollectedOutput` no longer carries spill paths.
- `OutputCollector` keeps bounded buffers only and deletes the temp-file machinery.
- `renderResult()` reports truncation without a filesystem path.
- Tests cover tail truncation and no longer assert full-output file contents.
- Security docs stop treating private spill files as a model-visible interface.
## What we give up
A model or user cannot recover the omitted prefix of a huge command output from a temp file. That is acceptable until there is a real artifact service. The current spill path is too much bespoke machinery for a feature whose lifecycle and permissions are not designed.

View File

@@ -0,0 +1,27 @@
# RFC: Drop durable step boundary events
Status: proposed
## Problem
The session log stores `step/start` and `step/end` events even though every step-scoped event already carries `{ turn, step }`: assistant chunks, assistant messages, tool calls, tool results, usage, and errors. `deriveMessages()` ignores step boundaries, ACP ignores them for UI, and the main consumers are invariants, tests, snapshot goldens, and crash repair.
The boundary events make the log more ceremonial than informative. The loop tracks open steps solely to close them, repair synthesizes `step/end` when a crash leaves a step open, invariants track a second nesting stack inside the turn, and snapshots carry lines that do not affect replayed message history. A model request that crashes before producing any step-scoped event is the only information represented by a bare `step/start`, and that case has no useful resumable content.
## Proposal
Make the turn the only durable boundary. Remove `step/start` and `step/end` from `SessionEventMap`; keep the numeric `step` field on events that need grouping. The loop increments the step counter and records step-scoped events with that number, but it no longer appends open/close boundary events. Consumers infer step groups from contiguous events sharing `(turn, step)`.
The invariants plugin should enforce that step-scoped events have valid positive step numbers within an open turn, not that separate boundary records surround them. Crash repair should not synthesize `step/end`; if [interrupted turns are truncated](2026-06-20-truncate-interrupted-turns.md), the repair path disappears entirely.
## Acceptance criteria
- `SessionEventMap` no longer includes `step/start` or `step/end`.
- The loop has no `closeStep()` finalization path.
- ACP snapshots and persistence contract fixtures stop expecting step-boundary lines.
- `deriveMessages()` and replay derive the same message history from step-scoped events.
- The event taxonomy docs describe turns as the durable boundary and steps as a field on step-scoped records.
## What we give up
The log no longer records "a model request started but produced no event before the process died" as a durable fact. That is acceptable: there is no assistant content, tool call, usage, or error to replay from that empty request. A live UI can still show an in-progress step from a transient event if it needs one; the durable log should not store an empty bracket.

View File

@@ -0,0 +1,26 @@
# RFC: Drop unused session lineage metadata
Status: proposed
## Problem
`SessionHeader.parentSession` records the session a new session was forked from. It is defined in `dsh-session`, preserved by persistence backends, copied through resume, documented as lineage metadata, and covered by round-trip tests. The repo has no production fork UI or sub-agent flow that reads it. The planned sub-agent/fork seam is still a TODO, so the field is currently stored future shape.
The cost is small per file but broad across the format: every backend schema and metadata serializer preserves a value that no feature uses. Because the header is an on-disk contract, even a placeholder field becomes something future refactors must either maintain, migrate, or deliberately break.
## Proposal
Remove `parentSession` from `SessionHeader` until a real fork/resume feature needs lineage. Forking can still seed a new session with prior events if such an API exists, but the durable parent pointer should be introduced alongside the feature that reads it and the UX that explains it.
If lineage returns, decide then whether it belongs in the immutable header, a session graph index, or a first-class event. The current field should not pre-commit that design.
## Acceptance criteria
- `SessionHeader` contains version, id, createdAt, and optional cwd only.
- JSONL and SQLite metadata schemas stop storing parent-session ids.
- Resume and list APIs no longer round-trip `parentSession`.
- Docs and tests remove fork-lineage claims that are not backed by a production consumer.
## What we give up
The codebase loses a ready-made lineage hook for future fork/sub-agent UX. That is intentional. The field is easy to reintroduce when the feature exists, and the unreleased stance lets the format change without migrations.

View File

@@ -0,0 +1,27 @@
# RFC: Fold the persistence interface into dsh-session
Status: proposed
## Problem
`dsh-session-persistence` is an interface package whose main concepts are already owned by `dsh-session`: `SessionHeader`, `SessionEvent`, `SessionId`, `session/event`, and `session/flush`. The package adds the abstract `SessionPersistence` service, the shared write coordinator, and contract helpers. Backend packages depend on it, and `agent-loop` has to optionally find a sibling service for resume.
The capability-seam split made sense when persistence was a new swappable backend design. After the mutable summary was removed, the interface package mostly wraps the session log's own storage concern. Keeping it separate may be more ceremony than clarity.
## Proposal
Move the abstract `SessionPersistence` service, the coordinator, and persistence contract helpers into `dsh-session`. Keep JSONL and SQLite as separate backend packages that register the session-owned service. This preserves backend swappability while deleting one support package and one cross-package seam.
The implementing PR should update the [capability seams](../implemented/2026-06-13-capability-seams.md) guidance with the exception: persistence is not like bash or LLM because its vocabulary and lifecycle events are already the session package's core domain.
## Acceptance criteria
- `@deepseek-ai/dsh-session-persistence` is removed as a package.
- `dsh-session` exports the persistence service type, coordinator, and contract helpers.
- JSONL and SQLite backend packages depend on `dsh-session` directly.
- `agent-loop` resume uses the session-owned service key.
- Persistence RFCs and package docs explain why backend implementations remain separate.
## What we give up
`dsh-session` becomes heavier: it owns both the in-memory log and the persistence interface. That is the trade. If third-party persistence backends were already a public ecosystem, the separate interface package would be a cleaner SDK boundary; pre-release, the extra package looks like abstraction before there is an external consumer.

View File

@@ -0,0 +1,27 @@
# RFC: Make the bash tool foreground-only
Status: proposed
## Problem
The bash capability seam supports both foreground commands and long-running background tasks. Background support is large: the abstract executor exposes `start`, `get`, `ownerOf`, `list`, `readOutput`, `kill`, and `onTaskDone`; the local executor tracks tasks, incremental reads, owner tokens, process cleanup, and completion listeners; the model sees three tools (`bash`, `bash_output`, `bash_kill`); the tool plugin injects completion notices back into the owning agent's session. Recent work added owner-token isolation because global predictable task ids become a cross-session read/kill hazard.
The cookbook already points at the real design smell: background bash is really generic long-running-tool infrastructure living inside one tool. If future tools need background execution, polling, kill, ownership, and completion notices, those semantics should not be hidden in `dsh-bash`.
## Proposal
Temporarily collapse `bash` to foreground-only execution. Remove `run_in_background`, `bash_output`, `bash_kill`, background task ownership, incremental task reads, completion injection, and task-listener APIs from the public bash executor seam. Long commands can still run with an explicit timeout; a command that needs to outlive a model step is not supported until a generic task service exists.
If long-running tasks return later, implement them once as a capability-agnostic task layer that owns ids, authorization, polling, cancellation, completion notifications, and any UI affordances. Bash can then opt into that layer like any other tool.
## Acceptance criteria
- `@deepseek-ai/dsh-tool-bash` registers only the `bash` tool.
- `BashExecutor` exposes `resolve()` and foreground `run()` only.
- `@deepseek-ai/dsh-bash-local` no longer tracks background task maps, owner tokens, task listeners, or incremental output cursors.
- ACP and snapshot fixtures no longer mention `bash_output` or `bash_kill`.
- The cookbook either removes the background example or redirects long-running work to the future generic task RFC.
## What we give up
The model loses the ability to start a server or long-running command, continue other work, and poll later. That is a real capability regression, but the current design makes one tool carry infrastructure that belongs above all tools. Foreground-only bash is smaller, safer, and easier to sandbox while the generic long-running-tool design is still absent.

View File

@@ -0,0 +1,31 @@
# RFC: Collapse tool-owned UI presentation
Status: proposed
## Problem
Tools can define `presentCall()` and `presentResult()` callbacks that return `ToolCallPresentation`, `ToolResultPresentation`, and optional `ToolTerminal` fields. The code itself flags the design as muddy: title, kind, raw input, content, terminal cwd, terminal output, exit code, and signal grew incrementally into a bag of optional fields. ACP then maintains pending call state to pair a result with the original args, creates replay-only presenters on `session/load`, and maps terminal subfields into Zed-specific `_meta`. `dsh-tool-bash` even parses exit status back out of rendered text because the pure replay-safe presenter no longer has the structured `BashRunResult`.
The real first-party use is bash presentation for ACP. That is too little evidence to freeze a cross-package UI presentation API.
## Proposal
Remove tool-owned UI presentation callbacks for now. The canonical tool events already carry the tool name, raw argument string, result content, and error state. UIs render a generic tool card from those fields. Tool-specific rich rendering can return later as a tagged render-intent union after there are at least two real tools and two real consumers to validate the vocabulary.
As a smaller alternative, replace the current optional-field bag with one explicit union in a single PR; but if the goal is simplification, the stronger move is to delete the callbacks and keep the generic path.
## Acceptance criteria
- `ToolDefinition` drops `presentCall` and `presentResult`.
- `ToolCallPresentation`, `ToolResultPresentation`, `ToolTerminal`, and `ToolCallKind` disappear unless a minimal generic UI type still needs one.
- ACP no longer keeps presenter pending state or calls tool callbacks during live streaming/load replay.
- `dsh-tool-bash` no longer parses rendered text to recover exit status for a UI pill.
- Snapshot goldens show generic tool cards and text results.
## What we give up
Bash loses its custom terminal-looking card and model-written description placement. The fallback remains reasonable: the command appears as tool input, and the output appears as text. Rich rendering should be designed when the product has enough UI/tool variety to justify a stable presentation contract.
## Related
This is the broad version of [dropping ACP terminal metadata](2026-06-20-drop-acp-terminal-meta.md). If this RFC is accepted, that narrower RFC becomes unnecessary.

View File

@@ -0,0 +1,27 @@
# RFC: Make the shared example base providerless
Status: proposed
## Problem
The examples have two shared base files: `examples/base-core.yml` is providerless, while `examples/base.yml` includes that core plus the real `llm-deepseek` adapter. Snapshot replay needs the providerless core with `llm-replay`, because loading the real adapter without a key throws. The normal demos need the real adapter. The result is a naming inversion: the file named `base.yml` is not the reusable base for all examples, while the true base is `base-core.yml`.
The split is understandable, but it makes every config explanation longer. It also leads to awkward test setup like a keyless smoke test carrying a dummy API key so an adapter can boot even though the model is not called.
## Proposal
Rename the providerless core to `examples/base.yml` and make adapter selection explicit in each concrete example. The coding and ACP real configs add a tiny `llm-deepseek` include or local block; snapshot config adds `llm-replay`. Delete `base-core.yml`.
The shared base should contain only provider-neutral services and tools: `llm`, sessions, system prompt, tools, agents, invariants, bash executor, and bash tool schemas. Anything that chooses a model provider belongs at the leaf config.
## Acceptance criteria
- `examples/base.yml` is providerless.
- `examples/base-core.yml` is deleted.
- Real demo configs explicitly add the DeepSeek adapter.
- Snapshot replay config includes the same providerless base and its replay adapter.
- README and RFC references stop explaining "base = base-core plus adapter".
## What we give up
Real demos lose one layer of convenience: each must opt into the adapter. That is the right default for examples, because adapter choice is the variable part and providerless wiring is the shared product core.

View File

@@ -0,0 +1,26 @@
# RFC: Keep one public stop primitive
Status: proposed
## Problem
The public `Agent` handle exposes three ways to reason about stopping work: `abort(reason?)`, `cancel(reason?)`, and `whenIdle()`. `abort()` kills only the in-flight step and leaves queued work alone; `cancel()` clears queued and steering work, aborts the running step, and handles the pre-step race; `whenIdle()` exposes the loop's private quiescence waiter to any consumer. In production, ACP uses `cancel()` for `session/cancel`, while lifecycle owners tear down agents through `AgentHandle.dispose()`. No production caller needs bare `abort()` or `whenIdle()`.
The extra surface area makes the loop carry public semantics that are mostly teardown internals. `whenIdle()` needs waiter state, special disposed-agent behavior, and a loop-exit promise so it resolves after quiescence rather than merely after a status flip. `abort()` has to be documented as distinct from queue-aware cancellation even though a UI cancellation almost always wants the broader operation.
## Proposal
Keep `cancel()` as the only public stop primitive on `Agent`. Lifecycle owners use `AgentHandle.dispose()` to stop and unregister an agent; non-owners use `cancel()` to abandon current and queued work. The implementation can keep private abort controllers and quiescence promises, but they are not part of the plugin-facing `Agent` contract.
Delete public `abort()` and `whenIdle()`, the tests that exercise them as standalone API, and the docs that describe step-only abort as an embedding feature. The disposer remains async and still waits for the loop to stop; that guarantee moves entirely onto `AgentHandle.dispose()`.
## Acceptance criteria
- `Agent` exposes `send()`, `inject()`, `cancel()`, status, options, session, and identity, with no public `abort()` or `whenIdle()`.
- ACP cancellation continues to call `cancel()`.
- Agent teardown continues to await quiescence through handle disposal.
- Tests cover cancellation and disposal as the two supported stop paths.
## What we give up
A future plugin cannot abort only the current model/tool step while preserving queued prompts through the public interface. If that use case becomes real, it should return with a named consumer and a narrower contract. Today it is latent generality that keeps private loop mechanics public.

View File

@@ -0,0 +1,30 @@
# RFC: Stop mirroring durable boundaries as agent events
Status: proposed
## Problem
The loop records the canonical transcript in `SessionEvent` and also emits a parallel set of live `agent/*` mirror events: `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end`, `agent/queued`, and `agent/steering`. The mirrors make consumers choose between two sources of truth. ACP already chose the session log for the editor-facing transcript because a throwing peer listener can prevent later `agent/*` listeners from observing a boundary, while the session event was already appended. The stdio UI is the only production consumer that still renders primarily from the mirror stream.
This duplication is not free. Every lifecycle change has to update the session event, the mirror event, docs, invariants, tests, and snapshot expectations. The duplicate boundary events also make failure ordering subtle: a turn can be durably closed before a live `agent/turn-end` listener runs, so a post-boundary listener failure has no valid in-log position left and must be reported out of band.
## Proposal
Make `session/event` the live transcript stream. Consumers that render turns, tool calls, tool results, assistant messages, and durable boundaries subscribe to `session/event` and derive their UI from the same event vocabulary persistence uses. Keep agent lifecycle/control events that are not transcript data: `agent/created`, `agent/disposed`, `agent/status`, and `agent/error`. Keep any live-only token stream only if the canonical log separately stops storing chunks; otherwise `assistant/chunk` session events cover that too.
Remove the duplicate durable-boundary mirrors from the agent event taxonomy. If a UI wants an agent handle from a session event, it can keep a small map from session id to agent built from `agent/created`/`agent/disposed`, or the registry can offer an explicit lookup. The canonical record remains the event-sourced session log.
## Acceptance criteria
- ACP and stdio render transcript content from `session/event`.
- `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end`, and `agent/steering` are removed or reduced to private implementation details.
- Tests assert the persisted event stream, not a second mirror stream, for turn and step ordering.
- Documentation presents `SessionEvent` as both the durable source and the live transcript feed.
## What we give up
A plugin can no longer observe turn/step boundaries from a convenient `Agent`-first event. It must either subscribe to `session/event` or maintain a session-to-agent association. That is an acceptable trade: transcript consumers should not depend on a second event feed that can drift from the durable log.
## Related
This is compatible with [assembled assistant messages only](2026-06-20-assembled-assistant-messages-only.md), but the exact fate of `agent/stream-chunk` depends on that decision. If chunks leave the canonical log, `agent/stream-chunk` can remain as a deliberately live-only UI signal while the other mirror events disappear.

View File

@@ -0,0 +1,27 @@
# RFC: Remove redundant recorded snapshot log goldens
Status: proposed
## Problem
Recorded ACP snapshot scenarios ship both `session.jsonl` and `session.golden.jsonl`. For normal recorded scenarios, `session.jsonl` is the replay fixture harvested from a real run, and the replay test normalizes the newly persisted log and compares it to `session.golden.jsonl`. In the current fixtures, the normalized recorded log and normalized golden are identical for the ordinary recorded scenarios.
The duplicate file can help review by showing "expected persisted log" separately from "model replay input", but for recorded scenarios those are intentionally the same artifact. Keeping both means a re-record churns two files with the same semantic content.
## Proposal
For recorded scenarios, compare the replay run's normalized session log directly against normalized `session.jsonl`. Keep explicit `session.golden.jsonl` only for authored scenarios where `replay.override.json` drives behavior that is not derivable from the fixture, or where the expected persisted log intentionally differs from the replay script.
Stdout goldens remain unchanged; they are the editor-facing projection and are not redundant with the session fixture.
## Acceptance criteria
- Recorded scenarios stop committing `session.golden.jsonl`.
- The snapshot test derives the expected session log from `session.jsonl` for `recorded: true` scenarios.
- Authored sidecar scenarios keep explicit session goldens when needed.
- Orphan-fixture guards understand which files are required by scenario kind.
- The snapshot-test RFC is updated to describe the reduced fixture set.
## What we give up
Reviewers lose one redundant artifact that made the expected persisted log visually separate from the replay fixture. The stdout golden still protects the editor transcript, and comparing replay output to the recorded fixture preserves the loop/persistence regression check without duplicating files.

View File

@@ -0,0 +1,31 @@
# RFC: Retire mid-turn steering
Status: proposed
## Problem
The agent exposes two user-message paths that look close but have different lifecycle semantics: `send()` queues a normal user turn, while `steer()` injects a message between steps of the currently running turn and falls back to `send()` when idle. That distinction leaks through the whole stack: `Agent.steer()` is public API, the session log has a durable `steering/message` event, the agent event taxonomy has `agent/steering`, the loop maintains a steering FIFO beside the queued-message FIFO, cancellation clears both queues, and `deriveMessages()` has to render steering as a tagged synthetic user message rather than a normal prompt.
The continuation seam amplifies the cost. `agent/turn-continuation` defaults to `hadToolCalls || steeringInjected`, so a same-turn steering message can force the loop to call the model again even if the model did not ask for tools. The comments name future `/goal`, `/loop`, and budget-guard uses, but the current repo has no production listener. The only production UI that mentions steering is the stdio demo; ACP already sends prompts through the ordinary queue while a turn is running.
## Proposal
Delete mid-turn user steering for now. `Agent.send()` becomes the single public way to submit user content; when the agent is running, the content waits for the next turn. The loop continues within a turn only for tool calls, not because a user typed while a step was running. A caller that wants to interrupt the current turn uses `cancel()` and then `send()`.
Remove `Agent.steer()`, the steering FIFO, `steering/message`, `agent/steering`, steering-derived continuation, and the cancellation logic that distinguishes queued messages from steering messages. Revisit `agent/turn-continuation` at the same time: if there is still no production listener, remove the waterfall too and let the loop continue only on the closed set of reasons it owns. If a real budget or goal plugin later needs forced continuation, it should reintroduce a narrower seam with that plugin as the concrete consumer.
## Acceptance criteria
- `Agent` exposes one user-message entry point, `send()`.
- The durable session event vocabulary no longer contains `steering/message`.
- `deriveMessages()` renders normal user messages and context injections, with no steering tag path.
- The loop has one queued-message FIFO and no same-turn user-message continuation path.
- The stdio UI and docs describe input while running as queued next-turn input.
## What we give up
A user cannot add same-turn steering content while a model is between tool steps. That behavior is useful in theory for "while you are already working, also consider X", but it is not the behavior ACP exposes today and it makes the turn boundary much harder to reason about. The simpler behavior is reasonable: user input becomes the next prompt, and cancellation remains the explicit tool for replacing in-flight work.
## Related
This pairs naturally with [dropping durable step boundaries](2026-06-20-drop-durable-step-boundaries.md), because removing same-turn steering leaves tool calls as the only reason a turn contains multiple model steps.

View File

@@ -0,0 +1,27 @@
# RFC: Return the ACP bridge to one live session per connection
Status: proposed
## Problem
The ACP bridge now supports multiple live sessions on one JSON-RPC connection. That capability brings multi-entry session maps, reverse session/agent lookups, per-session prompt state, loading ids, demux for every event, cross-session teardown, and isolation concerns for future permission prompts and background tasks. A separate proposed RFC still tracks the unfinished permission-ownership piece.
The product has not yet proven it needs concurrent editor conversations over one harness process. The snapshot replay tier also avoids concurrent model streams because its replay entries are positional; concurrency would require keying replay by request instead of by stream order.
## Proposal
Scope ACP back to one live session per connection. `session/new` or `session/load` creates the only session record; a second live session request is rejected until the existing session is disposed or the connection closes. If editors need multiple chat tabs, they can launch multiple agent subprocesses until the bridge has a concrete multi-session UX and permission model.
Remove the multi-session maps and demux where a single `SessionRecord | undefined` is enough. The bridge can still keep the agent/session lifecycle seams that make disposal correct; the simplification is only about multiplexing more than one active session through the same transport.
## Acceptance criteria
- ACP has one active session record per connection.
- `session/new` and `session/load` reject while that record exists.
- Event handlers no longer demux across a `Map<sessionId, record>`.
- Multi-session tests are removed or moved to a rejected/superseded proposal.
- The existing [multi-session ACP proposal](2026-06-14-acp-multi-session.md) is updated to link this RFC if rejected.
## What we give up
An ACP client cannot host several concurrent conversations on one server process. That is a meaningful capability cut. The simpler model is still reasonable for an unreleased harness: one editor conversation maps to one agent process, and cross-session permission/background-task isolation stops being a live correctness burden.

View File

@@ -0,0 +1,31 @@
# RFC: Truncate interrupted final turns on load
Status: proposed
## Problem
The current persistence contract preserves a final turn that was durably written but never closed. On load, `interruptedTurnClosers()` scans the tail, synthesizes error `tool/result` events for unanswered tool calls, appends a `step/end` when a step is open, appends `turn/end { kind: 'interrupted' }`, and asks the backend to durably commit that repair. The coordinator, JSONL backend, SQLite backend, session event vocabulary, invariants, docs, and tests all model this synthetic close path.
This is a lot of machinery to preserve partial work from the last crashed turn. It also invents events that never happened. A synthetic tool result is useful because it makes provider history valid, but it also means the resumed log contains model-visible text that no tool produced. The current design optimizes for maximum tail preservation before there is a released product or a real resume UX that proves partial-turn recovery matters.
## Proposal
On load, keep only the last completed turn. A backend still tolerates and truncates a torn final record, but if the parsed durable prefix ends after an open `turn/start`, the canonical repair is to drop every event after the previous `turn/end`. No synthetic `tool/result`, no synthetic `step/end`, no `turn/end { interrupted }`, and no `interrupted` turn-end reason.
This makes the persisted turn boundary simple: a completed `turn/end` is the checkpoint. Anything after the last checkpoint is crash tail. The next prompt resumes from the last known-valid provider transcript, not from a partially reconstructed final turn.
## Acceptance criteria
- `TurnEndReasonMap` drops the `interrupted` variant.
- `interruptedTurnClosers()` and its tests disappear.
- The persistence coordinator's repair hook truncates backend-specific torn/open tail state without appending closers.
- Persistence docs say load returns the last completed turn, plus no partial final turn.
- Snapshot and contract tests update together with the behavior they pin.
## What we give up
A crash can lose real work from the final turn: assistant text, tool calls, and tool output appended after the previous `turn/end`. That is the deliberate simplification. The product is unreleased, the final-turn recovery semantics are not user-proven, and a clean completed-turn checkpoint is much easier to explain, test, and implement. A future "recover partial crashed work" feature should be designed as an explicit user-facing recovery view, not as synthetic events silently inserted into the canonical transcript.
## Related
This is a direct simplification of [session persistence](../implemented/2026-06-14-session-persistence.md) and [turn enclosure](../implemented/2026-06-15-turn-enclosure-invariant.md). It also removes much of the motivation for durable step boundary events, making [drop durable step boundary events](2026-06-20-drop-durable-step-boundaries.md) smaller.

View File

@@ -3,6 +3,7 @@ import { resolve } from 'node:path'
// publint every publishable package (vendor/ is private upstream code and
// examples/ are not packages; both are out of scope).
// TODO(package-inventory): derive this from package metadata/classification.
const packages = [
'packages/llm',
'packages/session',