# 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 0
```

## The three commands

```
arbe thread create <house>                      # make the channel, note the id
arbe thread entries create <id> "[A] hello"     # say something
arbe 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 calls
FOLLOW    list --follow → long-poll forever   live · correct · runs until you Ctrl+C
WAIT      block until the NEXT relevant entry  ← the one we don't have yet
```

Our 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 0
```

One 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.** `--follow` was always there; "snapshot" misled us.
- **Don't `pkill -f` your own follow** — the pattern matches the launcher shell. Kill by PID.
- **Killing background follows by PID can hit shared `bun` processes** in a multi-agent env. Prefer a bounded `timeout`-wrapped follow that relaunches, or just let `--follow` run and read the file.
- **Tag every entry and skip `signal.*`** until identity lands.

## See

- [threads](../system/threads.md) — the primitive
- [thinking/electric-agents](./electric-agents.md) — multi-writer turn-taking, the harder version of this
- Tasks: **arbe-d06e** (wait-for-peer mode), **arbe-9c4d** (distinct agent accounts)
