@claude-flow/cli 3.42.5 → 3.44.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.
@@ -61,6 +61,26 @@ function validateMemoryInput(key, value, query, namespace) {
61
61
  throw new Error('Namespace contains disallowed characters');
62
62
  }
63
63
  }
64
+ /**
65
+ * #3374 — presence check for a schema-`required` string parameter.
66
+ *
67
+ * `validateMemoryInput` above is a bounds-and-charset validator: every branch
68
+ * is truthiness-guarded, so an omitted parameter passes it silently. Nothing
69
+ * else enforces `inputSchema.required` for these tools, so without this an
70
+ * omitted `query` travelled down to `generateHashEmbedding`'s
71
+ * `text.toLowerCase()` and came back as an unrelated TypeError.
72
+ */
73
+ const MISSING_REQUIRED_PARAM = 'MISSING_REQUIRED_PARAM';
74
+ function missingRequiredString(input, param, tool) {
75
+ const v = input[param];
76
+ if (typeof v === 'string' && v.length > 0)
77
+ return null;
78
+ const got = v === undefined ? 'it was omitted' : v === '' ? 'it was an empty string' : `got ${v === null ? 'null' : typeof v}`;
79
+ return {
80
+ error: `${tool}: required parameter "${param}" must be a non-empty string (${got})`,
81
+ code: MISSING_REQUIRED_PARAM,
82
+ };
83
+ }
64
84
  // #1884 — sanitize a key produced from arbitrary input (markdown headings,
65
85
  // frontmatter names, file names) so it survives validateMemoryInput on the
66
86
  // read/delete path. Replaces every dangerous char with `_`. Truncates to
@@ -360,6 +380,10 @@ export const memoryTools = [
360
380
  required: ['key', 'value'],
361
381
  },
362
382
  handler: async (input) => {
383
+ const missingKey = missingRequiredString(input, 'key', 'memory_store');
384
+ if (missingKey) {
385
+ return { success: false, key: input.key, stored: false, hasEmbedding: false, ...missingKey };
386
+ }
363
387
  await ensureInitialized();
364
388
  const { storeEntry } = await getMemoryFunctions();
365
389
  const key = input.key;
@@ -408,6 +432,8 @@ export const memoryTools = [
408
432
  backend: await describeBackend(),
409
433
  storeTime: `${duration.toFixed(2)}ms`,
410
434
  error: result.error,
435
+ // #3325: why hasEmbedding is false, when the bridge could not embed.
436
+ ...(result.embeddingError ? { embeddingError: result.embeddingError } : {}),
411
437
  };
412
438
  }
413
439
  catch (error) {
@@ -432,6 +458,10 @@ export const memoryTools = [
432
458
  required: ['key'],
433
459
  },
434
460
  handler: async (input) => {
461
+ const missingKey = missingRequiredString(input, 'key', 'memory_retrieve');
462
+ if (missingKey) {
463
+ return { key: input.key, namespace: input.namespace, value: null, found: false, ...missingKey };
464
+ }
435
465
  await ensureInitialized();
436
466
  const { getEntry } = await getMemoryFunctions();
437
467
  const key = input.key;
@@ -500,6 +530,10 @@ export const memoryTools = [
500
530
  required: ['query'],
501
531
  },
502
532
  handler: async (input) => {
533
+ const missingQuery = missingRequiredString(input, 'query', 'memory_search');
534
+ if (missingQuery) {
535
+ return { query: input.query, results: [], total: 0, ...missingQuery };
536
+ }
503
537
  await ensureInitialized();
504
538
  const { searchEntries } = await getMemoryFunctions();
505
539
  const query = input.query;
@@ -664,6 +698,10 @@ export const memoryTools = [
664
698
  required: ['key'],
665
699
  },
666
700
  handler: async (input) => {
701
+ const missingKey = missingRequiredString(input, 'key', 'memory_delete');
702
+ if (missingKey) {
703
+ return { success: false, key: input.key, namespace: input.namespace, deleted: false, ...missingKey };
704
+ }
667
705
  await ensureInitialized();
668
706
  const { deleteEntry } = await getMemoryFunctions();
669
707
  const key = input.key;
@@ -1145,6 +1183,10 @@ export const memoryTools = [
1145
1183
  required: ['query'],
1146
1184
  },
1147
1185
  handler: async (input) => {
1186
+ const missingQuery = missingRequiredString(input, 'query', 'memory_search_unified');
1187
+ if (missingQuery) {
1188
+ return { success: false, query: input.query, results: [], total: 0, ...missingQuery };
1189
+ }
1148
1190
  await ensureInitialized();
1149
1191
  const { searchEntries, listEntries } = await getMemoryFunctions();
1150
1192
  validateMemoryInput(undefined, undefined, input.query);
@@ -29,6 +29,16 @@
29
29
  *
30
30
  * @module v3/cli/memory/graph-edge-writer
31
31
  */
32
+ /**
33
+ * Checkpoint and close the cached handle if it is open (optionally only if it
34
+ * is open on `dbPath`). Returns true if a handle was released. Never throws.
35
+ *
36
+ * busy_timeout is dropped to 0 first so the TRUNCATE checkpoint cannot stall
37
+ * the event loop behind another connection: if another native connection is
38
+ * attached the sidecars stay anyway (it owns them), so a busy checkpoint loses
39
+ * nothing; if this is the only connection, the checkpoint is never busy.
40
+ */
41
+ export declare function releaseBridgeDb(dbPath?: string): boolean;
32
42
  /**
33
43
  * Return the better-sqlite3 Database instance for graph_edges writes.
34
44
  * Creates the graph_edges table if it is absent (idempotent).
@@ -40,6 +40,72 @@ import { encodeEmbedding } from './embedding-quantization.js';
40
40
  let _db = null;
41
41
  let _dbPath = '';
42
42
  let _dbInitializing = false;
43
+ // #3397 — the handle used to live for the whole MCP server process, keeping
44
+ // the -wal/-shm sidecars on disk forever; the #2735 guard then refused every
45
+ // later sql.js whole-image write (memory_store on Windows, where the native
46
+ // bridge is off by default). The handle is now released after a short idle
47
+ // window, and memory-initializer releases it on demand before its guard.
48
+ //
49
+ // Invariant this relies on: every caller of getBridgeDb() finishes using the
50
+ // returned handle synchronously after the await (no await between it and the
51
+ // last `db.` call), so a release can only land between operations. If a
52
+ // caller ever breaks that, it gets "database connection is not open", which
53
+ // every call site already catches.
54
+ const DEFAULT_IDLE_RELEASE_MS = 1000;
55
+ let _idleTimer = null;
56
+ let _exitHookInstalled = false;
57
+ function idleReleaseMs() {
58
+ const configured = Number(process.env.CLAUDE_FLOW_GRAPH_EDGE_IDLE_MS);
59
+ return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_IDLE_RELEASE_MS;
60
+ }
61
+ function armIdleRelease() {
62
+ if (_idleTimer)
63
+ clearTimeout(_idleTimer);
64
+ _idleTimer = setTimeout(() => { _idleTimer = null; releaseBridgeDb(); }, idleReleaseMs());
65
+ _idleTimer.unref?.();
66
+ if (!_exitHookInstalled) {
67
+ _exitHookInstalled = true;
68
+ // 'exit' (not SIGINT/SIGTERM handlers, which would change Node's default
69
+ // termination) — sync-only work, so a clean shutdown checkpoints and
70
+ // removes the sidecars instead of leaving them for the next process.
71
+ process.once('exit', () => { releaseBridgeDb(); });
72
+ }
73
+ }
74
+ /**
75
+ * Checkpoint and close the cached handle if it is open (optionally only if it
76
+ * is open on `dbPath`). Returns true if a handle was released. Never throws.
77
+ *
78
+ * busy_timeout is dropped to 0 first so the TRUNCATE checkpoint cannot stall
79
+ * the event loop behind another connection: if another native connection is
80
+ * attached the sidecars stay anyway (it owns them), so a busy checkpoint loses
81
+ * nothing; if this is the only connection, the checkpoint is never busy.
82
+ */
83
+ export function releaseBridgeDb(dbPath) {
84
+ if (!_db)
85
+ return false;
86
+ if (dbPath !== undefined && path.resolve(dbPath) !== path.resolve(_dbPath))
87
+ return false;
88
+ if (_idleTimer) {
89
+ clearTimeout(_idleTimer);
90
+ _idleTimer = null;
91
+ }
92
+ const db = _db;
93
+ _db = null;
94
+ _dbPath = '';
95
+ try {
96
+ db.pragma('busy_timeout = 0');
97
+ }
98
+ catch { /* best-effort */ }
99
+ try {
100
+ db.pragma('wal_checkpoint(TRUNCATE)');
101
+ }
102
+ catch { /* best-effort */ }
103
+ try {
104
+ db.close();
105
+ }
106
+ catch { /* best-effort */ }
107
+ return true;
108
+ }
43
109
  /**
44
110
  * Return the better-sqlite3 Database instance for graph_edges writes.
45
111
  * Creates the graph_edges table if it is absent (idempotent).
@@ -56,8 +122,10 @@ let _dbInitializing = false;
56
122
  export async function getBridgeDb(customDbPath, opts) {
57
123
  const dbPath = customDbPath ?? path.join(getMemoryRoot(), 'memory.db');
58
124
  const createIfMissing = opts?.createIfMissing === true;
59
- if (_db && _dbPath === dbPath)
125
+ if (_db && _dbPath === dbPath) {
126
+ armIdleRelease();
60
127
  return _db;
128
+ }
61
129
  if (_dbInitializing)
62
130
  return null;
63
131
  _dbInitializing = true;
@@ -120,8 +188,12 @@ export async function getBridgeDb(customDbPath, opts) {
120
188
  CREATE INDEX IF NOT EXISTS idx_graph_edges_relation ON graph_edges (relation);
121
189
  CREATE INDEX IF NOT EXISTS idx_graph_edges_reinforced ON graph_edges (last_reinforced);
122
190
  `);
191
+ // A handle cached for a different path would otherwise be orphaned
192
+ // (still open, sidecars still on disk) by the reassignment below.
193
+ releaseBridgeDb();
123
194
  _db = db;
124
195
  _dbPath = dbPath;
196
+ armIdleRelease();
125
197
  return db;
126
198
  }
127
199
  catch {
@@ -214,6 +286,10 @@ export async function countGraphEdges(dbPath) {
214
286
  * writes deterministically, regardless of platform/timing.
215
287
  */
216
288
  export function _resetBridgeDb() {
289
+ if (_idleTimer) {
290
+ clearTimeout(_idleTimer);
291
+ _idleTimer = null;
292
+ }
217
293
  if (_db) {
218
294
  try {
219
295
  _db.pragma('wal_checkpoint(TRUNCATE)');
@@ -86,6 +86,10 @@ export declare function bridgeStoreEntry(options: {
86
86
  * still true — this is advisory, not a failure — but callers should
87
87
  * surface it instead of only printing an unconditional success message. */
88
88
  persistWarning?: string;
89
+ /** #3325: set when an embedding was requested but none could be produced
90
+ * (agentdb embedder absent/threw AND no real local model). The row was
91
+ * written without a vector, so semantic search cannot find it. */
92
+ embeddingError?: string;
89
93
  } | null>;
90
94
  /**
91
95
  * Search entries via AgentDB v3.
@@ -337,6 +341,8 @@ export declare function bridgeStorePattern(options: {
337
341
  success: boolean;
338
342
  patternId: string;
339
343
  controller: string;
344
+ hasEmbedding?: boolean;
345
+ embeddingError?: string;
340
346
  } | null>;
341
347
  /**
342
348
  * Search patterns via ReasoningBank controller.
@@ -801,6 +801,52 @@ async function rescueAgentdbEmbedder(agentdb) {
801
801
  emb.__ruvectorRescued = true;
802
802
  _embedderPatched = true;
803
803
  }
804
+ /**
805
+ * #3325: embed text for a bridge row or query.
806
+ *
807
+ * AgentDB's embedder is tried first. If it is absent or throws, fall back to
808
+ * the LOCAL chain — `generateLocalEmbedding`, never the bridge-first
809
+ * `generateEmbedding`, which would re-enter this bridge (#2312). Only a real
810
+ * ONNX vector is accepted from the local chain; a hash vector would make the
811
+ * row look embedded while carrying no meaning. Previously both failure modes
812
+ * were a bare `catch {}` that stored embedding=NULL and reported nothing.
813
+ */
814
+ async function embedForBridge(agentdb, text) {
815
+ let agentdbProblem;
816
+ const embedder = agentdb?.embedder;
817
+ // Same mock signal bridgeGenerateEmbedding honours (AUDIT #3): the rescue
818
+ // tags a degraded embedder backend='mock' when it cannot replace it.
819
+ const agentdbIsMock = embedder?.isMock === true || embedder?.backend === 'mock';
820
+ if (agentdbIsMock) {
821
+ agentdbProblem = 'agentdb embedder is serving mock vectors';
822
+ }
823
+ else if (embedder && typeof embedder.embed === 'function') {
824
+ try {
825
+ const emb = await embedder.embed(text);
826
+ if (emb && emb.length > 0) {
827
+ return { vector: Array.from(emb), model: 'Xenova/all-MiniLM-L6-v2' };
828
+ }
829
+ agentdbProblem = 'agentdb embedder returned no vector';
830
+ }
831
+ catch (err) {
832
+ agentdbProblem = `agentdb embedder threw: ${err instanceof Error ? err.message : String(err)}`;
833
+ }
834
+ }
835
+ else {
836
+ agentdbProblem = 'agentdb embedder unavailable';
837
+ }
838
+ try {
839
+ const { generateLocalEmbedding } = await import('./memory-initializer.js');
840
+ const local = await generateLocalEmbedding(text);
841
+ if (local.backend === 'onnx' && local.embedding.length > 0) {
842
+ return { vector: Array.from(local.embedding), model: local.model };
843
+ }
844
+ return { vector: null, reason: `${agentdbProblem}; local embedding chain has no real model (backend=${local.backend})` };
845
+ }
846
+ catch (err) {
847
+ return { vector: null, reason: `${agentdbProblem}; local embedding chain failed: ${err instanceof Error ? err.message : String(err)}` };
848
+ }
849
+ }
804
850
  // ===== Bridge functions — match memory-initializer.ts signatures =====
805
851
  /**
806
852
  * Store an entry via AgentDB v3.
@@ -851,26 +897,24 @@ export async function bridgeStoreEntry(options) {
851
897
  if (!guardResult.allowed) {
852
898
  return { success: false, id, error: `MutationGuard rejected: ${guardResult.reason}` };
853
899
  }
854
- // Generate embedding via AgentDB's embedder
900
+ // Generate embedding via AgentDB's embedder, falling back to the local
901
+ // chain (#3325). If neither can produce one, the row is still written but
902
+ // the result says why instead of silently storing embedding=NULL.
855
903
  let embeddingJson = null;
856
904
  let embeddingArr = null;
857
905
  let dimensions = 0;
858
906
  let model = 'local';
907
+ let embeddingError;
859
908
  if (options.generateEmbeddingFlag !== false && value.length > 0) {
860
- try {
861
- const embedder = ctx.agentdb.embedder;
862
- if (embedder) {
863
- const emb = await embedder.embed(value);
864
- if (emb) {
865
- embeddingArr = Array.from(emb);
866
- embeddingJson = JSON.stringify(embeddingArr);
867
- dimensions = emb.length;
868
- model = 'Xenova/all-MiniLM-L6-v2';
869
- }
870
- }
909
+ const emb = await embedForBridge(ctx.agentdb, value);
910
+ if (emb.vector) {
911
+ embeddingArr = emb.vector;
912
+ embeddingJson = JSON.stringify(embeddingArr);
913
+ dimensions = embeddingArr.length;
914
+ model = emb.model;
871
915
  }
872
- catch {
873
- // Embedding failed — store without
916
+ else {
917
+ embeddingError = emb.reason;
874
918
  }
875
919
  }
876
920
  // #2775: strict-insert path now auto-resurrects soft-deleted tombstones
@@ -1011,6 +1055,7 @@ export async function bridgeStoreEntry(options) {
1011
1055
  cached: true,
1012
1056
  attested: true,
1013
1057
  ...(persistWarning ? { persistWarning } : {}),
1058
+ ...(embeddingError ? { embeddingError } : {}),
1014
1059
  };
1015
1060
  }
1016
1061
  catch (err) {
@@ -1061,18 +1106,10 @@ export async function bridgeSearchEntries(options) {
1061
1106
  const { query: queryStr, namespace, limit = 10, threshold = 0.3, provenanceFilter } = options;
1062
1107
  const effectiveNamespace = namespace || 'all';
1063
1108
  const startTime = Date.now();
1064
- // Generate query embedding
1065
- let queryEmbedding = null;
1066
- try {
1067
- const embedder = ctx.agentdb.embedder;
1068
- if (embedder) {
1069
- const emb = await embedder.embed(queryStr);
1070
- queryEmbedding = Array.from(emb);
1071
- }
1072
- }
1073
- catch {
1074
- // Fall back to keyword search
1075
- }
1109
+ // Generate query embedding — same agentdb-then-local chain as the write
1110
+ // path (#3325), so rows embedded by the local fallback are searchable.
1111
+ // No vector → keyword (BM25) search only.
1112
+ const queryEmbedding = (await embedForBridge(ctx.agentdb, queryStr)).vector;
1076
1113
  // better-sqlite3: .prepare().all() returns array of objects
1077
1114
  // ADR-323: compose namespace + provenance filters into one WHERE clause.
1078
1115
  const filters = [];
@@ -1300,7 +1337,10 @@ export async function bridgeGetEntry(options) {
1300
1337
  accessCount: cached.accessCount ?? 0,
1301
1338
  createdAt: cached.createdAt || new Date().toISOString(),
1302
1339
  updatedAt: cached.updatedAt || new Date().toISOString(),
1303
- hasEmbedding: !!cached.embedding,
1340
+ // #3325: the cache holds the `entry` object built below, which has
1341
+ // `hasEmbedding` but no `embedding` field — so `!!cached.embedding`
1342
+ // reported false on every cache hit, even for embedded rows.
1343
+ hasEmbedding: typeof cached.hasEmbedding === 'boolean' ? cached.hasEmbedding : !!cached.embedding,
1304
1344
  tags: cached.tags || [],
1305
1345
  },
1306
1346
  };
@@ -1888,7 +1928,15 @@ export async function bridgeStorePattern(options) {
1888
1928
  // was actually stored under. getEntry/memory_retrieve look up by `key`,
1889
1929
  // so returning result.id here handed the caller a handle that can never
1890
1930
  // be read back — return `patternId` (the real key) instead.
1891
- return { success: true, patternId, controller: 'bridge-fallback' };
1931
+ // #3325: say whether the row got a vector. Without one, Tier-1 (semantic)
1932
+ // pattern search cannot find it — report that here, at write time.
1933
+ return {
1934
+ success: true,
1935
+ patternId,
1936
+ controller: 'bridge-fallback',
1937
+ hasEmbedding: !!result.embedding,
1938
+ ...(result.embeddingError ? { embeddingError: result.embeddingError } : {}),
1939
+ };
1892
1940
  }
1893
1941
  catch (err) {
1894
1942
  // #3327 Finding A — this catch is what hid the defect for months. When
@@ -452,6 +452,9 @@ export declare function storeEntry(options: {
452
452
  /** #2968: set when the bridge's checkpoint failed in a way indicating
453
453
  * this write may not be durably persisted (sql.js fallback driver). */
454
454
  persistWarning?: string;
455
+ /** #3325: set by the bridge when an embedding was requested but none could
456
+ * be produced — the row is stored without a vector. */
457
+ embeddingError?: string;
455
458
  }>;
456
459
  /**
457
460
  * Search entries using sql.js with vector similarity
@@ -81,6 +81,23 @@ function hasNativeWalSidecars(dbPath) {
81
81
  return true;
82
82
  }
83
83
  }
84
+ /**
85
+ * #3397 — this process's own graph-edge-writer handle keeps -wal/-shm on disk
86
+ * between its idle-release ticks. Release (checkpoint + close) it before the
87
+ * #2735 guard runs, so the guard only sees sidecars some OTHER native
88
+ * connection is holding. The guard itself is unchanged: even a same-process
89
+ * open WAL connection makes a whole-image sql.js write unsafe until its WAL
90
+ * has been checkpointed, which is exactly what releasing does.
91
+ */
92
+ async function releaseOwnNativeHandle(dbPath) {
93
+ try {
94
+ const { releaseBridgeDb } = await import('./graph-edge-writer.js');
95
+ releaseBridgeDb(dbPath);
96
+ }
97
+ catch {
98
+ // Writer module unavailable — nothing of ours to release; the guard decides.
99
+ }
100
+ }
84
101
  /**
85
102
  * #1854: previously every site that needed the memory directory hardcoded
86
103
  * `getMemoryRoot()`, so the documented config entry
@@ -1957,7 +1974,17 @@ export async function applyTemporalDecay(dbPath) {
1957
1974
  };
1958
1975
  }
1959
1976
  }
1977
+ /**
1978
+ * State of the LOCAL embedding chain only (transformers.js / agentic-flow /
1979
+ * ruvector ONNX / hash). #3375: the AgentDB bridge's result is cached
1980
+ * separately in `bridgeEmbeddingInfo` and must never be written here — it used
1981
+ * to be recorded as `{ loaded: true, model: null }`, which made
1982
+ * generateLocalEmbedding() skip loading any local model and always return the
1983
+ * hash fallback, so rescueAgentdbEmbedder()'s `backend === 'onnx'` probe could
1984
+ * never pass.
1985
+ */
1960
1986
  let embeddingModelState = null;
1987
+ let bridgeEmbeddingInfo = null;
1961
1988
  /**
1962
1989
  * Lazy load ONNX embedding model
1963
1990
  * Only loads when first embedding is requested
@@ -1966,10 +1993,11 @@ export async function loadEmbeddingModel(options) {
1966
1993
  const { verbose = false } = options || {};
1967
1994
  const startTime = Date.now();
1968
1995
  // Already loaded
1969
- if (embeddingModelState?.loaded) {
1996
+ const cached = bridgeEmbeddingInfo ?? (embeddingModelState?.loaded ? embeddingModelState : null);
1997
+ if (cached) {
1970
1998
  return {
1971
1999
  success: true,
1972
- dimensions: embeddingModelState.dimensions,
2000
+ dimensions: cached.dimensions,
1973
2001
  modelName: 'cached',
1974
2002
  loadTime: 0
1975
2003
  };
@@ -1979,16 +2007,24 @@ export async function loadEmbeddingModel(options) {
1979
2007
  if (bridge) {
1980
2008
  const bridgeResult = await bridge.bridgeLoadEmbeddingModel();
1981
2009
  if (bridgeResult && bridgeResult.success) {
1982
- // Mark local state as loaded too so subsequent calls use cache
1983
- embeddingModelState = {
1984
- loaded: true,
1985
- model: null, // Bridge handles embedding
1986
- tokenizer: null,
1987
- dimensions: bridgeResult.dimensions
1988
- };
2010
+ // #3375: cache the bridge result on its own. Do NOT mark the local
2011
+ // chain as loaded — the bridge's model is not callable from here.
2012
+ bridgeEmbeddingInfo = { dimensions: bridgeResult.dimensions };
1989
2013
  return bridgeResult;
1990
2014
  }
1991
2015
  }
2016
+ return loadLocalEmbeddingChain(verbose, startTime);
2017
+ }
2018
+ /**
2019
+ * Load the LOCAL embedding chain into `embeddingModelState`, never consulting
2020
+ * the AgentDB bridge. Used by loadEmbeddingModel() after the bridge declines,
2021
+ * and directly by generateLocalEmbedding() so the "bridge-free" contract the
2022
+ * #2312 comment on that function describes actually holds (#3375).
2023
+ */
2024
+ async function loadLocalEmbeddingChain(verbose = false, startTime = Date.now()) {
2025
+ if (embeddingModelState?.loaded) {
2026
+ return { success: true, dimensions: embeddingModelState.dimensions, modelName: 'cached', loadTime: 0 };
2027
+ }
1992
2028
  try {
1993
2029
  // ADR-094: prefer @huggingface/transformers (clears protobufjs <7.5.5
1994
2030
  // critical RCE chain), fall back to legacy @xenova/transformers.
@@ -2195,9 +2231,12 @@ export async function generateEmbedding(text) {
2195
2231
  * Keeping the local chain as its own export breaks that cycle structurally.
2196
2232
  */
2197
2233
  export async function generateLocalEmbedding(text) {
2198
- // Ensure model is loaded
2234
+ // Ensure the LOCAL model is loaded. #3375: this must not go through
2235
+ // loadEmbeddingModel(), which is bridge-first — when the bridge answered
2236
+ // there, no local model was ever loaded and this function always returned
2237
+ // the hash fallback.
2199
2238
  if (!embeddingModelState?.loaded) {
2200
- await loadEmbeddingModel();
2239
+ await loadLocalEmbeddingChain();
2201
2240
  }
2202
2241
  // #2461: loadEmbeddingModel() can leave embeddingModelState null when an
2203
2242
  // earlier loader (transformers fetch, ruvector init) throws and we never
@@ -2535,6 +2574,7 @@ export async function storeEntry(options) {
2535
2574
  // this closes and its known residual (the narrow assess-then-write
2536
2575
  // race). This check gates ensureSchemaColumns()'s own whole-image
2537
2576
  // write below too, not just this function's.
2577
+ await releaseOwnNativeHandle(dbPath);
2538
2578
  if (hasNativeWalSidecars(dbPath)) {
2539
2579
  return {
2540
2580
  success: false,
@@ -3043,6 +3083,7 @@ export async function getEntry(options) {
3043
3083
  // this closes. Applies here too because the fallback's access_count
3044
3084
  // bump is itself a whole-image write, not a lightweight read, even
3045
3085
  // though this function's contract reads as a "get".
3086
+ await releaseOwnNativeHandle(dbPath);
3046
3087
  if (hasNativeWalSidecars(dbPath)) {
3047
3088
  return {
3048
3089
  success: false,
@@ -3162,6 +3203,7 @@ export async function deleteEntry(options) {
3162
3203
  }
3163
3204
  // #2735 — see storeEntry's identical gate for the corruption mechanism
3164
3205
  // this closes.
3206
+ await releaseOwnNativeHandle(dbPath);
3165
3207
  if (hasNativeWalSidecars(dbPath)) {
3166
3208
  return {
3167
3209
  success: false,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Router embedder (ADR-390).
3
+ *
4
+ * `hooks_route` compares a task with each agent pattern's keywords in a 384-d
5
+ * vector index. Historically both sides came from a character hash
6
+ * (`generateSimpleEmbedding`), which measures spelling, not meaning. This
7
+ * module lets the router use the local sentence model (all-MiniLM-L6-v2, 384-d)
8
+ * instead, with the hash as the fallback.
9
+ *
10
+ * Rules (ADR-390 §Decision):
11
+ * - The MiniLM path uses `generateLocalEmbedding` ONLY. Never the bridge-first
12
+ * `generateEmbedding` — that recursed without bound in #2312.
13
+ * - One embedder per index: if ANY text cannot be embedded by the real model
14
+ * (throw, backend !== 'onnx', or a non-384 vector), EVERY text in the call
15
+ * is embedded with the hash, and the result says so.
16
+ * - The default stays `hash` until ADR-391's benchmark says otherwise.
17
+ *
18
+ * Selection: `CLAUDE_FLOW_ROUTER_EMBEDDER=minilm|hash`. The router index is
19
+ * process-lifetime state, so the env var is read when the index is (re)built,
20
+ * not per CLI invocation.
21
+ */
22
+ export type RouterEmbedderKind = 'minilm' | 'hash';
23
+ /** Default embedder. ADR-391 decides whether this flips to 'minilm'. */
24
+ export declare const DEFAULT_ROUTER_EMBEDDER: RouterEmbedderKind;
25
+ /** Dimension of the router index (VectorDb + SemanticRouter are built at 384). */
26
+ export declare const ROUTER_EMBEDDING_DIM = 384;
27
+ export declare const ROUTER_EMBEDDER_ENV = "CLAUDE_FLOW_ROUTER_EMBEDDER";
28
+ export interface RouterEmbedderSelection {
29
+ kind: RouterEmbedderKind;
30
+ /** Set when the requested value was invalid and the default was used. */
31
+ reason?: string;
32
+ }
33
+ export interface RouterEmbeddingResult {
34
+ vectors: Float32Array[];
35
+ /** The embedder that actually produced `vectors` (after any degradation). */
36
+ embedder: RouterEmbedderKind;
37
+ /** Why the result is `hash` when `minilm` was requested. */
38
+ reason?: string;
39
+ }
40
+ /**
41
+ * Resolve which embedder the router should use.
42
+ * Precedence: explicit override > CLAUDE_FLOW_ROUTER_EMBEDDER > DEFAULT_ROUTER_EMBEDDER.
43
+ */
44
+ export declare function resolveRouterEmbedder(override?: RouterEmbedderKind, env?: NodeJS.ProcessEnv): RouterEmbedderSelection;
45
+ /**
46
+ * Deterministic character-hash embedding (the router's historical embedder).
47
+ * Moved verbatim from hooks-tools.ts; do not change the math — existing routes
48
+ * and thresholds were calibrated against it.
49
+ */
50
+ export declare function generateSimpleEmbedding(text: string, dimension?: number): Float32Array;
51
+ /**
52
+ * Embed texts for the router. All vectors in one result come from ONE embedder.
53
+ * `hash` never touches the model (no load cost for default users).
54
+ */
55
+ export declare function embedForRouter(texts: readonly string[], kind?: RouterEmbedderKind): Promise<RouterEmbeddingResult>;
56
+ /** Test hook: clear the MiniLM vector memo. */
57
+ export declare function clearRouterEmbedderCache(): void;
58
+ //# sourceMappingURL=router-embedder.d.ts.map