@sym-bot/mesh-channel 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +85 -0
- package/README.md +56 -5
- package/SECURITY.md +1 -1
- package/bin/install.js +122 -6
- package/package.json +2 -2
- package/server.js +39 -9
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"name": "Hongwei Xu",
|
|
19
19
|
"email": "hongwei@sym.bot"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://
|
|
21
|
+
"homepage": "https://meshcognition.org/spec/mmp",
|
|
22
22
|
"repository": "https://github.com/sym-bot/sym-mesh-channel",
|
|
23
23
|
"license": "Apache-2.0",
|
|
24
24
|
"keywords": [
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sym-mesh-channel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Real-time Claude-to-Claude mesh. Agent-to-agent cognitive signals over Bonjour LAN or WebSocket relay.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Hongwei Xu",
|
|
7
7
|
"email": "hongwei@sym.bot",
|
|
8
8
|
"url": "https://sym.bot"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://
|
|
10
|
+
"homepage": "https://meshcognition.org/spec/mmp",
|
|
11
11
|
"repository": "https://github.com/sym-bot/sym-mesh-channel",
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"keywords": ["mesh", "p2p", "mcp", "channel", "agents", "multi-agent", "bonjour", "cognitive", "svaf", "mmp"],
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Opaque payload on `sym_send` / `sym_observe`.** Both tools accept
|
|
8
|
+
an optional `payload` argument carrying data beyond CAT7 — any
|
|
9
|
+
JSON-serializable value. Forwarded to `SymNode.remember(fields, {
|
|
10
|
+
payload, … })` (requires `@sym-bot/sym` ≥ 0.5.8) and rides the wire
|
|
11
|
+
frame to peers. Used by substrate-level protocols that need to carry
|
|
12
|
+
structured data alongside CAT7 (e.g. LLM request/response, where the
|
|
13
|
+
prompt + request_id ride in `payload` rather than getting smuggled
|
|
14
|
+
through `motivation`).
|
|
15
|
+
- **Channel notifications surface payload-bearing CMBs.** When an
|
|
16
|
+
incoming peer CMB carries `cmb.payload`, the header gains a
|
|
17
|
+
`[+payload Nb]` indicator and the body stored by `sym_fetch`
|
|
18
|
+
includes a `---PAYLOAD---` section with the serialized payload.
|
|
19
|
+
Receivers learn from the header that there's structured data beyond
|
|
20
|
+
CAT7 and call `sym_fetch` to consume it.
|
|
21
|
+
- Base MCP instructions now teach agents to recognise the
|
|
22
|
+
`[+payload Nb]` header and to pass structured responses via the
|
|
23
|
+
`payload` argument when emitting substrate-level CMBs.
|
|
24
|
+
|
|
25
|
+
### Compatibility
|
|
26
|
+
|
|
27
|
+
- Omitting `payload` produces a v0.3.4-shaped CAT7 CMB byte-for-byte.
|
|
28
|
+
- Old peers (without `cmb.payload`) surface unchanged headers — no
|
|
29
|
+
`[+payload …]` indicator, no PAYLOAD section in the body.
|
|
30
|
+
|
|
31
|
+
## 0.3.4
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`SYM_GROUP` is now first-class in the installer.** `init` accepts a
|
|
36
|
+
`--group <name>` flag and reads the `SYM_GROUP` env var; both paths
|
|
37
|
+
persist the chosen group into the `~/.claude.json` (or project
|
|
38
|
+
`.mcp.json`) env block so every Claude Code launch auto-joins the
|
|
39
|
+
named group instead of the global `_sym._tcp` mesh.
|
|
40
|
+
|
|
41
|
+
Resolution order is `--force`-aware:
|
|
42
|
+
- With `--force` and an explicit `--group`/`SYM_GROUP`: flag/env wins
|
|
43
|
+
(one-command group switch on a live entry).
|
|
44
|
+
- Without `--force`, or with `--force` but no explicit value:
|
|
45
|
+
preserved value from any existing entry > explicit > none (omit).
|
|
46
|
+
|
|
47
|
+
`--force --group default` (or `SYM_GROUP=default`) is the explicit
|
|
48
|
+
escape hatch to revert a node from a named group back to the global
|
|
49
|
+
mesh — removes `SYM_GROUP` from the env block entirely rather than
|
|
50
|
+
writing the literal string "default".
|
|
51
|
+
|
|
52
|
+
Both `--group` and `SYM_GROUP` env values are validated against the
|
|
53
|
+
same kebab-case regex; malformed values exit with a clear error
|
|
54
|
+
before any file write.
|
|
55
|
+
|
|
56
|
+
- **`doctor` now reports the persisted group per entry** and warns when
|
|
57
|
+
user-global and project-scoped entries disagree on `SYM_GROUP`.
|
|
58
|
+
Group-mismatch is the most common cause of "peers never appear in
|
|
59
|
+
`sym_peers`" with no other failure signal — surfacing it inline saves
|
|
60
|
+
the diagnostic walk that motivated this release.
|
|
61
|
+
|
|
62
|
+
- **README** gains a "Persisting your group across restarts" subsection
|
|
63
|
+
under Team mesh groups, plus a troubleshooting entry covering the
|
|
64
|
+
group-mismatch failure mode. Quick-start shows the `--group` flag.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **Stale-entry heal preserves `SYM_GROUP` alongside `SYM_NODE_NAME`.**
|
|
69
|
+
Previously, healing a stale `claude-sym-mesh` entry (args[0] points at
|
|
70
|
+
a missing server.js) silently dropped any persisted `SYM_GROUP`,
|
|
71
|
+
reverting the node to the default mesh on next launch and stranding
|
|
72
|
+
teammates who stayed in the named group. The heal path now copies
|
|
73
|
+
both fields from the prior entry into the rewrite.
|
|
74
|
+
|
|
75
|
+
Same fix applied to project-scoped entry healing under
|
|
76
|
+
`claudeJson.projects[<path>].mcpServers`.
|
|
77
|
+
|
|
78
|
+
### Why this matters
|
|
79
|
+
|
|
80
|
+
Before 0.3.4, the only way to persist a group was to hand-edit
|
|
81
|
+
`~/.claude.json`. The README pitched `sym_join_group` as the team-mesh
|
|
82
|
+
UX, but that tool is runtime-only — the next Claude Code launch reverted
|
|
83
|
+
the node to the default mesh, peer count dropped to zero, and the user
|
|
84
|
+
saw no diagnostic signal. The 2026-05-02 SYM.BOT incident (CMO in
|
|
85
|
+
`default`, COO in `sym-bot-team`, ~24h of silent duplex outage) traced
|
|
86
|
+
directly to this gap.
|
|
87
|
+
|
|
3
88
|
## 0.3.3
|
|
4
89
|
|
|
5
90
|
### Fixed
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ npm install -g @sym-bot/mesh-channel && claude
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@sym-bot/mesh-channel)
|
|
10
10
|
[](https://claude.ai/settings/plugins/submit)
|
|
11
|
-
[](https://meshcognition.org/spec/mmp)
|
|
12
12
|
[](https://arxiv.org/abs/2604.03955)
|
|
13
13
|
[](https://arxiv.org/abs/2604.19540)
|
|
14
14
|
[](LICENSE)
|
|
@@ -28,7 +28,7 @@ Verified working: Mac ↔ Windows on the same wifi, pure Bonjour, no relay, no t
|
|
|
28
28
|
|
|
29
29
|
- **Small engineering teams** whose Claude Code sessions currently copy-paste findings over Slack. Replace that loop with direct agent-to-agent coordination.
|
|
30
30
|
- **Distributed teams** running Claude Code across offices, home networks, and coffee shops. Isolated team channels via mesh groups, no shared server.
|
|
31
|
-
- **Multi-agent developers** prototyping cognitive architectures — `sym-mesh-channel` is the reference Claude Code host for the [Mesh Memory Protocol](https://
|
|
31
|
+
- **Multi-agent developers** prototyping cognitive architectures — `sym-mesh-channel` is the reference Claude Code host for the [Mesh Memory Protocol](https://meshcognition.org/spec/mmp).
|
|
32
32
|
- **Not for:** single-user Claude sessions that don't need to coordinate with anyone. You'd get MCP tools but nothing to coordinate with.
|
|
33
33
|
|
|
34
34
|
## Quick start
|
|
@@ -63,6 +63,14 @@ To customise your mesh identity, set `SYM_NODE_NAME` before running init:
|
|
|
63
63
|
SYM_NODE_NAME=claude-alice npx @sym-bot/mesh-channel init --force
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
To pin this node into a named team group at install time so the membership survives Claude Code restarts, pass `--group <name>` (or set `SYM_GROUP=<name>` in the environment):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
SYM_NODE_NAME=claude-alice npx @sym-bot/mesh-channel init --force --group backend-team
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Without `--group`, the node joins the global `_sym._tcp` mesh on every launch — runtime hot-swaps via `sym_join_group` only last for the current session and revert on restart. See [Team mesh groups](#team-mesh-groups) for the full story.
|
|
73
|
+
|
|
66
74
|
**Real-time push is a separate upgrade.** The command above gives you all 11 MCP tools immediately. To additionally have peer messages *appear in Claude's context mid-turn without a tool call* (the "Claude thinks with the mesh" experience), launch Claude Code with the Channels flag:
|
|
67
75
|
|
|
68
76
|
```bash
|
|
@@ -121,7 +129,37 @@ Parsed invite: sym://group/backend-team
|
|
|
121
129
|
Hot-swapped from group "default" to "backend-team".
|
|
122
130
|
```
|
|
123
131
|
|
|
124
|
-
No restart
|
|
132
|
+
No restart needed for the current session. Teammates on the same LAN now see each other; `backend-team` and `frontend-team` live in isolated mDNS spaces.
|
|
133
|
+
|
|
134
|
+
> **`sym_join_group` is runtime-only.** On the next Claude Code launch, the node restarts from its `~/.claude.json` config — if `SYM_GROUP` isn't persisted there, it reverts to the global mesh and your teammates' peer count silently drops to zero. Persist your membership before closing the session (see below).
|
|
135
|
+
|
|
136
|
+
### Persisting your group across restarts
|
|
137
|
+
|
|
138
|
+
The hot-swap above is convenient for trying a group, but a real team setup needs the group baked into the MCP env block so every Claude Code launch joins automatically. Two paths:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# (a) Reinstall with the --group flag — preserves SYM_NODE_NAME from the
|
|
142
|
+
# existing entry, adds SYM_GROUP, atomically rewrites ~/.claude.json:
|
|
143
|
+
npx @sym-bot/mesh-channel init --force --group backend-team
|
|
144
|
+
|
|
145
|
+
# (b) For a project-scoped install (multi-project laptop):
|
|
146
|
+
cd path/to/project
|
|
147
|
+
SYM_NODE_NAME=claude-myproject npx @sym-bot/mesh-channel init --project --group backend-team
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
After either path, restart Claude Code once; subsequent sessions auto-join the group. To switch groups on a live entry use `--force` together with `--group`:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Switch from one named group to another (one command):
|
|
154
|
+
npx @sym-bot/mesh-channel init --force --group new-team
|
|
155
|
+
|
|
156
|
+
# Revert to the global mesh (escape hatch):
|
|
157
|
+
npx @sym-bot/mesh-channel init --force --group default
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Without `--force`, an existing persisted `SYM_GROUP` always wins over a flag — the heal path's job is to never lose user state on a routine reinstall. With `--force`, the flag is the explicit override and takes precedence.
|
|
161
|
+
|
|
162
|
+
Run `npx @sym-bot/mesh-channel doctor` any time to see which group each `claude-sym-mesh` entry is configured for. The doctor flags group mismatches across user-global and project-scoped entries — the most common cause of "we're on the same wifi but my teammate's node never appears in `sym_peers`".
|
|
125
163
|
|
|
126
164
|
### Distributed team (via relay)
|
|
127
165
|
|
|
@@ -167,7 +205,7 @@ sym-mesh-channel ←—— Bonjour mDNS ——→ sym-mesh-channel
|
|
|
167
205
|
The plugin composes two open specs:
|
|
168
206
|
|
|
169
207
|
- **[Claude Code Channels](https://code.claude.com/docs/en/mcp)** (Anthropic, 2026-03-20) — an MCP capability that lets servers push events directly into Claude's conversation context mid-turn via `notifications/claude/channel`. Anthropic built it for the Telegram/Discord/iMessage integrations. We use it for agent-to-agent cognitive coupling.
|
|
170
|
-
- **[MMP — the Mesh Memory Protocol](https://
|
|
208
|
+
- **[MMP — the Mesh Memory Protocol](https://meshcognition.org/spec/mmp)** — defines *what* gets pushed: typed seven-field cognitive bundles (CAT7: focus, issue, intent, motivation, commitment, perspective, mood), how receivers gate incoming signals ([SVAF](https://arxiv.org/abs/2604.03955)), and how peers maintain identity without a central orchestrator.
|
|
171
209
|
|
|
172
210
|
**What happens on each message.** When a peer broadcasts a cognitive memory block (CMB), the local SymNode evaluates it via SVAF — Symbolic-Vector Attention Fusion, a receiver-side relevance gate that rejects low-signal messages before they reach Claude's context. If accepted, the MCP server fires a `notifications/claude/channel` notification to Claude Code, which surfaces it as a `<channel>` block in the conversation. Claude sees it, can react, and can broadcast back via `sym_send` or `sym_observe`. No polling. No tool calls. The mesh thinks together.
|
|
173
211
|
|
|
@@ -258,6 +296,19 @@ npx -y @sym-bot/mesh-channel init
|
|
|
258
296
|
|
|
259
297
|
`init` preserves each entry's `SYM_NODE_NAME` so your mesh identity doesn't drift. Live entries are left alone; `--force` is only needed to overwrite a live entry deliberately. Restart Claude Code after healing — MCP servers are spawned at session start and won't pick up config changes mid-session.
|
|
260
298
|
|
|
299
|
+
### Peers connect (Claude Code starts cleanly) but never appear in `sym_peers`
|
|
300
|
+
|
|
301
|
+
Almost always a **mesh group mismatch** — Bonjour scopes discovery by service type (`_<group>._tcp`), so `default` and `backend-team` nodes on the same wifi are invisible to each other. Two diagnostics:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
> sym_status # shows: Group: <name> (<service-type>)
|
|
305
|
+
> sym_groups_discover # shows every group currently advertising on the LAN
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
If your teammate's node is on a different group, align via `sym_join_group` (this session) and persist via `init --group <name>` (future sessions). Run `npx @sym-bot/mesh-channel doctor` to confirm the persisted group on every entry — the doctor explicitly flags mismatches across user-global and project-scoped configs.
|
|
309
|
+
|
|
310
|
+
This is the failure pattern where every other indicator looks healthy: `sym_status` says `Peers: 0` but the underlying SymNode is fine, the mDNS service is registered, and the relay (if any) is connected — they're just announced on a service type your peer isn't browsing.
|
|
311
|
+
|
|
261
312
|
### Peers don't see each other on the same wifi
|
|
262
313
|
|
|
263
314
|
Check Bonjour is running:
|
|
@@ -301,7 +352,7 @@ Use this if you prefer the plugin surface for install and update management. The
|
|
|
301
352
|
|
|
302
353
|
- [SVAF paper](https://arxiv.org/abs/2604.03955) — Xu, 2026. *Symbolic-Vector Attention Fusion for Collective Intelligence*. arXiv:2604.03955.
|
|
303
354
|
- [MMP paper](https://arxiv.org/abs/2604.19540) — Xu, 2026. *Mesh Memory Protocol: Semantic Infrastructure for Multi-Agent LLM Systems*. arXiv:2604.19540.
|
|
304
|
-
- [MMP spec
|
|
355
|
+
- [MMP spec v1.0](https://meshcognition.org/spec/mmp) — Mesh Memory Protocol specification (canonical web version).
|
|
305
356
|
- [sym-swift](https://github.com/sym-bot/sym-swift) — iOS/macOS SDK implementing the same protocol.
|
|
306
357
|
- [sym-relay](https://github.com/sym-bot/sym-relay) — WebSocket relay for cross-network mesh.
|
|
307
358
|
|
package/SECURITY.md
CHANGED
|
@@ -85,5 +85,5 @@ exit code 2 rather than competing for the identity.
|
|
|
85
85
|
|
|
86
86
|
## References
|
|
87
87
|
|
|
88
|
-
- [MMP
|
|
88
|
+
- [MMP v1.0 Specification](https://meshcognition.org/spec/mmp) — Sections 5 (Connection), 8 (CAT7), 9 (SVAF)
|
|
89
89
|
- [SVAF Paper](https://arxiv.org/abs/2604.03955) — Xu, 2026
|
package/bin/install.js
CHANGED
|
@@ -46,11 +46,37 @@ const isPostinstall = args.includes('--postinstall');
|
|
|
46
46
|
const isProject = args.includes('--project');
|
|
47
47
|
const cmd = args.find((a) => !a.startsWith('--')) || 'init';
|
|
48
48
|
|
|
49
|
+
// --group <name>: persist a SYM_GROUP env entry into the written .mcp.json /
|
|
50
|
+
// ~/.claude.json so the node joins that group on every Claude Code launch.
|
|
51
|
+
// Without this flag, the env block omits SYM_GROUP and the node falls back
|
|
52
|
+
// to the default _sym._tcp mesh on startup. Runtime sym_join_group hot-swaps
|
|
53
|
+
// only last for the current session — without persistence, peers in named
|
|
54
|
+
// groups silently revert to default and become invisible to teammates.
|
|
55
|
+
const groupArgIdx = args.indexOf('--group');
|
|
56
|
+
const groupArg = groupArgIdx !== -1 ? args[groupArgIdx + 1] : null;
|
|
57
|
+
|
|
49
58
|
if (cmd !== 'init' && cmd !== 'doctor') {
|
|
50
|
-
process.stderr.write(`Unknown command: ${cmd}\nUsage: sym-mesh-channel init [--project] [--force]\n sym-mesh-channel doctor\n`);
|
|
59
|
+
process.stderr.write(`Unknown command: ${cmd}\nUsage: sym-mesh-channel init [--project] [--force] [--group <name>]\n sym-mesh-channel doctor\n`);
|
|
51
60
|
process.exit(1);
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
const KEBAB_CASE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
|
+
function validateGroupValue(value, source) {
|
|
65
|
+
if (!value) return;
|
|
66
|
+
if (value === 'default') return;
|
|
67
|
+
if (!KEBAB_CASE_RE.test(value)) {
|
|
68
|
+
process.stderr.write(`ERROR: ${source} "${value}" must be kebab-case (e.g. backend-team) or "default".\n`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
validateGroupValue(groupArg, '--group');
|
|
73
|
+
// Apply the same gate to the env-var path. Pre-0.3.4-followup, a malformed
|
|
74
|
+
// SYM_GROUP=' ' or SYM_GROUP=Backend_Team value flowed through unvalidated
|
|
75
|
+
// and got written into the .mcp.json env block as-is, producing an mDNS
|
|
76
|
+
// service type the SymNode would silently fail to register on. Now both
|
|
77
|
+
// inputs share the validator with the same error message shape.
|
|
78
|
+
validateGroupValue(process.env.SYM_GROUP, 'SYM_GROUP');
|
|
79
|
+
|
|
54
80
|
// ── isStaleEntry: a claude-sym-mesh entry whose server.js path is gone ──
|
|
55
81
|
// Returns true when the entry exists but its args[0] path does not resolve
|
|
56
82
|
// to a file on disk. Such an entry can never spawn the MCP server — every
|
|
@@ -74,6 +100,17 @@ function preserveNodeName(entry) {
|
|
|
74
100
|
return n || null;
|
|
75
101
|
}
|
|
76
102
|
|
|
103
|
+
// preserveGroup: return the SYM_GROUP from an existing entry's env so
|
|
104
|
+
// rewrites keep the mesh group. Same shape as preserveNodeName — without
|
|
105
|
+
// this, healing a stale entry would drop a previously-persisted group
|
|
106
|
+
// and silently downgrade the node to the default _sym._tcp mesh,
|
|
107
|
+
// stranding teammates who stay in the named group.
|
|
108
|
+
function preserveGroup(entry) {
|
|
109
|
+
if (!entry || !entry.env || typeof entry.env.SYM_GROUP !== 'string') return null;
|
|
110
|
+
const g = entry.env.SYM_GROUP.trim();
|
|
111
|
+
return g || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
77
114
|
// --postinstall always runs global install (npm postinstall runs from
|
|
78
115
|
// npm's staging directory, not the user's project dir). If both flags
|
|
79
116
|
// are passed, the --project flag is ignored during postinstall.
|
|
@@ -89,6 +126,39 @@ const defaultNodeName = `claude-${os.hostname().toLowerCase().replace(/[^a-z0-9-
|
|
|
89
126
|
// SYM_NODE_NAME from env wins over default
|
|
90
127
|
const nodeName = process.env.SYM_NODE_NAME || defaultNodeName;
|
|
91
128
|
|
|
129
|
+
// Capture the user's *explicit* group intent for this install, distinct
|
|
130
|
+
// from "user didn't ask, use existing or default":
|
|
131
|
+
// null → user didn't pass --group or SYM_GROUP
|
|
132
|
+
// 'default' → user explicitly wants the global _sym._tcp mesh
|
|
133
|
+
// (escape hatch: revert from a named group, with --force)
|
|
134
|
+
// '<kebab-name>' → user explicitly wants this named group
|
|
135
|
+
const explicitGroup = groupArg !== null ? groupArg
|
|
136
|
+
: (process.env.SYM_GROUP || null);
|
|
137
|
+
|
|
138
|
+
// resolveGroup: per-scope group resolution that respects both the user's
|
|
139
|
+
// explicit intent AND the existing entry's persisted state.
|
|
140
|
+
//
|
|
141
|
+
// With --force AND an explicit value: flag/env wins. The user is
|
|
142
|
+
// deliberately overriding state. `--force --group new-team` switches
|
|
143
|
+
// groups; `--force --group default` reverts to the global mesh.
|
|
144
|
+
//
|
|
145
|
+
// Without --force, OR with --force but no explicit value: preserve
|
|
146
|
+
// from the existing entry (heal-path job is to NOT lose user state).
|
|
147
|
+
// Falls back to the explicit value, then to none.
|
|
148
|
+
//
|
|
149
|
+
// Returns the SYM_GROUP value to write, or null to omit the key entirely
|
|
150
|
+
// (which the caller maps to "leave SYM_GROUP out of the env block, node
|
|
151
|
+
// uses default _sym._tcp on launch").
|
|
152
|
+
function resolveGroup(existingEntry) {
|
|
153
|
+
const preserved = preserveGroup(existingEntry);
|
|
154
|
+
if (force && explicitGroup !== null) {
|
|
155
|
+
return explicitGroup === 'default' ? null : explicitGroup;
|
|
156
|
+
}
|
|
157
|
+
if (preserved) return preserved;
|
|
158
|
+
if (explicitGroup && explicitGroup !== 'default') return explicitGroup;
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
92
162
|
// ── Resolve server.js path ────────────────────────────────────────
|
|
93
163
|
|
|
94
164
|
// Resolve server.js from the installed package location. require.resolve
|
|
@@ -153,6 +223,11 @@ if (useProjectMode) {
|
|
|
153
223
|
// back to the hostname default on every reinstall.
|
|
154
224
|
const projectNodeName = preserveNodeName(existingProjectEntry) || nodeName;
|
|
155
225
|
|
|
226
|
+
// Group resolution priority — see resolveGroup() at top of file.
|
|
227
|
+
// Summary: --force + explicit flag/env wins; otherwise preserve, then
|
|
228
|
+
// explicit, then omit. `--group default` with --force = revert to mesh.
|
|
229
|
+
const projectGroup = resolveGroup(existingProjectEntry);
|
|
230
|
+
|
|
156
231
|
// Build the MCP entry (identical shape to global mode)
|
|
157
232
|
const projectEntry = {
|
|
158
233
|
command: 'node',
|
|
@@ -165,6 +240,11 @@ if (useProjectMode) {
|
|
|
165
240
|
SYM_RELAY_TOKEN: '',
|
|
166
241
|
},
|
|
167
242
|
};
|
|
243
|
+
// SYM_GROUP is only written when explicitly set. Omitting it (rather than
|
|
244
|
+
// writing an empty string) keeps the JSON file minimal for the common
|
|
245
|
+
// single-team case AND avoids the "default group accidentally pinned"
|
|
246
|
+
// failure mode where a blank value masks the server.js fallback.
|
|
247
|
+
if (projectGroup) projectEntry.env.SYM_GROUP = projectGroup;
|
|
168
248
|
|
|
169
249
|
// Backup existing .mcp.json if present
|
|
170
250
|
let mcpBackupPath = null;
|
|
@@ -236,6 +316,7 @@ if (useProjectMode) {
|
|
|
236
316
|
`✓ sym-mesh-channel configured for project: ${projectDir}`,
|
|
237
317
|
'',
|
|
238
318
|
` Node name: ${projectNodeName}${projectEntryIsStale ? ' (preserved from stale entry)' : ''}`,
|
|
319
|
+
` Mesh group: ${projectGroup || 'default (global _sym._tcp mesh)'}`,
|
|
239
320
|
` Server path: ${serverJsPath}`,
|
|
240
321
|
` Wrote: ${mcpJsonPath}`,
|
|
241
322
|
];
|
|
@@ -312,6 +393,7 @@ if (cmd === 'doctor') {
|
|
|
312
393
|
scope: 'user-global',
|
|
313
394
|
path: (topEntry.args || [])[0] || '(no path)',
|
|
314
395
|
node: preserveNodeName(topEntry) || '(no SYM_NODE_NAME)',
|
|
396
|
+
group: preserveGroup(topEntry) || 'default',
|
|
315
397
|
live: !isStaleEntry(topEntry),
|
|
316
398
|
});
|
|
317
399
|
}
|
|
@@ -323,6 +405,7 @@ if (cmd === 'doctor') {
|
|
|
323
405
|
scope: `project ${projPath}`,
|
|
324
406
|
path: (e.args || [])[0] || '(no path)',
|
|
325
407
|
node: preserveNodeName(e) || '(no SYM_NODE_NAME)',
|
|
408
|
+
group: preserveGroup(e) || 'default',
|
|
326
409
|
live: !isStaleEntry(e),
|
|
327
410
|
});
|
|
328
411
|
}
|
|
@@ -336,16 +419,34 @@ if (cmd === 'doctor') {
|
|
|
336
419
|
console.log('');
|
|
337
420
|
for (const r of rows) {
|
|
338
421
|
console.log(` [${r.live ? 'live ' : 'STALE'}] ${r.scope}`);
|
|
339
|
-
console.log(` node:
|
|
340
|
-
console.log(`
|
|
422
|
+
console.log(` node: ${r.node}`);
|
|
423
|
+
console.log(` group: ${r.group}`);
|
|
424
|
+
console.log(` path: ${r.path}`);
|
|
341
425
|
}
|
|
342
426
|
const staleCount = rows.filter((r) => !r.live).length;
|
|
427
|
+
|
|
428
|
+
// Heuristic: if multiple entries reference the same Claude identity
|
|
429
|
+
// (same machine) but disagree on group, peers will see each other as
|
|
430
|
+
// disconnected — same incident pattern that cost ~24h of duplex outage
|
|
431
|
+
// at SYM.BOT (CMO=default vs COO=sym-bot-team, 2026-05-02). Surface as
|
|
432
|
+
// a warning so users can spot the mismatch before reaching for the
|
|
433
|
+
// troubleshooting section.
|
|
434
|
+
const groups = new Set(rows.map((r) => r.group));
|
|
435
|
+
const groupMismatch = rows.length > 1 && groups.size > 1;
|
|
436
|
+
|
|
343
437
|
console.log('');
|
|
344
438
|
if (staleCount > 0) {
|
|
345
439
|
console.log(`${staleCount} stale entr${staleCount === 1 ? 'y' : 'ies'} — run \`sym-mesh-channel init\` to heal.`);
|
|
346
440
|
} else {
|
|
347
441
|
console.log('All entries are live.');
|
|
348
442
|
}
|
|
443
|
+
if (groupMismatch) {
|
|
444
|
+
console.log('');
|
|
445
|
+
console.log(`⚠ Group mismatch across entries: ${Array.from(groups).join(', ')}.`);
|
|
446
|
+
console.log(' Nodes in different groups cannot discover each other on Bonjour.');
|
|
447
|
+
console.log(' If teammates expect to see each other, align the SYM_GROUP env var.');
|
|
448
|
+
console.log(' See README "Team mesh groups → Persisting your group across restarts".');
|
|
449
|
+
}
|
|
349
450
|
process.exit(0);
|
|
350
451
|
}
|
|
351
452
|
|
|
@@ -370,6 +471,11 @@ if (existingTopEntry && !force && !topEntryIsStale) {
|
|
|
370
471
|
// Preserve the prior node name on rewrite so mesh identity doesn't drift.
|
|
371
472
|
const topNodeName = preserveNodeName(existingTopEntry) || nodeName;
|
|
372
473
|
|
|
474
|
+
// Resolve SYM_GROUP for the global entry — see resolveGroup() at top.
|
|
475
|
+
// Heal-path default preserves; --force lets the user explicitly switch
|
|
476
|
+
// groups (or back to default mesh) in one command.
|
|
477
|
+
const topGroup = resolveGroup(existingTopEntry);
|
|
478
|
+
|
|
373
479
|
// ── Build the entry ───────────────────────────────────────────────
|
|
374
480
|
|
|
375
481
|
const entry = {
|
|
@@ -389,6 +495,9 @@ const entry = {
|
|
|
389
495
|
SYM_RELAY_TOKEN: '',
|
|
390
496
|
},
|
|
391
497
|
};
|
|
498
|
+
// SYM_GROUP only emitted when explicitly chosen — see project-mode comment
|
|
499
|
+
// for the rationale. Omitted = node uses the global _sym._tcp default.
|
|
500
|
+
if (topGroup) entry.env.SYM_GROUP = topGroup;
|
|
392
501
|
|
|
393
502
|
claudeJson.mcpServers['claude-sym-mesh'] = entry;
|
|
394
503
|
|
|
@@ -407,7 +516,11 @@ for (const [projPath, proj] of Object.entries(projects)) {
|
|
|
407
516
|
if (!projEntry) continue;
|
|
408
517
|
if (!isStaleEntry(projEntry)) continue;
|
|
409
518
|
const projNodeName = preserveNodeName(projEntry) || nodeName;
|
|
410
|
-
|
|
519
|
+
// Preserve SYM_GROUP on stale-heal — same reason as preserveNodeName.
|
|
520
|
+
// The user explicitly chose this group at some prior install; healing a
|
|
521
|
+
// path issue must not silently revert their group membership.
|
|
522
|
+
const projGroupName = preserveGroup(projEntry);
|
|
523
|
+
const healedEntry = {
|
|
411
524
|
command: 'node',
|
|
412
525
|
args: [serverJsPath],
|
|
413
526
|
env: {
|
|
@@ -416,7 +529,9 @@ for (const [projPath, proj] of Object.entries(projects)) {
|
|
|
416
529
|
SYM_RELAY_TOKEN: projEntry.env && typeof projEntry.env.SYM_RELAY_TOKEN === 'string' ? projEntry.env.SYM_RELAY_TOKEN : '',
|
|
417
530
|
},
|
|
418
531
|
};
|
|
419
|
-
|
|
532
|
+
if (projGroupName) healedEntry.env.SYM_GROUP = projGroupName;
|
|
533
|
+
proj.mcpServers['claude-sym-mesh'] = healedEntry;
|
|
534
|
+
healedProjects.push({ path: projPath, node: projNodeName, group: projGroupName });
|
|
420
535
|
}
|
|
421
536
|
|
|
422
537
|
// ── Atomic write ──────────────────────────────────────────────────
|
|
@@ -459,7 +574,7 @@ const launchCmd = `claude --dangerously-load-development-channels server:claude-
|
|
|
459
574
|
|
|
460
575
|
const healedLines = healedProjects.length
|
|
461
576
|
? '\n Healed stale project-scoped entries (now pointing at fresh server.js):\n' +
|
|
462
|
-
healedProjects.map((p) => ` • ${p.path} (node: ${p.node})`).join('\n') + '\n'
|
|
577
|
+
healedProjects.map((p) => ` • ${p.path} (node: ${p.node}${p.group ? `, group: ${p.group}` : ''})`).join('\n') + '\n'
|
|
463
578
|
: '';
|
|
464
579
|
|
|
465
580
|
const nodeNameSuffix = topEntryIsStale ? ' (preserved from stale entry)' : '';
|
|
@@ -468,6 +583,7 @@ console.log(`
|
|
|
468
583
|
✓ sym-mesh-channel configured globally in ~/.claude.json
|
|
469
584
|
|
|
470
585
|
Node name: ${topNodeName}${nodeNameSuffix}
|
|
586
|
+
Mesh group: ${topGroup || 'default (global _sym._tcp mesh)'}
|
|
471
587
|
Server path: ${serverJsPath}
|
|
472
588
|
Backup: ${backupPath}
|
|
473
589
|
${healedLines}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sym-bot/mesh-channel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "MCP server — real-time agent-to-agent cognition for Claude Code remote teams via the SYM mesh.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
25
|
-
"@sym-bot/sym": "^0.5.
|
|
25
|
+
"@sym-bot/sym": "^0.5.8"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18"
|
package/server.js
CHANGED
|
@@ -241,14 +241,25 @@ function registerNodeHandlers(n) {
|
|
|
241
241
|
const focus = entry.cmb?.fields?.focus?.text || entry.content || '';
|
|
242
242
|
const mood = entry.cmb?.fields?.mood?.text || '';
|
|
243
243
|
const moodSuffix = mood && mood !== 'neutral' ? ` (mood: ${mood})` : '';
|
|
244
|
-
// Store the rendered CMB body so the agent can sym_fetch it by [mNNN] ID
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
const
|
|
244
|
+
// Store the rendered CMB body so the agent can sym_fetch it by [mNNN] ID.
|
|
245
|
+
// When the CMB carries an opaque payload alongside CAT7 fields, append a
|
|
246
|
+
// PAYLOAD section to the stored body so sym_fetch returns it intact;
|
|
247
|
+
// header gains a [+payload Nb] indicator so the receiver knows there's
|
|
248
|
+
// structured data beyond CAT7 and should sym_fetch to consume it.
|
|
249
|
+
const payload = entry.cmb?.payload;
|
|
250
|
+
const hasPayload = payload !== undefined && payload !== null;
|
|
251
|
+
let body = entry.content || focus;
|
|
252
|
+
let payloadSuffix = '';
|
|
253
|
+
if (hasPayload) {
|
|
254
|
+
const serialized = (() => {
|
|
255
|
+
try { return JSON.stringify(payload, null, 2); }
|
|
256
|
+
catch { return String(payload); }
|
|
257
|
+
})();
|
|
258
|
+
body = `${body}\n\n---PAYLOAD---\n${serialized}`;
|
|
259
|
+
payloadSuffix = ` [+payload ${serialized.length}b]`;
|
|
260
|
+
}
|
|
250
261
|
const msgId = storeMessage(source, body);
|
|
251
|
-
pushChannel('cmb', `[${source}] ${focus}${moodSuffix} [${msgId}]`);
|
|
262
|
+
pushChannel('cmb', `[${source}] ${focus}${moodSuffix}${payloadSuffix} [${msgId}]`);
|
|
252
263
|
});
|
|
253
264
|
|
|
254
265
|
n.on('message', (from, content) => {
|
|
@@ -335,6 +346,14 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
335
346
|
'Target peer: either the peer display name (e.g. "claude-research-win") or the full nodeId. ' +
|
|
336
347
|
'Call sym_peers first if unsure which peers are connected. Omit to broadcast to all peers.',
|
|
337
348
|
},
|
|
349
|
+
payload: {
|
|
350
|
+
description:
|
|
351
|
+
'Optional opaque payload riding alongside CAT7 fields. Use when carrying data beyond ' +
|
|
352
|
+
'CAT7 — e.g. an LLM request/response substrate protocol puts the prompt + request_id ' +
|
|
353
|
+
'in `payload` rather than smuggling JSON through `motivation` (which is reserved for ' +
|
|
354
|
+
'CAT7 semantics). Receivers see the payload via sym_fetch on the channel notification. ' +
|
|
355
|
+
'Any JSON-serializable value.',
|
|
356
|
+
},
|
|
338
357
|
},
|
|
339
358
|
required: ['focus'],
|
|
340
359
|
},
|
|
@@ -362,6 +381,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
362
381
|
arousal: { type: 'number' },
|
|
363
382
|
},
|
|
364
383
|
},
|
|
384
|
+
payload: {
|
|
385
|
+
description:
|
|
386
|
+
'Optional opaque payload riding alongside CAT7 fields. Use when broadcasting data ' +
|
|
387
|
+
'beyond CAT7 (e.g. llm-capability-advertise carrying served_capabilities). ' +
|
|
388
|
+
'Any JSON-serializable value.',
|
|
389
|
+
},
|
|
365
390
|
},
|
|
366
391
|
required: ['focus'],
|
|
367
392
|
},
|
|
@@ -493,7 +518,10 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
493
518
|
targetPeerId = matches[0].peerId;
|
|
494
519
|
}
|
|
495
520
|
|
|
496
|
-
const
|
|
521
|
+
const sendOpts = {};
|
|
522
|
+
if (targetPeerId) sendOpts.to = targetPeerId;
|
|
523
|
+
if (args.payload !== undefined && args.payload !== null) sendOpts.payload = args.payload;
|
|
524
|
+
const entry = node.remember(fields, sendOpts);
|
|
497
525
|
if (!entry) {
|
|
498
526
|
return { content: [{ type: 'text', text: 'Duplicate — CMB already in memory, not re-broadcast.' }] };
|
|
499
527
|
}
|
|
@@ -513,7 +541,9 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
513
541
|
perspective: args.perspective || NODE_NAME,
|
|
514
542
|
mood: args.mood || { text: 'neutral', valence: 0, arousal: 0 },
|
|
515
543
|
};
|
|
516
|
-
const
|
|
544
|
+
const observeOpts = {};
|
|
545
|
+
if (args.payload !== undefined && args.payload !== null) observeOpts.payload = args.payload;
|
|
546
|
+
const entry = node.remember(fields, observeOpts);
|
|
517
547
|
return { content: [{ type: 'text', text: entry ? `Observed: ${entry.key}` : 'Duplicate — already in memory.' }] };
|
|
518
548
|
}
|
|
519
549
|
|