← Ledger


title: Phase 7b (ralph slice) close — console emits agent.* for RALPH date: 2026-06-29 status: Accepted phase: 7b (ralph slice) predecessor: docs/decisions/2026-06-29-phase-7a-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-ralph.md

Phase 7b ralph-slice close

Shipped

Locked decisions

1. invocation_id = run_id

Deterministic, no extra state, 1:1 invocation↔run. Re-submit of the same run is not a use case today. Future producers that do not map 1:1 to a durable resource id will need to mint UUIDs and persist them alongside the parent record.

2. agent.registered cadence: emit-on-every-submit

The HATCH ledger is the registration store (spec §HATCH event family). Re-emission with the same body is a no-op for the consumer projection. Emit-on-submit is recoverable across console restarts and avoids carrying boot-state, at the cost of trivially redundant audit-feed rows. Hatch consumer dedupes.

3. duration_ms on cancel-as-completed is 0

Schema requires the field. Cancel is operator-initiated and synchronous from the console's vantage point — there is no per-run wall-clock to attribute without the terminal observer (see §Deferred). 0 is a true statement about console-side time-in-flight; hatch consumer can ignore it for cancelled outcomes if it wants real timing later.

Deferred

Natural-terminal observer → Phase 7b-ralph-2

agent.completed fires only on operator-initiated cancel. Natural run termination (done | failed | blocked transitions inside ralph python) does not emit agent.completed in this slice. Closing the gap requires a console-side background worker that subscribes to /runs/{id}/events SSE for each in-flight run, watches for the terminal transition, and emits the paired completion event. That is its own engineering problem (lifecycle, restart resilience, dedup across reconnects) and was scoped out of this PR to keep it surgical.

Options for closing the gap:

Defaulted to 7b-ralph-2; 7c may revisit.

Polar entitlement check at invocation time

Spec §cross-cut interactions calls for the producer to read rate.tier_floor + rate.seats_required from the registration record and compare against the workspace's Polar tier before emitting agent.invoked. RALPH's registration declares tier_floor: "solo" + seats_required: 0, so the check is a no-op for this slice. Wiring the actual lookup waits for Phase 7c when Polar enters the loop for every producer.

Airlock agent.approve verb

Approval is required: false for RALPH, so no airlock interaction this slice. Verb exposure waits for the first producer that needs it (likely SNIFFER or driver registrations in 7b-sniffer / 7b-driver).

Carry-forward to other 7b producers

This slice establishes the pattern for the remaining producer-side PRs (COUNCIL, SNIFFER, STRATT, RELAY, hearth driver):

  1. Each producer subsystem adds a <subsystem>/agent.ts builder module: build<Producer>AgentId(...) + build<Producer>AgentRegistration(...) + re-uses the shared emitAgentHatch (factor out to src/lib/hatch/agent-emit.ts when the second producer lands — premature now).
  2. Emit agent.registered idempotently at the boundary that creates work (submit, persona-creation, analyzer-start).
  3. Emit agent.invoked + agent.completed pairs around invocation. Choose invocation_id deterministically when a 1:1 resource id exists; mint UUIDs otherwise.
  4. Document the agent.* event list in the producer's section of the relevant CLAUDE.md.