Skip to main content
Glama
README.md
# mesh

**The cross-agent communicator.** A standalone coordination layer for fleets of
agents working the same repository — presence, resource locks, and durable
messaging — exposed as a single zero-dependency MCP server.

`mesh` is the communication primitive extracted from [the machine](https://github.com/yesitsfebreeze/machine)'s
hub daemon and reimplemented as a self-contained plugin. No daemon to keep
alive, no port to bind, no toolchain to build: just `node`. State is a single
JSON file under a repo-scoped `.mesh/` directory, guarded by an OS-atomic lock
so multiple agents (and multiple processes) can coordinate safely.

## Why

When several agents share one codebase they trip over each other: two edit the
same file, a plan waits on an answer that never arrives, a crashed worker leaves
a lock wedged forever. `mesh` gives them three things and nothing else:

- **Awareness** — who is alive, on what branch, doing what.
- **Claims** — atomic, *leased* resource locks with a monotonic *fence* token,
  a fair FIFO queue, and self-healing when a holder dies.
- **Messaging** — durable, ULID-ordered messages with per-agent read cursors,
  broadcast (`*`), and topic subscriptions.

A dead agent never wedges the mesh: liveness has a TTL, leases expire, and locks
held by the dead are reaped and promoted to the next waiter automatically.

## Install (as a Claude Code plugin)

```
/plugin marketplace add yesitsfebreeze/mesh
/plugin install mesh@mesh
```

The plugin registers one MCP server (`mesh`) exposing nine tools. State is
written to `.mesh/` in the current project (add it to `.gitignore`).

## The tool surface

Every agent identifies itself with a stable `agent_id`.

### Awareness

| tool | purpose |
|------|---------|
| `register` | Announce presence and refresh liveness (heartbeat). Re-registering after death bumps an `epoch`. |
| `roster`   | List known agents, their `liveness` (`alive` / `stale` / `dead`), and the claims each holds. |

`register` takes `agent_id`, `branch`, `prompt_ptr`, optional `role` and
`ttl_seconds` (default 60). An agent is `alive` within its TTL, `stale` for a
30s grace window after, then `dead` — at which point it is hidden from the
roster (unless `include_stale: true`) and its locks become reapable.

### Claims (leased, fenced locks)

| tool | purpose |
|------|---------|
| `claim`   | Atomically acquire — or queue for — a resource lock. |
| `release` | Relinquish a held claim or cancel a queued ticket. |
| `claims`  | Inspect current locks and queues. |

```jsonc
claim { "agent_id": "a", "resource": "feature:auth", "mode": "exclusive",
        "lease_seconds": 120, "wait": "queue", "note": "wiring login" }
// -> { "status": "granted", "claim_id": "01K…", "fence": 7, "lease_expires_at": "…" }
```

- **`mode`** — `exclusive` (default) or `shared`. Shared holders co-exist;
  an exclusive request is denied/queued while any holder is present.
- **`lease_seconds`** — the lock auto-expires after this (default 120). Re-`claim`
  by the same holder is an *idempotent renewal* — same `claim_id`, same `fence`.
- **`wait`** — `no_wait` (default) returns `denied` if held; `queue` appends a
  FIFO ticket and returns `queued` with a `queue_position`.
- **`fence`** — a per-resource monotonically increasing token, bumped on every
  grant/promotion. Use it to reject stale writers (fencing tokens, à la Kleppmann).

On `release` the next queued ticket is promoted (and, for `shared`, a run of
consecutive shared waiters is promoted together). When a holder's agent dies or
its lease lapses, the lock is reaped on the next touch and the queue advances —
no manual cleanup.

### Messaging (durable, ordered, cursored)

| tool | purpose |
|------|---------|
| `post`  | Send a durable message — to an `agent_id`, `*` (broadcast), or `topic:<name>`. |
| `inbox` | Peek pending messages **without** advancing the cursor. |
| `read`  | Advance the read cursor up to a message id (acknowledge consumption). |

```jsonc
post  { "agent_id": "a", "to": "topic:build", "subject": "green",
        "body": "main is green", "ttl_seconds": 3600 }
inbox { "agent_id": "b", "topics": ["build"] }   // -> { messages: [...], cursor, unread }
read  { "agent_id": "b", "up_to": "01K…" }        // -> { cursor, remaining }
```

Messages are ULID-keyed, so the delivery log is totally ordered. Each agent has
its own cursor: `inbox` shows everything addressed to it above its cursor;
`read` moves the cursor forward (monotonic — it never goes backwards).
Addressing: direct (`to: "b"`), broadcast (`to: "*"`, delivered to all), or
topic (`to: "topic:x"`, delivered only to agents that pass `topics: ["x"]`).
Optional `ttl_seconds` makes a message self-expire.

### Maintenance

| tool | purpose |
|------|---------|
| `gc` | Drop TTL-expired messages and sweep dead claims; returns `{ reclaimed }`. |

Reaping also happens opportunistically inside `roster` and `claims`, so `gc` is
only needed to reclaim expired *messages* eagerly.

## Standalone use (without Claude Code)

It is a plain MCP stdio server — drive it from anything that speaks JSON-RPC:

```bash
node bin/mesh-mcp.mjs        # reads JSON-RPC lines on stdin, replies on stdout
MESH_DIR=/path/to/.mesh node bin/mesh-mcp.mjs   # override the state directory
```

Or import the core directly:

```js
import { Mesh } from "@yesitsfebreeze/mesh";
const mesh = new Mesh(".mesh");
mesh.register({ agent_id: "a", branch: "main", prompt_ptr: "demo" });
mesh.claim({ agent_id: "a", resource: "feature:x" });
```

The `Mesh` constructor accepts an injectable clock (`() => unixSeconds`) as its
second argument, which makes lease/liveness behavior deterministic in tests.

## State & concurrency

- All state lives in `.mesh/state.json` — `roster`, `claims`, `messages`, `log`,
  `cursors`, `events`, `fence_floor`. Delete the directory to reset.
- Every mutation runs under an OS-atomic lock directory (`mkdir` succeeds for
  exactly one process) and is persisted with a temp-file + atomic rename, so
  concurrent agents and processes never corrupt the file. A stale lock from a
  crashed holder is stolen after 5s.

## Test

```bash
npm test        # node --test — 12 behavioral-parity cases, zero dependencies
```

## License

MIT

TDQS

B3.2/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct operation: locking, inspecting, garbage collection, peeking, sending, reading, registering, releasing, and listing. Descriptions clearly differentiate them, with no overlapping purposes.

Naming Consistency4/5

Most tool names are single-word verbs, but 'gc' is an abbreviation and 'roster' is a noun, introducing slight inconsistency. However, the pattern is largely predictable.

Tool Count5/5

With 9 tools, the server covers essential operations for messaging and locking without superfluous tools. The count feels well-scoped.

Completeness4/5

The tool set provides full lifecycle for messages (post, read, inbox, gc) and locks (claim, claims, release), plus presence (register, roster). Minor missing features like explicit message deletion or broadcast-only send could be present but are not critical gaps given descriptions.

Maintenance

ActivityStale
ResponsivenessNo issues