Status: implemented
English | 中文
The harness extends the agent loop through a Cordis event taxonomy (see the microkernel event-taxonomy Agent Note). 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 theAgenthandle.tools/*carries the tool registry and execution pipeline.
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 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 vocabulary is the foundation for interception decisions, the durable hook/* log, and the Claude Code and Codex bridges.
Three domains, one job each, with a single boundary rule.
session/*— the durable, replayable FACT log. OwnsSessionEventMap; every entry is JSON-only (no live objects). Onesession/eventemit per append, plus thesession/flushparallel 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 replay projections share one path.agent/*— the LIVE runtime surface. Always carries the liveAgent. Interception waterfalls (agent/pre-step,agent/request,agent/request-error) transform, reject, or recover; awaitedagent/turn-stoppingobserves the stop boundary; transient emits report lifecycle, status, inbox insertion/claim/discard, and errors. Turn and step BOUNDARIES are NOT here — they are durable session events read offsession/event, as are the token stream (assistant/chunk) and mid-turn steering (auser/message).tools/*— the tool registry and execution pipeline.
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 turn or step boundary is a durable fact, so it lives in the session log and is read off the session/event feed — it is NOT mirrored as an agent/* emit.
Applying the rule to the boundary twins: all four boundary mirrors — agent/turn-start, agent/turn-end, agent/step-start, agent/step-end — are REMOVED. No production consumer needs the live Agent at a boundary: the ACP bridge correlates its in-flight prompt with the exact session/event turn/start/turn/end pair, and other transcript consumers likewise derive boundaries from the durable stream. See the remove-boundary-mirror-events Agent Note, which owns that decision. Removing the emits also simplifies the loop's closeStep/closeTurn (one append each, no paired emit).
- The loop no longer emits any boundary mirror;
closeStepappendsstep/endonly andcloseTurnappendsturn/endonly.Session.appendowns post-commit observer containment, so a throwing boundary observer cannot change the turn outcome or starve later consumers; an acceptance or internal validation failure still escapes before the boundary enters the log. - Tests that observed boundaries via the removed emits now observe the durable
turn/start/turn/end/step/start/step/endsession events — the behavior they pin (boundary ordering, step counting) is unchanged; only the feed they read moved to the canonical one. The tests that exercised a throwing turn-boundary emit listener were deleted, because that code path no longer exists (there is no emit to throw from). Per AGENTS.md "tests document behavior, not golden truth", the behavior and its test moved (or died) together. - The loop marks the step open (
stepOpen = true) only afterappend('step/start')returns. Internal dispatch validation runs before the log push and may reject without opening a step; post-commitsession/eventobserver failures are contained insideSession.append. The marker therefore represents exactly the committed boundary that owes a laterstep/end. - The full realization of this is the simplification Agent Note "Stop mirroring durable boundaries as agent events": all four boundary mirrors are removed and every consumer reads boundaries off
session/event.agent/steering(not a boundary mirror) stayed outside that Agent Note's scope and was removed by its own follow-up, Remove theagent/steeringmirror emit — it mirrored the durable mid-turn steeringuser/message. - The generated cordis event surface (the
docs/subsystems/pages) no longer lists the mirror events.