society-protocol 1.0.1 → 1.2.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.
Files changed (52) hide show
  1. package/dist/bootstrap.d.ts +8 -3
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +52 -9
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/content-store.d.ts +77 -0
  6. package/dist/content-store.d.ts.map +1 -0
  7. package/dist/content-store.js +178 -0
  8. package/dist/content-store.js.map +1 -0
  9. package/dist/identity.d.ts +22 -0
  10. package/dist/identity.d.ts.map +1 -1
  11. package/dist/identity.js +60 -0
  12. package/dist/identity.js.map +1 -1
  13. package/dist/index.js +174 -40
  14. package/dist/index.js.map +1 -1
  15. package/dist/knowledge.d.ts +168 -1
  16. package/dist/knowledge.d.ts.map +1 -1
  17. package/dist/knowledge.js +682 -3
  18. package/dist/knowledge.js.map +1 -1
  19. package/dist/lib.d.ts +7 -5
  20. package/dist/lib.d.ts.map +1 -1
  21. package/dist/lib.js +7 -5
  22. package/dist/lib.js.map +1 -1
  23. package/dist/p2p.d.ts +27 -0
  24. package/dist/p2p.d.ts.map +1 -1
  25. package/dist/p2p.js +165 -69
  26. package/dist/p2p.js.map +1 -1
  27. package/dist/planner.d.ts +1 -0
  28. package/dist/planner.d.ts.map +1 -1
  29. package/dist/planner.js +26 -4
  30. package/dist/planner.js.map +1 -1
  31. package/dist/proactive/watcher.d.ts +76 -0
  32. package/dist/proactive/watcher.d.ts.map +1 -0
  33. package/dist/proactive/watcher.js +246 -0
  34. package/dist/proactive/watcher.js.map +1 -0
  35. package/dist/registry.d.ts +4 -0
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +21 -0
  38. package/dist/registry.js.map +1 -1
  39. package/dist/reputation.d.ts +36 -0
  40. package/dist/reputation.d.ts.map +1 -1
  41. package/dist/reputation.js +76 -0
  42. package/dist/reputation.js.map +1 -1
  43. package/dist/rooms.d.ts +24 -0
  44. package/dist/rooms.d.ts.map +1 -1
  45. package/dist/rooms.js +90 -0
  46. package/dist/rooms.js.map +1 -1
  47. package/dist/sdk/index.d.ts +1 -1
  48. package/dist/sdk/index.js +1 -1
  49. package/dist/swp.d.ts +1 -1
  50. package/dist/swp.d.ts.map +1 -1
  51. package/dist/swp.js.map +1 -1
  52. package/package.json +1 -1
package/dist/knowledge.js CHANGED
@@ -10,7 +10,95 @@
10
10
  */
11
11
  import { EventEmitter } from 'events';
12
12
  import { ulid } from 'ulid';
13
- // ─── Knowledge Pool Engine ───────────────────────────────────────
13
+ /**
14
+ * Compare two vector clocks for causal ordering.
15
+ * Returns:
16
+ * 'before' — a causally precedes b (a < b)
17
+ * 'after' — a causally follows b (a > b)
18
+ * 'concurrent' — neither precedes the other
19
+ * 'equal' — identical clocks
20
+ */
21
+ export function compareVectorClocks(a, b) {
22
+ const allNodes = new Set([...Object.keys(a), ...Object.keys(b)]);
23
+ let aLess = false;
24
+ let bLess = false;
25
+ for (const node of allNodes) {
26
+ const va = a[node] || 0;
27
+ const vb = b[node] || 0;
28
+ if (va < vb)
29
+ aLess = true;
30
+ if (va > vb)
31
+ bLess = true;
32
+ if (aLess && bLess)
33
+ return 'concurrent';
34
+ }
35
+ if (!aLess && !bLess)
36
+ return 'equal';
37
+ if (aLess && !bLess)
38
+ return 'before';
39
+ return 'after';
40
+ }
41
+ /**
42
+ * Merge two vector clocks by taking component-wise maximum.
43
+ */
44
+ export function mergeVectorClocks(a, b) {
45
+ const merged = { ...a };
46
+ for (const [node, count] of Object.entries(b)) {
47
+ merged[node] = Math.max(merged[node] || 0, count);
48
+ }
49
+ return merged;
50
+ }
51
+ /**
52
+ * Advance HLC for a local event (Kulkarni et al. 2014, §3.1).
53
+ * l' = max(l.wallTime, pt) ; c' = (l' == l.wallTime) ? l.logical + 1 : 0
54
+ */
55
+ export function tickHLC(current) {
56
+ const pt = Date.now();
57
+ if (pt > current.wallTime) {
58
+ return { wallTime: pt, logical: 0, nodeId: current.nodeId };
59
+ }
60
+ return { wallTime: current.wallTime, logical: current.logical + 1, nodeId: current.nodeId };
61
+ }
62
+ /**
63
+ * Receive HLC from a remote message (Kulkarni et al. 2014, §3.2).
64
+ * Merges local and remote clocks to maintain causal ordering.
65
+ */
66
+ export function receiveHLC(local, remote) {
67
+ const pt = Date.now();
68
+ const maxWall = Math.max(local.wallTime, remote.wallTime, pt);
69
+ let logical;
70
+ if (maxWall === local.wallTime && maxWall === remote.wallTime) {
71
+ logical = Math.max(local.logical, remote.logical) + 1;
72
+ }
73
+ else if (maxWall === local.wallTime) {
74
+ logical = local.logical + 1;
75
+ }
76
+ else if (maxWall === remote.wallTime) {
77
+ logical = remote.logical + 1;
78
+ }
79
+ else {
80
+ logical = 0; // pt is strictly greater
81
+ }
82
+ return { wallTime: maxWall, logical, nodeId: local.nodeId };
83
+ }
84
+ /**
85
+ * Compare two HLCs for total ordering.
86
+ * Returns negative if a < b, positive if a > b, 0 if equal.
87
+ * Tie-breaking: wallTime → logical → nodeId (lexicographic).
88
+ */
89
+ export function compareHLC(a, b) {
90
+ if (a.wallTime !== b.wallTime)
91
+ return a.wallTime - b.wallTime;
92
+ if (a.logical !== b.logical)
93
+ return a.logical - b.logical;
94
+ return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
95
+ }
96
+ const DEFAULT_COMPACTION_CONFIG = {
97
+ compactAfterMessages: 20,
98
+ maxRecentMessages: 40,
99
+ ollamaUrl: 'http://127.0.0.1:11434',
100
+ ollamaModel: 'qwen3:1.7b',
101
+ };
14
102
  export class KnowledgePool extends EventEmitter {
15
103
  storage;
16
104
  identity;
@@ -25,10 +113,15 @@ export class KnowledgePool extends EventEmitter {
25
113
  // Link indexes for O(1) graph traversal (fixes N+1 query)
26
114
  linksBySource = new Map();
27
115
  linksByTarget = new Map();
28
- constructor(storage, identity) {
116
+ // Chat message buffers per space (for auto-compaction)
117
+ chatBuffers = new Map();
118
+ compactionConfig;
119
+ compacting = new Set(); // spaces currently compacting
120
+ constructor(storage, identity, compactionConfig) {
29
121
  super();
30
122
  this.storage = storage;
31
123
  this.identity = identity;
124
+ this.compactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...compactionConfig };
32
125
  this.loadFromStorage();
33
126
  }
34
127
  // ─── Space Management ────────────────────────────────────────
@@ -129,9 +222,10 @@ export class KnowledgePool extends EventEmitter {
129
222
  // TODO: Verificar permissões de edição
130
223
  throw new Error('No permission to edit');
131
224
  }
132
- // CRDT: Incrementar versão e atualizar clocks
225
+ // CRDT: Advance HLC (Kulkarni et al. 2014) and increment vector clock
133
226
  card.version++;
134
227
  card.updatedAt = Date.now();
228
+ card.crdt.hlc = tickHLC(card.crdt.hlc);
135
229
  card.crdt.vectorClock[this.identity.did] =
136
230
  (card.crdt.vectorClock[this.identity.did] || 0) + 1;
137
231
  // Aplicar updates
@@ -170,6 +264,166 @@ export class KnowledgePool extends EventEmitter {
170
264
  await this.saveCard(card);
171
265
  this.emit('card:deleted', id);
172
266
  }
267
+ // ─── CRDT Merge ────────────────────────────────────────────────
268
+ /**
269
+ * Merge a remote knowledge card with the local copy.
270
+ *
271
+ * Algorithm (state-based CRDT with causal metadata):
272
+ * 1. If card is unknown locally → accept remote (new knowledge).
273
+ * 2. Compare vector clocks for causal ordering:
274
+ * - remote ≤ local → discard (stale).
275
+ * - local < remote → accept remote (strictly newer).
276
+ * - concurrent → LWW tie-break on HLC (wallTime, logical, nodeId).
277
+ * 3. Merge vector clocks (component-wise max) regardless of winner.
278
+ * 4. Advance local HLC via receiveHLC to maintain causal consistency.
279
+ * 5. Tombstone wins: if either copy is tombstoned, result is tombstoned.
280
+ *
281
+ * Returns the merged card, or null if the remote was stale.
282
+ */
283
+ mergeCard(remote) {
284
+ const local = this.cards.get(remote.id);
285
+ // Case 1: New card — accept remote entirely
286
+ if (!local) {
287
+ // Advance our HLC on receipt of remote clock
288
+ const mergedHlc = receiveHLC({ wallTime: Date.now(), logical: 0, nodeId: this.identity.did }, remote.crdt.hlc);
289
+ const card = {
290
+ ...remote,
291
+ crdt: {
292
+ ...remote.crdt,
293
+ hlc: mergedHlc,
294
+ vectorClock: { ...remote.crdt.vectorClock }
295
+ }
296
+ };
297
+ this.cards.set(card.id, card);
298
+ const space = this.spaces.get(card.spaceId);
299
+ if (space && !card.crdt.tombstone) {
300
+ space.cards.add(card.id);
301
+ space.stats.cardCount = space.cards.size;
302
+ space.stats.lastActivity = Date.now();
303
+ }
304
+ this.indexCard(card);
305
+ this.saveCard(card);
306
+ this.emit('card:merged', { card, action: 'new' });
307
+ return card;
308
+ }
309
+ // Case 2: Compare vector clocks
310
+ const order = compareVectorClocks(local.crdt.vectorClock, remote.crdt.vectorClock);
311
+ if (order === 'equal' || order === 'after') {
312
+ // Local is same or newer — discard remote
313
+ return null;
314
+ }
315
+ // Determine the winner for content (used when concurrent)
316
+ let winner;
317
+ let action;
318
+ if (order === 'before') {
319
+ // Remote is strictly newer — accept remote content
320
+ winner = remote;
321
+ action = 'remote-wins';
322
+ }
323
+ else {
324
+ // Concurrent — LWW tie-break on HLC
325
+ const hlcCmp = compareHLC(local.crdt.hlc, remote.crdt.hlc);
326
+ winner = hlcCmp >= 0 ? local : remote;
327
+ action = hlcCmp >= 0 ? 'local-wins-concurrent' : 'remote-wins-concurrent';
328
+ }
329
+ // Merge metadata regardless of content winner
330
+ const mergedVectorClock = mergeVectorClocks(local.crdt.vectorClock, remote.crdt.vectorClock);
331
+ const mergedHlc = receiveHLC(local.crdt.hlc, remote.crdt.hlc);
332
+ // Tombstone wins: once deleted, stays deleted
333
+ const tombstone = local.crdt.tombstone || remote.crdt.tombstone;
334
+ // Build merged card
335
+ const merged = {
336
+ ...winner,
337
+ crdt: {
338
+ hlc: mergedHlc,
339
+ vectorClock: mergedVectorClock,
340
+ tombstone
341
+ },
342
+ // Take the higher version
343
+ version: Math.max(local.version, remote.version),
344
+ // Merge usage counters (take max of each)
345
+ usage: {
346
+ views: Math.max(local.usage.views, remote.usage.views),
347
+ citations: Math.max(local.usage.citations, remote.usage.citations),
348
+ applications: Math.max(local.usage.applications, remote.usage.applications),
349
+ lastAccessed: Math.max(local.usage.lastAccessed, remote.usage.lastAccessed)
350
+ },
351
+ // Union verifications (deduplicate by verifier+timestamp)
352
+ verifications: this.mergeVerifications(local.verifications, remote.verifications)
353
+ };
354
+ // Update indexes
355
+ this.removeFromIndex(local);
356
+ this.cards.set(merged.id, merged);
357
+ if (!tombstone) {
358
+ this.indexCard(merged);
359
+ }
360
+ // Handle tombstone side effects
361
+ const space = this.spaces.get(merged.spaceId);
362
+ if (space) {
363
+ if (tombstone) {
364
+ space.cards.delete(merged.id);
365
+ }
366
+ else {
367
+ space.cards.add(merged.id);
368
+ }
369
+ space.stats.cardCount = space.cards.size;
370
+ }
371
+ this.saveCard(merged);
372
+ this.emit('card:merged', { card: merged, action });
373
+ return merged;
374
+ }
375
+ /**
376
+ * Merge verification arrays, deduplicating by (verifier, timestamp).
377
+ */
378
+ mergeVerifications(a, b) {
379
+ const seen = new Set();
380
+ const merged = [];
381
+ for (const v of [...a, ...b]) {
382
+ const key = `${v.verifier}:${v.timestamp}`;
383
+ if (!seen.has(key)) {
384
+ seen.add(key);
385
+ merged.push(v);
386
+ }
387
+ }
388
+ return merged;
389
+ }
390
+ /**
391
+ * Serialize a card for network transmission (GossipSub).
392
+ */
393
+ serializeCard(card) {
394
+ return new TextEncoder().encode(JSON.stringify(card));
395
+ }
396
+ /**
397
+ * Deserialize a card received from the network.
398
+ */
399
+ deserializeCard(data) {
400
+ const raw = JSON.parse(new TextDecoder().decode(data));
401
+ return this.normalizeCard(raw);
402
+ }
403
+ /**
404
+ * Handle incoming knowledge sync message from GossipSub.
405
+ * Deserializes, merges, and emits sync events.
406
+ */
407
+ handleSyncMessage(data, from) {
408
+ try {
409
+ const remote = this.deserializeCard(data);
410
+ const result = this.mergeCard(remote);
411
+ if (result) {
412
+ this.emit('sync:merged', { card: result, from });
413
+ }
414
+ }
415
+ catch (err) {
416
+ this.emit('sync:error', { error: err, from });
417
+ }
418
+ }
419
+ /**
420
+ * Get all cards that have been modified since a given timestamp.
421
+ * Used for anti-entropy sync (periodic full-state exchange).
422
+ */
423
+ getModifiedSince(since) {
424
+ return Array.from(this.cards.values())
425
+ .filter(c => c.updatedAt > since);
426
+ }
173
427
  async linkCards(sourceId, targetId, type, strength = 0.5, evidence) {
174
428
  // Verificar se cards existem
175
429
  if (!this.cards.has(sourceId) || !this.cards.has(targetId)) {
@@ -385,7 +639,432 @@ ${cu.sharedState.decisions.slice(-5).join('\n')}
385
639
  ${cu.workingMemory.contextWindow}
386
640
  `.trim();
387
641
  }
642
+ // ─── Conversational Knowledge Exchange ─────────────────────
643
+ /**
644
+ * Get or create CollectiveUnconscious for a space/room.
645
+ * Public so rooms can initialize knowledge tracking.
646
+ */
647
+ async getOrCreateCU(spaceId) {
648
+ const existing = this.collectiveUnconscious.get(spaceId);
649
+ if (existing)
650
+ return existing;
651
+ return this.createCollectiveUnconscious(spaceId);
652
+ }
653
+ /**
654
+ * Ingest a chat message into the collaborative context.
655
+ * Called by RoomManager when chat messages are received.
656
+ * Triggers auto-compaction when buffer exceeds threshold.
657
+ */
658
+ async ingestChatMessage(spaceId, msg) {
659
+ const cu = await this.getOrCreateCU(spaceId);
660
+ // Add to raw buffer
661
+ if (!this.chatBuffers.has(spaceId)) {
662
+ this.chatBuffers.set(spaceId, []);
663
+ }
664
+ const buffer = this.chatBuffers.get(spaceId);
665
+ buffer.push(msg);
666
+ // Update working memory
667
+ const senderLabel = msg.senderName || msg.sender.slice(0, 16);
668
+ cu.workingMemory.recentMessages.push(`[${senderLabel}]: ${msg.content}`);
669
+ // Track participants
670
+ if (!cu.workingMemory.participants.includes(msg.sender)) {
671
+ cu.workingMemory.participants.push(msg.sender);
672
+ }
673
+ // Trim recent messages to max
674
+ const max = this.compactionConfig.maxRecentMessages;
675
+ if (cu.workingMemory.recentMessages.length > max) {
676
+ cu.workingMemory.recentMessages = cu.workingMemory.recentMessages.slice(-max);
677
+ }
678
+ cu.lastUpdate = Date.now();
679
+ // Auto-compact when buffer reaches threshold
680
+ if (buffer.length >= this.compactionConfig.compactAfterMessages && !this.compacting.has(spaceId)) {
681
+ this.compacting.add(spaceId);
682
+ this.compactContext(spaceId).finally(() => this.compacting.delete(spaceId));
683
+ }
684
+ this.emit('chat:ingested', { spaceId, msg });
685
+ }
686
+ /**
687
+ * Compact the conversation context using Ollama.
688
+ * Summarizes recent messages into a dense context window,
689
+ * extracts key concepts, decisions, and open questions.
690
+ */
691
+ async compactContext(spaceId) {
692
+ const cu = this.collectiveUnconscious.get(spaceId);
693
+ if (!cu)
694
+ return;
695
+ const buffer = this.chatBuffers.get(spaceId) || [];
696
+ if (buffer.length === 0)
697
+ return;
698
+ // Build conversation transcript
699
+ const transcript = buffer.map(m => {
700
+ const name = m.senderName || m.sender.slice(0, 16);
701
+ return `${name}: ${m.content}`;
702
+ }).join('\n');
703
+ const previousContext = cu.workingMemory.contextWindow || '';
704
+ try {
705
+ const response = await this.callOllama(`You are a context compactor for a multi-agent conversation system.
706
+
707
+ Given the previous context summary and new conversation messages, produce a COMPACT updated context.
708
+
709
+ PREVIOUS CONTEXT:
710
+ ${previousContext || '(none)'}
711
+
712
+ NEW MESSAGES:
713
+ ${transcript}
714
+
715
+ Produce a JSON response with these fields:
716
+ - "contextSummary": A concise summary of the conversation state (max 500 chars)
717
+ - "activeTopics": Array of topic strings currently being discussed
718
+ - "keyConcepts": Array of key facts/concepts established
719
+ - "decisions": Array of decisions made (if any)
720
+ - "openQuestions": Array of unresolved questions
721
+ - "recurringThemes": Array of recurring themes
722
+
723
+ Respond ONLY with valid JSON, no markdown.`);
724
+ const parsed = this.parseJsonResponse(response);
725
+ if (parsed) {
726
+ cu.workingMemory.contextWindow = parsed.contextSummary || previousContext;
727
+ cu.workingMemory.activeTopics = parsed.activeTopics || cu.workingMemory.activeTopics;
728
+ if (parsed.keyConcepts?.length) {
729
+ for (const concept of parsed.keyConcepts) {
730
+ if (!cu.longTermMemory.keyConcepts.includes(concept)) {
731
+ cu.longTermMemory.keyConcepts.push(concept);
732
+ }
733
+ }
734
+ // Keep bounded
735
+ cu.longTermMemory.keyConcepts = cu.longTermMemory.keyConcepts.slice(-50);
736
+ }
737
+ if (parsed.decisions?.length) {
738
+ cu.sharedState.decisions.push(...parsed.decisions);
739
+ cu.sharedState.decisions = cu.sharedState.decisions.slice(-20);
740
+ }
741
+ if (parsed.openQuestions?.length) {
742
+ cu.sharedState.openQuestions = parsed.openQuestions;
743
+ }
744
+ if (parsed.recurringThemes?.length) {
745
+ cu.longTermMemory.recurringThemes = parsed.recurringThemes;
746
+ }
747
+ }
748
+ }
749
+ catch {
750
+ // Ollama unavailable — use simple text compaction
751
+ cu.workingMemory.contextWindow = this.simpleCompact(previousContext, transcript);
752
+ }
753
+ // Clear the buffer after compaction
754
+ this.chatBuffers.set(spaceId, []);
755
+ cu.lastUpdate = Date.now();
756
+ cu.coherence = Math.min(1.0, cu.coherence + 0.05);
757
+ await this.saveCollectiveUnconscious(cu);
758
+ this.emit('context:compacted', { spaceId, cu });
759
+ }
760
+ /**
761
+ * Serialize the collaborative context for sharing with peers.
762
+ * Used by knowledge.context_sync SWP messages.
763
+ */
764
+ serializeContext(spaceId) {
765
+ const cu = this.collectiveUnconscious.get(spaceId);
766
+ if (!cu)
767
+ return null;
768
+ const payload = {
769
+ spaceId,
770
+ contextWindow: cu.workingMemory.contextWindow,
771
+ activeTopics: cu.workingMemory.activeTopics,
772
+ keyConcepts: cu.longTermMemory.keyConcepts,
773
+ recurringThemes: cu.longTermMemory.recurringThemes,
774
+ decisions: cu.sharedState.decisions,
775
+ openQuestions: cu.sharedState.openQuestions,
776
+ lastUpdate: cu.lastUpdate,
777
+ };
778
+ return new TextEncoder().encode(JSON.stringify(payload));
779
+ }
780
+ /**
781
+ * Merge a remote context sync into the local CollectiveUnconscious.
782
+ * Takes the union of topics, concepts, decisions, etc.
783
+ */
784
+ async mergeRemoteContext(data) {
785
+ try {
786
+ const remote = JSON.parse(new TextDecoder().decode(data));
787
+ const cu = await this.getOrCreateCU(remote.spaceId);
788
+ // Merge context window: keep longer/newer
789
+ if (remote.lastUpdate > cu.lastUpdate && remote.contextWindow) {
790
+ cu.workingMemory.contextWindow = remote.contextWindow;
791
+ }
792
+ // Union active topics
793
+ if (remote.activeTopics?.length) {
794
+ const topics = new Set([...cu.workingMemory.activeTopics, ...remote.activeTopics]);
795
+ cu.workingMemory.activeTopics = Array.from(topics).slice(-20);
796
+ }
797
+ // Union key concepts
798
+ if (remote.keyConcepts?.length) {
799
+ const concepts = new Set([...cu.longTermMemory.keyConcepts, ...remote.keyConcepts]);
800
+ cu.longTermMemory.keyConcepts = Array.from(concepts).slice(-50);
801
+ }
802
+ // Union recurring themes
803
+ if (remote.recurringThemes?.length) {
804
+ const themes = new Set([...cu.longTermMemory.recurringThemes, ...remote.recurringThemes]);
805
+ cu.longTermMemory.recurringThemes = Array.from(themes);
806
+ }
807
+ // Union decisions
808
+ if (remote.decisions?.length) {
809
+ const decisions = new Set([...cu.sharedState.decisions, ...remote.decisions]);
810
+ cu.sharedState.decisions = Array.from(decisions).slice(-20);
811
+ }
812
+ // Merge open questions
813
+ if (remote.openQuestions?.length) {
814
+ const questions = new Set([...cu.sharedState.openQuestions, ...remote.openQuestions]);
815
+ cu.sharedState.openQuestions = Array.from(questions);
816
+ }
817
+ cu.lastUpdate = Math.max(cu.lastUpdate, remote.lastUpdate);
818
+ await this.saveCollectiveUnconscious(cu);
819
+ this.emit('context:synced', { spaceId: remote.spaceId });
820
+ }
821
+ catch (err) {
822
+ this.emit('context:sync-error', { error: err });
823
+ }
824
+ }
825
+ /**
826
+ * Get the chat message buffer for a space (for inspection/testing).
827
+ */
828
+ getChatBuffer(spaceId) {
829
+ return this.chatBuffers.get(spaceId) || [];
830
+ }
831
+ // ─── Knowledge Gossip Sync ───────────────────────────────────
832
+ /**
833
+ * Get top knowledge cards for gossip broadcast to peers.
834
+ * Returns cards sorted by confidence * usage, most valuable first.
835
+ */
836
+ getGossipPayload(spaceId, limit = 10) {
837
+ return this.queryCards({
838
+ spaceId,
839
+ sortBy: 'relevance',
840
+ limit,
841
+ });
842
+ }
843
+ /**
844
+ * Apply knowledge decay to all cards.
845
+ * Cards not reinforced lose confidence over time.
846
+ * Should be called periodically (e.g., daily).
847
+ *
848
+ * @param decayRate - fraction of confidence lost per call (default 0.05 = 5%)
849
+ */
850
+ applyKnowledgeDecay(decayRate = 0.05) {
851
+ let decayed = 0;
852
+ const now = Date.now();
853
+ const dayMs = 24 * 60 * 60 * 1000;
854
+ for (const card of this.cards.values()) {
855
+ if (card.crdt.tombstone)
856
+ continue;
857
+ const daysSinceAccess = (now - card.usage.lastAccessed) / dayMs;
858
+ if (daysSinceAccess < 1)
859
+ continue; // Skip recently accessed
860
+ const oldConfidence = card.confidence;
861
+ card.confidence = Math.max(0.1, card.confidence * (1 - decayRate));
862
+ if (card.confidence !== oldConfidence) {
863
+ card.updatedAt = now;
864
+ this.saveCard(card);
865
+ decayed++;
866
+ }
867
+ }
868
+ this.emit('knowledge:decay', { decayed, decayRate });
869
+ return decayed;
870
+ }
871
+ /**
872
+ * Boost confidence when multiple agents confirm the same fact.
873
+ * If 2+ agents have verified a card, boost by confirmationBoost.
874
+ *
875
+ * @param cardId - card to boost
876
+ * @param verifierDid - DID of the confirming agent
877
+ * @param boostAmount - confidence boost (default 0.2 = 20%)
878
+ */
879
+ confirmKnowledge(cardId, verifierDid, boostAmount = 0.2) {
880
+ const card = this.cards.get(cardId);
881
+ if (!card || card.crdt.tombstone)
882
+ return null;
883
+ // Add verification if not already present
884
+ const alreadyVerified = card.verifications.some(v => v.verifier === verifierDid);
885
+ if (!alreadyVerified) {
886
+ card.verifications.push({
887
+ verifier: verifierDid,
888
+ timestamp: Date.now(),
889
+ method: 'consensus',
890
+ confidence: card.confidence + boostAmount,
891
+ });
892
+ }
893
+ // Boost confidence based on number of unique verifiers
894
+ const uniqueVerifiers = new Set(card.verifications.map(v => v.verifier));
895
+ if (uniqueVerifiers.size >= 2) {
896
+ card.confidence = Math.min(1.0, card.confidence + boostAmount);
897
+ card.verificationStatus = 'verified';
898
+ }
899
+ card.updatedAt = Date.now();
900
+ card.crdt.hlc = tickHLC(card.crdt.hlc);
901
+ card.crdt.vectorClock[this.identity.did] =
902
+ (card.crdt.vectorClock[this.identity.did] || 0) + 1;
903
+ this.saveCard(card);
904
+ this.emit('knowledge:confirmed', { cardId, verifier: verifierDid, confidence: card.confidence });
905
+ return card;
906
+ }
907
+ /**
908
+ * Distill lessons learned from a completed CoC chain.
909
+ * Creates knowledge cards from the chain's experience.
910
+ *
911
+ * @param chainId - ID of the completed chain
912
+ * @param summary - chain summary/final report
913
+ * @param goal - original chain goal
914
+ * @param spaceId - space to store knowledge in
915
+ * @param participants - DIDs of participating agents
916
+ */
917
+ async distillChainExperience(chainId, summary, goal, spaceId, participants) {
918
+ const space = this.spaces.get(spaceId);
919
+ if (!space)
920
+ return [];
921
+ const cards = [];
922
+ try {
923
+ const response = await this.callOllama(`You are a knowledge extractor for a multi-agent collaboration system.
924
+
925
+ A collaborative chain (goal: "${goal}") has completed. Extract the key lessons learned.
926
+
927
+ CHAIN SUMMARY:
928
+ ${summary}
929
+
930
+ PARTICIPANTS: ${participants.length} agents
931
+
932
+ Produce a JSON array of knowledge items to store. Each item should have:
933
+ - "type": one of "insight", "decision", "sop", "finding"
934
+ - "title": concise title (max 80 chars)
935
+ - "content": detailed description
936
+ - "tags": array of relevant tags
937
+ - "confidence": 0-1 confidence score
938
+
939
+ Respond ONLY with a valid JSON array.`);
940
+ const parsed = this.parseJsonResponse(response);
941
+ const items = Array.isArray(parsed) ? parsed : (parsed?.items || []);
942
+ for (const item of items.slice(0, 5)) {
943
+ if (!item.title || !item.content)
944
+ continue;
945
+ const card = await this.createCard(spaceId, item.type || 'insight', item.title, item.content, {
946
+ tags: item.tags || [],
947
+ source: { type: 'coc', id: chainId, context: goal },
948
+ confidence: item.confidence || 0.7,
949
+ });
950
+ cards.push(card);
951
+ }
952
+ }
953
+ catch {
954
+ // Ollama unavailable — create a single summary card
955
+ const card = await this.createCard(spaceId, 'finding', `Chain ${chainId.slice(0, 8)}: ${goal}`, summary, {
956
+ tags: ['chain-distill', 'auto-extracted'],
957
+ source: { type: 'coc', id: chainId, context: goal },
958
+ confidence: 0.6,
959
+ });
960
+ cards.push(card);
961
+ }
962
+ this.emit('knowledge:distilled', { chainId, cardCount: cards.length });
963
+ return cards;
964
+ }
965
+ /**
966
+ * Extract knowledge from a batch of chat messages.
967
+ * Used for periodic knowledge extraction from conversations.
968
+ */
969
+ async extractFromConversation(spaceId, messages) {
970
+ const space = this.spaces.get(spaceId);
971
+ if (!space || messages.length === 0)
972
+ return [];
973
+ const transcript = messages.map(m => {
974
+ const name = m.senderName || m.sender.slice(0, 16);
975
+ return `${name}: ${m.content}`;
976
+ }).join('\n');
977
+ const cards = [];
978
+ try {
979
+ const response = await this.callOllama(`You are a knowledge extractor. Extract key facts and insights from this conversation.
980
+
981
+ CONVERSATION:
982
+ ${transcript}
983
+
984
+ Extract structured knowledge items as a JSON array. Each item:
985
+ - "type": "fact" | "insight" | "decision" | "hypothesis"
986
+ - "title": concise title
987
+ - "content": the knowledge content
988
+ - "tags": relevant tags
989
+ - "confidence": 0-1
990
+
991
+ Only extract genuinely useful knowledge. Skip small talk or trivial messages.
992
+ Respond ONLY with a valid JSON array. Return empty array [] if nothing useful.`);
993
+ const parsed = this.parseJsonResponse(response);
994
+ const items = Array.isArray(parsed) ? parsed : [];
995
+ for (const item of items.slice(0, 5)) {
996
+ if (!item.title || !item.content)
997
+ continue;
998
+ const card = await this.createCard(spaceId, item.type || 'fact', item.title, item.content, {
999
+ tags: [...(item.tags || []), 'auto-extracted', 'chat'],
1000
+ source: { type: 'chat', context: spaceId },
1001
+ confidence: item.confidence || 0.6,
1002
+ });
1003
+ cards.push(card);
1004
+ }
1005
+ }
1006
+ catch {
1007
+ // Ollama unavailable — skip extraction
1008
+ }
1009
+ this.emit('knowledge:extracted', { spaceId, cardCount: cards.length });
1010
+ return cards;
1011
+ }
388
1012
  // ─── Private Helpers ─────────────────────────────────────────
1013
+ /**
1014
+ * Simple text-based compaction when Ollama is unavailable.
1015
+ * Keeps last few lines of previous context + summary of new transcript.
1016
+ */
1017
+ simpleCompact(previousContext, newTranscript) {
1018
+ const prevLines = previousContext ? previousContext.split('\n').slice(-5).join('\n') : '';
1019
+ const newLines = newTranscript.split('\n');
1020
+ const summary = newLines.length > 10
1021
+ ? `[${newLines.length} messages exchanged covering: ${newLines.slice(0, 3).join('; ')}...]`
1022
+ : newLines.join('\n');
1023
+ return [prevLines, summary].filter(Boolean).join('\n---\n').slice(-2000);
1024
+ }
1025
+ /**
1026
+ * Call Ollama for context summarization.
1027
+ */
1028
+ async callOllama(prompt) {
1029
+ const url = `${this.compactionConfig.ollamaUrl}/api/generate`;
1030
+ const res = await fetch(url, {
1031
+ method: 'POST',
1032
+ headers: { 'Content-Type': 'application/json' },
1033
+ body: JSON.stringify({
1034
+ model: this.compactionConfig.ollamaModel,
1035
+ prompt,
1036
+ stream: false,
1037
+ options: { temperature: 0.3, num_predict: 1024 },
1038
+ }),
1039
+ });
1040
+ if (!res.ok)
1041
+ throw new Error(`Ollama error: ${res.status}`);
1042
+ const json = await res.json();
1043
+ return json.response;
1044
+ }
1045
+ /**
1046
+ * Parse JSON from LLM response (handles markdown code blocks).
1047
+ */
1048
+ parseJsonResponse(text) {
1049
+ // Strip thinking tags if present
1050
+ let cleaned = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
1051
+ // Strip markdown code blocks
1052
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
1053
+ try {
1054
+ return JSON.parse(cleaned);
1055
+ }
1056
+ catch {
1057
+ // Try to extract JSON object
1058
+ const match = cleaned.match(/\{[\s\S]*\}/);
1059
+ if (match) {
1060
+ try {
1061
+ return JSON.parse(match[0]);
1062
+ }
1063
+ catch { /* ignore */ }
1064
+ }
1065
+ return null;
1066
+ }
1067
+ }
389
1068
  generateSummary(content, maxLength = 200) {
390
1069
  // Remover markdown
391
1070
  const plain = content