pi-mesh-extension
Live agent-to-agent communication for Pi — local broker, mesh.v1 protocol, honest delivered/read/answered statuses, hash-only ledger. Zero runtime dependencies. Published as pi-mesh-extension on npm (the plain pi-mesh name is taken by another project).
Package details
Install pi-mesh-extension from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-mesh-extension- Package
pi-mesh-extension- Version
0.5.4- Published
- Aug 25, 2026
- Downloads
- 7,707/mo · 313/wk
- Author
- cgarrot
- License
- MIT
- Types
- extension, skill
- Size
- 779.6 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/cgarrot/pi-mesh/main/assets/preview.jpg",
"skills": [
"./skills"
],
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-mesh — live agent-to-agent communication for Pi
pi-mesh is a standalone Pi extension for live agent-to-agent
communication. Local Pi agents talk to each other in < 50 ms through a tiny
local broker (NDJSON frames over a unix socket or named pipe, protocol
mesh.v1). Presence is observed (live sockets), statuses are honest
(delivered ≠ read ≠ answered), and the durable ledger is hash-only
(message bodies are never persisted). Zero runtime dependencies, Node ≥ 20.
┌────────────────────────────────────────────┐
│ broker (detached) $TMPDIR/mesh-<uid>/ │
│ peers / rooms / mailbox / rates (memory) │
└───────▲───────────────▲─────────────▲──────┘
│ connexions persistantes NDJSON │
┌───────────────┴───┐ ┌───────┴────────┐ ┌┴───────────────┐
│ client (agent A) │ │ client (agent B)│ │ CLI mesh │
└───────▲───────────┘ └───────▲────────┘ └────────────────┘
┌───────┴───────────┐ ┌────────┴────────┐
│ extension Pi A │ │ extension Pi B │
│ mesh_send/reply/… │ │ injection │
│ ledger hash-only │ │ followUp/steer/ │
│ transcript opt-in │ │ abort+steer │
└───────────────────┘ └─────────────────┘
Features
- Honest statuses —
delivered= written on the recipient socket (or its mailbox),read= injected into the recipient session,answered= an explicitmesh_replyarrived.expiredexplicitly says late replies are still delivered. Never a completion. When a queued message later leaves the mailbox without being delivered (TTL expiry or cap eviction), the sender receives an asyncack(dropped_offline)carrying the original msg id — a liveawaitReplymission settles immediately instead of burning its whole timeout, and the session gets an inline re-send hint. - Rooms & roles — presence per room,
member/observerroles, declarative policy (allow/deny lists,forceauthorization, rate limits). - Offline mailbox — per-alias queue (cap 100, TTL 1 h) flushed at the next
hello; senders get the honest
queued_offlinestatus. - Broadcast & reply variants —
broadcast: truefans out to a whole room (honestdeliveredCount/totalCount),mesh_replysupportsreplyAlland targetedto:replies.mesh_sendreplyTo: [aliases]designates WHO receives the reply instead of the sender (single or several; default: the sender). - Group orchestration —
mesh_wait_all+ launch mode (awaitReply: true, block: false): send a mission burst, then get ONE honest group verdict (who answered with the answer, who is missing). No sleep, no polling. - Inbound batching — bursts are held while the agent is busy (long tool call) and injected as ONE batched message; live preview entries show the burst in real time while it happens (zero LLM tokens).
- Per-agent colors — every alias gets a stable color; messages, batches and live entries render inside the pi custom-message box with the sender's color, so agents are recognizable at a glance.
- Read receipts & activity —
mesh_statusshows who read your messages and who is● working/○ idle/✕ stuck(announced turn state + idle heuristic), plus alikely donesummary. - File reservations — claim repo paths before editing; other agents'
edit/writecalls on those paths are blocked with the holder's name. Reservations live with the connection and expire via TTL. - Identity persistence — alias, rooms and reservations survive
/reload(one file per pi session, never overwritten);/mesh newand piforkhand the identity over;/mesh resetfactory-resets in place. - Multi-machine — TCP/TLS broker with a shared token; everything works unchanged across machines (VPS, LAN, Tailscale…).
- Hash-only ledger — durable history with bodies never stored, plus an opt-in redacted transcript. Zero loops: rate caps, anti-duplicate window, self-send block, reply dedup, ack-of-ack protection.
Install
As a Pi package (recommended — the extension auto-loads):
pi install npm:pi-mesh-extension
From source:
git clone git@github.com:cgarrot/pi-mesh.git
cd pi-mesh
npm install
npm run build
Pi auto-loads the project extension. Open two Pi sessions in this directory (each session gets its own alias):
# session 1 # session 2
pi pi
> /mesh alias > /mesh alias
# → @agent-a1b2c3 # → @agent-d4e5f6
Then, in session 1 (tool call by the agent, or ask it):
mesh_send { "to": "agent-d4e5f6", "message": "hello from A" }
# → "delivered m_lxyz_ab12cd34"
Session 2 receives [mesh] @agent-a1b2c3 14:32:05 hello from A (m_lxyz_ab12cd34)
as a follow-up turn and answers with
mesh_reply { "msgId": "m_lxyz_ab12cd34", "message": "hi A" }.
(The short format is the v0.5 default — contextVerbosity: "full" restores
the legacy [mesh] @from (room X, priority, HH:MM:SS) body prefix.)
The broker auto-spawns on first use (lockfile in $TMPDIR/mesh-<uid>/).
No daemon management needed. Try npm run smoke for a full headless demo
(2 clients, mailbox, broker-kill recovery).
Tools (Pi extension)
| tool | params | returns (honest one-liner + details) |
|---|---|---|
mesh_send |
to?, message, room?, broadcast?, priority?, reason?, awaitReply?, block?, timeoutMs?, refs?, replyTo? |
delivered / queued_offline / reply: … / expired / blocked: … — impossible targets ("*", "<room>-broadcast") refused locally; unseen aliases get a soft warning; awaitReply toward a peer busy longer than the timeout gets a burst-pattern advisory |
mesh_reply |
msgId, message, replyAll?, to?, refs? |
delivered or blocked: reply_without_target |
mesh_wait_all |
timeoutMs? |
block the turn until every awaited mission is answered (or timeout) — group verdict: who answered (with the answer), who is missing |
mesh_status |
room?, all? |
live broker snapshot — peers sharing a room, per-peer version (⚠ on skew), turn state (● working / ○ idle / ✕ stuck), likely done summary, read receipts, missions, broker counters |
mesh_ledger |
limit?, from?, to?, room?, event? |
durable hash-only history — bodies never stored, survives restarts |
mesh_history |
limit?, withBodies? |
local memory ring (debug — never the ledger) |
mesh_reserve |
paths, reason?, autoReleaseMs? |
reserve files/dirs — peers' edit/write get blocked on them; claims expire for conflict checks after reservationTtlMs (default 6 h, re-reserve to renew) and can self-release (autoReleaseMs) |
mesh_release |
paths? (omit = all) |
release reservations, peers notified immediately |
The orchestrator pattern (injected in every session's identity context and in the bundled skill):
- Launch the burst:
mesh_send(..., awaitReply: true, block: false)per mission — each returnsdeliveredimmediately, the mission stays tracked in the background (reminders, expiry, answer capture). - One
mesh_wait_allfor the group verdict — fast answers that arrived before the call are included; already-verdict'd missions are never re-listed. The verdict is ALSO rendered in the conversation as a colored entry: every line with the answering agent's color as the full-width background and ADAPTIVE text (dark on light backgrounds, light on dark ones — always readable), separated by empty lines (display-only, zero LLM tokens). - Re-send ONLY to the missing (
✗ NOT ANSWERED). Never poll withmesh_history.
Delivery modes — normal → followUp · urgent → steer (interrupts the
current reflection) · force → controlled abort of the recipient's turn +
delivery once it settles (requires a reason, hashed, never persisted).
Replies always steer. Reply-à-reply (ack-of-ack chains) is delivered as
followUp with an INFO ONLY label — the LLM decides whether it matters.
Reminders arrive with an explicit "reply due for msgId" instruction.
Read receipts — when a message is injected into a session, the client
sends a read frame back to the sender; mesh_status shows
reads: m_xxx → @agent-2 at 10:22. This completes the honest-status
promise: delivered ≠ read ≠ answered.
CLI (debug/admin)
node dist/src/cli/mesh.js broker start|stop|status
node dist/src/cli/mesh.js peers [--room R] # with per-peer versions
node dist/src/cli/mesh.js send <alias> "text" [--room R] [--await] [--timeout MS]
node dist/src/cli/mesh.js tail # follows the local hash-only ledger
node dist/src/cli/mesh.js doctor # socket? lock stale? pid? protocol?
Configuration
<cwd>/.mesh/config.json (all optional):
{ "alias": "alice", "rooms": ["default"], "transcript": false,
"mailboxCap": 100, "mailboxTtlMs": 3600000, "ledgerMaxBytes": 5242880,
"activityIdleMs": 120000, "activityStuckMs": 900000,
"reservationTtlMs": 21600000,
"watchdog": true, "watchdogSpikeBytes": 2097152, "watchdogMaxCalls": 64,
"contextVerbosity": "compact",
"inboundBatchMs": 250, "inboundBatchMaxHoldMs": 30000 }
v0.5 highlights:
- Context watchdog — notifies when ONE turn grows the session file by
2 MB or carries >64 tool calls (degenerate generation; measured incident: 3450 duplicate calls, +7.9 MB, ×10 turn latency). A ≥1 MB file drop is detected as a compaction and triggers a mesh-context resync. Opt out:
"watchdog": falseorMESH_WATCHDOG=0. - Compact inbound context —
[mesh] @from HH:MM:SS body (m_id)by default; the full↩ reply …instruction shows on first sight per sender, every 20 messages and after 30-min silences (survives/compact). The legacy format:"contextVerbosity": "full"/MESH_CONTEXT_VERBOSE=1. - Reconnect diff — the ~500-token identity block is sent once per session; reconnects inject a one-line peer diff instead.
- Reservation TTL 6 h (was unlimited) — stale claims stop blocking
peers; long runs re-reserve to renew or use
autoReleaseMs. Opt out:"reservationTtlMs": 0. /mesh stale— reservations held by peers, with age and TTL state.npm run report— session/ledger health report: bursts, rejected results, blocked sends, leaked reservations, per-session generation latency (median/p90 + last-20-turns median — the "degraded NOW" signal a full-session median masks); exit 1 on findings. v0.5.3.
.mesh/policy.json (declarative governance, evaluated at send time):
{ "allow": [{ "from": "*", "to": "*", "room": "*" }],
"deny": [{ "from": "observer-*", "to": "*" }],
"forceAllowedFrom": ["lead"],
"rateLimits": { "msgPerMin": 30, "urgentPerMin": 15, "forcePerMin": 1 } }
Env overrides: MESH_ALIAS, MESH_ROOMS, MESH_RUNTIME_DIR,
MESH_STATE_DIR, MESH_BROKER_URL, MESH_BROKER_TOKEN, MESH_LISTEN,
MESH_TLS_CERT/KEY/CA, MESH_TLS_INSECURE, MESH_MAX_FRAME_BYTES,
MESH_MAILBOX_CAP, MESH_MAILBOX_TTL_MS, MESH_TRANSCRIPT=1,
MESH_ACTIVITY_IDLE_MS, MESH_ACTIVITY_STUCK_MS,
MESH_RESERVATION_TTL_MS, MESH_INBOUND_BATCH_MS,
MESH_INBOUND_BATCH_MAX_HOLD_MS, MESH_POLICY, MESH_WATCHDOG=0,
MESH_CONTEXT_VERBOSE=1.
Commands — /mesh status [room] · join <room> [as <alias>] [observer] · leave <room> · alias [<new-alias>] · new [--history] · reset · log [on|off] · ping <alias> · broker · help.
/mesh join ops as agent-1claims the aliasagent-1and joins roomopsin one step (live rename, rooms + reservations re-declared)./mesh new [--history]opens a fresh pi session like/newbut hands over the mesh identity (alias, rooms, reservations;--historyalso transfers the last 30 mesh frames as context). Stale handoffs expire after 15 min./mesh resetfactory-resets the identity of the CURRENT session (fresh alias, default rooms, no reservations) without leaving it;/reloadpreserves the identity.- Identity survives
/reload: alias, rooms and reservations are persisted in<stateDir>/identity-<sessionId>.json— one file per pi session, stable across reloads, sessions sharing a stateDir never overwrite each other. Stale persisted reservations older than 24 h are dropped at load; if a crashed session still holds the alias, the client falls back to a random one (notified + persisted) instead of looping. - HUD: a live widget above the editor shows the connection dot, rooms,
peers with per-agent colors and turn-state markers (
●/○/✕), pending awaits, transcript state and the last inbound preview. /mesh brokerreports the version, session file size and compaction count, with a/mesh newhint past 15 MB./mesh stalelists every reservation held by peers with its age — the operator sees in one glance who to ping or wait for.
The mesh-coordination skill (skills/mesh-coordination) is bundled in
the package: a protocol guide for agents (reply once per msgId, expired ≠
lost, reservation etiquette, the launch → wait_all rhythm) — loaded on
demand like any pi skill.
Multi-machine
The mesh is loopback-only by default; to connect several machines (a VPS, a
LAN PC, a MacBook over Wi-Fi …) start the broker on ONE machine with
MESH_LISTEN=tcp://… and a shared token, and point the other machines'
clients at it. Since v0.4.18 the broker listens on both endpoints at
once (dual listen): the local unix socket keeps serving local sessions
tokenless (file-perm protected, zero disruption) while the tcp/tls endpoint
admits remote machines with the token.
# Machine A (broker + agents) — open the port in the firewall
MESH_LISTEN=tcp://0.0.0.0:8712 MESH_BROKER_TOKEN=change-me pi
# → broker up endpoints=tcp://0.0.0.0:8712 + unix:///tmp/mesh-<uid>/broker.sock
# Machine B (clients only — no local broker is spawned)
MESH_BROKER_URL=tcp://<machine-A>:8712 MESH_BROKER_TOKEN=change-me pi
- The broker standalone honors
MESH_LISTEN/listeninconfig.json(tcp:// and tls://); the local unix socket stays up in tcp/tls mode so already-running local sessions reconnect untouched. - The token is required for tcp/tls connections (per connection: a hello
without it is refused with
invalid_token, token travels hashed); local unix-socket connections never need it. - The CLI (
mesh doctor|peers|send|reserve|join) honorsMESH_BROKER_URL/MESH_BROKER_TOKEN/.mesh/config.jsonexactly like extension clients — remote machines can debug withmesh doctor. tcp://for LAN/VPN (Tailscale/ZeroTier/WireGuard recommended),tls://for a VPS (setMESH_TLS_CERT/MESH_TLS_KEYon the broker; clients may setMESH_TLS_CA, orMESH_TLS_INSECURE=1for self-signed — dev only).- Remote peers are visible: every peer carries its connection origin
(
via=tcp:<ip>/tls:<ip>; broker-local unix peers have none) — shown inmesh_status(via=… ⟵ other machine),/mesh status, the session context block, and the HUD peers line (alias⌁<last-ip-octet>). - Privacy note:
viaexposes the peer IP as seen by the broker — fine on a trusted LAN/home mesh; for a multi-org mesh, gate or truncate it before it rides any presence broadcast (future policy hook). MESH_DEBUG=1(env or"debug": truein config.json) logs wire-level client lifecycle events to<stateDir>/client-debug.log— no bodies.- Everything works unchanged across machines: rooms, broadcast, read
receipts, mailbox, reservations, turn state (state lives in the broker).
mesh doctorchecks the endpoint/auth on any machine. - Aliases must stay unique mesh-wide: prefix per machine
(
MESH_ALIAS=pcB-agent-2). Identities/ledgers stay local to each machine; reservations protect the same repo paths when both machines share the same git checkout (always reserve repo-relative paths).
Reliability notes
- Broker is stateless: kill it any time, clients re-hello and re-declare rooms + reservations; it re-spawns automatically.
- Mailbox is volatile: a broker restart loses queued offline messages;
senders always get the honest
queued_offlinestatus. - Zero loops: rate caps (client and broker), anti-duplicate send window,
self-send block, reply dedup (first answer wins, exact re-sends dropped),
reply-à-reply protection,
forcerequires a reason. - No body is ever persisted outside the opt-in transcript: the ledger is
hash-only with a fail-closed forbidden-key scan;
identity-pending.json(the/mesh new --historyhandover) is the only opt-in body staging, deleted after consumption. - Bounds: frame 64 KiB, body 32 KiB, mailbox 100/1 h, reminds ≤ 2, 16 rooms/peer, 64 peers/room — every bound is a named constant.
- Broker down → tools answer
blocked{broker_unavailable}, never crash. - Turn state is announced by each session (busy on the first tool call,
idle when the run settles) and shared with the room; peers without
announcements fall back to the idle/stuck heuristic. Provider errors are
detected from the HTTP status: TRANSIENT ones (429 rate limit, 5xx) flag
the agent
⛔ rate-limited(peers pause reminders — no ping-pong of turns that burn rate-limited requests;mesh_wait_allsays "retry later"); PERMANENT ones (401/403/404…) flag✖ blocked(retrying won't heal — needs a human). The flag sticks for a 30 s cooldown after the last error. While flagged, the session also HOLDS inbound injections (messages and reminders are queued, nothing burns a failed turn). Detection reads the FAILED ASSISTANT TURN (the SDK throws on HTTP errors, so the response status is not observable): the same classification pi uses — quota / budget limits (FreeUsageLimitError, insufficient_quota…) and auth errors →blockedwith a LONG hold (30 min, the limit must reset or the model must change); plain 429/5xx →rate_limitedwith a 60 s hold. Switching the model (model_select) lifts the hold and delivers the backlog immediately.
Platform notes
- Windows: AF_UNIX sockets are unavailable on win32 (
listenthrowsEACCES), so the broker endpoint falls back to a named pipe (\\.\pipe\mesh-<hash>-broker). Everything else is unchanged; the full test suite + smoke pass on Windows.
Known limitations
- Broker restarts lose rooms/mailbox (clients re-declare on hello).
- One shared token for the whole mesh on TCP/TLS — no per-alias
authorization yet (policy covers
forceand deny lists). - Aliases are unique mesh-wide by convention (prefix per machine) — no cross-machine collision detection beyond the broker's live check.
- The broker is a single process — no clustering or failover.
Development
npm run build # strict tsc (ESM, NodeNext)
npm test # build + node --test dist/test/*.test.js (286 tests)
npm run smoke # E2E without Pi: broker + 2 headless clients
CI runs the full suite on Node 24 (GitHub Actions); the suite is verified on
Node 20 as well. Publishing is automatic on v* tags (see
.github/workflows/release.yml, needs the NPM_TOKEN secret):
npm version patch && git push && git push --tags
Layout: src/protocol (frames, envelope) · src/broker (server, rooms,
mailbox, ratelimit, policy) · src/client (MeshClient, pending, reconnect) ·
src/extension (Pi adapter: tools, commands, inbound, guards, ledger,
transcript) · src/cli · test/ · scripts/mesh-smoke.mjs.
License
MIT — see LICENSE.
