@therealalexv/opencode-agent-mesh
v0.3.1
Published
Peer-to-peer messaging and edit coordination for OpenCode agents
Maintainers
Readme
opencode-agent-mesh
Peer-to-peer messaging and edit coordination for OpenCode agents.
When several OpenCode sessions run at once on the same machine, nothing stops two of them from editing the same file. This project gives each session a presence on a small local hub so they can find each other, send messages, claim files before editing, wait their turn in a FIFO queue, and see conflicts before they merge.
Published on npm as @therealalexv/opencode-agent-mesh.
How it works
A small Bun service (the hub) runs on localhost and tracks four things: live agents with their directory, worktree, git branch, agent name, and status; messages addressed to each agent; leases over file paths, soft or exclusive; and the FIFO queue of claims waiting behind those leases, plus detected git/merge conflicts.
An OpenCode plugin (the client) registers the current session, heartbeats every few seconds, and adds a set of mesh_* tools the agent can call. It also injects peer presence, pending messages, queue position, and conflict state into the session context on the next turn, so an idle agent does not need to be poked.
The hub never calls OpenCode. Each plugin instance delivers messages to its own server's sessions through the OpenCode SDK. The hub only routes and arbitrates.
Peer awareness
Presence and activity are always on. Every turn the plugin renders an [AGENT MESH] block with the live peers that matter to this session, ranked same-directory first, then same-worktree, then same-repo. Each peer line shows its status, branch, and a short activity summary drawn from its note, recent edits, or held leases. The block is deterministic for identical mesh state so it does not thrash provider prompt caches, and it is bounded by presenceMaxPeers and presenceMaxChars.
You do not have to call mesh_who to be aware of peers; read the injected block. Use the tool when you want the full roster or a scope other than the one you are in.
Cross-worktree identity
File paths are identified by a logical key, not just an absolute string. Inside a git repo the key is <git-common-dir>::<repo-relative-path>, so two linked worktrees of the same repository collide on the same file even though their absolute paths differ. That is what makes overlap detection and leases useful across worktrees instead of only within one checkout. Set repoIdentity to toplevel to treat each worktree as its own repo, or none to compare raw absolute paths.
Queueing
When an exclusive claim conflicts, the plugin can queue it instead of failing. Queued claims are promoted in FIFO order as the holder releases, deregisters, or its lease expires. Waiting is bounded by queue.waitMs; a claim that is still blocked when the budget runs out is cancelled and the edit is refused. The hub enforces a release-first rule: an owner that already holds an exclusive lease will not be queued at all (release-held-lease-first), which prevents the deadlock where two agents each wait on a lease the other holds. The queue itself is capped by queue.max and ages out after queue.ttlMs.
Git conflicts
The plugin detects git/merge conflicts from git diff --diff-filter=U, from conflict markers in edited files (when git.detectMarkers is on), and from the output of git merge/rebase/cherry-pick/pull/stash pop/am/apply run through the bash tool. It reports each conflicted path to the hub, which assigns a resolver: an existing exclusive lease holder if there is one, otherwise the reporter. While a conflict is unresolved and its resolver is live, other agents are told who owns it and are blocked from editing that path.
The mesh never auto-merges and never chooses a side. It detects, records, routes, and assigns; the resolver decides the content and calls mesh_resolve when the conflict is actually fixed.
Auto-start
The plugin starts the hub for you. On the first failed health check it spawns the hub in the background (rate-limited by autoStartHubCooldownMs, guarded by an on-disk lock) and retries until it is reachable. Set autoStartHub: false to manage the service yourself.
Install
Prerequisites: bun, and OpenCode with a global config.
From npm
bun add -g @therealalexv/opencode-agent-mesh
meshctl installThen add the plugin and the rules file to opencode.json. The package is a
hybrid plugin ({ id, setup, server }): OpenCode V2 loads the setup
entrypoint, and OpenCode 1.18.29+ loads the object server entrypoint. Both
share the same tools and hooks.
OpenCode V2 (plugins):
{
"plugins": ["@therealalexv/opencode-agent-mesh"],
"instructions": ["~/.config/opencode/plugin/global-rules/agent_mesh.md"]
}OpenCode V1 (plugin):
{
"plugin": ["@therealalexv/opencode-agent-mesh"],
"instructions": ["~/.config/opencode/plugin/global-rules/agent_mesh.md"]
}From source
git clone https://github.com/TheRealAlexV/opencode-agent-mesh
cd opencode-agent-mesh
bun install
./bin/meshctl installThe from-source install links ~/.config/opencode/plugin/agent-mesh.ts to the repo plugin, so use "./plugin/agent-mesh.ts" in opencode.json instead (under plugin for V1, or plugins for V2).
Either way, meshctl install writes a systemd user unit and generates ~/.config/opencode/agent-mesh/client.json with a random token.
Start and inspect:
./bin/meshctl status
./bin/meshctl agentsTools
Each agent gets these tools:
| Tool | Purpose |
| --- | --- |
| mesh_who | List live agents |
| mesh_send | Send a note, request, reply, or handoff to an agent or broadcast |
| mesh_inbox | Read messages addressed to this session |
| mesh_claim | Claim a soft or exclusive lease on paths |
| mesh_release | Release leases |
| mesh_leases | List active leases |
| mesh_announce | Set this session's status and note |
| mesh_overlaps | List overlap advisories (soft overlaps, same-branch peers, divergences) |
| mesh_queue | List this session's queued claims, or cancel one by id |
| mesh_conflicts | List detected git/merge conflicts on the mesh |
| mesh_resolve | Mark a conflicted path as resolved |
| mesh_git | Detect unmerged paths in this repo and report them |
mesh_claim takes optional queue and waitMs arguments: queue asks the hub to queue the claim instead of failing, and waitMs bounds how long this call waits for a promotion.
Messages
Messages are coordination notes, not a data channel. A sender can tag a message note, request, reply, or handoff. Messages arrive passively: they appear in the target's context on its next turn. A sender can set wake: true on a direct message to start a turn in an idle target, subject to a rate limit.
Leases
Soft leases never block anyone. They warn a second agent that touches the same path. Exclusive leases block another agent's edit to that path in the tool.execute.before hook; a conflicting exclusive claim can be queued and waited on as described above. Leases expire on a TTL and are released when their holder goes stale. The lease cap never evicts an active exclusive lease; soft leases are evicted first.
Enforcement is honest about what it can see. The hook inspects the path arguments of edit, write, patch, apply_patch, and multi_edit. It does not parse shell redirection (>, tee, sed -i) or arbitrary commands, so a bash write can bypass a lease. Treat leases as a coordination signal, not a security boundary.
If the hub is unreachable, everything fails open: edits proceed and the tools report "mesh offline". The mesh must not stop an agent from editing its own repo. A hub restart loses in-memory state, so enforcement degrades to advisory until the plugin reconciles against the next snapshot.
Configuration
~/.config/opencode/agent-mesh/config.json (shown with defaults):
{
"hub": { "host": "127.0.0.1", "port": 8788 },
"heartbeatMs": 10000,
"agentTtlMs": 45000,
"softTtlMs": 60000,
"exclusiveTtlMs": 600000,
"enforcement": "soft",
"injectInboxMax": 5,
"injectCharsMax": 1200,
"injectPresence": true,
"presenceScope": "repo",
"presenceMaxPeers": 6,
"presenceMaxChars": 1200,
"overlapAdvisories": true,
"queue": { "enabled": true, "max": 50, "ttlMs": 300000, "waitMs": 15000 },
"git": {
"detectToolOutput": true,
"detectMarkers": false,
"backup": true,
"scanDebounceMs": 5000
},
"repoIdentity": "git-common-dir",
"releaseExclusiveOnIdle": true,
"leaseIdleMs": 120000,
"autoStartHubCooldownMs": 30000,
"wake": { "enabled": true, "perTargetMinMs": 60000, "perTargetHourMax": 6 },
"neverLock": ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.opencode/**"],
"autoStartHub": true
}enforcement is advisory, soft, or hard. Advisory records touches but never blocks. Soft adds warnings and auto-taken soft claims. Hard blocks edits to paths under an exclusive lease held by another agent. presenceScope is worktree, repo, or all. repoIdentity is git-common-dir, toplevel, or none.
Reserved port
The hub uses loopback port 8788. See PORTS.md. To move it, set hub.port in config.json or set MESH_HUB_PORT, then update client.json and restart.
Development
bun test
bun run typecheckLicense
MIT.
