title: Phase 7b (relay slice) close — console emits agent.* for RELAY date: 2026-06-29 status: Accepted phase: 7b (relay slice) predecessor: docs/decisions/2026-06-29-phase-7b-ralph-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-relay.md
Phase 7b relay-slice close
Shipped
rocky-hq/consolePR #15 (merge commit411a9f0):POST /api/relay(the single entry toexecute()insrc/lib/relay/executor.ts:15) emits the fullagent.{registered,invoked,completed}triple per invocation peragent-registration.v1. Producer lives atsrc/lib/relay/agent.ts(builders only);emitAgentHatchis re-exported fromsrc/lib/ralph/agent.tsso both producers POST to the same/api/relay/agentingest endpoint (which landed in 7b-ralph).- One agent per workspace,
agent_id = <workspace_slug>-relay-executor, registered with the 5 route types asrelay.<route>capabilities. Per-invocationcapability = relay.${result.route},invocation_id = result.executionId. outcomemapping:success→ok,failure/timeout→error.duration_ms = result.durationMs. Emission gated onx-rocky-workspaceheader (no agent identity without owner); existing relay audit log unchanged.- Contracts version unchanged from 7b-ralph (
^0.3.0).
Locked decisions
1. Synchronous triple-emit (no separate cancel path)
Unlike RALPH (async runs + separate POST /cancel), relay invocations are synchronous from the route's POV — one HTTP request enters POST /api/relay, execute() resolves with outcome, response goes back. All three lifecycle events fire from one route handler. No background observer, no cancel route, no terminal-state coverage gap.
2. invocation_id = result.executionId
Already minted by execute() as a UUID. Deterministic 1:1 mapping to the audit row already keyed by executionId.
3. capability = relay.${result.route}
Each of the 5 route types is a registered capability. Hatch CAPABILITIES column shows the full set; per-invocation rows show the specific verb that fired. Future route types are additive (registration body re-emitted on next invocation; consumer dedupes).
4. outcome mapping: failure + timeout both → error
agent.completed.outcome enum is ok | error | cancelled. Timeout is a failure mode at the agent-projection altitude; the existing relay audit status column preserves the timeout distinction for operators who care. cancelled is not reachable on the synchronous relay path.
5. No 5xx-path emission
If execute() throws (caught by the outer 500 handler) there is no ExecutionResult and therefore no executionId to use as invocation_id. Emitting a partial triple with a synthetic id would muddy the projection. The 5xx case stays invisible to the agent view; the existing route's 500 response and server logs remain authoritative for that failure mode.
6. No header → no emission
Mirrors the cancel-path guard in 7b-ralph. Agent identity requires an owner; no x-rocky-workspace → skip the triple. Existing audit logging continues independently.
Deferred
Polar entitlement check at invocation time
tier_floor: "solo" + seats_required: 0 → no-op for this slice. Real check waits for Phase 7c.
Airlock agent.approve verb
approval.required: false → no airlock interaction. First user remains SNIFFER or hearth driver.
Per-tool agent identity
A relay executor is one agent per workspace. Individual pebble:/n8n: tool calls remain rows in the existing audit record's toolsCalled[], not nested agents. If operators want per-tool projection later, those tools register their own agents in their own producer subsystems (PLUGINS / n8n) — not here.
Carry-forward to remaining 7b producers
This slice confirms the carry-forward pattern from 2026-06-29-phase-7b-ralph-close.md §Carry-forward holds for synchronous producers:
<subsystem>/agent.tsbuilders + re-exportemitAgentHatchfromsrc/lib/ralph/agent.ts. (Factor tosrc/lib/hatch/agent-emit.tswhen the third producer lands — still not warranted at 2.)- Single-handler emit of all three lifecycle events when the producer is synchronous from the route's vantage point. Async producers (background workers, long-running runs) keep the split-emit pattern from RALPH.
- Capability list = the producer's declared route/verb space; per-invocation
capabilitypicked from that set. - Header gate for
x-rocky-workspaceis the consistent way to express "no agent identity without owner."
Remaining producers: COUNCIL (persona CRUD; static-fixture agent_id), SNIFFER (analyzer-run; first approval.required: true candidate), STRATT (strategy entry), hearth driver (cross-subsystem; Go producer in hearth/).
Acceptance verified
npm run typecheckclean.npm run test:run399/399 (delta vs 7b-ralph: +6 relay-agent suite, +6 relay route agent-projection suite; no regressions elsewhere).POST /api/relayround-trips emit the three events withx-rocky-workspacepresent; emits zero events when absent; emits nothing on 400 (missing intent) or 500 (execute throws), per locked decisions.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format.docs/decisions/2026-06-29-agentic-layer-projection.md— predecessor; producer-order ralph→relay→council→sniffer→stratt→hearth driver.docs/decisions/2026-06-29-phase-7b-ralph-close.md— pattern established by the predecessor slice.docs/plans/2026-06-29-phase-7b-relay.md— this slice's plan (approved at PR open).