@sym-bot/mesh-channel 0.3.19 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/server.js +57 -10
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.3.19
|
|
4
10
|
|
|
5
11
|
### Added
|
package/package.json
CHANGED
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.',
|
|
@@ -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);
|