@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sym-mesh-channel",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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",
package/.mcp.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@sym-bot/mesh-channel@0.3.19"
7
+ "@sym-bot/mesh-channel@0.3.21"
8
8
  ],
9
9
  "env": {
10
10
  "SYM_RELAY_URL": "${user_config.relay_url}",
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.19",
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.10",
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 msgId = storeMessage(source, body);
328
- pushChannel('cmb', `[${source}${dirTag}] ${focus}${moodSuffix}${memTag}${payloadSuffix} [${msgId}]`);
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 msgId = storeMessage(from, content);
336
- const header = extractCompactHeader(from, content);
337
- pushChannel('message', `[${from}] ${header} [${msgId}]`);
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 see 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). ' +
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
- 'Messages arrive as compact headers with [mNNN] IDs — use sym_fetch to read the full content when the header is relevant to your current task.';
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);