@sym-bot/mesh-channel 0.3.19 → 0.3.21
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/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/server.js +62 -11
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.21
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`sym_inbox` is now a thin adapter over the SDK primitive `node.inbox()`** (`@sym-bot/sym` ^0.7.11). The pull-based receive buffer + drain cursor moved down into the node, where it belongs alongside `node.remember()` (send) — so the SDK is sufficient for send **and** pull on its own, and the wrapper owns no buffering logic. Behaviour is unchanged (FIFO drain, `peek`, `limit`); `sym_fetch` now also resolves SDK inbox ids (`inNNNN`). The wrapper still applies the peer allowlist + prompt-injection filter on the pull path before anything enters context.
|
|
8
|
+
|
|
9
|
+
## 0.3.20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`sym_inbox` — pull-based receive (bypasses channel-push gating).** Claude Code 2.1.177 gates the real-time `<channel>` push behind a managed-settings policy + server-side allowlist, but the MCP tool layer is never gated. Every inbound CMB is already accumulated server-side (the cmb-accepted handler stores it *before* the gated push), so `sym_inbox` lets the agent **pull** messages received since its last check — directed `sym_send` addressed to it plus admitted broadcasts. FIFO drain with a read cursor (no message is skipped even past the page limit), `peek` for non-destructive reads, `limit` to page. The agent is instructed to poll `sym_inbox` at the start of a turn and periodically while coordinating, so receive works regardless of the channel-push policy gate. Compact `[mNNN]` headers; `sym_fetch` for full content.
|
|
14
|
+
|
|
3
15
|
## 0.3.19
|
|
4
16
|
|
|
5
17
|
### Added
|
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.21",
|
|
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.7.
|
|
25
|
+
"@sym-bot/sym": "^0.7.11",
|
|
26
26
|
"bonjour-service": "^1.3.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
package/server.js
CHANGED
|
@@ -324,17 +324,18 @@ function registerNodeHandlers(n) {
|
|
|
324
324
|
// not assume it is recallable later.
|
|
325
325
|
const dirTag = entry.directed ? ' →you' : '';
|
|
326
326
|
const memTag = entry.directed && entry.remixed === false ? ' ·not-stored' : '';
|
|
327
|
-
const
|
|
328
|
-
|
|
327
|
+
const header = `[${source}${dirTag}] ${focus}${moodSuffix}${memTag}${payloadSuffix}`;
|
|
328
|
+
const msgId = storeMessage(source, body, header);
|
|
329
|
+
pushChannel('cmb', `${header} [${msgId}]`);
|
|
329
330
|
});
|
|
330
331
|
|
|
331
332
|
n.on('message', (from, content) => {
|
|
332
333
|
if (!isPeerAllowed(from)) return;
|
|
333
334
|
const sec = checkSecurity(from, { focus: { text: content } }, null);
|
|
334
335
|
if (!sec.safe) { securityAudit(sec.reason, from, sec.excerpt); return; }
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
pushChannel('message',
|
|
336
|
+
const header = `[${from}] ${extractCompactHeader(from, content)}`;
|
|
337
|
+
const msgId = storeMessage(from, content, header);
|
|
338
|
+
pushChannel('message', `${header} [${msgId}]`);
|
|
338
339
|
});
|
|
339
340
|
}
|
|
340
341
|
|
|
@@ -343,12 +344,12 @@ function registerNodeHandlers(n) {
|
|
|
343
344
|
// Base instructions shown to the agent at every MCP initialize.
|
|
344
345
|
const BASE_INSTRUCTIONS =
|
|
345
346
|
`You are a peer node on the SYM mesh (identity: ${NODE_NAME}). ` +
|
|
346
|
-
'Mesh events arrive as <channel> notifications in real-time. ' +
|
|
347
|
-
'When you
|
|
347
|
+
'Mesh events may arrive as <channel> notifications in real-time, but that push can be gated by Claude Code policy — so to RECEIVE reliably, call sym_inbox to PULL messages addressed to you (directed sym_send + admitted broadcasts). Call sym_inbox at the start of your turn and periodically while coordinating with peers, so you never miss one. ' +
|
|
348
|
+
'When you receive a CMB from another node, respond via sym_send targeted at that node by name if the reply is for that specific peer (MMP §4.4.4 targeted CMB). ' +
|
|
348
349
|
'Share observations about your own state with the whole mesh via sym_observe (MMP §9.2 receiver-autonomous SVAF evaluation). ' +
|
|
349
350
|
'Both sym_send and sym_observe emit CAT7 CMBs; receivers run SVAF and, if admitted, remix-store with lineage pointing back to your CMB. ' +
|
|
350
351
|
'Search mesh memory via sym_recall. ' +
|
|
351
|
-
'
|
|
352
|
+
'sym_inbox and <channel> notifications give compact headers with [mNNN] IDs — use sym_fetch to read the full content when relevant to your current task.';
|
|
352
353
|
|
|
353
354
|
// Final startup step (MMP §4.2 O2 — rejoin-without-replay). The SymNode
|
|
354
355
|
// constructor builds the memory-store index from disk, so the primer is
|
|
@@ -487,6 +488,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
487
488
|
required: ['msg_id'],
|
|
488
489
|
},
|
|
489
490
|
},
|
|
491
|
+
{
|
|
492
|
+
name: 'sym_inbox',
|
|
493
|
+
description: 'PULL mesh messages received since your last inbox check — directed sym_send addressed to you, plus admitted broadcasts. This is the poll-based RECEIVE path: real-time channel push can be gated by Claude Code policy, but this tool always works. Call it at the start of a turn and periodically while coordinating so you never miss a peer. Returns compact headers with [mNNN] IDs (newest last); use sym_fetch for full content, reply via sym_send.',
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: 'object',
|
|
496
|
+
properties: {
|
|
497
|
+
peek: { type: 'boolean', description: 'If true, do not advance the read cursor (same items return next call). Default false — draining.' },
|
|
498
|
+
limit: { type: 'number', description: 'Max messages to return (default 50, newest last).' },
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
490
502
|
{
|
|
491
503
|
name: 'sym_group_info',
|
|
492
504
|
description: 'Report the mesh group this node is in (MMP §5.8). Shows service type + group name + peer count.',
|
|
@@ -639,6 +651,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
639
651
|
}
|
|
640
652
|
|
|
641
653
|
case 'sym_fetch': {
|
|
654
|
+
// inNNNN → SDK delivery inbox (pull path); mNNN → channel-push store.
|
|
655
|
+
if (typeof args.msg_id === 'string' && args.msg_id.startsWith('in')) {
|
|
656
|
+
const m = node.inboxGet(args.msg_id);
|
|
657
|
+
if (!m) return { content: [{ type: 'text', text: `Message ${args.msg_id} not found (expired or invalid ID).` }] };
|
|
658
|
+
return { content: [{ type: 'text', text: `[${m.from}] ${new Date(m.receivedAt).toISOString()}\n\n${m.content}` }] };
|
|
659
|
+
}
|
|
642
660
|
const entry = MESSAGE_STORE.get(args.msg_id);
|
|
643
661
|
if (!entry) {
|
|
644
662
|
return { content: [{ type: 'text', text: `Message ${args.msg_id} not found (expired or invalid ID).` }] };
|
|
@@ -651,6 +669,39 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
651
669
|
};
|
|
652
670
|
}
|
|
653
671
|
|
|
672
|
+
case 'sym_inbox': {
|
|
673
|
+
// Thin adapter over the SDK primitive: the node owns the delivery buffer
|
|
674
|
+
// + drain cursor (node.inbox()). This wrapper only formats for display.
|
|
675
|
+
const { messages, remaining } = node.inbox({ peek: !!args.peek, limit: args.limit });
|
|
676
|
+
if (!messages.length) {
|
|
677
|
+
return { content: [{ type: 'text', text: 'Inbox empty — no new mesh messages since your last check.' }] };
|
|
678
|
+
}
|
|
679
|
+
const now = Date.now();
|
|
680
|
+
const lines = messages.map((m) => {
|
|
681
|
+
if (m.from === NODE_NAME) return null; // never surface our own deliveries
|
|
682
|
+
// The security layer still gates the pull path: peer allowlist +
|
|
683
|
+
// prompt-injection filter run on every message before it enters context.
|
|
684
|
+
if (!isPeerAllowed(m.from)) return null;
|
|
685
|
+
const sec = checkSecurity(m.from, m.fields || {}, m.fields?.payload);
|
|
686
|
+
if (!sec.safe) { securityAudit(sec.reason, m.from, sec.excerpt); return null; }
|
|
687
|
+
const age = Math.round((now - m.receivedAt) / 1000);
|
|
688
|
+
const focus = m.fields?.focus?.text || m.content || '';
|
|
689
|
+
const dirTag = m.directed ? ' →you' : '';
|
|
690
|
+
const memTag = m.directed && m.remixed === false ? ' ·not-stored' : '';
|
|
691
|
+
return `[${m.from}${dirTag}] ${String(focus).replace(/\s+/g, ' ').slice(0, 90)}${memTag} [${m.id}] (${age}s ago)`;
|
|
692
|
+
}).filter(Boolean);
|
|
693
|
+
if (!lines.length) {
|
|
694
|
+
return { content: [{ type: 'text', text: 'Inbox empty — no new mesh messages since your last check.' }] };
|
|
695
|
+
}
|
|
696
|
+
const moreNote = remaining > 0 ? ` (+${remaining} more — call sym_inbox again)` : '';
|
|
697
|
+
return {
|
|
698
|
+
content: [{
|
|
699
|
+
type: 'text',
|
|
700
|
+
text: `${lines.length} new mesh message(s)${args.peek ? ' (peek — not drained)' : ''}${moreNote}:\n${lines.join('\n')}\n\nUse sym_fetch <id> for full content; reply via sym_send to=<peer>.`,
|
|
701
|
+
}],
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
654
705
|
case 'sym_status': {
|
|
655
706
|
const s = node.status();
|
|
656
707
|
return {
|
|
@@ -863,13 +914,13 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
863
914
|
// Per COO spec cmb_compact_channel_v0.1.md: push compact headers,
|
|
864
915
|
// store full content for on-demand sym_fetch retrieval. ~10% token
|
|
865
916
|
// savings on mesh traffic without context loss.
|
|
866
|
-
const MESSAGE_STORE = new Map();
|
|
917
|
+
const MESSAGE_STORE = new Map(); // channel-push surface (mNNN) for sym_fetch when channels are enabled
|
|
867
918
|
let msgSeq = 0;
|
|
868
919
|
const MAX_STORED = 200;
|
|
869
920
|
|
|
870
|
-
function storeMessage(from, content) {
|
|
921
|
+
function storeMessage(from, content, header) {
|
|
871
922
|
const msgId = `m${String(++msgSeq).padStart(3, '0')}`;
|
|
872
|
-
MESSAGE_STORE.set(msgId, { from, content, timestamp: Date.now() });
|
|
923
|
+
MESSAGE_STORE.set(msgId, { from, content, header: header || null, timestamp: Date.now() });
|
|
873
924
|
while (MESSAGE_STORE.size > MAX_STORED) {
|
|
874
925
|
const oldest = MESSAGE_STORE.keys().next().value;
|
|
875
926
|
MESSAGE_STORE.delete(oldest);
|