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.
- package/.claude/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +6 -3
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +0 -2
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
- package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
- package/v3/@claude-flow/cli/package.json +9 -9
- package/v3/@claude-flow/guidance/package.json +5 -6
- 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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
332
|
+
// SkillLibrary requires the same database and passage embedder as AgentDB.
|
|
293
333
|
try {
|
|
294
334
|
const SkillCtor = agentdb.SkillLibrary;
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
477
|
-
bridgeFailureReason = null;
|
|
518
|
+
bridgeFailureReasons.delete(resolvedPath);
|
|
478
519
|
return registry;
|
|
479
520
|
}
|
|
480
521
|
catch (err) {
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
1584
|
-
|
|
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
|
|
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 (
|
|
1878
|
-
return
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
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
|
|
1924
|
-
*
|
|
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
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2186
|
-
for (const pattern of
|
|
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
|
-
//
|
|
2207
|
-
if (options.success && options.quality >= 0.9 &&
|
|
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
|
-
|
|
2210
|
-
|
|
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.
|
|
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 { /*
|
|
2341
|
+
catch { /* failed promotion must not count as an update */ }
|
|
2216
2342
|
}
|
|
2217
|
-
|
|
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
|
-
|
|
2962
|
-
|
|
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
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
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
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
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
|
|
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',
|