@kernel.chat/kbot 3.11.0 → 3.13.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.
@@ -0,0 +1,845 @@
1
+ // kbot Cognitive Module Interference Tracker
2
+ //
3
+ // When multiple cognitive modules fire simultaneously, their signals can
4
+ // interfere — constructively (amplifying each other) or destructively
5
+ // (contradicting each other). This module tracks those interference events,
6
+ // maps known tensions and synergies, and provides aggregate statistics
7
+ // for the daemon to surface in diagnostic reports.
8
+ //
9
+ // Why this matters:
10
+ // - free-energy wants to converge on a low-surprise model, but
11
+ // quality-diversity wants to explore novel solutions. Which wins?
12
+ // - prompt-evolution rewrites prompts for better scores, but
13
+ // memory-synthesis preserves historical context. Evolving into forgetting.
14
+ // - reflection critiques past failures (backward-looking), but
15
+ // tree-planner searches forward for the best next action.
16
+ //
17
+ // By tracking which module "wins" and whether the outcome was good,
18
+ // we can learn which interferences to lean into and which to dampen.
19
+ //
20
+ // Storage: ~/.kbot/memory/interference.json (max 1000 events)
21
+ // All heuristic — no LLM calls.
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
25
+ // ══════════════════════════════════════════════════════════════════════
26
+ // Constants & Paths
27
+ // ══════════════════════════════════════════════════════════════════════
28
+ const MEMORY_DIR = join(homedir(), '.kbot', 'memory');
29
+ const INTERFERENCE_FILE = join(MEMORY_DIR, 'interference.json');
30
+ const MAX_EVENTS = 1000;
31
+ /** All cognitive module identifiers for validation */
32
+ export const ALL_MODULES = [
33
+ 'free-energy',
34
+ 'predictive-processing',
35
+ 'strange-loops',
36
+ 'integrated-information',
37
+ 'autopoiesis',
38
+ 'quality-diversity',
39
+ 'skill-rating',
40
+ 'tree-planner',
41
+ 'prompt-evolution',
42
+ 'memory-synthesis',
43
+ 'reflection',
44
+ ];
45
+ /** Threshold below which a signal is considered "near zero" */
46
+ const NEAR_ZERO_THRESHOLD = 0.05;
47
+ // ══════════════════════════════════════════════════════════════════════
48
+ // Known Tensions — module pairs that structurally conflict
49
+ // ══════════════════════════════════════════════════════════════════════
50
+ export const KNOWN_TENSIONS = [
51
+ {
52
+ moduleA: 'free-energy',
53
+ moduleB: 'quality-diversity',
54
+ description: 'convergence vs exploration',
55
+ },
56
+ {
57
+ moduleA: 'prompt-evolution',
58
+ moduleB: 'memory-synthesis',
59
+ description: 'evolving into forgetting',
60
+ },
61
+ {
62
+ moduleA: 'reflection',
63
+ moduleB: 'tree-planner',
64
+ description: 'backward critique vs forward search',
65
+ },
66
+ {
67
+ moduleA: 'skill-rating',
68
+ moduleB: 'free-energy',
69
+ description: 'infrastructure becoming intelligence',
70
+ },
71
+ {
72
+ moduleA: 'skill-rating',
73
+ moduleB: 'predictive-processing',
74
+ description: 'infrastructure becoming intelligence',
75
+ },
76
+ {
77
+ moduleA: 'skill-rating',
78
+ moduleB: 'strange-loops',
79
+ description: 'infrastructure becoming intelligence',
80
+ },
81
+ {
82
+ moduleA: 'skill-rating',
83
+ moduleB: 'integrated-information',
84
+ description: 'infrastructure becoming intelligence',
85
+ },
86
+ {
87
+ moduleA: 'skill-rating',
88
+ moduleB: 'autopoiesis',
89
+ description: 'infrastructure becoming intelligence',
90
+ },
91
+ {
92
+ moduleA: 'skill-rating',
93
+ moduleB: 'quality-diversity',
94
+ description: 'infrastructure becoming intelligence',
95
+ },
96
+ {
97
+ moduleA: 'skill-rating',
98
+ moduleB: 'tree-planner',
99
+ description: 'infrastructure becoming intelligence',
100
+ },
101
+ {
102
+ moduleA: 'skill-rating',
103
+ moduleB: 'prompt-evolution',
104
+ description: 'infrastructure becoming intelligence',
105
+ },
106
+ {
107
+ moduleA: 'skill-rating',
108
+ moduleB: 'memory-synthesis',
109
+ description: 'infrastructure becoming intelligence',
110
+ },
111
+ {
112
+ moduleA: 'skill-rating',
113
+ moduleB: 'reflection',
114
+ description: 'infrastructure becoming intelligence',
115
+ },
116
+ ];
117
+ // ══════════════════════════════════════════════════════════════════════
118
+ // Known Synergies — module pairs that amplify each other
119
+ // ══════════════════════════════════════════════════════════════════════
120
+ export const KNOWN_SYNERGIES = [
121
+ {
122
+ moduleA: 'memory-synthesis',
123
+ moduleB: 'reflection',
124
+ description: 'reflections feed synthesis',
125
+ },
126
+ {
127
+ moduleA: 'predictive-processing',
128
+ moduleB: 'skill-rating',
129
+ description: 'anticipation improves routing',
130
+ },
131
+ {
132
+ moduleA: 'strange-loops',
133
+ moduleB: 'autopoiesis',
134
+ description: 'self-reference enables self-maintenance',
135
+ },
136
+ ];
137
+ // ══════════════════════════════════════════════════════════════════════
138
+ // Storage — load / save / ensure
139
+ // ══════════════════════════════════════════════════════════════════════
140
+ function ensureDir() {
141
+ if (!existsSync(MEMORY_DIR))
142
+ mkdirSync(MEMORY_DIR, { recursive: true });
143
+ }
144
+ function loadState() {
145
+ ensureDir();
146
+ if (!existsSync(INTERFERENCE_FILE)) {
147
+ return { events: [], lastUpdated: new Date().toISOString() };
148
+ }
149
+ try {
150
+ const raw = readFileSync(INTERFERENCE_FILE, 'utf-8');
151
+ const parsed = JSON.parse(raw);
152
+ // Validate shape
153
+ if (!Array.isArray(parsed.events)) {
154
+ return { events: [], lastUpdated: new Date().toISOString() };
155
+ }
156
+ return parsed;
157
+ }
158
+ catch {
159
+ return { events: [], lastUpdated: new Date().toISOString() };
160
+ }
161
+ }
162
+ function saveState(state) {
163
+ ensureDir();
164
+ // Trim to max events — keep the most recent
165
+ if (state.events.length > MAX_EVENTS) {
166
+ state.events = state.events.slice(-MAX_EVENTS);
167
+ }
168
+ state.lastUpdated = new Date().toISOString();
169
+ writeFileSync(INTERFERENCE_FILE, JSON.stringify(state, null, 2));
170
+ }
171
+ // ══════════════════════════════════════════════════════════════════════
172
+ // Classification Helper
173
+ // ══════════════════════════════════════════════════════════════════════
174
+ /**
175
+ * Classify how two module signals interfere based on their output values.
176
+ *
177
+ * - Same sign (both positive or both negative) = CONSTRUCTIVE
178
+ * (both modules agree on direction)
179
+ * - Opposite signs = DESTRUCTIVE
180
+ * (modules disagree — one says go, the other says stop)
181
+ * - Either signal near zero = NEUTRAL
182
+ * (one module is inactive or indifferent)
183
+ *
184
+ * @param signalA - Numeric output from module A (positive = activate, negative = inhibit)
185
+ * @param signalB - Numeric output from module B
186
+ * @returns The interference type
187
+ */
188
+ export function classifyInterference(signalA, signalB) {
189
+ const absA = Math.abs(signalA);
190
+ const absB = Math.abs(signalB);
191
+ // If either signal is near zero, the module is essentially silent
192
+ if (absA < NEAR_ZERO_THRESHOLD || absB < NEAR_ZERO_THRESHOLD) {
193
+ return 'NEUTRAL';
194
+ }
195
+ // Same sign = both modules agree on direction
196
+ if ((signalA > 0 && signalB > 0) || (signalA < 0 && signalB < 0)) {
197
+ return 'CONSTRUCTIVE';
198
+ }
199
+ // Opposite signs = disagreement
200
+ return 'DESTRUCTIVE';
201
+ }
202
+ // ══════════════════════════════════════════════════════════════════════
203
+ // Event Recording
204
+ // ══════════════════════════════════════════════════════════════════════
205
+ /**
206
+ * Generate a unique event ID.
207
+ */
208
+ function generateEventId() {
209
+ return `intf_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
210
+ }
211
+ /**
212
+ * Validate that a string is a valid CognitiveModule.
213
+ */
214
+ export function isValidModule(id) {
215
+ return ALL_MODULES.includes(id);
216
+ }
217
+ /**
218
+ * Record an interference event between two cognitive modules.
219
+ *
220
+ * @param moduleA - First module in the interference
221
+ * @param moduleB - Second module (order doesn't matter for stats)
222
+ * @param type - CONSTRUCTIVE, DESTRUCTIVE, or NEUTRAL
223
+ * @param context - Free-form description of what triggered this
224
+ * @param resolution - Which module was ultimately followed
225
+ * @param outcome - Whether following that module led to success
226
+ * @returns The recorded event
227
+ */
228
+ export function recordInterference(moduleA, moduleB, type, context, resolution, outcome = 'pending') {
229
+ const event = {
230
+ id: generateEventId(),
231
+ timestamp: new Date().toISOString(),
232
+ moduleA,
233
+ moduleB,
234
+ type,
235
+ context: context.slice(0, 500), // Cap context length
236
+ resolution,
237
+ outcome,
238
+ };
239
+ const state = loadState();
240
+ state.events.push(event);
241
+ saveState(state);
242
+ return event;
243
+ }
244
+ /**
245
+ * Record an interference by classifying signals automatically.
246
+ * Convenience wrapper around classifyInterference + recordInterference.
247
+ */
248
+ export function recordSignalInterference(moduleA, signalA, moduleB, signalB, context, resolution, outcome = 'pending') {
249
+ const type = classifyInterference(signalA, signalB);
250
+ return recordInterference(moduleA, moduleB, type, context, resolution, outcome);
251
+ }
252
+ /**
253
+ * Update the outcome of a pending interference event.
254
+ * Called after the resolution's result is known.
255
+ *
256
+ * @param eventId - The event to update
257
+ * @param outcome - The actual outcome
258
+ * @returns true if the event was found and updated, false otherwise
259
+ */
260
+ export function resolveInterference(eventId, outcome) {
261
+ const state = loadState();
262
+ const event = state.events.find(e => e.id === eventId);
263
+ if (!event)
264
+ return false;
265
+ event.outcome = outcome;
266
+ saveState(state);
267
+ return true;
268
+ }
269
+ // ══════════════════════════════════════════════════════════════════════
270
+ // Known Interaction Lookups
271
+ // ══════════════════════════════════════════════════════════════════════
272
+ /**
273
+ * Normalize a module pair to a canonical order for consistent lookups.
274
+ * Alphabetical by module name.
275
+ */
276
+ function normalizePair(a, b) {
277
+ return a <= b ? [a, b] : [b, a];
278
+ }
279
+ /**
280
+ * Check if two modules have a known tension.
281
+ * Returns the tension description or null.
282
+ */
283
+ export function getKnownTension(moduleA, moduleB) {
284
+ for (const tension of KNOWN_TENSIONS) {
285
+ if ((tension.moduleA === moduleA && tension.moduleB === moduleB) ||
286
+ (tension.moduleA === moduleB && tension.moduleB === moduleA)) {
287
+ return tension.description;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ /**
293
+ * Check if two modules have a known synergy.
294
+ * Returns the synergy description or null.
295
+ */
296
+ export function getKnownSynergy(moduleA, moduleB) {
297
+ for (const synergy of KNOWN_SYNERGIES) {
298
+ if ((synergy.moduleA === moduleA && synergy.moduleB === moduleB) ||
299
+ (synergy.moduleA === moduleB && synergy.moduleB === moduleA)) {
300
+ return synergy.description;
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+ /**
306
+ * Get all known tensions for a specific module.
307
+ */
308
+ export function getTensionsFor(module) {
309
+ return KNOWN_TENSIONS.filter(t => t.moduleA === module || t.moduleB === module);
310
+ }
311
+ /**
312
+ * Get all known synergies for a specific module.
313
+ */
314
+ export function getSynergiesFor(module) {
315
+ return KNOWN_SYNERGIES.filter(s => s.moduleA === module || s.moduleB === module);
316
+ }
317
+ // ══════════════════════════════════════════════════════════════════════
318
+ // Aggregate Statistics
319
+ // ══════════════════════════════════════════════════════════════════════
320
+ /**
321
+ * Get the conflict rate (destructive / total) for a specific module pair.
322
+ * Returns 0 if no events exist for the pair.
323
+ */
324
+ export function getConflictRate(moduleA, moduleB) {
325
+ const state = loadState();
326
+ const pairEvents = state.events.filter(e => (e.moduleA === moduleA && e.moduleB === moduleB) ||
327
+ (e.moduleA === moduleB && e.moduleB === moduleA));
328
+ if (pairEvents.length === 0)
329
+ return 0;
330
+ const destructive = pairEvents.filter(e => e.type === 'DESTRUCTIVE').length;
331
+ return destructive / pairEvents.length;
332
+ }
333
+ /**
334
+ * Compute detailed statistics for a specific module pair.
335
+ */
336
+ function computePairStats(moduleA, moduleB, events) {
337
+ const pairEvents = events.filter(e => (e.moduleA === moduleA && e.moduleB === moduleB) ||
338
+ (e.moduleA === moduleB && e.moduleB === moduleA));
339
+ const total = pairEvents.length;
340
+ const constructive = pairEvents.filter(e => e.type === 'CONSTRUCTIVE').length;
341
+ const destructive = pairEvents.filter(e => e.type === 'DESTRUCTIVE').length;
342
+ const neutral = pairEvents.filter(e => e.type === 'NEUTRAL').length;
343
+ // Resolution stats
344
+ const aWinEvents = pairEvents.filter(e => e.resolution === moduleA);
345
+ const bWinEvents = pairEvents.filter(e => e.resolution === moduleB);
346
+ const aWins = aWinEvents.length;
347
+ const bWins = bWinEvents.length;
348
+ // Success rates (only count resolved events, not pending)
349
+ const aResolved = aWinEvents.filter(e => e.outcome !== 'pending');
350
+ const bResolved = bWinEvents.filter(e => e.outcome !== 'pending');
351
+ const aSuccesses = aResolved.filter(e => e.outcome === 'success').length;
352
+ const bSuccesses = bResolved.filter(e => e.outcome === 'success').length;
353
+ const aWinSuccessRate = aResolved.length > 0 ? aSuccesses / aResolved.length : 0;
354
+ const bWinSuccessRate = bResolved.length > 0 ? bSuccesses / bResolved.length : 0;
355
+ const conflictRate = total > 0 ? destructive / total : 0;
356
+ return {
357
+ moduleA,
358
+ moduleB,
359
+ total,
360
+ constructive,
361
+ destructive,
362
+ neutral,
363
+ aWins,
364
+ bWins,
365
+ aWinSuccessRate,
366
+ bWinSuccessRate,
367
+ conflictRate,
368
+ };
369
+ }
370
+ /**
371
+ * Get interference statistics for all module pairs that have events.
372
+ * Returns an array of PairStats, sorted by total events descending.
373
+ */
374
+ export function getInterferenceStats() {
375
+ const state = loadState();
376
+ if (state.events.length === 0)
377
+ return [];
378
+ // Discover all unique pairs that appear in events
379
+ const pairSet = new Set();
380
+ for (const event of state.events) {
381
+ const [a, b] = normalizePair(event.moduleA, event.moduleB);
382
+ pairSet.add(`${a}|${b}`);
383
+ }
384
+ const stats = [];
385
+ for (const pairKey of pairSet) {
386
+ const [a, b] = pairKey.split('|');
387
+ stats.push(computePairStats(a, b, state.events));
388
+ }
389
+ // Sort by total events descending
390
+ stats.sort((x, y) => y.total - x.total);
391
+ return stats;
392
+ }
393
+ /**
394
+ * Get statistics for a single module — how often it is involved
395
+ * in interference, and its win/success rates.
396
+ */
397
+ export function getModuleStats(module) {
398
+ const state = loadState();
399
+ const involved = state.events.filter(e => e.moduleA === module || e.moduleB === module);
400
+ if (involved.length === 0) {
401
+ return {
402
+ totalEvents: 0,
403
+ asModuleA: 0,
404
+ asModuleB: 0,
405
+ timesWon: 0,
406
+ winSuccessRate: 0,
407
+ topPartner: null,
408
+ dominantType: 'NEUTRAL',
409
+ };
410
+ }
411
+ const asModuleA = involved.filter(e => e.moduleA === module).length;
412
+ const asModuleB = involved.filter(e => e.moduleB === module).length;
413
+ const wonEvents = involved.filter(e => e.resolution === module);
414
+ const timesWon = wonEvents.length;
415
+ const resolvedWins = wonEvents.filter(e => e.outcome !== 'pending');
416
+ const winSuccesses = resolvedWins.filter(e => e.outcome === 'success').length;
417
+ const winSuccessRate = resolvedWins.length > 0 ? winSuccesses / resolvedWins.length : 0;
418
+ // Find top partner (the module it interferes with most)
419
+ const partnerCounts = new Map();
420
+ for (const e of involved) {
421
+ const partner = e.moduleA === module ? e.moduleB : e.moduleA;
422
+ partnerCounts.set(partner, (partnerCounts.get(partner) || 0) + 1);
423
+ }
424
+ let topPartner = null;
425
+ let maxCount = 0;
426
+ for (const [partner, count] of partnerCounts) {
427
+ if (count > maxCount) {
428
+ topPartner = partner;
429
+ maxCount = count;
430
+ }
431
+ }
432
+ // Dominant type
433
+ const constructive = involved.filter(e => e.type === 'CONSTRUCTIVE').length;
434
+ const destructive = involved.filter(e => e.type === 'DESTRUCTIVE').length;
435
+ const neutral = involved.filter(e => e.type === 'NEUTRAL').length;
436
+ let dominantType = 'NEUTRAL';
437
+ if (constructive >= destructive && constructive >= neutral)
438
+ dominantType = 'CONSTRUCTIVE';
439
+ else if (destructive >= constructive && destructive >= neutral)
440
+ dominantType = 'DESTRUCTIVE';
441
+ return {
442
+ totalEvents: involved.length,
443
+ asModuleA,
444
+ asModuleB,
445
+ timesWon,
446
+ winSuccessRate,
447
+ topPartner,
448
+ dominantType,
449
+ };
450
+ }
451
+ // ══════════════════════════════════════════════════════════════════════
452
+ // Reporting
453
+ // ══════════════════════════════════════════════════════════════════════
454
+ /**
455
+ * Generate a formatted interference report string for the daemon.
456
+ * Includes:
457
+ * - Total event count and time range
458
+ * - Top conflicting pairs
459
+ * - Top synergistic pairs
460
+ * - Known tensions with observed conflict rates
461
+ * - Recommendations based on resolution success rates
462
+ */
463
+ export function getInterferenceReport() {
464
+ const state = loadState();
465
+ const events = state.events;
466
+ const lines = [];
467
+ lines.push('═══ Cognitive Module Interference Report ═══');
468
+ lines.push('');
469
+ if (events.length === 0) {
470
+ lines.push('No interference events recorded yet.');
471
+ lines.push('Events are recorded when multiple cognitive modules fire simultaneously');
472
+ lines.push('with conflicting or reinforcing signals.');
473
+ return lines.join('\n');
474
+ }
475
+ // ── Summary ──
476
+ const oldest = events[0].timestamp.split('T')[0];
477
+ const newest = events[events.length - 1].timestamp.split('T')[0];
478
+ const constructiveCount = events.filter(e => e.type === 'CONSTRUCTIVE').length;
479
+ const destructiveCount = events.filter(e => e.type === 'DESTRUCTIVE').length;
480
+ const neutralCount = events.filter(e => e.type === 'NEUTRAL').length;
481
+ const resolvedCount = events.filter(e => e.outcome !== 'pending').length;
482
+ const successCount = events.filter(e => e.outcome === 'success').length;
483
+ const failureCount = events.filter(e => e.outcome === 'failure').length;
484
+ lines.push(`Total events: ${events.length} (${oldest} to ${newest})`);
485
+ lines.push(` Constructive: ${constructiveCount} (${pct(constructiveCount, events.length)})`);
486
+ lines.push(` Destructive: ${destructiveCount} (${pct(destructiveCount, events.length)})`);
487
+ lines.push(` Neutral: ${neutralCount} (${pct(neutralCount, events.length)})`);
488
+ lines.push(` Resolved: ${resolvedCount} — ${successCount} success, ${failureCount} failure`);
489
+ lines.push('');
490
+ // ── Top Conflicting Pairs ──
491
+ const stats = getInterferenceStats();
492
+ const conflicting = stats
493
+ .filter(s => s.destructive > 0)
494
+ .sort((a, b) => b.conflictRate - a.conflictRate)
495
+ .slice(0, 5);
496
+ if (conflicting.length > 0) {
497
+ lines.push('── Top Conflicting Pairs ──');
498
+ for (const pair of conflicting) {
499
+ const tension = getKnownTension(pair.moduleA, pair.moduleB);
500
+ const tensionLabel = tension ? ` [${tension}]` : '';
501
+ lines.push(` ${pair.moduleA} vs ${pair.moduleB}: ` +
502
+ `${pct(pair.destructive, pair.total)} conflict rate ` +
503
+ `(${pair.destructive}/${pair.total})${tensionLabel}`);
504
+ }
505
+ lines.push('');
506
+ }
507
+ // ── Top Synergistic Pairs ──
508
+ const synergistic = stats
509
+ .filter(s => s.constructive > 0)
510
+ .sort((a, b) => (b.constructive / b.total) - (a.constructive / a.total))
511
+ .slice(0, 5);
512
+ if (synergistic.length > 0) {
513
+ lines.push('── Top Synergistic Pairs ──');
514
+ for (const pair of synergistic) {
515
+ const synergy = getKnownSynergy(pair.moduleA, pair.moduleB);
516
+ const synergyLabel = synergy ? ` [${synergy}]` : '';
517
+ const synergyRate = pair.constructive / pair.total;
518
+ lines.push(` ${pair.moduleA} + ${pair.moduleB}: ` +
519
+ `${pct(pair.constructive, pair.total)} synergy rate ` +
520
+ `(${pair.constructive}/${pair.total})${synergyLabel}`);
521
+ }
522
+ lines.push('');
523
+ }
524
+ // ── Known Tensions vs Observed ──
525
+ lines.push('── Known Tensions (Observed) ──');
526
+ for (const tension of KNOWN_TENSIONS) {
527
+ const rate = getConflictRate(tension.moduleA, tension.moduleB);
528
+ const pairEvts = events.filter(e => (e.moduleA === tension.moduleA && e.moduleB === tension.moduleB) ||
529
+ (e.moduleA === tension.moduleB && e.moduleB === tension.moduleA));
530
+ if (pairEvts.length > 0) {
531
+ lines.push(` ${tension.moduleA} vs ${tension.moduleB}: ` +
532
+ `"${tension.description}" — ${(rate * 100).toFixed(0)}% conflict (${pairEvts.length} events)`);
533
+ }
534
+ else {
535
+ lines.push(` ${tension.moduleA} vs ${tension.moduleB}: ` +
536
+ `"${tension.description}" — no events yet`);
537
+ }
538
+ }
539
+ lines.push('');
540
+ // ── Known Synergies vs Observed ──
541
+ lines.push('── Known Synergies (Observed) ──');
542
+ for (const synergy of KNOWN_SYNERGIES) {
543
+ const pairEvts = events.filter(e => (e.moduleA === synergy.moduleA && e.moduleB === synergy.moduleB) ||
544
+ (e.moduleA === synergy.moduleB && e.moduleB === synergy.moduleA));
545
+ if (pairEvts.length > 0) {
546
+ const constructive = pairEvts.filter(e => e.type === 'CONSTRUCTIVE').length;
547
+ lines.push(` ${synergy.moduleA} + ${synergy.moduleB}: ` +
548
+ `"${synergy.description}" — ${pct(constructive, pairEvts.length)} constructive (${pairEvts.length} events)`);
549
+ }
550
+ else {
551
+ lines.push(` ${synergy.moduleA} + ${synergy.moduleB}: ` +
552
+ `"${synergy.description}" — no events yet`);
553
+ }
554
+ }
555
+ lines.push('');
556
+ // ── Recommendations ──
557
+ const recommendations = generateRecommendations(stats, events);
558
+ if (recommendations.length > 0) {
559
+ lines.push('── Recommendations ──');
560
+ for (const rec of recommendations) {
561
+ lines.push(` • ${rec}`);
562
+ }
563
+ lines.push('');
564
+ }
565
+ lines.push(`Last updated: ${state.lastUpdated}`);
566
+ return lines.join('\n');
567
+ }
568
+ // ══════════════════════════════════════════════════════════════════════
569
+ // Recommendation Engine
570
+ // ══════════════════════════════════════════════════════════════════════
571
+ /**
572
+ * Generate actionable recommendations based on observed interference patterns.
573
+ */
574
+ function generateRecommendations(stats, events) {
575
+ const recs = [];
576
+ // Recommendation 1: High conflict pairs where one module consistently wins
577
+ // and that winning leads to failures — suggest flipping the resolution
578
+ for (const pair of stats) {
579
+ if (pair.total < 5 || pair.conflictRate < 0.5)
580
+ continue;
581
+ // Check if module A dominates but fails
582
+ if (pair.aWins > pair.bWins * 2 && pair.aWinSuccessRate < 0.4 && pair.aWins >= 3) {
583
+ recs.push(`${pair.moduleA} dominates ${pair.moduleB} but has ${(pair.aWinSuccessRate * 100).toFixed(0)}% success rate. ` +
584
+ `Consider deferring to ${pair.moduleB} more often.`);
585
+ }
586
+ // Check the reverse
587
+ if (pair.bWins > pair.aWins * 2 && pair.bWinSuccessRate < 0.4 && pair.bWins >= 3) {
588
+ recs.push(`${pair.moduleB} dominates ${pair.moduleA} but has ${(pair.bWinSuccessRate * 100).toFixed(0)}% success rate. ` +
589
+ `Consider deferring to ${pair.moduleA} more often.`);
590
+ }
591
+ }
592
+ // Recommendation 2: Known synergies that are actually conflicting
593
+ for (const synergy of KNOWN_SYNERGIES) {
594
+ const rate = getConflictRate(synergy.moduleA, synergy.moduleB);
595
+ const pairEvts = events.filter(e => (e.moduleA === synergy.moduleA && e.moduleB === synergy.moduleB) ||
596
+ (e.moduleA === synergy.moduleB && e.moduleB === synergy.moduleA));
597
+ if (pairEvts.length >= 3 && rate > 0.5) {
598
+ recs.push(`Expected synergy "${synergy.description}" between ${synergy.moduleA} and ${synergy.moduleB} ` +
599
+ `is actually conflicting (${(rate * 100).toFixed(0)}% conflict rate). Investigate module integration.`);
600
+ }
601
+ }
602
+ // Recommendation 3: Known tensions with unexpectedly low conflict
603
+ for (const tension of KNOWN_TENSIONS) {
604
+ const rate = getConflictRate(tension.moduleA, tension.moduleB);
605
+ const pairEvts = events.filter(e => (e.moduleA === tension.moduleA && e.moduleB === tension.moduleB) ||
606
+ (e.moduleA === tension.moduleB && e.moduleB === tension.moduleA));
607
+ if (pairEvts.length >= 5 && rate < 0.2) {
608
+ recs.push(`Expected tension "${tension.description}" between ${tension.moduleA} and ${tension.moduleB} ` +
609
+ `has only ${(rate * 100).toFixed(0)}% conflict rate. The modules may have naturally aligned.`);
610
+ }
611
+ }
612
+ // Recommendation 4: Modules with many pending outcomes — need feedback loop
613
+ const pendingCount = events.filter(e => e.outcome === 'pending').length;
614
+ if (pendingCount > events.length * 0.5 && events.length >= 10) {
615
+ recs.push(`${pendingCount}/${events.length} events (${pct(pendingCount, events.length)}) are still pending. ` +
616
+ `Wire up outcome resolution for better interference learning.`);
617
+ }
618
+ // Recommendation 5: Module that participates in many destructive events
619
+ const moduleCounts = new Map();
620
+ for (const event of events) {
621
+ for (const mod of [event.moduleA, event.moduleB]) {
622
+ const current = moduleCounts.get(mod) || { destructive: 0, total: 0 };
623
+ current.total++;
624
+ if (event.type === 'DESTRUCTIVE')
625
+ current.destructive++;
626
+ moduleCounts.set(mod, current);
627
+ }
628
+ }
629
+ for (const [mod, counts] of moduleCounts) {
630
+ if (counts.total >= 10 && counts.destructive / counts.total > 0.6) {
631
+ recs.push(`${mod} is involved in destructive interference ${(counts.destructive / counts.total * 100).toFixed(0)}% of the time ` +
632
+ `(${counts.destructive}/${counts.total}). Consider dampening its signal or improving its integration.`);
633
+ }
634
+ }
635
+ return recs;
636
+ }
637
+ // ══════════════════════════════════════════════════════════════════════
638
+ // Query Helpers
639
+ // ══════════════════════════════════════════════════════════════════════
640
+ /**
641
+ * Get all interference events, optionally filtered.
642
+ */
643
+ export function getEvents(filter) {
644
+ const state = loadState();
645
+ let events = state.events;
646
+ if (filter) {
647
+ if (filter.module) {
648
+ const mod = filter.module;
649
+ events = events.filter(e => e.moduleA === mod || e.moduleB === mod);
650
+ }
651
+ if (filter.type) {
652
+ const t = filter.type;
653
+ events = events.filter(e => e.type === t);
654
+ }
655
+ if (filter.outcome) {
656
+ const o = filter.outcome;
657
+ events = events.filter(e => e.outcome === o);
658
+ }
659
+ if (filter.since) {
660
+ const sinceTime = new Date(filter.since).getTime();
661
+ events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
662
+ }
663
+ if (filter.limit && filter.limit > 0) {
664
+ events = events.slice(-filter.limit);
665
+ }
666
+ }
667
+ return events;
668
+ }
669
+ /**
670
+ * Get the most recent interference events (for dashboard display).
671
+ */
672
+ export function getRecentEvents(count = 10) {
673
+ const state = loadState();
674
+ return state.events.slice(-count);
675
+ }
676
+ /**
677
+ * Count total recorded interference events.
678
+ */
679
+ export function getEventCount() {
680
+ const state = loadState();
681
+ return state.events.length;
682
+ }
683
+ /**
684
+ * Clear all interference events. Use with caution — this is destructive.
685
+ * Returns the number of events that were cleared.
686
+ */
687
+ export function clearEvents() {
688
+ const state = loadState();
689
+ const count = state.events.length;
690
+ state.events = [];
691
+ saveState(state);
692
+ return count;
693
+ }
694
+ // ══════════════════════════════════════════════════════════════════════
695
+ // Predictive Helpers
696
+ // ══════════════════════════════════════════════════════════════════════
697
+ /**
698
+ * Predict the likely interference type for a module pair based on
699
+ * historical data and known interactions.
700
+ *
701
+ * Returns a prediction with confidence, or null if insufficient data.
702
+ */
703
+ export function predictInterference(moduleA, moduleB) {
704
+ const state = loadState();
705
+ const pairEvents = state.events.filter(e => (e.moduleA === moduleA && e.moduleB === moduleB) ||
706
+ (e.moduleA === moduleB && e.moduleB === moduleA));
707
+ // Check known interactions first
708
+ const tension = getKnownTension(moduleA, moduleB);
709
+ const synergy = getKnownSynergy(moduleA, moduleB);
710
+ // If we have historical data, use it
711
+ if (pairEvents.length >= 3) {
712
+ const constructive = pairEvents.filter(e => e.type === 'CONSTRUCTIVE').length;
713
+ const destructive = pairEvents.filter(e => e.type === 'DESTRUCTIVE').length;
714
+ const neutral = pairEvents.filter(e => e.type === 'NEUTRAL').length;
715
+ const total = pairEvents.length;
716
+ let predicted;
717
+ let maxCount;
718
+ if (constructive >= destructive && constructive >= neutral) {
719
+ predicted = 'CONSTRUCTIVE';
720
+ maxCount = constructive;
721
+ }
722
+ else if (destructive >= constructive && destructive >= neutral) {
723
+ predicted = 'DESTRUCTIVE';
724
+ maxCount = destructive;
725
+ }
726
+ else {
727
+ predicted = 'NEUTRAL';
728
+ maxCount = neutral;
729
+ }
730
+ // Confidence is the proportion of the dominant type, scaled by sample size
731
+ const rawConfidence = maxCount / total;
732
+ // Small sample penalty: sqrt(n/20) capped at 1
733
+ const sampleFactor = Math.min(1, Math.sqrt(total / 20));
734
+ const confidence = rawConfidence * sampleFactor;
735
+ return {
736
+ predicted,
737
+ confidence: Math.round(confidence * 100) / 100,
738
+ basis: `historical (${total} events: ${constructive}C/${destructive}D/${neutral}N)`,
739
+ };
740
+ }
741
+ // Fall back to known interactions
742
+ if (tension) {
743
+ return {
744
+ predicted: 'DESTRUCTIVE',
745
+ confidence: 0.6,
746
+ basis: `known tension: "${tension}"`,
747
+ };
748
+ }
749
+ if (synergy) {
750
+ return {
751
+ predicted: 'CONSTRUCTIVE',
752
+ confidence: 0.6,
753
+ basis: `known synergy: "${synergy}"`,
754
+ };
755
+ }
756
+ // Not enough data
757
+ return null;
758
+ }
759
+ /**
760
+ * Suggest which module to follow when two modules conflict,
761
+ * based on historical win/success rates.
762
+ *
763
+ * Returns the recommended module and the basis for the recommendation,
764
+ * or null if insufficient data.
765
+ */
766
+ export function suggestResolution(moduleA, moduleB) {
767
+ const state = loadState();
768
+ const pairEvents = state.events.filter(e => (e.moduleA === moduleA && e.moduleB === moduleB) ||
769
+ (e.moduleA === moduleB && e.moduleB === moduleA));
770
+ // Need enough resolved events to make a recommendation
771
+ const resolved = pairEvents.filter(e => e.outcome !== 'pending');
772
+ if (resolved.length < 3)
773
+ return null;
774
+ const aWins = resolved.filter(e => e.resolution === moduleA);
775
+ const bWins = resolved.filter(e => e.resolution === moduleB);
776
+ const aSuccesses = aWins.filter(e => e.outcome === 'success').length;
777
+ const bSuccesses = bWins.filter(e => e.outcome === 'success').length;
778
+ const aRate = aWins.length > 0 ? aSuccesses / aWins.length : 0;
779
+ const bRate = bWins.length > 0 ? bSuccesses / bWins.length : 0;
780
+ // If one module has a clearly better success rate
781
+ if (Math.abs(aRate - bRate) > 0.15 && (aWins.length >= 2 || bWins.length >= 2)) {
782
+ if (aRate > bRate) {
783
+ return {
784
+ recommended: moduleA,
785
+ confidence: Math.round(aRate * 100) / 100,
786
+ reason: `${moduleA} has ${(aRate * 100).toFixed(0)}% success rate vs ${(bRate * 100).toFixed(0)}% for ${moduleB} (${resolved.length} resolved events)`,
787
+ };
788
+ }
789
+ else {
790
+ return {
791
+ recommended: moduleB,
792
+ confidence: Math.round(bRate * 100) / 100,
793
+ reason: `${moduleB} has ${(bRate * 100).toFixed(0)}% success rate vs ${(aRate * 100).toFixed(0)}% for ${moduleA} (${resolved.length} resolved events)`,
794
+ };
795
+ }
796
+ }
797
+ // Too close to call
798
+ return null;
799
+ }
800
+ // ══════════════════════════════════════════════════════════════════════
801
+ // Compact Summary (for system prompt injection)
802
+ // ══════════════════════════════════════════════════════════════════════
803
+ /**
804
+ * Generate a compact interference summary suitable for system prompt injection.
805
+ * Returns empty string if no meaningful patterns exist.
806
+ */
807
+ export function getInterferenceSummary() {
808
+ const state = loadState();
809
+ if (state.events.length < 5)
810
+ return '';
811
+ const stats = getInterferenceStats();
812
+ if (stats.length === 0)
813
+ return '';
814
+ const lines = [];
815
+ lines.push('[Cognitive Module Interference]');
816
+ // Only include pairs with significant data
817
+ const significant = stats.filter(s => s.total >= 3);
818
+ if (significant.length === 0)
819
+ return '';
820
+ for (const pair of significant.slice(0, 5)) {
821
+ const tension = getKnownTension(pair.moduleA, pair.moduleB);
822
+ const synergy = getKnownSynergy(pair.moduleA, pair.moduleB);
823
+ const label = tension ? `tension: ${tension}` : synergy ? `synergy: ${synergy}` : 'observed';
824
+ // Determine which module to favor
825
+ let favor = '';
826
+ if (pair.aWinSuccessRate > pair.bWinSuccessRate + 0.2) {
827
+ favor = ` → favor ${pair.moduleA}`;
828
+ }
829
+ else if (pair.bWinSuccessRate > pair.aWinSuccessRate + 0.2) {
830
+ favor = ` → favor ${pair.moduleB}`;
831
+ }
832
+ lines.push(`- ${pair.moduleA}/${pair.moduleB}: ${pair.conflictRate > 0.5 ? 'high conflict' : pair.conflictRate > 0.2 ? 'moderate' : 'low conflict'} (${label})${favor}`);
833
+ }
834
+ return lines.join('\n');
835
+ }
836
+ // ══════════════════════════════════════════════════════════════════════
837
+ // Helpers
838
+ // ══════════════════════════════════════════════════════════════════════
839
+ /** Format a fraction as a percentage string */
840
+ function pct(numerator, denominator) {
841
+ if (denominator === 0)
842
+ return '0%';
843
+ return `${((numerator / denominator) * 100).toFixed(0)}%`;
844
+ }
845
+ //# sourceMappingURL=interference.js.map