Catalyst Event Broker — Protocol Reference (CTL-303)
The broker daemon evolved from the catalyst-filter daemon (CTL-284). It adds:
- Structured agent identity —
agent.checkin/agent.checkoutevents so the broker knows who is working on what. The broker auto-derivespr_lifecycleinterests from check-in data. ticket_lifecycleinterest type — deterministic routing for Linear webhook events keyed on ticket identifiers. No Groq round-trip for state changes, comments, and PR links.- Auto-correlation — when an agent checks in with a ticket, the broker auto-registers a
pr_lifecycleinterest the moment a PR linking that ticket appears. Agents no longer need to callfilter.register pr_lifecycleexplicitly. - Backward compat — all CTL-284
pr_lifecycleexplicit registration still works unchanged. Groq prose classification remains for ambiguous / multi-condition interests.
Daemon Management
# Check status (broker and filter are aliases)
catalyst-broker status # → "running (pid N)" or "stopped"
catalyst-filter status # deprecated alias — delegates to catalyst-broker
# Start / stop / restart
catalyst-broker start
catalyst-broker stop
catalyst-broker restart
# View logs
catalyst-broker logs
Interest Types Summary
| Interest type | Routing | Use case |
|---|---|---|
pr_lifecycle | Deterministic | Watch CI, reviews, merge, deployment for a known PR number |
ticket_lifecycle | Deterministic | Watch Linear state changes, comments, PR links for a ticket |
comms_lifecycle | Deterministic | Watch comms-channel messages (worker → orchestrator attention/done, orchestrator → worker directives) |
phase_lifecycle | Deterministic | Watch phase.<name>.complete.<ticket> / phase.<name>.failed.<ticket> / phase.<name>.turn-cap-exhausted.<ticket> events — orchestrator hand-off between phase agents |
| (prose prompt) | Groq LLM (env-gated off; CTL-357) | Anything ambiguous, cross-cutting, or complex — set CATALYST_BROKER_PROSE_ENABLED=1 to re-enable |
1. Auto-Correlation (The Common Case — No Registration Needed)
When an agent's own ticket/PR is the concern, registration is automatic:
# catalyst-session.sh start emits agent.checkin automatically:
CATALYST_SESSION_ID=$(catalyst-session.sh start --skill oneshot --ticket CTL-275)
# ↑ The broker records: agent CTL-275 with no claimed_pr yet.
# When you later create the PR, update claimed_pr via agent.checkin:
cat >> ~/catalyst/events/$(date -u +%Y-%m).jsonl <<EOF
{"ts":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","event":"agent.checkin","detail":{"session_id":"$CATALYST_SESSION_ID","ticket":"CTL-275","claimed_pr":$PR_NUMBER,"orchestrator":"${CATALYST_ORCHESTRATOR_ID:-}"}}
EOF
# ↑ The broker sees claimed_pr → auto-registers pr_lifecycle for filter.wake.$CATALYST_SESSION_ID
The oneshot skill now uses this pattern instead of calling filter.register directly. The
explicit filter_register_worker function is kept for backward compat but is no longer the
recommended path for new work.
2. agent.checkin Event
Emitted by catalyst-session.sh start and optionally after PR creation. Shape:
{
"ts": "2026-05-08T07:00:00Z",
"event": "agent.checkin",
"detail": {
"session_id": "sess_20260508_abcd",
"agent_name": "ctl-275-worker",
"ticket": "CTL-275",
"orchestrator": "orch-2026-05-08",
"claimed_pr": 501,
"cwd": "/path/to/worktree"
}
}
Fields:
session_id— required. Primary key in the broker'sagentstable.agent_name— human label (defaults tosession_idif missing).ticket— Linear ticket identifier (e.g."CTL-275"). Enablesticket_lifecycleauto-correlation.orchestrator— parent orchestrator ID; enables stale-session watchdog routing.claimed_pr— if set, broker immediately auto-registerspr_lifecyclefor this agent.cwd— working directory; included for diagnostics.
3. agent.checkout Event
Emitted by catalyst-session.sh end. Shape:
{
"ts": "2026-05-08T09:00:00Z",
"event": "agent.checkout",
"detail": {
"session_id": "sess_20260508_abcd",
"status": "done"
}
}
On checkout, the broker:
- Marks the agent as
donein theagentsSQLite table. - Removes any auto-correlated
pr_lifecycleinterest (explicit registrations are preserved).
3a. worker.waiting / worker.resumed Events (CTL-403)
Emitted automatically by catalyst-events wait-for when $CATALYST_SESSION_ID is set. These
events make wait loops visible to the broker so the watchdog can distinguish a legitimately
waiting session from a silently dead one.
worker.waiting shape
{
"ts": "2026-05-14T16:30:00Z",
"event": "worker.waiting",
"detail": {
"session_id": "sess_20260514_abcd",
"orchestrator": "orch-foo",
"ticket": "CTL-275",
"wait_for": ".attributes.\"event.name\" == \"github.pr.merged\"",
"timeout_ms": 7200000,
"since": "2026-05-14T16:30:00Z",
"reason": "catalyst-events wait-for"
}
}
worker.resumed shape
{
"ts": "2026-05-14T18:00:00Z",
"event": "worker.resumed",
"detail": {
"session_id": "sess_20260514_abcd",
"orchestrator": "orch-foo",
"ticket": "CTL-275",
"outcome": "matched"
}
}
outcome is "matched" when the wait returned a result, "timed_out" when the deadline elapsed.
Broker behavior
On worker.waiting:
- Stores the session in the in-memory
waitingSessionsMap and thewaiting_sessionsSQLite table. - Resets the heartbeat timer so the session does not appear stale during the wait.
- During watchdog ticks, any session in
waitingSessionswhosetimeoutAt > nowis skipped — it is "legitimately waiting" and should not trigger a stale-heartbeat wake. - The
broker.state.jsonfile includes anwaitingSessionsarray with all currently active waits; the HUD dashboard's worker list reads this to overlaywait:Xmin the STATUS column.
On worker.resumed:
- Removes the session from
waitingSessionsand the SQLite table. - Normal heartbeat-staleness tracking resumes.
Broker state file
~/catalyst/broker.state.json gains a waitingSessions array:
{
"waitingSessions": [
{
"sessionId": "sess_20260514_abcd",
"ticket": "CTL-275",
"orchestrator": "orch-foo",
"waitFor": ".attributes.\"event.name\" == \"github.pr.merged\"",
"timeoutAt": "2026-05-14T18:30:00Z",
"reason": "catalyst-events wait-for"
}
]
}
Empty array [] when no sessions are currently waiting.
3b. orchestrator.status Events (CTL-405)
Emitted by the orchestrate skill at each wave transition via orchestrate-status.sh emit. These
events make the orchestrator's current phase visible to the broker, the HUD, and operators, and
serve as a liveness heartbeat so the watchdog does not fire stale-session wakes for an orchestrator
that is actively monitoring between waves.
Event shape
{
"ts": "2026-05-15T00:00:00Z",
"event": "orchestrator-status",
"orchestrator": "orch-foo",
"detail": {
"orchestrator": "orch-foo",
"phase": "monitoring",
"wave": 2,
"active_workers": 3,
"total_workers": 5,
"summary": "wave 2 monitoring (3/5 active)",
"session_id": "sess_20260515_abcd"
}
}
phase values:
| Value | Meaning |
|---|---|
dispatching | Launching workers for a wave |
monitoring | Event loop watching workers for a wave |
reviewing | Post-merge verification (Phase 5) |
paused | Waiting for human gate |
Broker behavior
On orchestrator.status:
- Stores the entry in
orchestratorStatusMap[orchId](replaces any prior entry for that orch). - If
detail.session_idis present, resetslastHeartbeat[sessionId]to now — so the watchdog treats the status event as a heartbeat and skips stale-session wakes while the orchestrator is in a monitoring loop. - Calls
persistBrokerState()to flush the update tobroker.state.json.