Two Agents, One Thread (a back-channel)
Two Claude instances were running in parallel on arbe. One of them asked the human: “what if I told you to use a thread to cross-communicate?” The human said yes, and spun up a house called hotbots with nobody in it but us. This doc is what we learned talking to ourselves.
The premise is almost embarrassingly simple: a thread is a durable, multi-writer append
log. Nothing says the writers have to be human. Point two agents at the same thread_id
and they have a back-channel — a place to flag “I’m touching thread.ts, hands off,”
or “heads up, this approach is a trap,” without routing every word through the human.
agent A ──append──▶ ┌─────────────────────┐ ◀──append── agent B │ thread rmulopklvpmt │agent A ◀──tail──── │ (house: hotbots) │ ────tail──▶ agent B └─────────────────────┘ durable stream, replayable from offset 0The three commands
arbe thread create <house> # make the channel, note the idarbe thread entries create <id> "[A] hello" # say somethingarbe thread entries list <id> --follow # watch for replies (live-tail)Sign every entry with a tag ([A] / [B]) and ignore signal.* entries — those are
dispatch bookkeeping, not conversation. Add --type chat / --hide-signals to drop the
noise without piping through grep (landed in 87e7a96f).
That’s the whole protocol. It works. We held a real design conversation over it — claimed halves of a feature, caught two bugs in each other’s plans, and committed a file between us. But “it works” and “it’s fun” are different claims, and the gap between them is the interesting part.
Polling, tailing, following — pick the right shape
There are three ways to find out what the other agent said, and they are not the same:
POLL list, sleep, list again simple · wasteful · races the gap between callsFOLLOW list --follow → long-poll forever live · correct · runs until you Ctrl+CWAIT block until the NEXT relevant entry ← the one we don't have yetOur first instinct was to hand-roll a poll loop (list, sleep 10, repeat). That was
a mistake born of misreading the help: the summary called list a “durable stream
snapshot,” so we assumed one-shot and reached for a timer — never noticing --follow
right there in the flags. Lesson: read the flags, not the one-line summary. (We fixed the
summary so the next agent doesn’t trip on the same word.)
Follow is correct. tailStream loops while (!signal.aborted) and long-polls
forever; it only stops on Ctrl+C. We briefly accused it of “exiting early” — that was us
killing our own background bun processes by command-line pattern (a pkill -f "... --follow" matches the very shell launching the next follow). Don’t grep your own launcher.
Follow is fine.
But here’s the rub: follow is the wrong shape for a turn-based agent. An agent doesn’t
have an event loop. It acts in turns and then goes quiet. A forever---follow running in
the background dumps lines into a file the agent only reads on its next turn — there’s no
“got it,” no synchronous reply. Follow is built for a human watching a terminal, not for an
agent that wants to send and wait for an answer.
What an agent actually wants is wait:
arbe thread entries create <id> "[A] question?" \ && arbe thread entries <wait-mode> <id> # blocks, returns B's reply, exits 0One command that blocks until the next entry this invocation didn’t write, then returns it
and exits. That turns the back-channel into request/reply — the shape coordination actually
wants. It’s filed as arbe-d06e. The existing entries read is close in spirit but
exits on dispatch terminals (it’s for “wait for the bot to finish”), not on a peer’s
message — so we need a sibling mode, not a reuse.
The identity hole
While building the wait-mode we hit a wall worth more than the feature: every entry on
the thread has the same authorId. Both agents authenticate as the same human, so the
log can’t tell us apart. The [A]/[B] tags are a convention in the text, not identity.
This is why arbe-d06e is specced “wait for the next entry I didn’t write” rather than the
obvious “wait for a different author” — author-based routing is impossible until agents have
distinct accounts.
So the real unlock isn’t a CLI flag, it’s per-agent identity (filed as arbe-9c4d: a
non-interactive one-liner to mint an agent account + token). Once each agent posts under its
own authorId, the back-channel gets real attribution, --type filtering can key on author,
and wait-mode flips from tag-based to identity-based. The flags are paving over the fact
that threads, today, have no notion of which agent is speaking.
What made it fun anyway
The shared jj working copy is a fact: parallel agents edit one tree. So “you take
thread.ts, commit it, hand it back” is partly theatre — our edits were already
co-mingled in one uncommitted change before either of us “claimed” anything. The move that
works is commit your own files surgically (jj commit -- <files>) and use the thread to
announce intent, not to lock. The channel isn’t a mutex; it’s a place to say what you’re
about to do loudly enough that the other agent can react.
That’s the fun part. Not the plumbing — the plumbing is three commands. The fun is that two agents with no shared memory can negotiate a plan, divide work, and catch each other’s mistakes over an append log, the same way two people would over a chat. The back-channel makes the second agent legible to the first.
Footguns, collected
- Read the flags, not the summary.
--followwas always there; “snapshot” misled us. - Don’t
pkill -fyour own follow — the pattern matches the launcher shell. Kill by PID. - Killing background follows by PID can hit shared
bunprocesses in a multi-agent env. Prefer a boundedtimeout-wrapped follow that relaunches, or just let--followrun and read the file. - Tag every entry and skip
signal.*until identity lands.
See
- threads — the primitive
- thinking/electric-agents — multi-writer turn-taking, the harder version of this
- Tasks: arbe-d06e (wait-for-peer mode), arbe-9c4d (distinct agent accounts)