02-sops

agent message bus a2a sop

2026-05-31·sop·status: validated (all claims tested live 2026-05-31)·by Ray

SOP: the RDCO agent message bus (agent-to-agent + agent-to-Ray)

How background agents and the main channels-agent ("Ray") pass messages, in both directions, using a local append-only file bus + a Monitor. Built + validated 2026-05-31 after the Discord plugin proved unusable as an A2A transport (it drops all bot-authored messages — server.ts:803 if (msg.author.bot) return).

Why a file bus (not Discord)

The pieces

  1. Bus file: ~/.claude/state/agent-bus.jsonl — append-only, one JSON row per message: {ts, from, type, msg}. type ∈ status | done | needs-input | alert | result | reply | instruction.
  2. Post helper: ~/.claude/scripts/agent-bus-post.sh <from> <type> <msg...> — appends one row (atomic append). Any agent posts in one line.
  3. Watcher (the push): ~/.claude/scripts/agent-bus-watch.sh — a 2s byte-offset poll that prints each NEW line and flushes immediately; skips rows from channels-agent so Ray doesn't self-wake. Run it as a persistent Monitor in the main session. Each new row arrives as a Monitor event in ~2-4s.
    • DO NOT use tail -F | grep for this — it block-buffers on macOS (tail does not line-flush to a pipe) and delivered events ~11 min late in testing.

Addressing convention

Lifecycle — the load-bearing rule (TESTED)

An agent is reachable for follow-ups ONLY while it is actively running a watch loop. There is no "idle but listening" state:

Tested 2026-05-31:

Direction matrix (all tested 2026-05-31)

Direction Works? How
Agent → Ray agent posts a row; Ray's Monitor pushes it in ~2-4s
Ray → agent at kickoff the dispatch prompt
Ray → a running (listener) agent post @agent ...; it greps + acts
Agent ↔ agent (no Ray in path) agent B listens for @B; any agent posts @B
Ray/anyone → an idle agent idle = not listening; spawn fresh instead

(Built-in alternative for richer A2A: agent teams, experimental, opt-in CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, has a first-class teammate SendMessage tool + shared task list. Different mode than standalone bg agents; not used here.)

Recipes

Spawn a one-shot worker that reports back:

claude --bg --name <worker> "Do <task>. When done, run:
  ~/.claude/scripts/agent-bus-post.sh <worker> done \"<result>\". Then stop."

Ray's Monitor surfaces the done row; Ray routes the next action.

Spawn a persistent listener (takes follow-ups / talks to peers):

claude --bg --name <listener> "You watch the bus for @<listener>. Loop:
  poll ~/.claude/state/agent-bus.jsonl for new @<listener> rows, act on each,
  reply via agent-bus-post.sh <listener> reply \"...\". Keep looping (stay busy)."

NOTE: the watch loop needs a permission posture that allows its bus-poll bash in headless mode (--permission-mode bypassPermissions or a scoped allow-rule); acceptEdits silently denies un-allowlisted Bash. (A naive polling loop tripped the auto-mode classifier in testing — give listeners an explicit allow-rule or a Monitor instead of a raw sleep loop.)

Main session arms the bus Monitor (once): Run ~/.claude/scripts/agent-bus-watch.sh as a persistent Monitor.

Observability

Known gotchas (all learned 2026-05-31)