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.7.2- Published
- Oct 4, 2026
- Downloads
- 1,028/mo · 312/wk
- Author
- cgarrot
- License
- MIT
- Types
- extension, skill
- Size
- 1 MB
- 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. Each answer is ALSO delivered to the session as a[mesh]reply event (wake-on-answer): an idle sender wakes the moment a mission is answered, and the answer frame lands in the inbox so it can itself be replied to. While amesh_wait_allis in flight the verdict carries the batch instead (no double delivery). A blockingmesh_send(default) can be aborted with ESC — the pending settles immediately ascancelledand a late reply is still injected. - 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). Every answer arrives as a[mesh]reply event the moment it lands (the session wakes if idle; keep working in the meantime), somesh_wait_allis only needed when you must collect the whole batch before continuing. - 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. Since the standalone CLI peer
(pimesh attach), read also covers the terminal: an interactive TTY render
is the same "reached the recipient's attention surface" as a session
injection — pipes/--json never read. Read frames are online-only and
silent (never acked, never mailboxed): a read toward an offline sender is
lost without error.
CLI (pimesh)
Installed as the pimesh bin with the package (npm i -g pi-mesh-extension),
runnable without a global install via npx pi-mesh-extension … (single bin).
When the package is installed through pi install, use npx or the
node dist/src/cli/mesh.js path from the package directory (pi installs do
not put npm bins on PATH). The CLI honors .mesh/config.json + env
(MESH_BROKER_URL/MESH_BROKER_TOKEN…) exactly like extension clients —
remote machines can debug with pimesh doctor.
pimesh broker start|stop|status # local broker lifecycle (detached, lockfile)
pimesh peers [--room R] # compact snapshot (legacy format)
pimesh status [room] [--all] [--reservations] [--json]
# ●/○/✕/⛔ activity, via= origins, versions,
# reservations + TTL state (stale = alias)
pimesh send [alias] <text…> # full SendOpts: --priority --reason --refs
# --reply-to --broadcast --await --launch
# --timeout --alias --require-online
# trailing `-` reads the body from stdin
pimesh reply <msgId> <text…> (--to A | --reply-all) --room R [--refs A,B]
pimesh ping <alias> [--timeout MS] # expired (exit 3) ≠ peer down
pimesh wait [--timeout MS] # honest one-shot limit: missions live in
# the launching process
pimesh join <room> [observer] # debug-only: membership dies with the process
pimesh leave <room>
pimesh reserve <path>… [--reason R] [--hold MS]
# dry-run conflicts by default (exit 4);
# --hold keeps the claim alive (Ctrl-C releases)
pimesh release [<pattern>…] [--all] # honest no-op one-shot (claims are
# connection-scoped)
pimesh ledger [--limit N] [--from --to --room --event] [--json]
# local hash-only ledger + rotations
pimesh tail [-f] [--limit N] # last N lines, or follow (rotation-safe)
pimesh sessions [--json] # persisted identities (adoption targets,
# current stateDir scope only)
pimesh attach [alias] [--session ID] [--room R] [--json] [--no-read]
# STANDALONE PEER: a full mesh member with
# no session — REPL or NDJSON script pipe
pimesh config show # resolved config (token masked)
pimesh doctor # endpoint/lock/config/version diagnostics
Exit codes — 0 success · 1 mesh failure (blocked/error) · 2
usage · 3 expired/timeout (a late reply is still delivered) · 4 honest
partial (queued_offline, broadcast N/M, conflicts, wait timeout with
answers).
Standalone peer (attach) — use or create a mesh without a Pi session:
receive messages WITH bodies (you are the recipient), send/reply/reserve
from a REPL (/help inside) or a scriptable NDJSON pipe (--json: stdout
events / stdin commands with correlated ref). attach <alias> adopts a
dead session's identity — rooms, fresh reservations (since refreshed) and
its queued mailbox; if the original session returns it gets a fresh alias
(a documented steal). Read receipts are emitted ONLY in interactive TTY
mode at render time (--no-read opts out; --json/pipes never read).
No activity announcements: peers see the idle heuristic.
Observer (watch) — pimesh watch [room] joins as an observer and
streams frames live; bodies are NEVER shown, only bodyHash (text and
--json alike — safe to | tee).
The one-shot CLI is not a session: apart from interactive attach, it
never emits read receipts.
Unblocking a stuck agent (force + interrupt)
An agent can get stuck inside a long or hung command (sleep 600, an
interactive prompt, a process that ignores everything). mesh_send has a
last-resort lever for exactly that:
mesh_send {
"to": "stuck-agent", "message": "stop the render, deploy first",
"priority": "force", "reason": "stuck 20 min on a dead command",
"interrupt": true }
interrupt aborts the recipient's blocked turn — the host abort kills
the running tool's process tree (the same thing ESC does for a human) —
then delivers the message as a prioritized steer. Aborts are retried a
bounded number of times (an abort can be swallowed by an end-of-turn
race) within a 10 s window; after that the message is queued anyway.
Precautions (by design): interrupt requires priority: force AND a
reason; it is rate-limited like force (1/min) and gated by the same
policy. Force is denied by default — the recipient's mesh must opt in:
// .mesh/policy.json on the RECIPIENT's machine
{ "allow": [{ "from": "*", "to": "*", "room": "*" }],
"forceAllowedFrom": ["lead", "operator-*"] }
Honest receipts: the recipient answers with a machine receipt —
turn aborted, message delivered, already idle, still busy after N aborts or abort unavailable on this host — rendered INFO ONLY (it
never wakes the sender, never settles an awaited mission, never
cascades). The recipient also sees a local notification. Receipts can be
disabled with "interruptReceipts": false / MESH_INTERRUPT_RECEIPTS=0.
Limits: an interrupt never claims a killed process (not observable), and
it does not bypass a provider rate-limit hold (⛔ peers stay held — an
abort cannot heal a dead provider). CLI: pimesh send <alias> <text…> --interrupt --priority force --reason "…".
Configuration
<stateDir>/config.json (default <cwd>/.mesh/config.json, all optional).
Precedence: defaults < config file < environment.
{ "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",
"inboundBroadcasts": "immediate",
"interruptReceipts": true,
"inboundBatchMs": 250, "inboundBatchMaxHoldMs": 30000 }
Deferring unrelated broadcasts
Set "inboundBroadcasts": "deferred" or MESH_INBOUND_BROADCASTS=deferred
to avoid a model turn for every unrelated room update. The default,
"immediate", preserves existing behavior. Invalid values are ignored
(invalid file values fall back to the default).
Only broadcasts and orphan replyAll replies without a case-insensitive
mention of your alias (@alice or alice, whole alias) are deferred.
Direct messages, mission answers (including LAUNCH wake-on-answer), urgent
and force priority, reminders, and reservation updates remain immediate.
Receipt, transcript, ledger, and mailbox behavior is unchanged.
Deferred frames form a separate, timer-free batch; the next user prompt
queues it with deliverAs: "nextTurn" and no triggerTurn. It begins
[mesh deferred — N broadcast(s) not addressed to you]. In the TUI,
mesh:deferred N remains in the footer until that prompt starts.
/mesh inbox lists sender, time, and a 120-character preview;
/mesh inbox flush delivers the pending batch now as one triggered follow-up.
An unrelated direct-message turn does not consume the deferred inbox.
The batch is held locally until input, because Pi has no public API to
cancel an already-enqueued nextTurn message (otherwise manual flush would
deliver it twice). If prompt preflight is cancelled after input, messages
already handed to Pi remain queued for the next successful prompt, not
re-sent by flush. Deferred state is session-local and cleared on reset,
reload, or shutdown; use the existing mesh history for older frames.
v0.7 highlights:
- Deferred broadcasts (opt-in) —
"inboundBroadcasts": "deferred"(orMESH_INBOUND_BROADCASTS=deferred) stops unrelated room broadcasts from burning model turns: only broadcasts and orphanreplyAllreplies without a whole-alias mention are held. Direct messages, mission answers (LAUNCH wake-on-answer included), urgent/force, reminders and reservations stay immediate. Held frames surface asmesh:deferred Nin the TUI footer and via/mesh inbox;/mesh inbox flushdelivers them now as one triggered follow-up. Receipts, ledger and mailbox behavior are unchanged. Contributed by @avifenesh (#7). mesh:aliasevent — after connect/reconnect (and rename), mesh publishes its resolved identity on the shared event bus plus aSymbol.for("pi-mesh:alias")snapshot for other extensions. Contributed by @avifenesh (#7).
v0.6 highlights:
- Wake-on-answer — every LAUNCH mission answer (
awaitReply: true, block: false) is delivered to the session the moment it lands (stored in the inbox, injected withtriggerTurnthrough the hardened inbound path): an idle sender wakes, a busy one gets the batch, and the answer frame can itself be replied to. While amesh_wait_allis in flight the verdict carries the batch instead — never a double delivery. Answers are now MORE visible than late (orphan) replies, never less. - Cancelable blocking sends — ESC on a blocking
awaitReplysend settles it immediately as an honestcancelled(was: hanging until the 30-min timeout); the mission is dropped and a late reply still arrives via the orphan-inject path.
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_INTERRUPT_RECEIPTS=0,
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 (405 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.
For other extensions
After connect/reconnect (and rename), mesh publishes the resolved identity:
pi.events.on("mesh:alias", (data) => {
const { alias, rooms } = data as { alias: string; rooms: string[] };
// Refresh your own status or integration. No model turn is triggered.
});
const alias = (globalThis as Record<symbol, unknown>)[Symbol.for("pi-mesh:alias")];
alias has no leading @. The symbol is a convenience snapshot of the
latest connected alias in this process, not a cross-process registry;
subscribe to the event for updates. It is undefined before the first
connection and is not a connection-health signal. In-process child sessions
share globalThis; consumers needing per-session identity should use their
session's event lifecycle rather than treating the symbol as session-local.
Contributors
Thanks to everyone who has made pi-mesh better:
