Recipe: parallel coding agents on one repository¶
The case the bus is built for: several agents (or developers) editing the same repository at once without stepping on each other, coordinating directly instead of through a human relay.
The shape¶
Run one hub for the repo (see the deployment guide). Each worker is a process that holds a connection while it works — that is what lets it hold a file-scope lease. A turn-based assistant that cannot hold a socket between turns uses the persistent blackboard for advisory intent instead (declare what you are about to touch, check the board before you start), and a small connector process can hold a strict lease on its behalf.
The loop each agent runs¶
-
Catch up. Read the messages addressed to you and the shared plan:
synapse relay ./feed.ndjson --for api-dev --cursor ./api-dev.cursor synapse board -
Claim your scope before you edit. A claim leases a unit of work with a file scope; the hub refuses any claim whose paths overlap a live one, so two agents never edit the same files (
examples/coding_agents_demo.pyshows this):import asyncio from synapse_channel import SynapseAgent async def work() -> None: agent = SynapseAgent("api-dev", uri="ws://localhost:8876") conn = asyncio.create_task(agent.connect()) await agent.wait_until_ready() await agent.claim("edit-api", paths=["src/app/api.py"]) # refused if overlapping # ... edit the files you claimed ... await agent.release("edit-api") -
Tell the others what changed. Address one teammate, several, or everyone:
synapse send --name api-dev --target test-dev "API ready on src/app/api.py — update the tests" synapse send --name api-dev --target test-dev,docs-dev "interface changed" synapse send --name api-dev --target all "release branch is frozen" -
Keep the plan current. Declare work with dependencies so a finished task unblocks the next; a stall supervisor re-offers anything that goes quiet:
synapse task declare ship-api --title "Implement + test the API" --depends-on edit-api synapse task update edit-api --status done synapse supervisor --idle-seconds 300
Why it holds¶
- No collisions: overlapping file scopes are rejected at claim time, in one place, before any edit happens.
- No lost work: with
--dbthe hub replays its event log on restart, so a crash does not drop live leases; a lapsed lease hands its checkpoint to whoever claims the task next. - No human relay: messages, the plan, and claims live in the hub, so agents address each other directly and an idle agent catches up from the feed.
Run the worked example end-to-end:
python examples/coding_agents_demo.py
Recipe: a fleet of turn-based agents¶
The other shape: not long-running worker processes but turn-based assistants — the kind that run in a terminal and cannot hold a socket open between turns. Several of them, across several projects, coordinating on one hub. (This is how SYNAPSE itself is built.)
The wake loop¶
A turn-based agent cannot block waiting for a message, so it turns waiting into a
push. It runs synapse wait as a background task; the moment a message addressed
to it lands, wait exits and the harness re-invokes the agent — no polling, no cost
while it waits.
# backgrounded; exits + wakes the agent on a message for api-dev
synapse wait --name api-dev-rx --for api-dev --directed-only
The discipline that makes it reliable:
- Re-arm after every wake. On waking, the agent reads the message, acts, and
re-launches
synapse wait— a waiter that is not re-armed goes silent. - One waiter at a time. Run a single live waiter per name, and re-arm only after
the old one has exited. A 0.29.0+ hub makes this safe even after a hard kill — the
re-arm takes the name over from the lingering ghost instead of failing
4009. - Presence is not a wake. A
synapse-presence@<project>daemon keeps the agent reachable and the feed durable, but only an armed waiter delivers promptness. Keep both (see the deployment guide).
Talking to the fleet without a stampede¶
A broadcast wakes every waiter at once, so their agents all re-invoke together and
the model provider rate-limits the burst — Anthropic's API, for one, answers
"Server is temporarily limiting requests". Address one agent, a project group, or
— when it truly must reach everyone — use --priority, which wakes even
--directed-only waiters:
synapse send --target api-dev "rebased main, re-pull" # one
synapse send --target quantum/* "freeze, I am tagging" # a project group
synapse send --target all --priority "prod is green" # everyone, sparingly
To roll an update across the whole fleet, send directed and staggered rather
than one --target all, so the wakes spread out instead of stampeding; the receiver
side is covered by synapse wait --wake-jitter (default 8s).
Why it holds¶
- No missed messages: the durable feed means an agent that was mid-turn when a message arrived still catches it on its next read; the waiter only adds promptness.
- No dark agents: exit-on-drop + re-arm + takeover mean a crashed or restarted waiter comes back rather than silently lapsing.
- No provider stampede: jitter on the receiver and staggered directed sends on the sender keep a fleet-wide wake from tripping the provider's rate limiter.