@sym-bot/mesh-channel 0.3.18 → 0.3.20

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.18",
3
+ "version": "0.3.20",
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.18"
7
+ "@sym-bot/mesh-channel@0.3.20"
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.20
4
+
5
+ ### Added
6
+
7
+ - **`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.
8
+
9
+ ## 0.3.19
10
+
11
+ ### Added
12
+
13
+ - **Signed CMBs (MMP §8.3) — authenticity + integrity.** Pins `@sym-bot/sym` to `^0.7.10`: every CMB is now Ed25519-signed by its author and verified receiver-side against the sending peer's handshake-announced identity key **and** its content hash. A forged, tampered, or content-swapped CMB is rejected before it can reach Claude's context (audit-metered). Unsigned CMBs are allowed for interop unless `SYM_REQUIRE_SIGNED_CMB` is set. This is the cryptographic layer above the existing Ed25519 transport identity + SVAF relevance gate + prompt-injection filter. Version-bumped (plugin + `.mcp.json` pin) so installed plugins reinstall and pick up signing on restart.
14
+
3
15
  ## 0.3.18
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.18",
3
+ "version": "0.3.20",
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.9",
25
+ "@sym-bot/sym": "^0.7.10",
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.',
@@ -651,6 +663,40 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
651
663
  };
652
664
  }
653
665
 
666
+ case 'sym_inbox': {
667
+ const limit = Math.max(1, Math.min(args.limit || 50, MAX_STORED));
668
+ const fresh = [];
669
+ for (const [id, entry] of MESSAGE_STORE) {
670
+ const seq = parseInt(id.slice(1), 10);
671
+ if (seq > inboxCursor) fresh.push({ id, seq, entry });
672
+ }
673
+ fresh.sort((a, b) => a.seq - b.seq);
674
+ // FIFO: drain the OLDEST `limit` first so the cursor advances contiguously
675
+ // and a backlog larger than `limit` is never skipped — the rest come on the
676
+ // next call. (Newest-first would jump the cursor past un-returned items.)
677
+ const slice = fresh.slice(0, limit);
678
+ if (!args.peek && slice.length) {
679
+ inboxCursor = Math.max(inboxCursor, ...slice.map((i) => i.seq));
680
+ }
681
+ if (!slice.length) {
682
+ return { content: [{ type: 'text', text: 'Inbox empty — no new mesh messages since your last check.' }] };
683
+ }
684
+ const more = fresh.length - slice.length;
685
+ const now = Date.now();
686
+ const lines = slice.map(({ id, entry }) => {
687
+ const age = Math.round((now - entry.timestamp) / 1000);
688
+ const head = entry.header || `[${entry.from}] ${String(entry.content || '').replace(/\s+/g, ' ').slice(0, 80)}`;
689
+ return `${head} [${id}] (${age}s ago)`;
690
+ });
691
+ const moreNote = more > 0 ? ` (+${more} more — call sym_inbox again)` : '';
692
+ return {
693
+ content: [{
694
+ type: 'text',
695
+ text: `${slice.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>.`,
696
+ }],
697
+ };
698
+ }
699
+
654
700
  case 'sym_status': {
655
701
  const s = node.status();
656
702
  return {
@@ -865,11 +911,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
865
911
  // savings on mesh traffic without context loss.
866
912
  const MESSAGE_STORE = new Map();
867
913
  let msgSeq = 0;
914
+ let inboxCursor = 0; // highest msgSeq drained by sym_inbox (pull-based receive)
868
915
  const MAX_STORED = 200;
869
916
 
870
- function storeMessage(from, content) {
917
+ function storeMessage(from, content, header) {
871
918
  const msgId = `m${String(++msgSeq).padStart(3, '0')}`;
872
- MESSAGE_STORE.set(msgId, { from, content, timestamp: Date.now() });
919
+ MESSAGE_STORE.set(msgId, { from, content, header: header || null, timestamp: Date.now() });
873
920
  while (MESSAGE_STORE.size > MAX_STORED) {
874
921
  const oldest = MESSAGE_STORE.keys().next().value;
875
922
  MESSAGE_STORE.delete(oldest);