@claude-flow/cli 3.1.0-alpha.51 → 3.1.0-alpha.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * Memory Bridge — Routes CLI memory operations through ControllerRegistry + AgentDB v3
3
3
  *
4
- * Per ADR-053: Replaces raw sql.js operations in memory-initializer.ts with
5
- * ControllerRegistryHybridBackend → AgentDB v3 pipeline.
4
+ * Per ADR-053 Phases 1-6: Full controller activation pipeline.
5
+ * CLIControllerRegistry → AgentDB v3 controllers.
6
6
  *
7
- * All functions match memory-initializer.ts signatures exactly so consumers
8
- * need zero changes. The bridge is loaded lazily on first call and cached.
7
+ * Phase 1: Core CRUD + embeddings + HNSW + controller access (complete)
8
+ * Phase 2: BM25 hybrid search, TieredCache read/write, MutationGuard validation
9
+ * Phase 3: ReasoningBank pattern store, recordFeedback, CausalMemoryGraph edges
10
+ * Phase 4: SkillLibrary promotion, ExplainableRecall provenance, AttestationLog
11
+ * Phase 5: ReflexionMemory session lifecycle, WitnessChain attestation
12
+ * Phase 6: AgentDB MCP tools (separate file), COW branching
9
13
  *
10
14
  * Uses better-sqlite3 API (synchronous .all()/.get()/.run()) since that's
11
15
  * what AgentDB v3 uses internally.
@@ -74,6 +78,130 @@ async function getRegistry(dbPath) {
74
78
  }
75
79
  return registryPromise;
76
80
  }
81
+ // ===== Phase 2: BM25 hybrid scoring =====
82
+ /**
83
+ * BM25 scoring for keyword-based search.
84
+ * Replaces naive String.includes() with proper information retrieval scoring.
85
+ * Parameters tuned for short memory entries (k1=1.2, b=0.75).
86
+ */
87
+ function bm25Score(queryTerms, docContent, avgDocLength, docCount, termDocFreqs) {
88
+ const k1 = 1.2;
89
+ const b = 0.75;
90
+ const docWords = docContent.toLowerCase().split(/\s+/);
91
+ const docLength = docWords.length;
92
+ let score = 0;
93
+ for (const term of queryTerms) {
94
+ const tf = docWords.filter(w => w === term || w.includes(term)).length;
95
+ if (tf === 0)
96
+ continue;
97
+ const df = termDocFreqs.get(term) || 1;
98
+ const idf = Math.log((docCount - df + 0.5) / (df + 0.5) + 1);
99
+ const tfNorm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLength / Math.max(1, avgDocLength))));
100
+ score += idf * tfNorm;
101
+ }
102
+ return score;
103
+ }
104
+ /**
105
+ * Compute BM25 term document frequencies for a set of rows.
106
+ */
107
+ function computeTermDocFreqs(queryTerms, rows) {
108
+ const termDocFreqs = new Map();
109
+ let totalLength = 0;
110
+ for (const row of rows) {
111
+ const content = (row.content || '').toLowerCase();
112
+ const words = content.split(/\s+/);
113
+ totalLength += words.length;
114
+ for (const term of queryTerms) {
115
+ if (content.includes(term)) {
116
+ termDocFreqs.set(term, (termDocFreqs.get(term) || 0) + 1);
117
+ }
118
+ }
119
+ }
120
+ return { termDocFreqs, avgDocLength: rows.length > 0 ? totalLength / rows.length : 1 };
121
+ }
122
+ // ===== Phase 2: TieredCache helpers =====
123
+ /**
124
+ * Try to read from TieredCache before hitting DB.
125
+ * Returns cached value or null if cache miss.
126
+ */
127
+ async function cacheGet(registry, cacheKey) {
128
+ try {
129
+ const cache = registry.get('tieredCache');
130
+ if (!cache || typeof cache.get !== 'function')
131
+ return null;
132
+ return cache.get(cacheKey) ?? null;
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ /**
139
+ * Write to TieredCache after DB write.
140
+ */
141
+ async function cacheSet(registry, cacheKey, value) {
142
+ try {
143
+ const cache = registry.get('tieredCache');
144
+ if (cache && typeof cache.set === 'function') {
145
+ cache.set(cacheKey, value);
146
+ }
147
+ }
148
+ catch {
149
+ // Non-fatal
150
+ }
151
+ }
152
+ /**
153
+ * Invalidate a cache key after mutation.
154
+ */
155
+ async function cacheInvalidate(registry, cacheKey) {
156
+ try {
157
+ const cache = registry.get('tieredCache');
158
+ if (cache && typeof cache.delete === 'function') {
159
+ cache.delete(cacheKey);
160
+ }
161
+ }
162
+ catch {
163
+ // Non-fatal
164
+ }
165
+ }
166
+ // ===== Phase 2: MutationGuard helpers =====
167
+ /**
168
+ * Validate a mutation through MutationGuard before executing.
169
+ * Returns true if the mutation is allowed, false if rejected.
170
+ * Falls back to allowing if guard is unavailable.
171
+ */
172
+ async function guardValidate(registry, operation, params) {
173
+ try {
174
+ const guard = registry.get('mutationGuard');
175
+ if (!guard || typeof guard.validate !== 'function') {
176
+ return { allowed: true }; // No guard = allow
177
+ }
178
+ const result = guard.validate({ operation, params, timestamp: Date.now() });
179
+ return { allowed: result?.allowed !== false, reason: result?.reason };
180
+ }
181
+ catch {
182
+ return { allowed: true }; // Guard error = allow (graceful degradation)
183
+ }
184
+ }
185
+ // ===== Phase 3: AttestationLog helpers =====
186
+ /**
187
+ * Log a write operation to AttestationLog/WitnessChain.
188
+ */
189
+ async function logAttestation(registry, operation, entryId, metadata) {
190
+ try {
191
+ const attestation = registry.get('attestationLog');
192
+ if (!attestation)
193
+ return;
194
+ if (typeof attestation.record === 'function') {
195
+ attestation.record({ operation, entryId, timestamp: Date.now(), ...metadata });
196
+ }
197
+ else if (typeof attestation.log === 'function') {
198
+ attestation.log(operation, entryId, metadata);
199
+ }
200
+ }
201
+ catch {
202
+ // Non-fatal — attestation is observability, not correctness
203
+ }
204
+ }
77
205
  /**
78
206
  * Get the AgentDB database handle and ensure memory_entries table exists.
79
207
  * Returns null if not available.
@@ -118,6 +246,7 @@ function getDb(registry) {
118
246
  // ===== Bridge functions — match memory-initializer.ts signatures =====
119
247
  /**
120
248
  * Store an entry via AgentDB v3.
249
+ * Phase 2-5: Routes through MutationGuard → TieredCache → DB → AttestationLog.
121
250
  * Returns null to signal fallback to sql.js.
122
251
  */
123
252
  export async function bridgeStoreEntry(options) {
@@ -131,6 +260,11 @@ export async function bridgeStoreEntry(options) {
131
260
  const { key, value, namespace = 'default', tags = [], ttl } = options;
132
261
  const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
133
262
  const now = Date.now();
263
+ // Phase 5: MutationGuard validation before write
264
+ const guardResult = await guardValidate(registry, 'store', { key, namespace, size: value.length });
265
+ if (!guardResult.allowed) {
266
+ return { success: false, id, error: `MutationGuard rejected: ${guardResult.reason}` };
267
+ }
134
268
  // Generate embedding via AgentDB's embedder
135
269
  let embeddingJson = null;
136
270
  let dimensions = 0;
@@ -165,10 +299,18 @@ export async function bridgeStoreEntry(options) {
165
299
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
166
300
  const stmt = ctx.db.prepare(insertSql);
167
301
  stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
302
+ // Phase 2: Write-through to TieredCache
303
+ const cacheKey = `entry:${namespace}:${key}`;
304
+ await cacheSet(registry, cacheKey, { id, key, namespace, content: value, embedding: embeddingJson });
305
+ // Phase 4: AttestationLog write audit
306
+ await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
168
307
  return {
169
308
  success: true,
170
309
  id,
171
310
  embedding: embeddingJson ? { dimensions, model } : undefined,
311
+ guarded: true,
312
+ cached: true,
313
+ attested: true,
172
314
  };
173
315
  }
174
316
  catch {
@@ -177,6 +319,8 @@ export async function bridgeStoreEntry(options) {
177
319
  }
178
320
  /**
179
321
  * Search entries via AgentDB v3.
322
+ * Phase 2: BM25 hybrid scoring replaces naive String.includes() keyword fallback.
323
+ * Combines cosine similarity (semantic) with BM25 (lexical) via reciprocal rank fusion.
180
324
  */
181
325
  export async function bridgeSearchEntries(options) {
182
326
  const registry = await getRegistry(options.dbPath);
@@ -217,32 +361,47 @@ export async function bridgeSearchEntries(options) {
217
361
  catch {
218
362
  return null;
219
363
  }
364
+ // Phase 2: Compute BM25 term stats for the corpus
365
+ const queryTerms = queryStr.toLowerCase().split(/\s+/).filter(t => t.length > 1);
366
+ const { termDocFreqs, avgDocLength } = computeTermDocFreqs(queryTerms, rows);
367
+ const docCount = rows.length;
220
368
  const results = [];
221
369
  for (const row of rows) {
222
- let score = 0;
370
+ let semanticScore = 0;
371
+ let bm25ScoreVal = 0;
372
+ // Semantic scoring via cosine similarity
223
373
  if (queryEmbedding && row.embedding) {
224
374
  try {
225
375
  const embedding = JSON.parse(row.embedding);
226
- score = cosineSim(queryEmbedding, embedding);
376
+ semanticScore = cosineSim(queryEmbedding, embedding);
227
377
  }
228
378
  catch {
229
379
  // Invalid embedding
230
380
  }
231
381
  }
232
- // Keyword fallback
233
- if (score < threshold) {
234
- const lowerContent = (row.content || '').toLowerCase();
235
- const words = queryStr.toLowerCase().split(/\s+/);
236
- const matchCount = words.filter((w) => lowerContent.includes(w)).length;
237
- score = Math.max(score, (matchCount / words.length) * 0.5);
382
+ // Phase 2: BM25 keyword scoring (replaces String.includes fallback)
383
+ if (queryTerms.length > 0 && row.content) {
384
+ bm25ScoreVal = bm25Score(queryTerms, row.content, avgDocLength, docCount, termDocFreqs);
385
+ // Normalize BM25 to 0-1 range (cap at 10 for normalization)
386
+ bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
238
387
  }
388
+ // Reciprocal rank fusion: combine semantic and BM25
389
+ // Weight: 0.7 semantic + 0.3 BM25 (semantic preferred when embeddings available)
390
+ const score = queryEmbedding
391
+ ? (0.7 * semanticScore + 0.3 * bm25ScoreVal)
392
+ : bm25ScoreVal; // BM25-only when no embeddings
239
393
  if (score >= threshold) {
394
+ // Phase 4: ExplainableRecall provenance
395
+ const provenance = queryEmbedding
396
+ ? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}`
397
+ : `bm25:${bm25ScoreVal.toFixed(3)}`;
240
398
  results.push({
241
399
  id: String(row.id).substring(0, 12),
242
400
  key: row.key || String(row.id).substring(0, 15),
243
401
  content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
244
402
  score,
245
403
  namespace: row.namespace || 'default',
404
+ provenance,
246
405
  });
247
406
  }
248
407
  }
@@ -251,6 +410,7 @@ export async function bridgeSearchEntries(options) {
251
410
  success: true,
252
411
  results: results.slice(0, limit),
253
412
  searchTime: Date.now() - startTime,
413
+ searchMethod: queryEmbedding ? 'hybrid-bm25-semantic' : 'bm25-only',
254
414
  };
255
415
  }
256
416
  catch {
@@ -316,6 +476,7 @@ export async function bridgeListEntries(options) {
316
476
  }
317
477
  /**
318
478
  * Get a specific entry via AgentDB v3.
479
+ * Phase 2: TieredCache consulted before DB hit.
319
480
  */
320
481
  export async function bridgeGetEntry(options) {
321
482
  const registry = await getRegistry(options.dbPath);
@@ -326,6 +487,27 @@ export async function bridgeGetEntry(options) {
326
487
  return null;
327
488
  try {
328
489
  const { key, namespace = 'default' } = options;
490
+ // Phase 2: Check TieredCache first
491
+ const cacheKey = `entry:${namespace}:${key}`;
492
+ const cached = await cacheGet(registry, cacheKey);
493
+ if (cached && cached.content) {
494
+ return {
495
+ success: true,
496
+ found: true,
497
+ cacheHit: true,
498
+ entry: {
499
+ id: String(cached.id || ''),
500
+ key: cached.key || key,
501
+ namespace: cached.namespace || namespace,
502
+ content: cached.content || '',
503
+ accessCount: cached.accessCount || 0,
504
+ createdAt: cached.createdAt || new Date().toISOString(),
505
+ updatedAt: cached.updatedAt || new Date().toISOString(),
506
+ hasEmbedding: !!cached.embedding,
507
+ tags: cached.tags || [],
508
+ },
509
+ };
510
+ }
329
511
  let row;
330
512
  try {
331
513
  const stmt = ctx.db.prepare(`
@@ -356,21 +538,20 @@ export async function bridgeGetEntry(options) {
356
538
  }
357
539
  catch { /* invalid */ }
358
540
  }
359
- return {
360
- success: true,
361
- found: true,
362
- entry: {
363
- id: String(row.id),
364
- key: row.key || String(row.id),
365
- namespace: row.namespace || 'default',
366
- content: row.content || '',
367
- accessCount: (row.access_count || 0) + 1,
368
- createdAt: row.created_at || new Date().toISOString(),
369
- updatedAt: row.updated_at || new Date().toISOString(),
370
- hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
371
- tags,
372
- },
541
+ const entry = {
542
+ id: String(row.id),
543
+ key: row.key || String(row.id),
544
+ namespace: row.namespace || 'default',
545
+ content: row.content || '',
546
+ accessCount: (row.access_count || 0) + 1,
547
+ createdAt: row.created_at || new Date().toISOString(),
548
+ updatedAt: row.updated_at || new Date().toISOString(),
549
+ hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
550
+ tags,
373
551
  };
552
+ // Phase 2: Populate cache for next read
553
+ await cacheSet(registry, cacheKey, entry);
554
+ return { success: true, found: true, cacheHit: false, entry };
374
555
  }
375
556
  catch {
376
557
  return null;
@@ -378,6 +559,7 @@ export async function bridgeGetEntry(options) {
378
559
  }
379
560
  /**
380
561
  * Delete an entry via AgentDB v3.
562
+ * Phase 5: MutationGuard validation, cache invalidation, attestation logging.
381
563
  */
382
564
  export async function bridgeDeleteEntry(options) {
383
565
  const registry = await getRegistry(options.dbPath);
@@ -388,6 +570,11 @@ export async function bridgeDeleteEntry(options) {
388
570
  return null;
389
571
  try {
390
572
  const { key, namespace = 'default' } = options;
573
+ // Phase 5: MutationGuard validation before delete
574
+ const guardResult = await guardValidate(registry, 'delete', { key, namespace });
575
+ if (!guardResult.allowed) {
576
+ return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `MutationGuard rejected: ${guardResult.reason}` };
577
+ }
391
578
  // Soft delete using parameterized query
392
579
  let changes = 0;
393
580
  try {
@@ -401,6 +588,12 @@ export async function bridgeDeleteEntry(options) {
401
588
  catch {
402
589
  return null;
403
590
  }
591
+ // Phase 2: Invalidate cache
592
+ await cacheInvalidate(registry, `entry:${namespace}:${key}`);
593
+ // Phase 4: AttestationLog delete audit
594
+ if (changes > 0) {
595
+ await logAttestation(registry, 'delete', key, { namespace });
596
+ }
404
597
  let remaining = 0;
405
598
  try {
406
599
  const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`).get();
@@ -415,6 +608,7 @@ export async function bridgeDeleteEntry(options) {
415
608
  key,
416
609
  namespace,
417
610
  remainingEntries: remaining,
611
+ guarded: true,
418
612
  };
419
613
  }
420
614
  catch {
@@ -675,6 +869,369 @@ export async function shutdownBridge() {
675
869
  bridgeAvailable = null;
676
870
  }
677
871
  }
872
+ // ===== Phase 3: ReasoningBank pattern operations =====
873
+ /**
874
+ * Store a pattern via ReasoningBank controller.
875
+ * Falls back to raw SQL if ReasoningBank unavailable.
876
+ */
877
+ export async function bridgeStorePattern(options) {
878
+ const registry = await getRegistry(options.dbPath);
879
+ if (!registry)
880
+ return null;
881
+ try {
882
+ const reasoningBank = registry.get('reasoningBank');
883
+ const patternId = `pattern-${Date.now()}-${Math.random().toString(36).substring(7)}`;
884
+ if (reasoningBank && typeof reasoningBank.store === 'function') {
885
+ await reasoningBank.store({
886
+ id: patternId,
887
+ content: options.pattern,
888
+ type: options.type,
889
+ confidence: options.confidence,
890
+ metadata: options.metadata,
891
+ timestamp: Date.now(),
892
+ });
893
+ return { success: true, patternId, controller: 'reasoningBank' };
894
+ }
895
+ // Fallback: store via bridge SQL
896
+ const result = await bridgeStoreEntry({
897
+ key: patternId,
898
+ value: JSON.stringify({ pattern: options.pattern, type: options.type, confidence: options.confidence, metadata: options.metadata }),
899
+ namespace: 'pattern',
900
+ generateEmbeddingFlag: true,
901
+ tags: [options.type, 'reasoning-pattern'],
902
+ dbPath: options.dbPath,
903
+ });
904
+ return result ? { success: true, patternId: result.id, controller: 'bridge-fallback' } : null;
905
+ }
906
+ catch {
907
+ return null;
908
+ }
909
+ }
910
+ /**
911
+ * Search patterns via ReasoningBank controller.
912
+ */
913
+ export async function bridgeSearchPatterns(options) {
914
+ const registry = await getRegistry(options.dbPath);
915
+ if (!registry)
916
+ return null;
917
+ try {
918
+ const reasoningBank = registry.get('reasoningBank');
919
+ if (reasoningBank && typeof reasoningBank.search === 'function') {
920
+ const results = await reasoningBank.search(options.query, {
921
+ topK: options.topK || 5,
922
+ minScore: options.minConfidence || 0.3,
923
+ });
924
+ return {
925
+ results: Array.isArray(results) ? results.map((r) => ({
926
+ id: r.id || r.patternId || '',
927
+ content: r.content || r.pattern || '',
928
+ score: r.score || r.confidence || 0,
929
+ })) : [],
930
+ controller: 'reasoningBank',
931
+ };
932
+ }
933
+ // Fallback: search via bridge
934
+ const result = await bridgeSearchEntries({
935
+ query: options.query,
936
+ namespace: 'pattern',
937
+ limit: options.topK || 5,
938
+ threshold: options.minConfidence || 0.3,
939
+ dbPath: options.dbPath,
940
+ });
941
+ return result ? {
942
+ results: result.results.map(r => ({ id: r.id, content: r.content, score: r.score })),
943
+ controller: 'bridge-fallback',
944
+ } : null;
945
+ }
946
+ catch {
947
+ return null;
948
+ }
949
+ }
950
+ // ===== Phase 3: Feedback recording =====
951
+ /**
952
+ * Record task feedback for learning via ReasoningBank or LearningSystem.
953
+ * Wired into hooks_post-task handler.
954
+ */
955
+ export async function bridgeRecordFeedback(options) {
956
+ const registry = await getRegistry(options.dbPath);
957
+ if (!registry)
958
+ return null;
959
+ try {
960
+ let controller = 'none';
961
+ let updated = 0;
962
+ // Try LearningSystem first (Phase 4)
963
+ const learningSystem = registry.get('learningSystem');
964
+ if (learningSystem) {
965
+ try {
966
+ if (typeof learningSystem.recordFeedback === 'function') {
967
+ await learningSystem.recordFeedback({
968
+ taskId: options.taskId, success: options.success, quality: options.quality,
969
+ agent: options.agent, duration: options.duration, timestamp: Date.now(),
970
+ });
971
+ controller = 'learningSystem';
972
+ updated++;
973
+ }
974
+ else if (typeof learningSystem.record === 'function') {
975
+ await learningSystem.record(options.taskId, options.quality, options.success ? 'success' : 'failure');
976
+ controller = 'learningSystem';
977
+ updated++;
978
+ }
979
+ }
980
+ catch { /* API mismatch — skip */ }
981
+ }
982
+ // Also record in ReasoningBank for pattern reinforcement
983
+ const reasoningBank = registry.get('reasoningBank');
984
+ if (reasoningBank) {
985
+ try {
986
+ if (typeof reasoningBank.recordOutcome === 'function') {
987
+ await reasoningBank.recordOutcome({
988
+ taskId: options.taskId, verdict: options.success ? 'success' : 'failure',
989
+ score: options.quality, timestamp: Date.now(),
990
+ });
991
+ controller = controller === 'none' ? 'reasoningBank' : `${controller}+reasoningBank`;
992
+ updated++;
993
+ }
994
+ else if (typeof reasoningBank.record === 'function') {
995
+ await reasoningBank.record(options.taskId, options.quality);
996
+ controller = controller === 'none' ? 'reasoningBank' : `${controller}+reasoningBank`;
997
+ updated++;
998
+ }
999
+ }
1000
+ catch { /* API mismatch — skip */ }
1001
+ }
1002
+ // Phase 4: SkillLibrary promotion for high-quality patterns
1003
+ if (options.success && options.quality >= 0.9 && options.patterns?.length) {
1004
+ const skills = registry.get('skills');
1005
+ if (skills && typeof skills.promote === 'function') {
1006
+ for (const pattern of options.patterns) {
1007
+ try {
1008
+ await skills.promote(pattern, options.quality);
1009
+ updated++;
1010
+ }
1011
+ catch { /* skip */ }
1012
+ }
1013
+ controller += '+skills';
1014
+ }
1015
+ }
1016
+ // Always store feedback as a memory entry for retrieval (ensures it persists)
1017
+ const storeResult = await bridgeStoreEntry({
1018
+ key: `feedback-${options.taskId}`,
1019
+ value: JSON.stringify(options),
1020
+ namespace: 'feedback',
1021
+ tags: [options.success ? 'success' : 'failure', options.agent || 'unknown'],
1022
+ dbPath: options.dbPath,
1023
+ });
1024
+ if (storeResult?.success) {
1025
+ controller = controller === 'none' ? 'bridge-store' : `${controller}+bridge-store`;
1026
+ updated++;
1027
+ }
1028
+ return { success: true, controller, updated };
1029
+ }
1030
+ catch {
1031
+ return null;
1032
+ }
1033
+ }
1034
+ // ===== Phase 3: CausalMemoryGraph =====
1035
+ /**
1036
+ * Record a causal edge between two entries (e.g., task → result).
1037
+ */
1038
+ export async function bridgeRecordCausalEdge(options) {
1039
+ const registry = await getRegistry(options.dbPath);
1040
+ if (!registry)
1041
+ return null;
1042
+ try {
1043
+ const causalGraph = registry.get('causalGraph');
1044
+ if (causalGraph && typeof causalGraph.addEdge === 'function') {
1045
+ causalGraph.addEdge(options.sourceId, options.targetId, {
1046
+ relation: options.relation,
1047
+ weight: options.weight || 1.0,
1048
+ timestamp: Date.now(),
1049
+ });
1050
+ return { success: true, controller: 'causalGraph' };
1051
+ }
1052
+ // Fallback: store edge as metadata
1053
+ const ctx = getDb(registry);
1054
+ if (ctx) {
1055
+ try {
1056
+ ctx.db.prepare(`
1057
+ INSERT OR REPLACE INTO memory_entries (id, key, namespace, content, type, created_at, updated_at, status)
1058
+ VALUES (?, ?, 'causal-edges', ?, 'procedural', ?, ?, 'active')
1059
+ `).run(`edge-${Date.now()}`, `${options.sourceId}→${options.targetId}`, JSON.stringify(options), Date.now(), Date.now());
1060
+ return { success: true, controller: 'bridge-fallback' };
1061
+ }
1062
+ catch { /* skip */ }
1063
+ }
1064
+ return null;
1065
+ }
1066
+ catch {
1067
+ return null;
1068
+ }
1069
+ }
1070
+ // ===== Phase 5: ReflexionMemory session lifecycle =====
1071
+ /**
1072
+ * Start a session with ReflexionMemory episodic replay.
1073
+ * Loads relevant past session patterns for the new session.
1074
+ */
1075
+ export async function bridgeSessionStart(options) {
1076
+ const registry = await getRegistry(options.dbPath);
1077
+ if (!registry)
1078
+ return null;
1079
+ try {
1080
+ let restoredPatterns = 0;
1081
+ let controller = 'none';
1082
+ // Try ReflexionMemory for episodic session replay
1083
+ const reflexion = registry.get('reflexion');
1084
+ if (reflexion && typeof reflexion.startEpisode === 'function') {
1085
+ await reflexion.startEpisode(options.sessionId, { context: options.context });
1086
+ controller = 'reflexion';
1087
+ }
1088
+ // Load recent patterns from past sessions
1089
+ const searchResult = await bridgeSearchEntries({
1090
+ query: options.context || 'session patterns',
1091
+ namespace: 'session',
1092
+ limit: 10,
1093
+ threshold: 0.2,
1094
+ dbPath: options.dbPath,
1095
+ });
1096
+ if (searchResult?.results) {
1097
+ restoredPatterns = searchResult.results.length;
1098
+ }
1099
+ return {
1100
+ success: true,
1101
+ controller: controller === 'none' ? 'bridge-search' : controller,
1102
+ restoredPatterns,
1103
+ sessionId: options.sessionId,
1104
+ };
1105
+ }
1106
+ catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ /**
1111
+ * End a session and persist episodic summary to ReflexionMemory.
1112
+ */
1113
+ export async function bridgeSessionEnd(options) {
1114
+ const registry = await getRegistry(options.dbPath);
1115
+ if (!registry)
1116
+ return null;
1117
+ try {
1118
+ let controller = 'none';
1119
+ let persisted = false;
1120
+ // End episode in ReflexionMemory
1121
+ const reflexion = registry.get('reflexion');
1122
+ if (reflexion && typeof reflexion.endEpisode === 'function') {
1123
+ await reflexion.endEpisode(options.sessionId, {
1124
+ summary: options.summary,
1125
+ tasksCompleted: options.tasksCompleted,
1126
+ patternsLearned: options.patternsLearned,
1127
+ });
1128
+ controller = 'reflexion';
1129
+ persisted = true;
1130
+ }
1131
+ // Persist session summary as memory entry
1132
+ await bridgeStoreEntry({
1133
+ key: `session-${options.sessionId}`,
1134
+ value: JSON.stringify({
1135
+ sessionId: options.sessionId,
1136
+ summary: options.summary || 'Session ended',
1137
+ tasksCompleted: options.tasksCompleted || 0,
1138
+ patternsLearned: options.patternsLearned || 0,
1139
+ endedAt: new Date().toISOString(),
1140
+ }),
1141
+ namespace: 'session',
1142
+ tags: ['session-end'],
1143
+ upsert: true,
1144
+ dbPath: options.dbPath,
1145
+ });
1146
+ if (controller === 'none')
1147
+ controller = 'bridge-store';
1148
+ persisted = true;
1149
+ // Phase 3: Trigger NightlyLearner consolidation if available
1150
+ const nightlyLearner = registry.get('nightlyLearner');
1151
+ if (nightlyLearner && typeof nightlyLearner.consolidate === 'function') {
1152
+ try {
1153
+ await nightlyLearner.consolidate({ sessionId: options.sessionId });
1154
+ controller += '+nightlyLearner';
1155
+ }
1156
+ catch { /* non-fatal */ }
1157
+ }
1158
+ return { success: true, controller, persisted };
1159
+ }
1160
+ catch {
1161
+ return null;
1162
+ }
1163
+ }
1164
+ // ===== Phase 5: SemanticRouter bridge =====
1165
+ /**
1166
+ * Route a task via AgentDB's SemanticRouter.
1167
+ * Returns null to fall back to local ruvector router.
1168
+ */
1169
+ export async function bridgeRouteTask(options) {
1170
+ const registry = await getRegistry(options.dbPath);
1171
+ if (!registry)
1172
+ return null;
1173
+ try {
1174
+ // Try AgentDB's SemanticRouter
1175
+ const semanticRouter = registry.get('semanticRouter');
1176
+ if (semanticRouter && typeof semanticRouter.route === 'function') {
1177
+ const result = await semanticRouter.route(options.task, { context: options.context });
1178
+ if (result) {
1179
+ return {
1180
+ route: result.route || result.category || 'general',
1181
+ confidence: result.confidence || result.score || 0.5,
1182
+ agents: result.agents || result.suggestedAgents || [],
1183
+ controller: 'semanticRouter',
1184
+ };
1185
+ }
1186
+ }
1187
+ // Try LearningSystem recommendAlgorithm (Phase 4)
1188
+ const learningSystem = registry.get('learningSystem');
1189
+ if (learningSystem && typeof learningSystem.recommendAlgorithm === 'function') {
1190
+ const rec = await learningSystem.recommendAlgorithm(options.task);
1191
+ if (rec) {
1192
+ return {
1193
+ route: rec.algorithm || rec.route || 'general',
1194
+ confidence: rec.confidence || 0.5,
1195
+ agents: rec.agents || [],
1196
+ controller: 'learningSystem',
1197
+ };
1198
+ }
1199
+ }
1200
+ return null; // Fall back to local router
1201
+ }
1202
+ catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+ // ===== Phase 4: Health check with attestation =====
1207
+ /**
1208
+ * Get comprehensive bridge health including all controller statuses.
1209
+ */
1210
+ export async function bridgeHealthCheck(dbPath) {
1211
+ const registry = await getRegistry(dbPath);
1212
+ if (!registry)
1213
+ return null;
1214
+ try {
1215
+ const controllers = registry.listControllers();
1216
+ // Phase 4: AttestationLog stats
1217
+ let attestationCount = 0;
1218
+ const attestation = registry.get('attestationLog');
1219
+ if (attestation && typeof attestation.count === 'function') {
1220
+ attestationCount = attestation.count();
1221
+ }
1222
+ // Phase 2: TieredCache stats
1223
+ let cacheStats = { size: 0, hits: 0, misses: 0 };
1224
+ const cache = registry.get('tieredCache');
1225
+ if (cache && typeof cache.stats === 'function') {
1226
+ const s = cache.stats();
1227
+ cacheStats = { size: s.size || 0, hits: s.hits || 0, misses: s.misses || 0 };
1228
+ }
1229
+ return { available: true, controllers, attestationCount, cacheStats };
1230
+ }
1231
+ catch {
1232
+ return null;
1233
+ }
1234
+ }
678
1235
  // ===== Utility =====
679
1236
  function cosineSim(a, b) {
680
1237
  if (!a || !b || a.length === 0 || b.length === 0)