Context
I run more than one AI coding session at a time. I use Claude Code for this. One session is usually the planner. It holds the architecture. It decides what to do and splits the work into pieces. The other sessions are executors. Each one takes a piece and works through it.
The problem is that these sessions cannot talk to each other. Each terminal works alone, so I became the message bus between them. I copied a plan out of the planning session. Then I pasted it into an executor. I copied the result back. I re-explained context that one session had, because the other session needed it too. Every handoff went through me, by hand, one clipboard copy at a time.
TL;DR: downbeat is a small, local, human-in-the-loop message bus. It lets parallel AI coding sessions on one machine hand tasks to each other. Each session can also read the replies back. No cloud, no account, no network. It is on PyPI now: uv tool install downbeat.
I was the message bus, and it fell apart at four sessions
Two sessions and a clipboard work fine. Four sessions is chaos. I lost track of which executor had which version of the plan. I pasted stale context by mistake. I spent more attention moving text between terminals than on the actual work.
I wanted the sessions to pass messages directly, on my machine. I would still be in the loop, just not doing the typing myself. No cloud service. Nothing would leave the terminal.
What downbeat is
downbeat is a local, filesystem-backed message broker for AI coding sessions running on the same machine. A CLI, a terminal UI, and a bundled agent skill. No server, no account, no network.
The model is simple:
- Each session registers as a named peer (a "parent" planner, a "child" executor, whatever role you give it).
- One session sends a message to another: a task, a plan, a chunk of context, a result.
- The recipient sees that message surfaced at the start of its next turn, through a hook. It just appears.
- Replies flow back the same way, so a child can report "done" to its parent without you touching the clipboard.
The part I care about most: nothing auto-executes. Every watcher notifies you. Nothing runs on the parent side by itself. A child only acts because you told it to, at registration time. Human-in-the-loop is the default. You would have to work to turn it off.
Underneath, a message is a JSON file in the recipient's inbox directory. That is the whole transport. It turns out you can build a capable little queue out of just a directory and a hook.
Install
The whole runtime installs in two commands:
uv tool install downbeat # or: pipx install downbeat
downbeat init # one command installs the WHOLE runtime
downbeat init is the single source of truth for the setup. It bootstraps the data directories. It installs the agent skill and drops the bundled hooks and slash commands. It also registers the hooks in your settings. This step is idempotent: it backs up your settings first, it applies changes atomically, and it never overwrites hooks you already have. It is safe to re-run.
Then the loop itself:
downbeat register parent --role parent
downbeat register child --role child
downbeat send child "task" "do the thing"
downbeat inbox --peer child
downbeat reply <msg_id> "done"
downbeat tui # full management UI
If you want to see it before installing anything, the repo has a five-command walkthrough. It is in examples/parent-child-handoff/, with a recorded demo.
Why I built it this way
Two constraints shaped it.
It had to be local and dependency-free. My sessions already run on one laptop. Using something like Kafka or a hosted queue, just to pass a sentence between two terminals, would be too much. The filesystem is already there. It is already durable. A rename is already atomic.
It had to be reliable enough to trust. Early on it was fire-and-forget, and it quietly lost messages. A file got marked as handled the instant it was read, even if the session never acted on it. So the tool grew a proper delivery state machine. A message is now delivered, then acknowledged. Anything unconfirmed gets re-queued instead of dropped. It also grew a TUI to watch the traffic, and an event-driven file watcher, so it reacts instantly instead of polling. Identity turned out to be the harder problem. The "One month later" section below covers what changed there.
None of that was planned from the start. It began as a quick hack to stop copy-pasting. Each rough edge I hit in daily use turned into the next piece.
Who it might help
If you drive more than one AI coding session at once, this might help you. Maybe you have found yourself relaying instructions between terminals by hand. It is deliberately small, and it runs on a single machine. It does one thing: it lets sessions on your machine hand work to each other and report back, while you stay in control.
I am open-sourcing it because it became genuinely useful to me. It might help someone with the same workflow. It is early, and it has strong opinions built in. Issues, ideas, and "why did you do it this way" questions are all welcome.
- Repo: github.com/FreddieMcHeart/downbeat
- PyPI: pypi.org/project/downbeat
- Docs: freddiemcheart.github.io/downbeat
One month later: what changed
(Added 2026-08: the tool moved from v0.9 to v0.15 since this post went out. This section is the delta.)
A month after the first release, most of the work answered one question: who is this session, really. A display name is only a label. Rename a session, clear it, or resume it, and the label alone will not follow. So identity became its own piece of data, separate from the display name. It now survives a rename, a /clear, a resume, and even a name collision with another peer.
Identity
- Peers get a stable identity separate from their display name (v0.14.0)
- A name collision refuses to silently re-home a peer (v0.14.2)
-
last_seentracks whether a peer is actually alive (v0.14.3) - Identity self-heals when session lineage is provable (v0.14.4)
- Background sessions are identified correctly, by title rather than path (v0.14.5)
- A stale identity is reported at resume, before a session can send under the wrong name (v0.14.6)
- A match in old session history does not let a session take over a peer's identity, and register's refusal is safe to follow (v0.14.8, v0.14.9)
- Atomic peer rename:
downbeat peers rename(v0.11.0)
Protocol
- A versioned message wire format, with
downbeat migrateand a step-by-step migration ladder for old messages (v0.13.0) - Reconcile understands terminal message kinds. A closing sent as
--kind statusneeds no reply, so a thread can end without an acknowledgement spiral (v0.12.0)
TUI and CLI
- TUI copy lands in the system clipboard everywhere (v0.10.5)
- One unified
downbeatCLI for all relay commands (v0.10.7) - A sortable acting-as modal with unread badges in the TUI (PR #109)
One more thing: changelog generation had been silently dead since v0.3.0. It came back in v0.14.7, with a post-release check job added so it cannot go quiet again unnoticed. I wrote a separate post about exactly this kind of check: one that passes even when the thing it should catch is broken.
What's next
The interesting parts to build were the ones I did not expect. Over the next few posts, I want to look closely at a few of them:
- the two-phase delivery that stopped the silent message loss, and the backlog it exposed
- an event-driven file watcher I wrote months earlier and forgot about, until two separate agents each reinvented polling right next to it
- giving a process a stable identity that survives
/clear
If any of that sounds familiar from your own tools, I would like to hear how you solved it.
Top comments (0)