claude-flow 3.47.1 → 3.48.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 (48) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  2. package/.claude/helpers/learning-service.mjs +4 -2
  3. package/.claude/helpers/metrics-db.mjs +4 -2
  4. package/node_modules/@claude-flow/codex/package.json +1 -1
  5. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  6. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  7. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  8. package/node_modules/@claude-flow/mcp/package.json +6 -3
  9. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  10. package/node_modules/@claude-flow/security/package.json +2 -3
  11. package/package.json +7 -7
  12. package/v3/@claude-flow/cli/README.md +0 -2
  13. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  14. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  15. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  16. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  17. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
  18. package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
  19. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  20. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  21. package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
  22. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  23. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
  24. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  30. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
  32. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  33. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  34. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  35. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  36. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  37. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  38. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  39. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
  40. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  41. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  42. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  43. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  44. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  45. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  46. package/v3/@claude-flow/cli/package.json +9 -9
  47. package/v3/@claude-flow/guidance/package.json +5 -6
  48. package/v3/@claude-flow/shared/package.json +6 -3
@@ -16,9 +16,13 @@
16
16
  *
17
17
  * @module v3/cli/memory-bridge
18
18
  */
19
+ import { liveMemoryRowSql } from './live-memory-row.js';
19
20
  import * as path from 'path';
20
21
  import * as crypto from 'crypto';
22
+ import { realpathSync } from 'node:fs';
21
23
  import { createRequire } from 'node:module';
24
+ import { AsyncLocalStorage } from 'node:async_hooks';
25
+ import { validateFeedbackPatterns } from './feedback-patterns.js';
22
26
  // ===== Lazy registry cache, keyed by database path =====
23
27
  /**
24
28
  * #3196: this cache is keyed by resolved database path, and that is the whole
@@ -37,6 +41,33 @@ import { createRequire } from 'node:module';
37
41
  */
38
42
  const registryPromises = new Map();
39
43
  const registryInstances = new Map();
44
+ // One lease spans an entire bridge operation, including its awaited controller
45
+ // effects and nested bridge calls. Initializer ownership alone is insufficient:
46
+ // a ReasoningBank write can still be pending after its registry has opened.
47
+ const operationContext = new AsyncLocalStorage();
48
+ let activeOperations = 0;
49
+ let drained = null;
50
+ let shutdownPromise = null;
51
+ async function withBridgeOperation(operation) {
52
+ if (operationContext.getStore()?.active)
53
+ return operation();
54
+ // New calls belong to the next lifecycle once retirement starts. A loop
55
+ // also handles another shutdown requested before a queued call resumes.
56
+ while (shutdownPromise)
57
+ await shutdownPromise;
58
+ const lease = { active: true };
59
+ activeOperations++;
60
+ try {
61
+ return await operationContext.run(lease, operation);
62
+ }
63
+ finally {
64
+ lease.active = false;
65
+ if (--activeOperations === 0) {
66
+ drained?.();
67
+ drained = null;
68
+ }
69
+ }
70
+ }
40
71
  /**
41
72
  * Test seam: when set, every path resolves to this registry.
42
73
  *
@@ -46,22 +77,36 @@ const registryInstances = new Map();
46
77
  * pass the same path the seam guessed, which is how #2968's fixture broke.
47
78
  */
48
79
  let testRegistryOverride = null;
49
- let bridgeAvailable = null;
80
+ let testRegistryFactory = null;
50
81
  // #2652/#2120: rows created before the status column existed receive NULL
51
82
  // during migration. They are live rows, not tombstones. Every user-facing
52
83
  // read/delete path must agree with list() about their visibility.
53
84
  const ACTIVE_MEMORY_ROW_SQL = `(status = 'active' OR status IS NULL)`;
54
85
  /**
55
- * Why the bridge is unavailable, when it is.
56
- *
57
- * `bridgeAvailable = false` latches for the life of the process, so a single
58
- * transient init failure (a slow Xenova/ONNX fetch, a locked db) routes every
59
- * later write to the sql.js whole-image fallback — which then refuses whenever
60
- * -wal/-shm sidecars are present. Without this, that refusal is the only
61
- * symptom the caller ever sees, and it names a cause ("restore the native
62
- * better-sqlite3 bridge") the caller has no way to check.
86
+ * A failed open must only affect its own database. A process can serve several
87
+ * projects, and an error from one must not disable the native bridge for all.
63
88
  */
64
- let bridgeFailureReason = null;
89
+ const bridgeFailureReasons = new Map();
90
+ /** Resolve aliases even when the database file or some parent dirs do not exist yet. */
91
+ function canonicalDbPath(dbPath) {
92
+ const resolved = dbPath === ':memory:' ? ':memory:' : path.resolve(dbPath ?? getAgentDbPath());
93
+ if (resolved === ':memory:')
94
+ return resolved;
95
+ let ancestor = resolved;
96
+ const suffix = [];
97
+ for (;;) {
98
+ try {
99
+ return path.join(realpathSync(ancestor), ...suffix);
100
+ }
101
+ catch {
102
+ const parent = path.dirname(ancestor);
103
+ if (parent === ancestor)
104
+ return resolved;
105
+ suffix.unshift(path.basename(ancestor));
106
+ ancestor = parent;
107
+ }
108
+ }
109
+ }
65
110
  /**
66
111
  * #3024: AgentDB's optional native controller stack can abort the whole Node
67
112
  * process on Windows during registry initialization (a Rust allocation panic),
@@ -190,29 +235,24 @@ export function shouldSuppressInitLog(msg) {
190
235
  * Returns null if @claude-flow/memory is not available.
191
236
  */
192
237
  async function getRegistry(dbPath) {
193
- if (shouldDisableNativeBridge()) {
194
- bridgeFailureReason = process.platform === 'win32'
195
- ? 'AgentDB native bridge disabled on Windows after #3024; set CLAUDE_FLOW_ENABLE_NATIVE_BRIDGE_ON_WINDOWS=1 to opt in'
196
- : 'AgentDB native bridge disabled by CLAUDE_FLOW_DISABLE_BRIDGE=1';
238
+ if (shouldDisableNativeBridge())
197
239
  return null;
198
- }
199
240
  if (testRegistryOverride)
200
241
  return testRegistryOverride;
201
- if (bridgeAvailable === false)
202
- return null;
203
- // Resolve first, then cache on the resolved value: `undefined`, a relative
204
- // path and its absolute form must not become three different registries over
205
- // the same file.
206
- const resolvedPath = dbPath ? path.resolve(dbPath) : getAgentDbPath();
242
+ // Resolve before caching: relative, absolute and symlink paths to one file
243
+ // must not open multiple native handles to the same database.
244
+ const resolvedPath = canonicalDbPath(dbPath);
207
245
  const cached = registryInstances.get(resolvedPath);
208
246
  if (cached)
209
247
  return cached;
210
248
  let registryPromise = registryPromises.get(resolvedPath);
211
249
  if (!registryPromise) {
212
250
  registryPromise = (async () => {
251
+ let registry;
213
252
  try {
214
- const { ControllerRegistry } = await import('@claude-flow/memory');
215
- const registry = new ControllerRegistry();
253
+ registry = testRegistryFactory
254
+ ? testRegistryFactory()
255
+ : new (await import('@claude-flow/memory')).ControllerRegistry();
216
256
  // Suppress noisy console.log during init — but never suppress a
217
257
  // DEGRADATION notice (see shouldSuppressInitLog).
218
258
  const origLog = console.log;
@@ -289,11 +329,13 @@ async function getRegistry(dbPath) {
289
329
  catch {
290
330
  return; /* AgentDB not available */
291
331
  }
292
- // SkillLibrary (no db required)
332
+ // SkillLibrary requires the same database and passage embedder as AgentDB.
293
333
  try {
294
334
  const SkillCtor = agentdb.SkillLibrary;
295
- if (SkillCtor && !reg.get('skills')) {
296
- const sk = new SkillCtor();
335
+ const backing = reg.getAgentDB?.();
336
+ if (SkillCtor && !reg.get('skills') && backing?.database
337
+ && typeof backing.embedder?.embedPassage === 'function') {
338
+ const sk = new SkillCtor(backing.database, backing.embedder);
297
339
  if (typeof reg.set === 'function')
298
340
  reg.set('skills', sk);
299
341
  else
@@ -473,17 +515,18 @@ async function getRegistry(dbPath) {
473
515
  // Top-level catch — registry stays usable even if post-init wiring fails wholesale.
474
516
  }
475
517
  registryInstances.set(resolvedPath, registry);
476
- bridgeAvailable = true;
477
- bridgeFailureReason = null;
518
+ bridgeFailureReasons.delete(resolvedPath);
478
519
  return registry;
479
520
  }
480
521
  catch (err) {
481
- // Record WHY. This latches for the process lifetime (see the
482
- // bridgeFailureReason doc comment), so discarding the error here
483
- // makes the resulting sql.js-fallback refusal undiagnosable.
484
- bridgeFailureReason = err instanceof Error ? err.message : String(err);
485
- bridgeAvailable = false;
486
- registryPromises.delete(resolvedPath);
522
+ // initialize() may have opened a native handle before it failed.
523
+ try {
524
+ await registry?.shutdown();
525
+ }
526
+ catch { /* best-effort cleanup */ }
527
+ // Keep the failed promise for this path only. Other databases can
528
+ // still initialize; shutdownBridge() clears the latch for a retry.
529
+ bridgeFailureReasons.set(resolvedPath, err instanceof Error ? err.message : String(err));
487
530
  return null;
488
531
  }
489
532
  })();
@@ -496,8 +539,12 @@ export function _resetRegistryCacheForTest() {
496
539
  registryPromises.clear();
497
540
  registryInstances.clear();
498
541
  testRegistryOverride = null;
499
- bridgeAvailable = null;
500
- bridgeFailureReason = null;
542
+ testRegistryFactory = null;
543
+ bridgeFailureReasons.clear();
544
+ }
545
+ /** Inject registry construction without requiring the optional native package in tests. */
546
+ export function __setMemoryBridgeRegistryFactoryForTests(factory) {
547
+ testRegistryFactory = factory;
501
548
  }
502
549
  /** #3196: the sibling store AgentDB owns next to a given sql.js database. */
503
550
  export function siblingAgentDbPath(dbPath) {
@@ -550,6 +597,10 @@ function computeTermDocFreqs(queryTerms, docs) {
550
597
  return { termDocFreqs, avgDocLength: docs.length > 0 ? totalLength / docs.length : 1 };
551
598
  }
552
599
  // ===== Phase 2: TieredCache helpers =====
600
+ /** Preserve the exact namespace/key tuple; punctuation is valid memory data. */
601
+ function entryCacheKey(namespace, key) {
602
+ return `entry:${JSON.stringify([namespace, key])}`;
603
+ }
553
604
  /**
554
605
  * Try to read from TieredCache before hitting DB.
555
606
  * Returns cached value or null if cache miss.
@@ -774,6 +825,15 @@ export function ensureBridgeSchema(db) {
774
825
  throw err;
775
826
  }
776
827
  }
828
+ // Reads now enforce TTL, including on stores created before expires_at
829
+ // existed. A missing expiration means the legacy row does not expire.
830
+ try {
831
+ db.exec('ALTER TABLE memory_entries ADD COLUMN expires_at INTEGER');
832
+ }
833
+ catch (error) {
834
+ if (!/duplicate column name:\s*expires_at/i.test(error instanceof Error ? error.message : String(error)))
835
+ throw error;
836
+ }
777
837
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
778
838
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
779
839
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
@@ -884,6 +944,11 @@ async function rescueAgentdbEmbedder(agentdb) {
884
944
  emb.__ruvectorRescued = true;
885
945
  _embedderPatched = true;
886
946
  }
947
+ /** AgentDB uses a null pipeline while its transformer failed and it serves mock vectors. */
948
+ function isMockAgentdbEmbedder(embedder) {
949
+ return embedder?.isMock === true || embedder?.backend === 'mock'
950
+ || (embedder?.pipeline === null && embedder.__ruvectorRescued !== true);
951
+ }
887
952
  /**
888
953
  * #3325: embed text for a bridge row or query.
889
954
  *
@@ -899,7 +964,9 @@ async function embedForBridge(agentdb, text) {
899
964
  const embedder = agentdb?.embedder;
900
965
  // Same mock signal bridgeGenerateEmbedding honours (AUDIT #3): the rescue
901
966
  // tags a degraded embedder backend='mock' when it cannot replace it.
902
- const agentdbIsMock = embedder?.isMock === true || embedder?.backend === 'mock';
967
+ // The rescue launched by getDb() is fire-and-forget. Do not persist mock
968
+ // vectors during that window just because they happen to be 384-dimension.
969
+ const agentdbIsMock = isMockAgentdbEmbedder(embedder);
903
970
  if (agentdbIsMock) {
904
971
  agentdbProblem = 'agentdb embedder is serving mock vectors';
905
972
  }
@@ -937,6 +1004,8 @@ async function embedForBridge(agentdb, text) {
937
1004
  * Returns null to signal fallback to sql.js.
938
1005
  */
939
1006
  export async function bridgeStoreEntry(options) {
1007
+ if (!operationContext.getStore()?.active)
1008
+ return withBridgeOperation(() => bridgeStoreEntry(options));
940
1009
  // ADR-323 — validated once in storeEntry() before this is reached on that
941
1010
  // path, but bridgeStoreEntry() also has direct internal callers, so check
942
1011
  // again here rather than trust every call site.
@@ -1058,7 +1127,7 @@ export async function bridgeStoreEntry(options) {
1058
1127
  // A completed native write proves the bridge is currently healthy. Do not
1059
1128
  // retain a diagnostic from an earlier transient failure and append it to a
1060
1129
  // later, unrelated sql.js fallback refusal.
1061
- bridgeFailureReason = null;
1130
+ bridgeFailureReasons.delete(canonicalDbPath(options.dbPath));
1062
1131
  // #2775: strict insert against an ACTIVE existing row → changes === 0
1063
1132
  // (the ON CONFLICT WHERE clause above suppressed the update). Surface
1064
1133
  // this as a typed data-level error rather than a bridge failure —
@@ -1123,9 +1192,7 @@ export async function bridgeStoreEntry(options) {
1123
1192
  // next `bridgeGetEntry` to load the authoritative row (which correctly
1124
1193
  // parses tags/metadata from the JSON columns) and repopulate the cache
1125
1194
  // from that full shape.
1126
- const safeNs = String(namespace).replace(/:/g, '_');
1127
- const safeKey = String(key).replace(/:/g, '_');
1128
- const cacheKey = `entry:${safeNs}:${safeKey}`;
1195
+ const cacheKey = entryCacheKey(namespace, key);
1129
1196
  await cacheInvalidate(registry, cacheKey);
1130
1197
  // Phase 4: AttestationLog write audit
1131
1198
  await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
@@ -1164,7 +1231,7 @@ export async function bridgeStoreEntry(options) {
1164
1231
  // whose WAL-sidecar guard then reports a cause that has nothing to do with
1165
1232
  // what actually went wrong here. Record the real error so it can be
1166
1233
  // surfaced alongside that guard's message.
1167
- bridgeFailureReason = msg;
1234
+ bridgeFailureReasons.set(canonicalDbPath(options.dbPath), msg);
1168
1235
  return null;
1169
1236
  }
1170
1237
  }
@@ -1174,6 +1241,8 @@ export async function bridgeStoreEntry(options) {
1174
1241
  * Combines cosine similarity (semantic) with BM25 (lexical) via reciprocal rank fusion.
1175
1242
  */
1176
1243
  export async function bridgeSearchEntries(options) {
1244
+ if (!operationContext.getStore()?.active)
1245
+ return withBridgeOperation(() => bridgeSearchEntries(options));
1177
1246
  if (options.provenanceFilter?.length) {
1178
1247
  const invalid = options.provenanceFilter.filter(p => !isValidProvenanceType(p));
1179
1248
  if (invalid.length > 0)
@@ -1217,7 +1286,7 @@ export async function bridgeSearchEntries(options) {
1217
1286
  const stmt = ctx.db.prepare(`
1218
1287
  SELECT id, key, namespace, content, embedding, provenance_type
1219
1288
  FROM memory_entries
1220
- WHERE ${ACTIVE_MEMORY_ROW_SQL} ${whereExtra}
1289
+ WHERE ${liveMemoryRowSql()} ${whereExtra}
1221
1290
  ORDER BY updated_at DESC
1222
1291
  LIMIT 1000
1223
1292
  `);
@@ -1309,6 +1378,8 @@ export async function bridgeSearchEntries(options) {
1309
1378
  * List entries via AgentDB v3.
1310
1379
  */
1311
1380
  export async function bridgeListEntries(options) {
1381
+ if (!operationContext.getStore()?.active)
1382
+ return withBridgeOperation(() => bridgeListEntries(options));
1312
1383
  const registry = await getRegistry(options.dbPath);
1313
1384
  if (!registry)
1314
1385
  return null;
@@ -1338,7 +1409,7 @@ export async function bridgeListEntries(options) {
1338
1409
  // the `status = 'active'` filter matched zero. Treat NULL as
1339
1410
  // "legacy-active" — the safe default for any entry that predates the
1340
1411
  // status column.
1341
- const statusFilter = ACTIVE_MEMORY_ROW_SQL;
1412
+ const statusFilter = liveMemoryRowSql();
1342
1413
  // Count
1343
1414
  let total = 0;
1344
1415
  try {
@@ -1394,6 +1465,8 @@ export async function bridgeListEntries(options) {
1394
1465
  * Phase 2: TieredCache consulted before DB hit.
1395
1466
  */
1396
1467
  export async function bridgeGetEntry(options) {
1468
+ if (!operationContext.getStore()?.active)
1469
+ return withBridgeOperation(() => bridgeGetEntry(options));
1397
1470
  const registry = await getRegistry(options.dbPath);
1398
1471
  if (!registry)
1399
1472
  return null;
@@ -1403,12 +1476,10 @@ export async function bridgeGetEntry(options) {
1403
1476
  try {
1404
1477
  const { key, namespace = 'default' } = options;
1405
1478
  // Phase 2: Check TieredCache first
1406
- const safeNs = String(namespace).replace(/:/g, '_');
1407
- const safeKey = String(key).replace(/:/g, '_');
1408
- const cacheKey = `entry:${safeNs}:${safeKey}`;
1479
+ const cacheKey = entryCacheKey(namespace, key);
1409
1480
  dropCacheIfDbChangedElsewhere(registry, ctx);
1410
1481
  const cached = await cacheGet(registry, cacheKey);
1411
- if (cached && cached.content) {
1482
+ if (cached && cached.content && (cached.expiresAt == null || cached.expiresAt > Date.now())) {
1412
1483
  return {
1413
1484
  success: true,
1414
1485
  found: true,
@@ -1432,9 +1503,9 @@ export async function bridgeGetEntry(options) {
1432
1503
  let row;
1433
1504
  try {
1434
1505
  const stmt = ctx.db.prepare(`
1435
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
1506
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, expires_at
1436
1507
  FROM memory_entries
1437
- WHERE ${ACTIVE_MEMORY_ROW_SQL} AND key = ? AND namespace = ?
1508
+ WHERE ${liveMemoryRowSql()} AND key = ? AND namespace = ?
1438
1509
  LIMIT 1
1439
1510
  `);
1440
1511
  row = stmt.get(key, namespace);
@@ -1471,7 +1542,7 @@ export async function bridgeGetEntry(options) {
1471
1542
  tags,
1472
1543
  };
1473
1544
  // Phase 2: Populate cache for next read
1474
- await cacheSet(registry, cacheKey, entry);
1545
+ await cacheSet(registry, cacheKey, { ...entry, expiresAt: row.expires_at });
1475
1546
  return { success: true, found: true, cacheHit: false, entry };
1476
1547
  }
1477
1548
  catch {
@@ -1483,6 +1554,8 @@ export async function bridgeGetEntry(options) {
1483
1554
  * Phase 5: MutationGuard validation, cache invalidation, attestation logging.
1484
1555
  */
1485
1556
  export async function bridgeDeleteEntry(options) {
1557
+ if (!operationContext.getStore()?.active)
1558
+ return withBridgeOperation(() => bridgeDeleteEntry(options));
1486
1559
  const registry = await getRegistry(options.dbPath);
1487
1560
  if (!registry)
1488
1561
  return null;
@@ -1525,9 +1598,7 @@ export async function bridgeDeleteEntry(options) {
1525
1598
  }
1526
1599
  catch { /* non-WAL, busy, or unsupported — non-fatal */ }
1527
1600
  // Phase 2: Invalidate cache
1528
- const safeNs = String(namespace).replace(/:/g, '_');
1529
- const safeKey = String(key).replace(/:/g, '_');
1530
- await cacheInvalidate(registry, `entry:${safeNs}:${safeKey}`);
1601
+ await cacheInvalidate(registry, entryCacheKey(namespace, key));
1531
1602
  // Phase 4: AttestationLog delete audit
1532
1603
  if (changes > 0) {
1533
1604
  await logAttestation(registry, 'delete', key, { namespace });
@@ -1560,6 +1631,8 @@ export async function bridgeDeleteEntry(options) {
1560
1631
  // step) that need a namespace to be genuinely empty rather than tombstoned.
1561
1632
  // Irreversible — callers must gate this behind an explicit confirmation.
1562
1633
  export async function bridgePurgeNamespace(options) {
1634
+ if (!operationContext.getStore()?.active)
1635
+ return withBridgeOperation(() => bridgePurgeNamespace(options));
1563
1636
  const registry = await getRegistry(options.dbPath);
1564
1637
  if (!registry)
1565
1638
  return null;
@@ -1580,8 +1653,15 @@ export async function bridgePurgeNamespace(options) {
1580
1653
  catch (e) {
1581
1654
  return { success: false, deletedCount: 0, remainingEntries: 0, error: e instanceof Error ? e.message : String(e) };
1582
1655
  }
1583
- const safeNs = String(namespace).replace(/:/g, '_');
1584
- await cacheInvalidate(registry, `namespace:${safeNs}`);
1656
+ // Entries are cached individually, not under a namespace sentinel.
1657
+ // SQLite data_version does not change for writes on this connection, so
1658
+ // the cross-process freshness check cannot invalidate this purge for us.
1659
+ try {
1660
+ const cache = registry.get('tieredCache');
1661
+ if (cache && typeof cache.clear === 'function')
1662
+ await cache.clear();
1663
+ }
1664
+ catch { /* cache cleanup is best-effort */ }
1585
1665
  if (deletedCount > 0) {
1586
1666
  await logAttestation(registry, 'purge', namespace, { namespace, deletedCount });
1587
1667
  }
@@ -1610,13 +1690,15 @@ export async function bridgePurgeNamespace(options) {
1610
1690
  * Returns null if bridge unavailable — caller falls back to own ONNX/hash.
1611
1691
  */
1612
1692
  export async function bridgeGenerateEmbedding(text, dbPath) {
1693
+ if (!operationContext.getStore()?.active)
1694
+ return withBridgeOperation(() => bridgeGenerateEmbedding(text, dbPath));
1613
1695
  const registry = await getRegistry(dbPath);
1614
1696
  if (!registry)
1615
1697
  return null;
1616
1698
  try {
1617
1699
  const agentdb = registry.getAgentDB();
1618
1700
  const embedder = agentdb?.embedder;
1619
- if (!embedder)
1701
+ if (!embedder || isMockAgentdbEmbedder(embedder))
1620
1702
  return null;
1621
1703
  const emb = await embedder.embed(text);
1622
1704
  if (!emb)
@@ -1653,6 +1735,8 @@ export async function bridgeGenerateEmbedding(text, dbPath) {
1653
1735
  * Returns null if unavailable.
1654
1736
  */
1655
1737
  export async function bridgeLoadEmbeddingModel(dbPath) {
1738
+ if (!operationContext.getStore()?.active)
1739
+ return withBridgeOperation(() => bridgeLoadEmbeddingModel(dbPath));
1656
1740
  const startTime = Date.now();
1657
1741
  const registry = await getRegistry(dbPath);
1658
1742
  if (!registry)
@@ -1660,7 +1744,7 @@ export async function bridgeLoadEmbeddingModel(dbPath) {
1660
1744
  try {
1661
1745
  const agentdb = registry.getAgentDB();
1662
1746
  const embedder = agentdb?.embedder;
1663
- if (!embedder)
1747
+ if (!embedder || isMockAgentdbEmbedder(embedder))
1664
1748
  return null;
1665
1749
  // Verify embedder works by generating a test embedding
1666
1750
  const test = await embedder.embed('test');
@@ -1690,6 +1774,8 @@ export async function bridgeLoadEmbeddingModel(dbPath) {
1690
1774
  * search works" for "vector search is HNSW-accelerated".
1691
1775
  */
1692
1776
  export async function bridgeGetVectorSearchStatus(dbPath) {
1777
+ if (!operationContext.getStore()?.active)
1778
+ return withBridgeOperation(() => bridgeGetVectorSearchStatus(dbPath));
1693
1779
  const registry = await getRegistry(dbPath);
1694
1780
  if (!registry)
1695
1781
  return null;
@@ -1724,6 +1810,8 @@ export async function bridgeGetVectorSearchStatus(dbPath) {
1724
1810
  * null if unavailable.
1725
1811
  */
1726
1812
  export async function bridgeSearchBruteForceCosine(queryEmbedding, options, dbPath) {
1813
+ if (!operationContext.getStore()?.active)
1814
+ return withBridgeOperation(() => bridgeSearchBruteForceCosine(queryEmbedding, options, dbPath));
1727
1815
  const registry = await getRegistry(dbPath);
1728
1816
  if (!registry)
1729
1817
  return null;
@@ -1744,7 +1832,7 @@ export async function bridgeSearchBruteForceCosine(queryEmbedding, options, dbPa
1744
1832
  const stmt = ctx.db.prepare(`
1745
1833
  SELECT id, key, namespace, content, embedding
1746
1834
  FROM memory_entries
1747
- WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
1835
+ WHERE ${liveMemoryRowSql()} AND embedding IS NOT NULL ${nsFilter}
1748
1836
  ORDER BY updated_at DESC
1749
1837
  LIMIT 10000
1750
1838
  `);
@@ -1790,6 +1878,8 @@ export async function bridgeSearchBruteForceCosine(queryEmbedding, options, dbPa
1790
1878
  * brute-force scan. Returns null if unavailable.
1791
1879
  */
1792
1880
  export async function bridgeAddEmbedding(id, embedding, entry, dbPath) {
1881
+ if (!operationContext.getStore()?.active)
1882
+ return withBridgeOperation(() => bridgeAddEmbedding(id, embedding, entry, dbPath));
1793
1883
  const registry = await getRegistry(dbPath);
1794
1884
  if (!registry)
1795
1885
  return null;
@@ -1828,9 +1918,12 @@ export async function bridgeAddEmbedding(id, embedding, entry, dbPath) {
1828
1918
  // ===== Phase 4: Controller access =====
1829
1919
  /**
1830
1920
  * Get a named controller from AgentDB v3 via ControllerRegistry.
1921
+ * This borrowed handle must not be retained across shutdownBridge().
1831
1922
  * Returns null if unavailable.
1832
1923
  */
1833
1924
  export async function bridgeGetController(name, dbPath) {
1925
+ if (!operationContext.getStore()?.active)
1926
+ return withBridgeOperation(() => bridgeGetController(name, dbPath));
1834
1927
  const registry = await getRegistry(dbPath);
1835
1928
  if (!registry)
1836
1929
  return null;
@@ -1845,6 +1938,8 @@ export async function bridgeGetController(name, dbPath) {
1845
1938
  * Check if a controller is available.
1846
1939
  */
1847
1940
  export async function bridgeHasController(name, dbPath) {
1941
+ if (!operationContext.getStore()?.active)
1942
+ return withBridgeOperation(() => bridgeHasController(name, dbPath));
1848
1943
  const registry = await getRegistry(dbPath);
1849
1944
  if (!registry)
1850
1945
  return false;
@@ -1860,6 +1955,8 @@ export async function bridgeHasController(name, dbPath) {
1860
1955
  * List all controllers and their status.
1861
1956
  */
1862
1957
  export async function bridgeListControllers(dbPath) {
1958
+ if (!operationContext.getStore()?.active)
1959
+ return withBridgeOperation(() => bridgeListControllers(dbPath));
1863
1960
  const registry = await getRegistry(dbPath);
1864
1961
  if (!registry)
1865
1962
  return null;
@@ -1874,22 +1971,26 @@ export async function bridgeListControllers(dbPath) {
1874
1971
  * Check if the AgentDB v3 bridge is available.
1875
1972
  */
1876
1973
  export async function isBridgeAvailable(dbPath) {
1877
- if (bridgeAvailable !== null)
1878
- return bridgeAvailable;
1974
+ if (!operationContext.getStore()?.active)
1975
+ return withBridgeOperation(() => isBridgeAvailable(dbPath));
1879
1976
  const registry = await getRegistry(dbPath);
1880
1977
  return registry !== null;
1881
1978
  }
1882
1979
  /**
1883
1980
  * Get the ControllerRegistry instance (for advanced consumers).
1981
+ * This is a borrowed handle, valid only until shutdownBridge(). Callers must
1982
+ * finish using it before requesting shutdown; bridge operations own their own
1983
+ * leases. Prefer bridgeListControllers() for status inspection.
1884
1984
  */
1885
1985
  export async function getControllerRegistry(dbPath) {
1986
+ if (!operationContext.getStore()?.active)
1987
+ return withBridgeOperation(() => getControllerRegistry(dbPath));
1886
1988
  return getRegistry(dbPath);
1887
1989
  }
1888
1990
  /**
1889
1991
  * Why the bridge last declined a write, or null when it has not.
1890
1992
  *
1891
- * Deliberately NOT gated on `bridgeAvailable === false`. A bridge that
1892
- * initialised fine can still fail every write — a schema mismatch throws
1993
+ * A bridge that initialised fine can still fail every write — a schema mismatch throws
1893
1994
  * per-operation while the registry stays healthy — and that case is exactly
1894
1995
  * the one worth reporting, since the caller then demotes to a fallback whose
1895
1996
  * error message describes something else entirely.
@@ -1897,8 +1998,13 @@ export async function getControllerRegistry(dbPath) {
1897
1998
  * Callers that surface a degraded-path error should include this so the
1898
1999
  * operator learns the cause instead of only the symptom.
1899
2000
  */
1900
- export function getBridgeFailureReason() {
1901
- return bridgeFailureReason;
2001
+ export function getBridgeFailureReason(dbPath) {
2002
+ if (shouldDisableNativeBridge()) {
2003
+ return process.platform === 'win32'
2004
+ ? 'AgentDB native bridge disabled on Windows after #3024; set CLAUDE_FLOW_ENABLE_NATIVE_BRIDGE_ON_WINDOWS=1 to opt in'
2005
+ : 'AgentDB native bridge disabled by CLAUDE_FLOW_DISABLE_BRIDGE=1';
2006
+ }
2007
+ return bridgeFailureReasons.get(canonicalDbPath(dbPath)) ?? null;
1902
2008
  }
1903
2009
  /**
1904
2010
  * Install a pre-initialized registry for deterministic bridge tests.
@@ -1913,34 +2019,37 @@ export function __setMemoryBridgeRegistryForTests(registry) {
1913
2019
  registryPromises.clear();
1914
2020
  registryInstances.clear();
1915
2021
  testRegistryOverride = registry;
1916
- bridgeAvailable = registry ? true : null;
1917
- bridgeFailureReason = null;
2022
+ testRegistryFactory = null;
2023
+ bridgeFailureReasons.clear();
1918
2024
  }
1919
2025
  /**
1920
2026
  * Shutdown the bridge and release resources.
1921
2027
  *
1922
2028
  * The cached state is cleared unconditionally. Previously the reset lived
1923
- * inside `if (registryInstance)`, so it could not clear a FAILED init — the
1924
- * one state that actually needs clearing, since `registryInstance` is null
1925
- * precisely when init failed. A process that latched `bridgeAvailable = false`
1926
- * therefore had no recovery path short of a restart.
2029
+ * inside `if (registryInstance)`, so it could not clear a FAILED init. This
2030
+ * also clears per-path failed promises, allowing a later retry.
1927
2031
  */
1928
- export async function shutdownBridge() {
1929
- // #3196: every cached registry owns an open database handle, so shutting down
1930
- // one of several would leave the rest holding files open.
1931
- for (const registry of registryInstances.values()) {
1932
- try {
1933
- await registry.shutdown();
1934
- }
1935
- catch {
1936
- // Best-effort
1937
- }
1938
- }
1939
- registryInstances.clear();
1940
- registryPromises.clear();
1941
- testRegistryOverride = null;
1942
- bridgeAvailable = null;
1943
- bridgeFailureReason = null;
2032
+ export function shutdownBridge() {
2033
+ if (shutdownPromise)
2034
+ return shutdownPromise;
2035
+ // Publish the barrier before any await: no later operation can join the
2036
+ // retiring lifecycle. Concurrent callers share the same retirement promise.
2037
+ shutdownPromise = (async () => {
2038
+ if (activeOperations > 0)
2039
+ await new Promise(resolve => { drained = resolve; });
2040
+ await Promise.allSettled([...registryPromises.values()]);
2041
+ for (const registry of new Set(registryInstances.values())) {
2042
+ try {
2043
+ await registry.shutdown();
2044
+ }
2045
+ catch { /* best-effort cleanup */ }
2046
+ }
2047
+ registryInstances.clear();
2048
+ registryPromises.clear();
2049
+ testRegistryOverride = null;
2050
+ bridgeFailureReasons.clear();
2051
+ })().finally(() => { shutdownPromise = null; });
2052
+ return shutdownPromise;
1944
2053
  }
1945
2054
  // ===== Phase 3: ReasoningBank pattern operations =====
1946
2055
  /**
@@ -1948,6 +2057,8 @@ export async function shutdownBridge() {
1948
2057
  * Falls back to raw SQL if ReasoningBank unavailable.
1949
2058
  */
1950
2059
  export async function bridgeStorePattern(options) {
2060
+ if (!operationContext.getStore()?.active)
2061
+ return withBridgeOperation(() => bridgeStorePattern(options));
1951
2062
  const registry = await getRegistry(options.dbPath);
1952
2063
  if (!registry)
1953
2064
  return null;
@@ -2028,7 +2139,7 @@ export async function bridgeStorePattern(options) {
2028
2139
  // discarded and the caller saw an ordinary fallback, indistinguishable
2029
2140
  // from "no controller registered". Record it so `agentdb_health` and the
2030
2141
  // degraded `reason` can name the real cause instead of guessing.
2031
- bridgeFailureReason = err instanceof Error ? err.message : String(err);
2142
+ bridgeFailureReasons.set(canonicalDbPath(options.dbPath), err instanceof Error ? err.message : String(err));
2032
2143
  return null;
2033
2144
  }
2034
2145
  }
@@ -2036,6 +2147,8 @@ export async function bridgeStorePattern(options) {
2036
2147
  * Search patterns via ReasoningBank controller.
2037
2148
  */
2038
2149
  export async function bridgeSearchPatterns(options) {
2150
+ if (!operationContext.getStore()?.active)
2151
+ return withBridgeOperation(() => bridgeSearchPatterns(options));
2039
2152
  const registry = await getRegistry(options.dbPath);
2040
2153
  if (!registry)
2041
2154
  return null;
@@ -2120,7 +2233,7 @@ export async function bridgeSearchPatterns(options) {
2120
2233
  // function` died here silently, which is why search reported
2121
2234
  // `reasoningBank-unavailable:registry-null` (a null return) even though
2122
2235
  // the registry was present and the controller was reported enabled.
2123
- bridgeFailureReason = err instanceof Error ? err.message : String(err);
2236
+ bridgeFailureReasons.set(canonicalDbPath(options.dbPath), err instanceof Error ? err.message : String(err));
2124
2237
  return null;
2125
2238
  }
2126
2239
  }
@@ -2130,6 +2243,9 @@ export async function bridgeSearchPatterns(options) {
2130
2243
  * Wired into hooks_post-task handler.
2131
2244
  */
2132
2245
  export async function bridgeRecordFeedback(options) {
2246
+ if (!operationContext.getStore()?.active)
2247
+ return withBridgeOperation(() => bridgeRecordFeedback(options));
2248
+ const patterns = validateFeedbackPatterns(options.patterns);
2133
2249
  const registry = await getRegistry(options.dbPath);
2134
2250
  if (!registry)
2135
2251
  return null;
@@ -2182,8 +2298,8 @@ export async function bridgeRecordFeedback(options) {
2182
2298
  // add them to LocalReasoningBank via its real `.store()` method (the
2183
2299
  // one method that DOES exist on the class).
2184
2300
  const reasoningBank = registry.get('reasoningBank');
2185
- if (reasoningBank && Array.isArray(options.patterns) && options.patterns.length) {
2186
- for (const pattern of options.patterns) {
2301
+ if (reasoningBank && Array.isArray(patterns) && patterns.length) {
2302
+ for (const pattern of patterns) {
2187
2303
  try {
2188
2304
  if (typeof reasoningBank.store === 'function') {
2189
2305
  reasoningBank.store({
@@ -2203,18 +2319,29 @@ export async function bridgeRecordFeedback(options) {
2203
2319
  else if (updated > 0 && controller === 'none')
2204
2320
  controller = 'reasoningBank';
2205
2321
  }
2206
- // Phase 4: SkillLibrary promotion for high-quality patterns
2207
- if (options.success && options.quality >= 0.9 && options.patterns?.length) {
2322
+ // SkillLibrary's public API is createSkill(Skill), not promote().
2323
+ if (options.success && options.quality >= 0.9 && patterns?.length) {
2208
2324
  const skills = registry.get('skills');
2209
- if (skills && typeof skills.promote === 'function') {
2210
- for (const pattern of options.patterns) {
2325
+ let created = 0;
2326
+ if (skills && typeof skills.createSkill === 'function') {
2327
+ for (const [index, pattern] of patterns.entries()) {
2211
2328
  try {
2212
- await skills.promote(pattern, options.quality);
2329
+ await skills.createSkill({
2330
+ name: `feedback-${options.taskId}-${index}`,
2331
+ description: pattern,
2332
+ successRate: 1,
2333
+ uses: 1,
2334
+ avgReward: options.quality,
2335
+ avgLatencyMs: options.duration,
2336
+ metadata: { taskId: options.taskId, task: options.task, agent: options.agent, source: 'bridge-feedback' },
2337
+ });
2213
2338
  updated++;
2339
+ created++;
2214
2340
  }
2215
- catch { /* skip */ }
2341
+ catch { /* failed promotion must not count as an update */ }
2216
2342
  }
2217
- controller += '+skills';
2343
+ if (created > 0)
2344
+ controller = controller === 'none' ? 'skills' : `${controller}+skills`;
2218
2345
  }
2219
2346
  }
2220
2347
  // Always store feedback as a memory entry for retrieval (ensures it persists)
@@ -2240,6 +2367,8 @@ export async function bridgeRecordFeedback(options) {
2240
2367
  * Record a causal edge between two entries (e.g., task → result).
2241
2368
  */
2242
2369
  export async function bridgeRecordCausalEdge(options) {
2370
+ if (!operationContext.getStore()?.active)
2371
+ return withBridgeOperation(() => bridgeRecordCausalEdge(options));
2243
2372
  const registry = await getRegistry(options.dbPath);
2244
2373
  if (!registry)
2245
2374
  return null;
@@ -2287,6 +2416,8 @@ export async function bridgeRecordCausalEdge(options) {
2287
2416
  * limitation honestly instead of silently returning success.
2288
2417
  */
2289
2418
  export async function bridgeDeleteHierarchical(options) {
2419
+ if (!operationContext.getStore()?.active)
2420
+ return withBridgeOperation(() => bridgeDeleteHierarchical(options));
2290
2421
  const registry = await getRegistry(options.dbPath);
2291
2422
  if (!registry)
2292
2423
  return null;
@@ -2378,6 +2509,8 @@ export async function bridgeDeleteHierarchical(options) {
2378
2509
  * surface that explicitly via controller: 'native-unsupported'.
2379
2510
  */
2380
2511
  export async function bridgeDeleteCausalEdge(options) {
2512
+ if (!operationContext.getStore()?.active)
2513
+ return withBridgeOperation(() => bridgeDeleteCausalEdge(options));
2381
2514
  const registry = await getRegistry(options.dbPath);
2382
2515
  if (!registry)
2383
2516
  return null;
@@ -2451,6 +2584,8 @@ export async function bridgeDeleteCausalEdge(options) {
2451
2584
  * memory_entries row) and every edge whose key contains the nodeId.
2452
2585
  */
2453
2586
  export async function bridgeDeleteCausalNode(options) {
2587
+ if (!operationContext.getStore()?.active)
2588
+ return withBridgeOperation(() => bridgeDeleteCausalNode(options));
2454
2589
  const registry = await getRegistry(options.dbPath);
2455
2590
  if (!registry)
2456
2591
  return null;
@@ -2530,6 +2665,8 @@ export async function bridgeDeleteCausalNode(options) {
2530
2665
  * Loads relevant past session patterns for the new session.
2531
2666
  */
2532
2667
  export async function bridgeSessionStart(options) {
2668
+ if (!operationContext.getStore()?.active)
2669
+ return withBridgeOperation(() => bridgeSessionStart(options));
2533
2670
  const registry = await getRegistry(options.dbPath);
2534
2671
  if (!registry)
2535
2672
  return null;
@@ -2568,6 +2705,8 @@ export async function bridgeSessionStart(options) {
2568
2705
  * End a session and persist episodic summary to ReflexionMemory.
2569
2706
  */
2570
2707
  export async function bridgeSessionEnd(options) {
2708
+ if (!operationContext.getStore()?.active)
2709
+ return withBridgeOperation(() => bridgeSessionEnd(options));
2571
2710
  const registry = await getRegistry(options.dbPath);
2572
2711
  if (!registry)
2573
2712
  return null;
@@ -2624,6 +2763,8 @@ export async function bridgeSessionEnd(options) {
2624
2763
  * Returns null to fall back to local ruvector router.
2625
2764
  */
2626
2765
  export async function bridgeRouteTask(options) {
2766
+ if (!operationContext.getStore()?.active)
2767
+ return withBridgeOperation(() => bridgeRouteTask(options));
2627
2768
  const registry = await getRegistry(options.dbPath);
2628
2769
  if (!registry)
2629
2770
  return null;
@@ -2665,6 +2806,8 @@ export async function bridgeRouteTask(options) {
2665
2806
  * Get comprehensive bridge health including all controller statuses.
2666
2807
  */
2667
2808
  export async function bridgeHealthCheck(dbPath) {
2809
+ if (!operationContext.getStore()?.active)
2810
+ return withBridgeOperation(() => bridgeHealthCheck(dbPath));
2668
2811
  const registry = await getRegistry(dbPath);
2669
2812
  if (!registry)
2670
2813
  return null;
@@ -2748,6 +2891,8 @@ function getHierarchicalFallback(registry) {
2748
2891
  : null;
2749
2892
  }
2750
2893
  export async function bridgeHierarchicalStore(params) {
2894
+ if (!operationContext.getStore()?.active)
2895
+ return withBridgeOperation(() => bridgeHierarchicalStore(params));
2751
2896
  const registry = await getRegistry();
2752
2897
  if (!registry)
2753
2898
  return null;
@@ -2818,6 +2963,8 @@ export async function bridgeHierarchicalStore(params) {
2818
2963
  * recall(query: string, topK: number) → synchronous array
2819
2964
  */
2820
2965
  export async function bridgeHierarchicalRecall(params) {
2966
+ if (!operationContext.getStore()?.active)
2967
+ return withBridgeOperation(() => bridgeHierarchicalRecall(params));
2821
2968
  const registry = await getRegistry();
2822
2969
  if (!registry)
2823
2970
  return null;
@@ -2873,16 +3020,15 @@ export async function bridgeHierarchicalRecall(params) {
2873
3020
  return { results: [], error: e.message };
2874
3021
  }
2875
3022
  }
2876
- /**
2877
- * Run memory consolidation.
2878
- *
2879
- * Real MemoryConsolidation API (agentdb alpha.10+):
2880
- * consolidate() → Promise<ConsolidationReport>
2881
- * ConsolidationReport = { episodicProcessed, semanticCreated, memoriesForgotten, ... }
2882
- * Stub API (fallback):
2883
- * consolidate() → { promoted, pruned, timestamp }
2884
- */
3023
+ /** Run consolidation when a real controller is available. */
2885
3024
  export async function bridgeConsolidate(params) {
3025
+ if (!operationContext.getStore()?.active)
3026
+ return withBridgeOperation(() => bridgeConsolidate(params));
3027
+ // No installed consolidation API accepts these historical options. Reject
3028
+ // them explicitly rather than silently claiming their limits were applied.
3029
+ if (params.minAge !== undefined || params.maxEntries !== undefined) {
3030
+ return { success: false, status: 'unsupported', error: 'minAge and maxEntries are not supported by the consolidation controller' };
3031
+ }
2886
3032
  const registry = await getRegistry();
2887
3033
  if (!registry)
2888
3034
  return null;
@@ -2891,6 +3037,9 @@ export async function bridgeConsolidate(params) {
2891
3037
  if (!mc)
2892
3038
  return { success: false, error: 'MemoryConsolidation not available' };
2893
3039
  const result = await mc.consolidate();
3040
+ if (result?.source === 'stub') {
3041
+ return { success: false, status: 'unsupported', error: 'Memory consolidation is unavailable; no consolidation ran', consolidated: result };
3042
+ }
2894
3043
  return { success: true, consolidated: result };
2895
3044
  }
2896
3045
  catch (e) {
@@ -2904,6 +3053,8 @@ export async function bridgeConsolidate(params) {
2904
3053
  * - update: calls bulkUpdate(table, updates, conditions) on episodes table
2905
3054
  */
2906
3055
  export async function bridgeBatchOperation(params) {
3056
+ if (!operationContext.getStore()?.active)
3057
+ return withBridgeOperation(() => bridgeBatchOperation(params));
2907
3058
  const registry = await getRegistry();
2908
3059
  if (!registry)
2909
3060
  return null;
@@ -2957,45 +3108,102 @@ export async function bridgeBatchOperation(params) {
2957
3108
  return { success: false, error: e.message };
2958
3109
  }
2959
3110
  }
2960
- /**
2961
- * Synthesize context from memories.
2962
- * ContextSynthesizer.synthesize is a static method that takes MemoryPattern[] (not a string).
2963
- */
3111
+ function contextEpisodeFrom(value) {
3112
+ if (typeof value === 'string') {
3113
+ try {
3114
+ value = JSON.parse(value);
3115
+ }
3116
+ catch {
3117
+ return null;
3118
+ }
3119
+ }
3120
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3121
+ return null;
3122
+ const record = value;
3123
+ const owns = (key) => Object.prototype.hasOwnProperty.call(record, key);
3124
+ if (!owns('task') || !owns('reward') || !owns('success')
3125
+ || typeof record.task !== 'string' || !record.task.trim()
3126
+ || typeof record.reward !== 'number' || !Number.isFinite(record.reward)
3127
+ || typeof record.success !== 'boolean')
3128
+ return null;
3129
+ const episode = {
3130
+ task: record.task.trim(),
3131
+ reward: record.reward,
3132
+ success: record.success,
3133
+ };
3134
+ for (const field of ['critique', 'input', 'output']) {
3135
+ if (owns(field)) {
3136
+ if (typeof record[field] !== 'string')
3137
+ return null;
3138
+ episode[field] = record[field];
3139
+ }
3140
+ }
3141
+ return episode;
3142
+ }
3143
+ function contextEpisodeFromRecall(row) {
3144
+ if (!row || typeof row !== 'object' || Array.isArray(row))
3145
+ return null;
3146
+ const record = row;
3147
+ for (const field of [null, 'metadata', 'value', 'content']) {
3148
+ const candidate = field === null ? record
3149
+ : Object.prototype.hasOwnProperty.call(record, field) ? record[field] : undefined;
3150
+ const episode = contextEpisodeFrom(candidate);
3151
+ if (episode)
3152
+ return episode;
3153
+ }
3154
+ return null;
3155
+ }
2964
3156
  export async function bridgeContextSynthesize(params) {
3157
+ if (!operationContext.getStore()?.active)
3158
+ return withBridgeOperation(() => bridgeContextSynthesize(params));
2965
3159
  const registry = await getRegistry();
2966
3160
  if (!registry)
2967
3161
  return null;
2968
3162
  try {
2969
3163
  const CS = registry.get('contextSynthesizer');
2970
3164
  if (!CS || typeof CS.synthesize !== 'function') {
2971
- return { success: false, error: 'ContextSynthesizer not available' };
3165
+ return { success: false, reason: 'synthesizer-unavailable', error: 'ContextSynthesizer not available' };
2972
3166
  }
2973
- // Gather memory patterns from hierarchical memory as input
2974
3167
  const hm = registry.get('hierarchicalMemory');
2975
- let memories = [];
2976
- if (hm && typeof hm.recall === 'function') {
2977
- // Detect real HierarchicalMemory (MemoryQuery object) vs stub (string, number)
2978
- let recalled;
2979
- if (typeof hm.promote === 'function') {
2980
- // Real agentdb HierarchicalMemory
2981
- recalled = await hm.recall({ query: params.query, k: params.maxEntries || 10 });
2982
- }
2983
- else {
2984
- // Stub
2985
- recalled = hm.recall(params.query, params.maxEntries || 10);
3168
+ if (!hm || typeof hm.recall !== 'function') {
3169
+ return { success: false, reason: 'memory-unavailable', error: 'HierarchicalMemory not available' };
3170
+ }
3171
+ let recalled;
3172
+ try {
3173
+ // Native HierarchicalMemory accepts a query object; the tiered fallback
3174
+ // accepts positional arguments. Either implementation may be async.
3175
+ recalled = typeof hm.promote === 'function'
3176
+ ? await hm.recall({ query: params.query, k: params.maxEntries || 10 })
3177
+ : await hm.recall(params.query, params.maxEntries || 10);
3178
+ }
3179
+ catch (e) {
3180
+ return { success: false, reason: 'recall-failed', error: e.message };
3181
+ }
3182
+ if (!Array.isArray(recalled)) {
3183
+ return { success: false, reason: 'invalid-recall-result', error: 'HierarchicalMemory recall did not return an array' };
3184
+ }
3185
+ const recalledCount = recalled.length;
3186
+ if (recalledCount === 0) {
3187
+ return { success: false, reason: 'no-memories', error: 'No memories matched the query', recalled: 0, eligible: 0, skipped: 0 };
3188
+ }
3189
+ const memories = recalled.map(contextEpisodeFromRecall).filter((episode) => episode !== null);
3190
+ const counts = { recalled: recalledCount, eligible: memories.length, skipped: recalledCount - memories.length };
3191
+ if (memories.length === 0) {
3192
+ return { success: false, reason: 'no-eligible-episodes', error: 'Recalled memories have no validated task outcomes', ...counts };
3193
+ }
3194
+ try {
3195
+ const result = await CS.synthesize(memories, { includeRecommendations: true });
3196
+ if (!result || typeof result !== 'object') {
3197
+ return { success: false, reason: 'invalid-synthesis', error: 'ContextSynthesizer returned no result', ...counts };
2986
3198
  }
2987
- memories = (recalled || []).map((r) => ({
2988
- content: r.value || r.content || '',
2989
- key: r.key || r.id || '',
2990
- reward: 1,
2991
- verdict: 'success',
2992
- }));
3199
+ return { success: true, synthesis: result, ...counts };
3200
+ }
3201
+ catch (e) {
3202
+ return { success: false, reason: 'synthesizer-failed', error: e.message, ...counts };
2993
3203
  }
2994
- const result = CS.synthesize(memories, { includeRecommendations: true });
2995
- return { success: true, synthesis: result };
2996
3204
  }
2997
3205
  catch (e) {
2998
- return { success: false, error: e.message };
3206
+ return { success: false, reason: 'bridge-error', error: e.message };
2999
3207
  }
3000
3208
  }
3001
3209
  /**
@@ -3004,6 +3212,8 @@ export async function bridgeContextSynthesize(params) {
3004
3212
  * semantic matching with keyword fallback.
3005
3213
  */
3006
3214
  export async function bridgeSemanticRoute(params) {
3215
+ if (!operationContext.getStore()?.active)
3216
+ return withBridgeOperation(() => bridgeSemanticRoute(params));
3007
3217
  const registry = await getRegistry();
3008
3218
  if (!registry)
3009
3219
  return null;
@@ -3034,6 +3244,8 @@ export async function bridgeSemanticRoute(params) {
3034
3244
  * Returns null if bridge is unavailable (caller falls back to sql.js).
3035
3245
  */
3036
3246
  export async function bridgeGetAllEmbeddings(options) {
3247
+ if (!operationContext.getStore()?.active)
3248
+ return withBridgeOperation(() => bridgeGetAllEmbeddings(options));
3037
3249
  const registry = await getRegistry(options?.dbPath);
3038
3250
  if (!registry)
3039
3251
  return null;
@@ -3046,7 +3258,7 @@ export async function bridgeGetAllEmbeddings(options) {
3046
3258
  const rows = ctx.db.prepare(`
3047
3259
  SELECT id, key, namespace, embedding
3048
3260
  FROM memory_entries
3049
- WHERE status = 'active' AND embedding IS NOT NULL
3261
+ WHERE ${liveMemoryRowSql()} AND embedding IS NOT NULL
3050
3262
  LIMIT ?
3051
3263
  `).all(maxRows);
3052
3264
  const results = [];
@@ -3094,6 +3306,8 @@ function cosineSim(a, b) {
3094
3306
  * stores honestly. (#2245 follow-up.)
3095
3307
  */
3096
3308
  export async function getMemoryBridgeStats(options = {}) {
3309
+ if (!operationContext.getStore()?.active)
3310
+ return withBridgeOperation(() => getMemoryBridgeStats(options));
3097
3311
  const namespaces = options.namespaces ?? [
3098
3312
  'default', 'patterns', 'claude-memories', 'auto-memory',
3099
3313
  'tasks', 'feedback', 'pretrain', 'trajectories',