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
rocky-hq/consolePR #14 (merge commit01367df): console wrapper around RALPH now emitsagent.{registered,invoked,completed}peragent-registration.v1. Producer lives atsrc/lib/ralph/agent.ts(builders +emitAgentHatch). New ingest endpointPOST /api/relay/agentmirrors/api/relay/ralph(role-gateadmin|operator|observer, throw-on-invalid viaparseAgentHatchEventmapped to 400, best-effortappendAudit, 202 on accept).- Submit path (
POST /api/ralph/runs) emitsagent.registered(idempotent re-emit per spec) +agent.invokedaftersubmitRunreturns;invocation_id = run_id. - Cancel path (
POST /api/ralph/runs/[id]/cancel) emitsagent.completed{outcome:cancelled, duration_ms:0}paired with the existingralph.run.cancelledevent. - Contracts dependency bumped from
0.1.0→^0.3.0to pick up the./agentsubpath shipped in Phase 7a.
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:
- 7b-ralph-2 (separate console PR). Add the observer as a registry-managed background task. Smallest blast radius.
- Fold into 7c (hatch consumer). The hatch consumer already replays the journal — it can synthesise
agent.completed{outcome:timeout}from the absence of a paired event after a window, and synthesiseagent.completed{outcome:ok|error}from the existingralph.run.terminalevent when one arrives. Eliminates the console-side worker entirely but couples the consumer to the producer's other event family.
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):
- Each producer subsystem adds a
<subsystem>/agent.tsbuilder module:build<Producer>AgentId(...)+build<Producer>AgentRegistration(...)+ re-uses the sharedemitAgentHatch(factor out tosrc/lib/hatch/agent-emit.tswhen the second producer lands — premature now). - Emit
agent.registeredidempotently at the boundary that creates work (submit, persona-creation, analyzer-start). - Emit
agent.invoked+agent.completedpairs around invocation. Chooseinvocation_iddeterministically when a 1:1 resource id exists; mint UUIDs otherwise. - Document the
agent.*event list in the producer's section of the relevantCLAUDE.md.