Merge branch 'worktree-hooks-a-taxonomy' into worktree-hooks-b-bash-seam
This commit is contained in:
@@ -37,4 +37,4 @@ Costs: `agent.inject()` while idle now writes three log lines instead of one, an
|
||||
|
||||
The rule is intentionally producer-enforced and dev-checked rather than reader-tolerated: a future backend (SQLite/WAL) inherits the same clean boundary for free, and a plugin that records an event outside a turn fails loudly in dev instead of silently losing data on the next reload.
|
||||
|
||||
The invariant also constrains where the loop may record an `error` event. A failure detected while a turn is open is appended INSIDE the turn (before `turn/end`); but a failure that surfaces once the turn is already closed — a rejecting `session/flush` (which runs as the post-`turn/end` durability checkpoint) or a throwing `agent/turn-end` listener (after `closeTurn` already appended `turn/end`) — has no in-turn position left. Appending an `error` there would land it past the last `turn/end`, exactly the crash-tail position a backend discards. So those post-turn failures are reported via the `agent/error` event and the logger only, never as a `SessionEvent`; the turn stays balanced and persistence keeps its buffered events for the next checkpoint. If durable operational diagnostics are ever needed, they belong on a separate telemetry channel, not the replayable session log.
|
||||
The invariant also constrains where the loop may record an `error` event. A failure detected while a turn is open is appended INSIDE the turn (before `turn/end`); but a failure that surfaces once the turn is already closed — a rejecting `session/flush`, which runs as the post-`turn/end` durability checkpoint — has no in-turn position left. Appending an `error` there would land it past the last `turn/end`, exactly the crash-tail position a backend discards. So that post-turn failure is reported via the `agent/error` event and the logger only, never as a `SessionEvent`; the turn stays balanced and persistence keeps its buffered events for the next checkpoint. If durable operational diagnostics are ever needed, they belong on a separate telemetry channel, not the replayable session log.
|
||||
|
||||
@@ -19,21 +19,17 @@ This is the foundational change in a stack that adds a Hooks subsystem; it estab
|
||||
**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.
|
||||
- **`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`) that notify with the `Agent` in hand. Turn and step BOUNDARIES are NOT here — they are durable session events read off `session/event`.
|
||||
- **`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.
|
||||
**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 (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).
|
||||
**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 settles from `session/event` `turn/end` plus `agent/status`, and the only turn-mirror consumer (`dsh-ui-stdio`, a disposable test REPL) was migrated to render boundaries from `session/event`, recovering the short agent label from an `agent/created`→id map. The step mirrors were removed first (they had no consumer at all); the turn mirrors followed once ui-stdio was migrated — see [the remove-boundary-mirror-events RFC](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md), which owns that decision. Removing the emits also simplifies the loop's `closeStep`/`closeTurn` (one append each, 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"](../../../../AGENTS.md), the behavior and its test moved together.
|
||||
- The loop no longer emits any boundary mirror; `closeStep` appends `step/end` only and `closeTurn` appends `turn/end` only. A throwing `step/end`/`turn/end` session-event listener is the surviving boundary-listener failure path (contained inside `closeStep`/`closeTurn` — `Session.append` pushes the event before notifying listeners, so the boundary is durable and the turn closes balanced regardless).
|
||||
- Tests that observed boundaries via the removed emits now observe the durable `turn/start`/`turn/end`/`step/start`/`step/end` session 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"](../../../../AGENTS.md), the behavior and its test moved (or died) 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](../../../core-data-structures/session.md)). 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"](../../proposed/simplification/2026-06-20-remove-agent-boundary-mirror-events.md): 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.
|
||||
- The full realization of this is [the simplification RFC "Stop mirroring durable boundaries as agent events"](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md): all four boundary mirrors are removed and every consumer reads boundaries off `session/event`. `agent/steering` (a live control signal, not a boundary mirror) is retained; see that RFC's scope section.
|
||||
- The cordis catalog (`docs/cordis-catalog/events-and-services.md`) is regenerated to drop the mirror events.
|
||||
|
||||
Reference in New Issue
Block a user