@sym-bot/sym 0.1.0
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/settings.local.json +10 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +12 -0
- package/LICENSE +200 -0
- package/PRD.md +173 -0
- package/README.md +231 -0
- package/TECHNICAL-SPEC.md +335 -0
- package/bin/setup-claude.sh +55 -0
- package/integrations/claude-code/mcp-server-minimal.js +51 -0
- package/integrations/claude-code/mcp-server.js +142 -0
- package/lib/claude-memory-bridge.js +217 -0
- package/lib/config.js +50 -0
- package/lib/context-encoder.js +62 -0
- package/lib/frame-parser.js +59 -0
- package/lib/memory-store.js +97 -0
- package/lib/node.js +507 -0
- package/package.json +38 -0
package/lib/node.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { EventEmitter } = require('events');
|
|
7
|
+
const { MeshNode } = require('mesh-cognition');
|
|
8
|
+
const { nodeDir, loadOrCreateIdentity, ensureDir, log: logMsg } = require('./config');
|
|
9
|
+
const { MemoryStore } = require('./memory-store');
|
|
10
|
+
const { FrameParser, sendFrame } = require('./frame-parser');
|
|
11
|
+
const { encode, DIM } = require('./context-encoder');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SymNode — a sovereign mesh node with cognitive coupling.
|
|
15
|
+
*
|
|
16
|
+
* Each node encodes its memories into a hidden state vector.
|
|
17
|
+
* When peers connect, the coupling engine evaluates drift between
|
|
18
|
+
* their cognitive states and autonomously decides whether to couple.
|
|
19
|
+
*
|
|
20
|
+
* Aligned peers share memories. Divergent peers stay independent.
|
|
21
|
+
* The intelligence is in the decision to share, not in the sharing itself.
|
|
22
|
+
*/
|
|
23
|
+
class SymNode extends EventEmitter {
|
|
24
|
+
|
|
25
|
+
constructor(opts = {}) {
|
|
26
|
+
super();
|
|
27
|
+
if (!opts.name) throw new Error('SymNode requires a name');
|
|
28
|
+
this._silent = opts.silent || false;
|
|
29
|
+
|
|
30
|
+
this.name = opts.name;
|
|
31
|
+
this._cognitiveProfile = opts.cognitiveProfile || null;
|
|
32
|
+
this._moodThreshold = opts.moodThreshold ?? 0.8;
|
|
33
|
+
this._identity = loadOrCreateIdentity(this.name);
|
|
34
|
+
this._dir = nodeDir(this.name);
|
|
35
|
+
this._memoriesDir = path.join(this._dir, 'memories');
|
|
36
|
+
this._store = new MemoryStore(this._memoriesDir, this.name);
|
|
37
|
+
|
|
38
|
+
// Coupling engine — evaluates peer cognitive state
|
|
39
|
+
this._meshNode = new MeshNode({ hiddenDim: DIM });
|
|
40
|
+
this._initLocalState();
|
|
41
|
+
|
|
42
|
+
// Peer state
|
|
43
|
+
this._peers = new Map();
|
|
44
|
+
this._server = null;
|
|
45
|
+
this._bonjour = null;
|
|
46
|
+
this._browser = null;
|
|
47
|
+
this._port = 0;
|
|
48
|
+
this._running = false;
|
|
49
|
+
|
|
50
|
+
// Timers
|
|
51
|
+
this._heartbeatInterval = opts.heartbeatInterval || 5000;
|
|
52
|
+
this._heartbeatTimeout = opts.heartbeatTimeout || 15000;
|
|
53
|
+
this._heartbeatTimer = null;
|
|
54
|
+
this._encodeInterval = opts.encodeInterval || 30000;
|
|
55
|
+
this._encodeTimer = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Context Encoding ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
_initLocalState() {
|
|
61
|
+
const context = this._buildContext();
|
|
62
|
+
if (context.length > 5) {
|
|
63
|
+
const { h1, h2 } = encode(context);
|
|
64
|
+
this._meshNode.updateLocalState(h1, h2, 0.8);
|
|
65
|
+
} else {
|
|
66
|
+
const h1 = Array.from({ length: DIM }, () => (Math.random() - 0.5) * 0.1);
|
|
67
|
+
const h2 = Array.from({ length: DIM }, () => (Math.random() - 0.5) * 0.1);
|
|
68
|
+
this._meshNode.updateLocalState(h1, h2, 0.3);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_buildContext() {
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (this._cognitiveProfile) parts.push(this._cognitiveProfile);
|
|
75
|
+
const entries = this._store.allEntries().slice(0, 20);
|
|
76
|
+
parts.push(...entries.map(e => e.content || ''));
|
|
77
|
+
return parts.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_reencodeAndBroadcast() {
|
|
81
|
+
const context = this._buildContext();
|
|
82
|
+
if (context.length < 5) return;
|
|
83
|
+
|
|
84
|
+
const { h1, h2 } = encode(context);
|
|
85
|
+
this._meshNode.updateLocalState(h1, h2, 0.8);
|
|
86
|
+
|
|
87
|
+
// Broadcast cognitive state to all peers for re-evaluation
|
|
88
|
+
this._broadcastToPeers({ type: 'state-sync', h1, h2, confidence: 0.8 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update cognitive state from external context (e.g. Claude Code's memories).
|
|
93
|
+
* Does not store anything — just re-encodes and broadcasts.
|
|
94
|
+
*/
|
|
95
|
+
updateContext(text) {
|
|
96
|
+
if (!text || text.length < 5) return;
|
|
97
|
+
const { h1, h2 } = encode(text);
|
|
98
|
+
this._meshNode.updateLocalState(h1, h2, 0.8);
|
|
99
|
+
this._broadcastToPeers({ type: 'state-sync', h1, h2, confidence: 0.8 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Share content with cognitively aligned peers without storing locally.
|
|
104
|
+
* Used by ClaudeMemoryBridge — Claude Code's memory dir is the source of truth.
|
|
105
|
+
*/
|
|
106
|
+
shareWithPeers(content, opts = {}) {
|
|
107
|
+
const entry = {
|
|
108
|
+
key: opts.key || `memory-${Date.now()}`,
|
|
109
|
+
content,
|
|
110
|
+
source: opts.source || this.name,
|
|
111
|
+
tags: opts.tags || [],
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
this._meshNode.coupledState();
|
|
116
|
+
const decisions = this._meshNode.couplingDecisions;
|
|
117
|
+
|
|
118
|
+
let shared = 0;
|
|
119
|
+
for (const [peerId, peer] of this._peers) {
|
|
120
|
+
const d = decisions.get(peerId);
|
|
121
|
+
if (d && d.decision === 'rejected') {
|
|
122
|
+
this._log(`Not sharing with ${peer.name} — rejected (drift: ${d.drift.toFixed(3)})`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
sendFrame(peer.socket, { type: 'memory-share', ...entry });
|
|
126
|
+
shared++;
|
|
127
|
+
if (d) {
|
|
128
|
+
this._log(`Shared with ${peer.name} — ${d.decision} (drift: ${d.drift.toFixed(3)})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this._log(`Shared: "${content.slice(0, 50)}${content.length > 50 ? '...' : ''}" → ${shared}/${this._peers.size} peers`);
|
|
133
|
+
return entry;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Lifecycle ──────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
async start() {
|
|
139
|
+
if (this._running) return;
|
|
140
|
+
this._running = true;
|
|
141
|
+
|
|
142
|
+
await this._startServer();
|
|
143
|
+
this._startDiscovery();
|
|
144
|
+
|
|
145
|
+
this._heartbeatTimer = setInterval(() => this._checkHeartbeats(), this._heartbeatInterval);
|
|
146
|
+
this._encodeTimer = setInterval(() => this._reencodeAndBroadcast(), this._encodeInterval);
|
|
147
|
+
|
|
148
|
+
this._log(`Started (port: ${this._port}, id: ${this._identity.nodeId.slice(0, 8)})`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async stop() {
|
|
152
|
+
if (!this._running) return;
|
|
153
|
+
this._running = false;
|
|
154
|
+
|
|
155
|
+
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
156
|
+
if (this._encodeTimer) clearInterval(this._encodeTimer);
|
|
157
|
+
|
|
158
|
+
for (const [, peer] of this._peers) {
|
|
159
|
+
try { peer.socket.destroy(); } catch {}
|
|
160
|
+
}
|
|
161
|
+
this._peers.clear();
|
|
162
|
+
|
|
163
|
+
if (this._bonjour) {
|
|
164
|
+
try { this._bonjour.destroy(); } catch {}
|
|
165
|
+
this._bonjour = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this._server) {
|
|
169
|
+
this._server.close();
|
|
170
|
+
this._server = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this._log('Stopped');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Memory (with cognitive coupling) ───────────────────────
|
|
177
|
+
|
|
178
|
+
remember(content, opts = {}) {
|
|
179
|
+
const entry = this._store.write(content, opts);
|
|
180
|
+
|
|
181
|
+
// Re-encode context with new memory
|
|
182
|
+
const context = this._buildContext();
|
|
183
|
+
const { h1, h2 } = encode(context);
|
|
184
|
+
this._meshNode.updateLocalState(h1, h2, 0.8);
|
|
185
|
+
|
|
186
|
+
// Evaluate coupling with each peer — only share with aligned/guarded
|
|
187
|
+
// Trigger coupling evaluation
|
|
188
|
+
this._meshNode.coupledState();
|
|
189
|
+
const decisions = this._meshNode.couplingDecisions;
|
|
190
|
+
|
|
191
|
+
let shared = 0;
|
|
192
|
+
for (const [peerId, peer] of this._peers) {
|
|
193
|
+
const d = decisions.get(peerId);
|
|
194
|
+
|
|
195
|
+
if (d && d.decision === 'rejected') {
|
|
196
|
+
this._log(`Not sharing with ${peer.name} — rejected (drift: ${d.drift.toFixed(3)})`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
sendFrame(peer.socket, { type: 'memory-share', ...entry });
|
|
201
|
+
shared++;
|
|
202
|
+
|
|
203
|
+
if (d) {
|
|
204
|
+
this._log(`Shared with ${peer.name} — ${d.decision} (drift: ${d.drift.toFixed(3)})`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._log(`Remembered: "${content.slice(0, 50)}${content.length > 50 ? '...' : ''}" → ${shared}/${this._peers.size} peers`);
|
|
209
|
+
return entry;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
recall(query) {
|
|
213
|
+
return this._store.search(query);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Mood (with cognitive evaluation) ───────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Broadcast mood to the mesh. Receiving agents evaluate this
|
|
220
|
+
* against their own cognitive state and autonomously decide
|
|
221
|
+
* whether to act. No routing. No registration. The coupling
|
|
222
|
+
* engine decides.
|
|
223
|
+
*/
|
|
224
|
+
broadcastMood(mood, opts = {}) {
|
|
225
|
+
const frame = {
|
|
226
|
+
type: 'mood',
|
|
227
|
+
from: this._identity.nodeId,
|
|
228
|
+
fromName: this.name,
|
|
229
|
+
mood,
|
|
230
|
+
context: opts.context || null,
|
|
231
|
+
timestamp: Date.now(),
|
|
232
|
+
};
|
|
233
|
+
this._broadcastToPeers(frame);
|
|
234
|
+
this._log(`Mood broadcast: "${mood.slice(0, 50)}"`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Communication ──────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
send(message, opts = {}) {
|
|
240
|
+
const frame = {
|
|
241
|
+
type: 'message',
|
|
242
|
+
from: this._identity.nodeId,
|
|
243
|
+
fromName: this.name,
|
|
244
|
+
content: message,
|
|
245
|
+
timestamp: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
if (opts.to) {
|
|
248
|
+
const peer = this._peers.get(opts.to);
|
|
249
|
+
if (peer) sendFrame(peer.socket, frame);
|
|
250
|
+
} else {
|
|
251
|
+
this._broadcastToPeers(frame);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Monitoring ─────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
peers() {
|
|
258
|
+
const result = [];
|
|
259
|
+
const decisions = this._meshNode.couplingDecisions;
|
|
260
|
+
for (const [id, peer] of this._peers) {
|
|
261
|
+
const d = decisions.get(id);
|
|
262
|
+
result.push({
|
|
263
|
+
id: id.slice(0, 8),
|
|
264
|
+
name: peer.name || 'unknown',
|
|
265
|
+
connected: true,
|
|
266
|
+
lastSeen: peer.lastSeen,
|
|
267
|
+
coupling: d ? d.decision : 'pending',
|
|
268
|
+
drift: d ? parseFloat(d.drift.toFixed(3)) : null,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
memories() {
|
|
275
|
+
return this._store.count();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
coherence() {
|
|
279
|
+
return this._meshNode.coherence;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
status() {
|
|
283
|
+
return {
|
|
284
|
+
name: this.name,
|
|
285
|
+
nodeId: this._identity.nodeId.slice(0, 8),
|
|
286
|
+
running: this._running,
|
|
287
|
+
port: this._port,
|
|
288
|
+
peers: this.peers(),
|
|
289
|
+
peerCount: this._peers.size,
|
|
290
|
+
memoryCount: this.memories(),
|
|
291
|
+
coherence: this.coherence(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── TCP Server ─────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
_startServer() {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
this._server = net.createServer((socket) => {
|
|
300
|
+
this._handleInboundConnection(socket);
|
|
301
|
+
});
|
|
302
|
+
this._server.on('error', (err) => {
|
|
303
|
+
this._log(`Server error: ${err.message}`);
|
|
304
|
+
reject(err);
|
|
305
|
+
});
|
|
306
|
+
this._server.listen(0, '0.0.0.0', () => {
|
|
307
|
+
this._port = this._server.address().port;
|
|
308
|
+
resolve();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_handleInboundConnection(socket) {
|
|
314
|
+
const parser = new FrameParser();
|
|
315
|
+
let identified = false;
|
|
316
|
+
const timeout = setTimeout(() => { if (!identified) socket.destroy(); }, 10000);
|
|
317
|
+
|
|
318
|
+
socket.on('data', (chunk) => { if (!identified) parser.feed(chunk); });
|
|
319
|
+
|
|
320
|
+
parser.on('message', (msg) => {
|
|
321
|
+
if (identified) return;
|
|
322
|
+
if (msg.type !== 'handshake') { socket.destroy(); return; }
|
|
323
|
+
identified = true;
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
if (this._peers.has(msg.nodeId)) { socket.destroy(); return; }
|
|
326
|
+
const peer = this._createPeer(socket, msg.nodeId, msg.name, false);
|
|
327
|
+
if (parser.buffer.length > 0) peer.parser.feed(parser.buffer);
|
|
328
|
+
this._addPeer(peer);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
socket.on('error', () => clearTimeout(timeout));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Bonjour Discovery ──────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
_startDiscovery() {
|
|
337
|
+
const { Bonjour } = require('bonjour-service');
|
|
338
|
+
this._bonjour = new Bonjour();
|
|
339
|
+
|
|
340
|
+
this._bonjour.publish({
|
|
341
|
+
name: this._identity.nodeId,
|
|
342
|
+
type: 'sym',
|
|
343
|
+
port: this._port,
|
|
344
|
+
txt: { 'node-id': this._identity.nodeId, 'node-name': this.name, 'hostname': this._identity.hostname },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
this._browser = this._bonjour.find({ type: 'sym' });
|
|
348
|
+
|
|
349
|
+
this._browser.on('up', (service) => {
|
|
350
|
+
const peerId = service.txt?.['node-id'];
|
|
351
|
+
if (!peerId || peerId === this._identity.nodeId) return;
|
|
352
|
+
const peerName = service.txt?.['node-name'] || 'unknown';
|
|
353
|
+
const address = service.referer?.address || service.addresses?.[0];
|
|
354
|
+
const port = service.port;
|
|
355
|
+
if (!address || !port) return;
|
|
356
|
+
if (this._identity.nodeId < peerId && !this._peers.has(peerId)) {
|
|
357
|
+
this._connectToPeer(address, port, peerId, peerName);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_connectToPeer(address, port, peerId, peerName) {
|
|
363
|
+
if (this._peers.has(peerId)) return;
|
|
364
|
+
const socket = net.createConnection({ host: address, port }, () => {
|
|
365
|
+
const peer = this._createPeer(socket, peerId, peerName, true);
|
|
366
|
+
this._addPeer(peer);
|
|
367
|
+
});
|
|
368
|
+
socket.on('error', (err) => this._log(`Connect failed to ${peerName}: ${err.message}`));
|
|
369
|
+
socket.setTimeout(10000, () => socket.destroy());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Peer Management ────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
_createPeer(socket, peerId, peerName, isOutbound) {
|
|
375
|
+
const parser = new FrameParser();
|
|
376
|
+
socket.on('data', (chunk) => parser.feed(chunk));
|
|
377
|
+
parser.on('message', (msg) => {
|
|
378
|
+
const peer = this._peers.get(peerId);
|
|
379
|
+
if (peer) peer.lastSeen = Date.now();
|
|
380
|
+
this._handlePeerMessage(peerId, peerName, msg);
|
|
381
|
+
});
|
|
382
|
+
parser.on('error', (err) => this._log(`Frame error from ${peerName}: ${err.message}`));
|
|
383
|
+
socket.on('close', () => {
|
|
384
|
+
this._peers.delete(peerId);
|
|
385
|
+
this._meshNode.removePeer(peerId);
|
|
386
|
+
this._log(`Peer disconnected: ${peerName}`);
|
|
387
|
+
this.emit('peer-left', { id: peerId, name: peerName });
|
|
388
|
+
});
|
|
389
|
+
socket.on('error', () => {});
|
|
390
|
+
return { socket, parser, peerId, name: peerName, isOutbound, lastSeen: Date.now() };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_addPeer(peer) {
|
|
394
|
+
this._peers.set(peer.peerId, peer);
|
|
395
|
+
|
|
396
|
+
// Handshake
|
|
397
|
+
sendFrame(peer.socket, { type: 'handshake', nodeId: this._identity.nodeId, name: this.name });
|
|
398
|
+
|
|
399
|
+
// Send cognitive state for coupling evaluation
|
|
400
|
+
const [h1, h2] = this._meshNode.coupledState();
|
|
401
|
+
sendFrame(peer.socket, { type: 'state-sync', h1, h2, confidence: 0.8 });
|
|
402
|
+
|
|
403
|
+
this._log(`Peer connected: ${peer.name} (${peer.isOutbound ? 'outbound' : 'inbound'})`);
|
|
404
|
+
this.emit('peer-joined', { id: peer.peerId, name: peer.name });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_handlePeerMessage(peerId, peerName, msg) {
|
|
408
|
+
switch (msg.type) {
|
|
409
|
+
case 'handshake':
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case 'state-sync':
|
|
413
|
+
// Peer sent their cognitive state — evaluate coupling
|
|
414
|
+
if (msg.h1?.length === DIM && msg.h2?.length === DIM) {
|
|
415
|
+
this._meshNode.addPeer(peerId, msg.h1, msg.h2, msg.confidence || 0.5);
|
|
416
|
+
this._meshNode.coupledState(); // trigger evaluation
|
|
417
|
+
const d = this._meshNode.couplingDecisions.get(peerId);
|
|
418
|
+
if (d) {
|
|
419
|
+
this._log(`Coupling with ${peerName}: ${d.decision} (drift: ${d.drift.toFixed(3)})`);
|
|
420
|
+
this.emit('coupling-decision', { peer: peerName, decision: d.decision, drift: d.drift });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'memory-share':
|
|
426
|
+
if (msg.content) {
|
|
427
|
+
// Check coupling before accepting
|
|
428
|
+
const d = this._meshNode.couplingDecisions.get(peerId);
|
|
429
|
+
if (d && d.decision === 'rejected') {
|
|
430
|
+
this._log(`Rejected memory from ${peerName} (drift: ${d.drift.toFixed(3)})`);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
this._store.receiveFromPeer(peerId, msg);
|
|
434
|
+
this._log(`Memory from ${peerName}: "${(msg.content || '').slice(0, 40)}..." [${d?.decision || 'accepted'}]`);
|
|
435
|
+
this.emit('memory-received', { from: peerName, entry: msg, decision: d?.decision });
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case 'mood':
|
|
440
|
+
// Peer broadcast their mood. Use the SDK's coupling engine to evaluate
|
|
441
|
+
// whether this mood is relevant to our cognitive state.
|
|
442
|
+
if (msg.mood) {
|
|
443
|
+
const { h1: moodH1, h2: moodH2 } = encode(msg.mood);
|
|
444
|
+
const moodPeerId = `mood-${peerId}`;
|
|
445
|
+
|
|
446
|
+
// Evaluate mood using the SDK's coupling engine
|
|
447
|
+
this._meshNode.addPeer(moodPeerId, moodH1, moodH2, 0.8);
|
|
448
|
+
this._meshNode.coupledState();
|
|
449
|
+
const d = this._meshNode.couplingDecisions.get(moodPeerId);
|
|
450
|
+
this._meshNode.removePeer(moodPeerId);
|
|
451
|
+
|
|
452
|
+
const from = msg.fromName || peerName;
|
|
453
|
+
const drift = d ? d.drift : 1;
|
|
454
|
+
|
|
455
|
+
// Mood uses the agent's moodThreshold (default 0.8) — more permissive
|
|
456
|
+
// than memory sharing (0.5). User wellbeing crosses domain boundaries.
|
|
457
|
+
if (drift <= this._moodThreshold) {
|
|
458
|
+
this._log(`Mood from ${from}: "${msg.mood.slice(0, 50)}" → ACCEPTED (drift: ${drift.toFixed(3)}, threshold: ${this._moodThreshold})`);
|
|
459
|
+
this.emit('mood-accepted', { from, mood: msg.mood, drift, context: msg.context });
|
|
460
|
+
} else {
|
|
461
|
+
this._log(`Mood from ${from}: "${msg.mood.slice(0, 50)}" → IGNORED (drift: ${drift.toFixed(3)}, threshold: ${this._moodThreshold})`);
|
|
462
|
+
this.emit('mood-rejected', { from, mood: msg.mood, drift });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case 'message':
|
|
468
|
+
this._log(`Message from ${msg.fromName || peerName}: ${(msg.content || '').slice(0, 60)}`);
|
|
469
|
+
this.emit('message', msg.fromName || peerName, msg.content, msg);
|
|
470
|
+
break;
|
|
471
|
+
|
|
472
|
+
case 'ping':
|
|
473
|
+
const peer = this._peers.get(peerId);
|
|
474
|
+
if (peer) sendFrame(peer.socket, { type: 'pong' });
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
case 'pong':
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_broadcastToPeers(frame) {
|
|
483
|
+
for (const [, peer] of this._peers) {
|
|
484
|
+
sendFrame(peer.socket, frame);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
_checkHeartbeats() {
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
for (const [id, peer] of this._peers) {
|
|
491
|
+
if (now - peer.lastSeen > this._heartbeatTimeout) {
|
|
492
|
+
this._log(`Heartbeat timeout: ${peer.name}`);
|
|
493
|
+
peer.socket.destroy();
|
|
494
|
+
this._peers.delete(id);
|
|
495
|
+
this._meshNode.removePeer(id);
|
|
496
|
+
} else if (now - peer.lastSeen > this._heartbeatInterval) {
|
|
497
|
+
sendFrame(peer.socket, { type: 'ping' });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
_log(msg) {
|
|
503
|
+
if (!this._silent) logMsg(this.name, msg);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
module.exports = { SymNode };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sym-bot/sym",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local AI mesh — every agent is a sovereign node, the mesh is the agents",
|
|
5
|
+
"main": "lib/node.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sym-mcp": "./integrations/claude-code/mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test tests/*.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mesh",
|
|
14
|
+
"p2p",
|
|
15
|
+
"collective-intelligence",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"mesh-cognition",
|
|
18
|
+
"mmp",
|
|
19
|
+
"peer-to-peer",
|
|
20
|
+
"local-ai",
|
|
21
|
+
"privacy"
|
|
22
|
+
],
|
|
23
|
+
"author": "SYM.BOT Ltd <info@sym.bot> (https://sym.bot)",
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"homepage": "https://github.com/sym-bot/sym",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/sym-bot/sym.git"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
|
+
"bonjour-service": "^1.3.0",
|
|
36
|
+
"mesh-cognition": "^1.1.0"
|
|
37
|
+
}
|
|
38
|
+
}
|