The loop recorded every model token delta as a durable `assistant/chunk` session event AND emitted an identical live `agent/stream-chunk` Cordis event one line later. Same StreamChunk, same turn/step; the emit added only the live Agent handle, which the sole consumer discarded. This is the boundary-mirror duplication the event-domain work removed for turn/step boundaries, applied to the token stream — a follow-up the boundary RFC explicitly deferred. The premise is settled: chunk persistence is authoritative (the proposal to stop persisting chunks was rejected — replay/snapshots depend on it), so `assistant/chunk` on `session/event` is the load-bearing token stream and `agent/stream-chunk` is pure redundancy. - Remove the `agent/stream-chunk` declaration + emit; drop the now-unused StreamChunk import from dsh-agent's types. - Migrate `dsh-ui-stdio` (the only live consumer; ACP already reads assistant/chunk off session/event) to render assistant/chunk in its existing session/event listener. Consolidating to one listener also makes the inReasoning dim-SGR flag deterministic across chunk/boundary events (they no longer race across two listeners). - Repoint the agent-loop tests (cancel/loop) and ui-stdio tests to the session/event assistant/chunk feed. - New RFC (implemented/simplification/2026-07-02-remove-stream-chunk-mirror); amend the boundary RFC's retained-list entry to cross-link; update architecture, cookbook, event-domain-semantics, the ACP proposal, and the regenerated cordis catalog. Snapshot goldens unchanged (ACP never used the mirror), confirming no editor-facing transcript change.
4.5 KiB
RFC: Stop mirroring durable boundaries as agent events
Status: implemented (accepted 2026-07-01)
Problem
The loop records the canonical transcript in SessionEvent and also emitted a parallel set of live agent/* boundary mirror events: agent/turn-start, agent/turn-end, agent/step-start, and agent/step-end. The mirrors made consumers choose between two sources of truth for the SAME durable fact. 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 was the only production consumer that still rendered turn boundaries from the mirror events; it already rendered tool calls and results from session/event.
This duplication is not free. Every lifecycle change had to update the session event, the mirror event, docs, invariants, tests, and snapshot expectations. The duplicate boundary events also made 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.
Decision
Make session/event the single live boundary/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.
The four durable-boundary mirrors — agent/turn-start, agent/turn-end, agent/step-start, agent/step-end — are removed from the agent event taxonomy. A UI that wants the agent handle (or its short id) at a boundary keeps a small map from session id to agent id built from agent/created/agent/disposed; dsh-ui-stdio does exactly this to label its [<agent> turn N] header, since the turn/start session event carries only the turn number. The canonical record remains the event-sourced session log.
The step mirrors (which had no consumer at all) were removed first, in the event-domain-semantics RFC; that RFC KEPT the turn mirrors on the stated justification that the stdio UI needed the Agent handle at the turn boundary. This RFC finishes the job: dsh-ui-stdio is a disposable test REPL whose rendering can change freely, so "ui-stdio needs it" is not a reason to keep a mirror — it was migrated to session/event + the id map, and the turn mirrors were removed too.
Scope: what is and isn't removed
Removed (durable-boundary mirrors — the session log is authoritative for each): agent/turn-start, agent/turn-end, agent/step-start, agent/step-end.
RETAINED — NOT durable-boundary mirrors, so out of scope for this decision:
agent/steering— a live control signal, not a boundary. (The original proposal bundled it into the removal; validating against the code, it is not a duplicate of a durable boundary, so removing it here would have been scope creep. Its fate is a separate future decision.)agent/stream-chunk— the live token stream. Out of scope for THIS decision (a mirror of the durableassistant/chunk, not a boundary), it was removed by its own follow-up: Stop mirroring the token stream as an agent event.agent/created,agent/disposed,agent/status,agent/error,agent/queued— lifecycle/control events that are not transcript data.agent/queuedin particular is an inbox acknowledgement that fires before any durable event exists (cancelled queued work may never enter the log), so it is deliberately live-only.
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: boundary consumers should not depend on a second event feed that can drift from the durable log.