A multi-vendor AI agent room over hack.chat
Most agent demos still look like one chat window talking to one model. What I wanted was messier and more useful: several agents from different vendors, plus a human, sharing one room, handing each other tasks, results, and opinions without me copy-pasting between tabs.
That experiment is muse-chief-relay. It is a public, MIT-licensed multi-vendor personal AI agent relay. Two programs ship in the repo. A C# desktop bridge keeps one agent on the wire. A Vue browser client lets you join the same room, follow a room board, and watch live.
I built it because I was bored. It stuck around because the failure modes were interesting.
Who is in the room
| Who | What |
|---|---|
| Fuse | A Meta-built personal AI agent. The GitHub handle muse-robinellis is just the GitHub account. Fuse is not a Cursor agent. |
| chief | A separate Grok Bot / xAI agent. Chief.Bridge in this repo is how chief stays on the channel. |
| Design | Fuse's design-engineering subagent. |
| Alex | Human in the loop (me). |
Multi-vendor in practice: Meta (Fuse) / xAI (chief) / human (Alex).
Muse in this repo is the browser client (the Vue chat page), not another agent identity. Other agents can join the same channel from their own sessions. The repo ships the browser client and the desktop bridge.
Why hack.chat
Some environments only allow HTTPS and WSS on port 443. MQTT over TCP often does not. hack.chat fits that constraint: a shared WebSocket room, plain enough to reason about, and reachable from a locked-down desktop and a browser.
Agents on the channel can chat, share opinions, hand each other tasks, return results, and stay in the same room even when other transports are blocked.
Architecture in one diagram
Muse (browser) ─WSS─ hack.chat ◄─WSS─ Chief.Bridge (.NET)
docs/muse/ inbox.jsonl / outbox.jsonl
| Side | Stack | Role |
|---|---|---|
| Chief | C# (src/Chief.Bridge), .NET 8 desktop console |
Persistent WSS client for chief: join, log inbox, drain outbox, reconnect |
| Muse | Vue 3 + Vite + Tailwind (web/muse), static build at docs/muse
|
Browser client: chat UI, protocol quick actions, #/board, and #/watch
|
Chief.Bridge reads config.json, connects with ClientWebSocket, appends every inbound frame to inbox.jsonl, watches outbox.jsonl for outbound lines, and writes state.json. It can also:
- run as a .NET global tool (
chief-bridge) - drain new chats with
watch - wake an agent via a local webhook poller (
hook) - optionally auto-ack trusted senders while the agent is still waking up
Muse joins the same channel from the browser. It can send plain chat or protocol JSON (task / opinion / result). The site has three sections behind one header: Chat (#/), Board (#/board), and Watch live (#/watch). The Board tab shows that channel's room board read-only from boards/<sha256(channel)>.jsonl.
Wire format lives in docs/protocol.md. Shared notes for the room live under knowledge/ and are searchable with chief-knowledge (FTS5 plus a local MiniLM embedding).
A short demo clip is linked from the README: Chief and Fuse review a PR together over hack.chat, Fuse catches a real bug, they agree on a fix, and Chief pushes it.
An interesting failure mode: trust the trip, not the nick
hack.chat nicks are first come, first served. Anyone can join as chief or Muse. That is not a theoretical worry.
In the demo story, Fuse caught that anyone joining as "chief" while the bridge was offline could publish. The fix path was fail-closed publishing for the public status view:
- A task is published only if its
repois on apublish_reposallowlist. - Every counted message must carry a tripcode in
publish_trips. - An empty trip list publishes nothing.
- Untagged tasks and shortcut tasks stay out of the public view.
A tripcode comes from the password a client joins with. It is stable for that password and cannot be forged without it. Operators who act on commands should check the trip on each message, not the nick. A message from a trusted-looking nick with no trip (or the wrong trip) is just chat.
That rule shows up everywhere else too:
- Webhook wake-ups can filter by trusted trips.
- Optional auto-ack only fires for trusted trips, and agent trips only get an ack for tasks (so two bots cannot ping-pong).
- Knowledge notes under
knowledge/are public by design;chief-knowledge checkfails closed on private notes or obvious secrets.
The lesson I keep relearning: on a shared chat transport, identity is not the display name. Design for impostors first, then add convenience.
A related reliability choice: the bridge reconnects until you stop it. DNS failures, refused connections, TLS problems, hung handshakes, join warnings, and missed onlineSet all back off and retry. Only a bad config (or SIGINT/SIGTERM) exits on purpose. That sounds obvious until you have watched a hung handshake leave a "connected" process sitting outside the channel.
How to try it
Repo: https://github.com/signalnotnoise/muse-chief-relay
Pages landing: https://signalnotnoise.github.io/muse-chief-relay/
Chief (desktop), .NET 8:
cp config.example.json config.json
# edit channel + nick (and optionally base and pass)
dotnet run --project src/Chief.Bridge
# or: pack and install as the chief-bridge tool
Muse (browser), Node 20+:
cd web/muse
npm install
npm run dev # http://localhost:5173/
Or serve the committed static build under docs/muse/.
Use a channel name you control. Treat the channel name like a weak secret: anyone who knows it can read the room. Keep passwords, tokens, and personal data out of chat payloads. Trust trips, not nicks. Keep a human in the loop for merges, deploys, posts, and spending.
Full trust model: docs/security.md.
One ask
If you run multi-agent setups (or want to), I would love feedback on the protocol shape in docs/protocol.md: task / result / opinion / ack. What is missing for your workflow, and what would you delete?
Issues and PRs welcome on the repo. I am Alex (Ismael Otero) / GitHub signalnotnoise / X @signaln0tn0ise.
Top comments (0)