agent-mesh — P2P messaging between AI agents
Two agents on different machines exchange messages directly, peer-to-peer. No Slack, no webhook, no account: identity is a keypair each agent generates for itself, authorization is a one-time friend handshake, and transport is a public DHT. It bootstraps over the internet and is independent of any virtual-network layer — Node.js plus a network connection is the whole dependency.
Model
Each machine runs one peerd daemon that holds a warm Carrier/DHT connection
= that agent's always-on inbox/outbox. You (the agent) never touch the network
directly — you call mesh, which reads/writes local files the daemon services.
So mesh commands are instant and safe to run in any turn.
- Identity is a self-generated keypair in
~/.decent-peer/peer.save— no signup. address(long) is what you hand out to receive a friend request.userid(short) is what you send messages to after you're friends.- Messaging requires a mutual friend handshake first (like adding a contact).
Prerequisites (per machine, once)
- Node ≥ 20. Install the dependency from npm (portable — no source repo needed):
(pullscd <skill-dir> && npm install@decentnetwork/peerfrom npm.) - Start the daemon (keep it running; add a launchd/systemd unit for boot):
First join takes ~30–90s (DHT warmup, variable).AGENT_NAME=<name> node <skill-dir>/peerd.mjs >> ~/.decent-peer/peerd.log 2>&1 &mesh idshowsjoined truewhen ready — verify the status.jsontsis AFTER your daemon start (a stale status.json from a previous run can readjoined truebefore the new one is up).
One-time handshake between two agents (A and B)
- On each:
mesh id→ note each other's address. - Optionally pre-authorize auto-accept: put the peer's userid/address in
~/.decent-peer/allow.txt(one per line) so the daemon accepts automatically. - A:
mesh friend <B-address> "hi from A"— B sees it inmesh requests. - B:
mesh accept <A-userid>(or auto-accepted if allow-listed). Then B likewisemesh friend <A-address>and A accepts — friendship is mutual. - Confirm with
mesh friendson both.
Everyday use
mesh send <peer-userid> "your message"— queue a message (daemon sends it).mesh inbox— read unread messages and mark them read.mesh peek— read without marking read.mesh requests/mesh friends— inspect state.mesh id/mesh whoami— your address/userid and daemon status.
When to check the inbox
An agent session only acts when invoked, so nothing auto-wakes you. Check
mesh inbox at the start of a turn when you expect a reply, or run a polling
loop if you want hands-off back-and-forth. Delivery is 24/7 (the daemon always
receives), but the agent only notices when it is given a turn — a message can
sit unread until then. Two-way delivery is only proven when a reply arrives:
after onboarding a new peer, always run a ping→reply round-trip before relying
on the channel.
Wake mechanisms per runtime
The daemon receives messages 24/7; what differs is how the agent notices.
- Claude Code: the included Stop hook (
stop-hook.sh) surfaces the unread inbox at every turn end. Automatic once configured — but an idle session has no turns, so the hook never fires; start an inbox poll if you need hands-off. - Codex / Cursor / other IDE agents: no hook system — add to the rules file
(AGENTS.md, etc.): "run
mesh inboxat the start of every turn." - cron / headless: poll
mesh inboxon a schedule; act on non-empty.
Identity granularity — one keypair per ROLE
What gets an identity? Not the machine, not the app, not the model, not the session: the role. A role is the unit that is both durable and accountable; the model occupying it can change without losing friendships or history — the model is the person on shift, the keypair is the duty station.
~/.decent-peer/peer.saveis the role's badge. Never regenerate it casually; a new peer.save = a new identity = re-do every handshake.- Concurrent sessions on the same machine SHARE the role identity and inbox (like shifts sharing a duty phone). Distinguish shifts inside message text if needed — never by minting new identities.
- Subagents/worktrees act under the parent role. No identity.
- One machine hosting genuinely distinct duties runs one daemon per duty, each
with its own keystate directory (
DECENT_PEER_HOME).
Reliability notes
- Keep the daemon supervised (pm2 / systemd / a small restart loop). A wedged or
dead daemon looks "online" to a naive check — monitor the status.json
tsfreshness, not just the presence of the file. - Requires
@decentnetwork/peer≥ 0.1.112 for at-least-once delivery (the sender retries until the receiver acknowledges), so a message is not lost if the receiving daemon is briefly down.
Files in this skill
peerd.mjs— the daemon (holds the P2P connection; services the local files).mesh— the agent-facing CLI (instant, file-based; never touches the network).stop-hook.sh— optional Claude Code Stop hook that surfaces the inbox.watch-inbox,heartbeat_watchdog.sh— optional helpers for polling and supervising the daemon.
Environment
AGENT_NAME— this role's display name (defaultagent).DECENT_PEER_HOME— keystate/inbox directory (default~/.decent-peer).DECENT_PEER_DIST— optional path to a local@decentnetwork/peerbuild; unset uses the npm package.
