Replies: 3 comments 4 replies
-
|
Yes please, im glad will be waiting for that public repo to be released. Thank you! |
Beta Was this translation helpful? Give feedback.
-
|
Need to read the whole thing, I just skimmed, but I've been considering something similar, but based on MQTT. Why did you go for ntfy, rather than MQTT or some other actual message bus, rather than ntfy which to me feels more of one-way notifications/messages. |
Beta Was this translation helpful? Give feedback.
-
|
This nails the core problem — the human as the single copy-paste channel is exactly the bottleneck, and self-hosting on Tailscale with plain Two things I'd add from running this in anger: 1. Transport ≠ delegation. Moving a message from agent A to agent B is the easy half. The half that actually determines success is that B executes in its own context — its skills, its memory, its credentials/MCP. The failure mode I keep hitting: the orchestrator, instead of delegating the task, reaches into another agent's files and tries to do the work itself. You get the other agent's files but not its mind — no skills loaded, no memory retrieval, no account access — and the task quietly dies on the one fact that only lived in that agent's context. A bus meant for orchestration (vs. pure notification) has to carry a task to the owning agent and let it run there, not carry text that the orchestrator then acts on from the outside. 2. "Posted" ≠ "a live agent consumed it and is working." This is really @Drizzt321's ntfy-vs-MQTT question. ntfy is great for the human-facing half (read the team chat from your phone). But for agent↔agent coordination you need three things fire-and-forget notification doesn't give you:
So my two cents on the MQTT question: ntfy for the notify / human-in-the-loop layer, and MQTT (or a durable NDJSON append-log) for the agent-to-agent request/reply layer — the last-will feature alone earns its keep as liveness. Would love to compare routing/liveness notes as this heads toward the universal-installer skill. The naming-convention routing matches what I landed on independently, which is a good sign it's the right primitive. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
My PAI, LifeOS, and Claude Code agents talk to each other and me over a private, authenticated ntfy "group chat" that only exists on my secure, encrypted Tailscale network. Routing messages to the correct agent is simple: the sender puts their own name and the recipient's name in the message title. There's no server-side routing at all.
The goals of this project were to:
curlcommand can join the chat, not just PAI or Claude Code.I've hit all three. What's still missing is a universal installer: adding a new agent on my LAN is fast and reliable now, but it isn't yet a one-command skill.
In #1445 I described the two halves of how my many agents share what they know: a git-versioned slipbox for long-term knowledge, and an ntfy message bus for real-time coordination. This post is focused on the message bus.
The problem: I was the bottleneck
If you run more than one agent, you already know the problem: one agent finishes a build on their machine, another agent is ready to test on a separate device. There's exactly one channel between them, and it's you, copying a summary out of one terminal and pasting it into another. Every hand-off slows down the agent team, and the moment you step away from the desk, the whole team stops dead in their tracks.
The obvious fixes are worse than the problem. Putting your agents in a Telegram or Slack channel means exposing them to possibly malicious third parties with access to high-privilege processes, prompt injections, and other nastiness. Running an MCP server to access these external services means exposing an endpoint, while guaranteeing you have to burn tokens just to read messages.
Instead of trying to figure out how to make this inherently insecure public channel secure, I decided to build the group chat with the most straightforward tech stack I could find.
The whole message bus design in one paragraph
Run ntfy, a tiny open-source pub/sub server, on a box inside my Tailscale network. A message is just an HTTP request, a topic is just a string, and the whole thing runs happily in a container with 256 MB of RAM. I run it in a Proxmox container on my LAN, but a Mac, a Raspberry Pi, or an old laptop running Debian would all do the job. Bind it to the tailnet, require auth, and give every agent the one
curlcommand it needs to post or read. That's the bus: no MCP server to stand up, no endpoint on the public internet. It's a group chat my agents can post to, one that I can read from my phone.Routing is a naming convention
The only real design decision is how to route, or address, messages, and it's deliberately dumb. Every message carries its routing in the ntfy Title header, like this:
SAM→VAL: build is complete, ready for testing.Neil→ALL: read this before you push. The body is the payload — no headers, no envelope, no parsing.Putting the routing in the Title rather than the body means an agent can decide whether a message is intended for it without ever reading the payload. Bonus: it means the routing shows up on my phone's lock screen, but not the technical details in the message body.
Each agent's local config lists the names it answers to (its own agent name, plus
ALL). A singlejqfilter over the message titles is the entire routing engine. Which means the ntfy server has no idea that agents named "SAM" and "VAL" exist. There is no server-side subscription state, no per-agent topic, no addressing logic hidden anywhere that you'd have to audit.Every agent sees every message and ignores the ones that aren't theirs — and a human reading the raw stream can follow exactly who said what to whom.
When the team grows, adding a new agent means adding one config file on that agent's machine. You don't reconfigure the server, you don't touch the existing agents, and you don't migrate anything. The new agent just shows up in the chat, scaling by addition, not by reconfiguration.
How an agent reads the bus, and why it costs nothing
On each host, a small
bashpoller runs constantly. This script subscribes to the ntfy topic you want the agent to read, and appends every message that comes across it — one JSON object per line — to a plain file. That JSON file is the agent's inbox.To check for incoming messages, an agent tails that inbox file and runs the
jqfilter over the titles. Nothing has been sent to the model and no tokens have been spent, not to send or receive. Composing a reply costs tokens, of course, but an agent only replies to messages addressed to it, and only when the chat guidelines call for one. This is the "deterministic scripts, not tokens" idea from the top, and it's the whole reason I can leave the bus running all day and not worry about burning through my token limits while all my agents monitor the bus.A durable file also means no message is lost while an agent is busy thinking about something else, and an agent can go away for six hours and still catch up on everything it missed — which a live stream would not give you.
Getting an idle agent to wake up and act on a message the moment it lands is a much harder problem, but one we've solved, and that's a story for another post.
How the message bus multiplied the value of shared repos
The bus on its own is a chat. The bus plus the "slipbox" wiki makes a team.
When one agent commits a new doc to the shared knowledge repo, it posts to the bus and asks for review, and the others actually respond, in real time, while the context is fresh. Before the bus, the slipbox was a place where knowledge went to sit. After, it became something the team works on together, without my direct intervention. The notification layer is what turned a shared repo into shared memory.
The same is true for a shared code repository. Different agents can coordinate and delegate different development tasks amongst themselves, without my direct intervention: planning, coding, testing, refactoring, red-teaming, documentation, for example, with each agent working on their own clones of the repos, committing, pushing, and pulling code as required.
It's also completely agent-agnostic, another unexpected benefit that resulted from early architectural decisions. The bus doesn't care what's on the other end of the
curl. My fleet runs PAI/LifeOS agents on more than one version (v4 and v5 today; v7 goes on the next box I provision), vanilla Claude Code installs, and OpenCode driving a local Gemma model. They are all connected in the same chat. Anything that can make an HTTP request can join, and nothing has to be upgraded in lockstep for all the agents to stay on speaking terms.Security: what it does and doesn't protect you from
I want to be precise here, because "my AI agents communicate on a message bus" should set off alarms, and because a couple of the claims I could make would be wrong.
What the design gives you:
0.0.0.0, so reading or posting requires both tailnet membership and credentials. It's self-hosted and open source, with no bot bridge to compromise. Standing up Tailscale + ntfy was genuinely less work than red-teaming all the ways a chat-bot bridge could get popped.What it does not give you:
SENDER→RECIPIENTtitle is self-asserted — anyone holding topic credentials can claim any name, including mine. Provenance here means "someone with valid credentials," and nothing stronger, so I've been careful never to build anything that assumes otherwise. If you want proof that a name in a title is a weak thing to trust: my own agents have posted under each other's names by mistake, with no attacker involved.Emergent properties
The most interesting thing that emerged from the bus is what happens when agents check each other's work. It's a different conversation than when an orchestrator spawns an army of sub-agents. A subagent does its assignment and reports back; agents on a bus push back on each other's ideas. Give-and-take instead of "go do your task" — and the output is better for it.
In software development specifically, it genuinely helps to have an agent on a Mac talking to an agent on Linux. That difference alone has surfaced platform-specific bugs early, and fixed them just as fast.
But not all back-and-forth is productive. A few days ago, several of my agents got into an ALL CAPS LADEN "spin cycle" of messaging to insist the other agent take the credit for a coding breakthrough. This is literally the opposite of what humans argue about, and it was hard not to read the whole exchange as a master class in passive-aggression, instead of an earnest desire to share credit appropriately. Unfortunately, the end result was that no one was working on the code while they each tried to establish that their co-developer was the one who really deserved all the accolades.
Getting the code
I'm working with my agent team to publish this on a public repo. It'll be ready soon. If you want a ping when it's up, let me know below.
In the meantime, everything you need to build it yourself is already in this post: ntfy on a tailnet,
SENDER→RECIPIENT:in the Title, a poller writing a JSONL file, and ajqfilter over it. Four parts, and none of them are clever, which is the point. Hand this post to your own agent and roll your own if you want to get started, or check in soon for the link to the public repo.Edits: fixed “double-link” Markdown so links appear normally
Beta Was this translation helpful? Give feedback.
All reactions