Files
deepseek-harness/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md
Tianyi Cui a821dcbe0d fix(events): address Codex confirmation review — strengthen /goal guard, fix doc drift
Second-round Codex review of the PR-A taxonomy change found four issues, all
verified against the code:

- The /goal regression guard asserted only that the steered content reached
  requests[1], which passes even with the hasSteering override (loop.ts) disabled:
  leftover steering is re-enqueued as a next-turn queued message and also lands in
  requests[1], one turn later. The guard now asserts the same-turn shape — ONE
  turn, TWO steps, a steering/message recorded before step 2 — which is the
  mechanism the override drives. Proven to fail red with the override disabled.
- The event-domain-semantics RFC's consequence list still described the pre-fix
  behavior (step marked open AFTER step/start, so no step/end owed). It now states
  the shipped behavior: the loop marks the step open BEFORE the append, so a
  throwing step/start listener gets a balancing step/end via closeStep().
- architecture.md's loop pseudocode said only continuation listeners force
  continuation; step/end session-event listeners (the /goal pattern) do too.
- The agent/turn-end JSDoc listed a `rejected` TurnEndReason that does not exist on
  this branch (it belongs to the later interception work). Removed it and
  regenerated the cordis catalog; `interrupted` (a real variant) stays.
2026-06-30 12:46:59 +08:00

5.6 KiB

RFC: Event-domain semantics — session is the fact log, agent is the live surface

Status: implemented (accepted 2026-06-30)

Context

The harness extends the agent loop through a Cordis event taxonomy (see the microkernel event-taxonomy RFC). As that taxonomy grew, the line between the three event domains blurred:

  • session/* carries the durable, event-sourced log (SessionEventMap).
  • agent/* carries live runtime signals that hand a plugin the Agent handle.
  • tools/* carries the tool registry + execution seam.

Two problems motivated pinning the semantics down. First, several turn/step boundaries existed BOTH as a durable SessionEvent (turn/start, turn/end, step/start, step/end) AND as a mirrored agent/* emit (agent/turn-start, agent/turn-end, agent/step-start, agent/step-end). A consumer had two sources of truth for the same fact, and every lifecycle change had to update both. Second, the upcoming Hooks subsystem needs ONE coherent, documented surface to subscribe to — a plugin author (and the Claude Code / Codex hook bridges built on top) must know, without reading the loop, whether to listen on a session event or an agent event, and why.

This is the foundational change in a stack that adds a Hooks subsystem; it establishes the vocabulary the later PRs (interception-Decision reshape, the hook/* durable log, the bridges) build on.

Decision

Three domains, one job each, with a single boundary rule.

  • session/* — the durable, replayable FACT log. Owns SessionEventMap; every entry is JSON-only (no live objects). One session/event emit per append, plus the session/flush parallel durability checkpoint. It is also the live transcript feed: a consumer that wants to render or react to what happened subscribes here, so live rendering and session/load replay share one path.
  • agent/* — the LIVE runtime surface. Always carries the live Agent. Two shapes: INTERCEPTION waterfalls (agent/request, agent/step-result, agent/turn-continuation) that mutate or veto, and TRANSIENT emits (agent/status, agent/stream-chunk, agent/error, agent/created/agent/disposed, agent/queued, agent/steering, and the turn boundaries) that notify with the Agent in hand.
  • tools/* — the tool registry + execution seam.

The boundary rule: a durable, replayable fact is a SessionEvent; a live interception or a transient/live-object signal is an agent/tools Cordis event. A datum that is BOTH — a turn or step boundary — lives in the session log, and is mirrored as an agent/* emit ONLY where a live consumer provably needs the Agent handle at that instant.

Applying the rule to the boundary twins (prune case-by-case):

  • agent/turn-start — KEPT. The stdio UI (dsh-ui-stdio) labels turn output by agent.id, which the turn/start session event does not carry. A genuine live-object need.
  • agent/turn-end — KEPT. The stdio UI listens to print the next-prompt glyph. (Note: the ACP bridge does NOT settle on this event — it settles from session/event turn/end plus agent/status; the surviving justification is the stdio UI alone.)
  • agent/step-start, agent/step-end — REMOVED. No production consumer needs the live Agent at a step boundary; a consumer that wants per-step boundaries reads the durable step/start/step/end session events. Removing the two emits also simplifies the loop's closeStep (one append, no paired emit).

Consequences

  • The loop no longer emits agent/step-start/agent/step-end; closeStep appends step/end only, and a throwing step/end session-event listener is the surviving step-boundary-listener failure path (contained by closeStep → failTurn, the turn closes balanced).
  • Tests that observed step boundaries via the removed emits now observe the durable step/start/step/end session events — the behavior they pin (boundary ordering, step counting, a throwing boundary listener failing the turn balanced) is unchanged; only the feed they read moved to the canonical one. Per AGENTS.md "tests document behavior, not golden truth", the behavior and its test moved together.
  • The loop marks the step open (stepOpen = true) BEFORE appending step/start, because Session.append pushes the event to the log before notifying session/event listeners (validation throws happen earlier, before the push — see the session append contract). So a throwing step/start session-event listener runs with the step already open and the event already in the log: the loop's outer catch then calls closeStep(), which appends the balancing step/end, and the turn closes balanced with an error (turn/start → step/start → step/end → turn/end — verified by the invariants oracle in the regression test). Closing the open step is owed precisely because the marker is set first.
  • This is a partial, conservative realization of the broader proposed simplification "Stop mirroring durable boundaries as agent events": that RFC proposes removing ALL boundary mirrors (including the turn boundaries and agent/steering) and migrating the stdio UI's turn rendering onto session/event. This RFC removes only the two step mirrors that have no live consumer; the turn mirrors stay until the stdio UI is migrated. The proposed RFC remains the home for finishing that migration.
  • The cordis catalog (docs/cordis-catalog/events-and-services.md) is regenerated to drop the two events.