docs: address simplification RFC review

This commit is contained in:
Tianyi Cui
2026-06-20 17:26:23 +08:00
parent cc47f76cea
commit ea3f138ae9
20 changed files with 50 additions and 36 deletions

View File

@@ -5,6 +5,8 @@ Status: proposed
<!-- XXX: legacy ADR/RFC body format, not yet normalized to a unified RFC template. -->
> **Implementation status:** the multi-session bridge (steps 1, 3, 4) and the bash task-ownership isolation are implemented in `packages/acp` + `packages/tool-bash`. **Per-session *permission* ownership is deferred** — it depends on [the ACP support permission gate](2026-06-14-acp-agent-client-protocol.md) (`TODO(rfc010-permission-gate)`), which is itself deferred; the `agent→sessionId` reverse map the gate will route through is in place. Step 2's per-session disposer scope is now implemented (see [agent lifecycle & ownership seams](../implemented/2026-06-18-agent-lifecycle-and-ownership-seams.md)): the factory returns a per-agent `AgentHandle` whose `dispose()` stops the loop, awaits quiescence, unregisters the agent, and removes its session, so a bare client disconnect leaves no registered agent or session-store entry. Status stays `proposed` until per-session permission ownership lands.
> **Competing simplification:** [Return the ACP bridge to one live session per connection](2026-06-20-single-session-acp-bridge.md) proposes reversing the multiplexing scope until the product has a concrete multi-session UX and permission model. While both RFCs remain proposed, this one represents the "finish multiplexing" path and the newer RFC represents the "remove multiplexing" path.
## Problem
[ACP support](2026-06-14-acp-agent-client-protocol.md) ships with a single active session per connection: a second `session/new` is rejected. Editors expect to run several conversations over one agent subprocess — a user opens multiple threads, or a client pre-warms sessions. The single-session guard is a deliberate MVP scope cut, not an architectural limit; this RFC lifts it.

View File

@@ -4,27 +4,28 @@ 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 canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The [session persistence RFC](../implemented/2026-06-14-session-persistence.md) 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.
For successful steps that assemble completed content, the loop already appends an `assistant/message`. That is the event `deriveMessages()` uses for the next model request. In other words, the normal resumable conversation state is already present without the chunks; chunks are a live rendering and deterministic-test artifact, not required conversation history. Failed or aborted streams are different: partial assistant output may exist only as chunks, and empty max-token steps may produce no `assistant/message` at all.
## 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.
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. Scenarios that need partial failed-stream output must record that output in the replay fixture or accept that it is not part of completed conversation history.
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.
- [Session persistence docs](../../../packages/session-persistence/README.md) 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.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy.
## 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.
The canonical user session no longer reconstructs the exact token stream of an old turn. It also loses partial assistant output from failed or aborted streams unless another event or fixture records it. 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

View File

@@ -4,7 +4,7 @@ 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.
`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. The [package README](../../../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.

View File

@@ -21,6 +21,7 @@ If analytics become real, add a projection helper or a dedicated telemetry store
- 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.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy.
## What we give up

View File

@@ -4,19 +4,19 @@ 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.
Package and gate inventories are repeated by hand. [scripts/publint-all.ts](../../../scripts/publint-all.ts) has a static list of publishable packages. The [package cookbook](../../cookbook/adding-a-package.md) tells authors to update several files. The [package README](../../../packages/README.md) carries a hand-written dependency graph. [CI](../../../.github/workflows/ci.yml) and [development docs](../../development.md) 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.
Make package/gate inventories discoverable. Publishability should come from explicit package classification metadata, not from a static array in a script or the npm `private` flag. 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.
- `publint-all` discovers publishable packages from manifests plus 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.

View File

@@ -18,7 +18,7 @@ For now, ACP starts fresh sessions only. `initialize` advertises `loadSession: f
- `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.
- [ACP docs](../../../packages/acp/README.md) describe fresh-session support only.
## What we give up

View File

@@ -4,7 +4,7 @@ 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 ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented [rich ACP bash rendering RFC](../implemented/2026-06-18-acp-terminal-and-tool-rendering.md) 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.
@@ -20,7 +20,7 @@ This proposal is narrower than [collapsing tool-owned UI presentation](2026-06-2
- `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.
- The implemented [rich ACP bash rendering RFC](../implemented/2026-06-18-acp-terminal-and-tool-rendering.md) stays in `implemented/` as shipped history and is cross-linked from this proposal if superseded.
## What we give up

View File

@@ -20,7 +20,7 @@ This proposal can land independently of [foreground-only bash](2026-06-20-foregr
- `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.
- Security guidance in [root AGENTS.md](../../../AGENTS.md) stops treating private spill files as a model-visible interface.
## What we give up

View File

@@ -20,7 +20,8 @@ The invariants plugin should enforce that step-scoped events have valid positive
- 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.
- The [event taxonomy docs](../../architecture.md) describe turns as the durable boundary and steps as a field on step-scoped records.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy.
## What we give up

View File

@@ -20,6 +20,7 @@ If lineage returns, decide then whether it belongs in the immutable header, a se
- 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.
- The session format version, backend schema versions, and recorded fixtures are refreshed as needed; non-current stored data is rejected per the pre-release format policy, with no migration path.
## What we give up

View File

@@ -20,7 +20,7 @@ The implementing PR should update the [capability seams](../implemented/2026-06-
- `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.
- [Session persistence](../implemented/2026-06-14-session-persistence.md), [shared persistence write coordinator](../implemented/2026-06-18-shared-persistence-write-coordinator.md), and [package docs](../../../packages/session-persistence/README.md) explain why backend implementations remain separate.
## What we give up

View File

@@ -4,13 +4,13 @@ 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 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. The local executor fences task access behind owner tokens because predictable global task ids are 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`.
The [tool cookbook](../../cookbook/adding-a-tool.md) 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.
Temporarily collapse `bash` to foreground-only execution. Remove the model-facing `run_in_background` schema field, the `bash_output` and `bash_kill` tools, background task ownership, incremental task reads, completion injection, and task-listener APIs from the bash executor seam. The `BashExecRequest` request type is already foreground-shaped; the removal surface is the tool schema plus the executor's background-task methods. 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.
@@ -20,7 +20,7 @@ If long-running tasks return later, implement them once as a capability-agnostic
- `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.
- The [tool cookbook](../../cookbook/adding-a-tool.md) either removes the background example or redirects long-running work to a future generic task proposal.
## What we give up

View File

@@ -4,23 +4,23 @@ 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 examples have two shared base files: [examples/base-core.yml](../../../examples/base-core.yml) is providerless, while [examples/base.yml](../../../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`.
Rename the providerless core to [examples/base.yml](../../../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 [examples/base-core.yml](../../../examples/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.
- [examples/base.yml](../../../examples/base.yml) is providerless.
- [examples/base-core.yml](../../../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".
- The [examples README](../../../examples/README.md), example-specific READMEs, and RFC references stop explaining "base = base-core plus adapter".
## What we give up

View File

@@ -16,7 +16,7 @@ Delete public `abort()` and `whenIdle()`, the tests that exercise them as standa
## Acceptance criteria
- `Agent` exposes `send()`, `inject()`, `cancel()`, status, options, session, and identity, with no public `abort()` or `whenIdle()`.
- `Agent` exposes no public `abort()` or `whenIdle()`; if [retiring mid-turn steering](2026-06-20-retire-mid-turn-steering.md) has not landed, `steer()` remains part of the message surface.
- 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.
@@ -24,3 +24,7 @@ Delete public `abort()` and `whenIdle()`, the tests that exercise them as standa
## 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.
## Related
This RFC only removes the stop/quiescence methods. If it lands before [retiring mid-turn steering](2026-06-20-retire-mid-turn-steering.md), `steer()` remains part of the `Agent` message surface; if the steering RFC lands first, the resulting surface is `send()`, `inject()`, `cancel()`, status, options, session, and identity.

View File

@@ -4,13 +4,13 @@ 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.
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/stream-chunk`, 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 turn boundaries and the token stream from the mirror events; it already renders tool calls and results from `session/event`.
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.
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`, `agent/error`, and `agent/queued`. `agent/queued` is an inbox acknowledgement rather than a transcript mirror: it fires before any durable event exists, and cancelled queued work may never enter the log.
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.
@@ -18,6 +18,7 @@ Remove the duplicate durable-boundary mirrors from the agent event taxonomy. If
- 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.
- `agent/queued` is either retained and documented as live-only inbox/control state, or deleted in a separate proposal that names the queue-acknowledgement capability loss.
- 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.

View File

@@ -20,7 +20,7 @@ Stdout goldens remain unchanged; they are the editor-facing projection and are n
- 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.
- The [ACP snapshot tests RFC](../implemented/2026-06-19-acp-snapshot-tests.md) is updated to describe the reduced fixture set.
## What we give up

View File

@@ -6,13 +6,13 @@ Status: proposed
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.
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; only tests register the waterfall. Separately, the only production UI that calls `steer()` 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.
Remove `Agent.steer()`, the steering FIFO, `steering/message`, `agent/steering`, steering-derived continuation, and the cancellation logic that distinguishes queued messages from steering messages. Remove `agent/turn-continuation` in the same change unless the implementing PR discovers a production listener; without steering, the current repo has no concrete continuation consumer left. 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
@@ -20,7 +20,9 @@ Remove `Agent.steer()`, the steering FIFO, `steering/message`, `agent/steering`,
- 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.
- `agent/turn-continuation` is removed or narrowed to a named production consumer.
- The stdio UI and docs describe input while running as queued next-turn input.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy.
## What we give up
@@ -28,4 +30,4 @@ A user cannot add same-turn steering content while a model is between tool steps
## 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.
This pairs naturally with [dropping durable step boundaries](2026-06-20-drop-durable-step-boundaries.md), because removing same-turn steering and `agent/turn-continuation` leaves tool calls as the only reason a turn contains multiple model steps.

View File

@@ -4,7 +4,7 @@ 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 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. The older [multi-session ACP proposal](2026-06-14-acp-multi-session.md) still tracks the unfinished permission-ownership piece; this RFC is the competing simplification path.
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.
@@ -19,8 +19,8 @@ Remove the multi-session maps and demux where a single `SessionRecord | undefine
- 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.
- Multi-session tests are removed or moved under the proposal that continues to defend multiplexing.
- The existing [multi-session ACP proposal](2026-06-14-acp-multi-session.md) is updated to link this RFC while both proposals remain live.
## What we give up

View File

@@ -19,8 +19,9 @@ This makes the persisted turn boundary simple: a completed `turn/end` is the che
- `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.
- [Session persistence docs](../../../packages/session-persistence/README.md) say load returns the last completed turn, plus no partial final turn.
- Snapshot and contract tests update together with the behavior they pin.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy, with no migration path.
## What we give up