@claude-flow/cli 3.42.4 → 3.43.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.42.4",
3
+ "version": "3.43.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
6
6
  "hook-handler.cjs": "209d9fafe10e17d1be0866727f6f9cf9ac66f9a0793f1c793a4f58319e8e4583",
@@ -8,6 +8,6 @@
8
8
  "statusline.cjs": "4a48353b4f1566fa4379b00fd0321b6676a22b6cc91fbbd8380ac5183d619468"
9
9
  }
10
10
  },
11
- "signature": "zyopDoOW51Qkc0k2PpDbEv/puqT45lcx8EFnRQmwioIEDI/mv8KV140ZWcjEaa7lvIDW/epdB333K7bMW122Dw==",
11
+ "signature": "OlZvg4x9E5xfjdOatTAJz53uTxtK9dOj56GdkqnvqSWjFOKYCWWmbdijaMeVugo9e/u1y06VtWDGXrKSL82xDA==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -30,7 +30,7 @@ const AGENT_CAPABILITIES = {
30
30
  const TASK_PATTERNS = [
31
31
  // Code patterns
32
32
  { tokens: ['implement', 'create', 'build', 'add', 'write code', 'refactor', 'debug'], agent: 'coder' },
33
- { tokens: ['test', 'tests', 'spec', 'coverage', 'unit test', 'integration test'], agent: 'tester' },
33
+ { tokens: ['test', 'tests', 'testing', 'spec', 'specs', 'coverage', 'unit test', 'integration test'], agent: 'tester' },
34
34
  { tokens: ['review', 'audit', 'check', 'validate', 'security'], agent: 'reviewer' },
35
35
  { tokens: ['research', 'find', 'search', 'documentation', 'explore'], agent: 'researcher' },
36
36
  { tokens: ['design', 'architect', 'architecture', 'structure', 'plan'], agent: 'architect' },
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 6,
4
- "generatedAt": "2026-09-17T21:28:47.530Z",
5
- "gitSha": "702d1461",
4
+ "generatedAt": "2026-09-23T15:00:55.000Z",
5
+ "gitSha": "025842bb",
6
6
  "catalog": {
7
7
  "agents": 167,
8
8
  "tools": 418,
@@ -2059,6 +2059,26 @@ async function checkMetaharness() {
2059
2059
  };
2060
2060
  }
2061
2061
  }
2062
+ // Opt-in @ruvector/typesafe task router (optional peer). `--component typesafe` only.
2063
+ async function checkTypesafeRouter() {
2064
+ const name = '@ruvector/typesafe router';
2065
+ const { readTypesafeConfig } = await import('../ruvector/typesafe-router.js');
2066
+ const cfg = readTypesafeConfig();
2067
+ const gate = cfg.enabled ? `enabled (${cfg.embedder === 'hash' ? 'hash embedder, uncalibrated' : 'onnx embedder'})` : 'disabled (set CLAUDE_FLOW_ROUTER_TYPESAFE=1)';
2068
+ try {
2069
+ const { createRequire } = await import('module');
2070
+ const pj = createRequire(import.meta.url)('@ruvector/typesafe/package.json');
2071
+ return { name, status: 'pass', message: `v${pj.version ?? '?'} installed; ${gate}` };
2072
+ }
2073
+ catch {
2074
+ return {
2075
+ name,
2076
+ status: cfg.enabled ? 'warn' : 'pass',
2077
+ message: `Not installed; ${gate} — hooks_route uses the built-in router`,
2078
+ ...(cfg.enabled ? { fix: 'npm install @ruvector/typesafe # optional peer' } : {}),
2079
+ };
2080
+ }
2081
+ }
2062
2082
  async function checkClaudeCode() {
2063
2083
  try {
2064
2084
  const version = await runCommand('claude --version');
@@ -2252,7 +2272,7 @@ export const doctorCommand = {
2252
2272
  {
2253
2273
  name: 'component',
2254
2274
  short: 'c',
2255
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, metaharness)',
2275
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, metaharness)',
2256
2276
  type: 'string'
2257
2277
  },
2258
2278
  {
@@ -2435,6 +2455,7 @@ export const doctorCommand = {
2435
2455
  // a user would actually debug them (is it installed? running? exposed?).
2436
2456
  'proxy': [checkProxySponsoredConsent, checkProxyBinary, checkProxyProcess, checkProxyBindAddress],
2437
2457
  'auth': checkAuth, // ADR-306
2458
+ 'typesafe': checkTypesafeRouter, // opt-in @ruvector/typesafe task router
2438
2459
  };
2439
2460
  let checksToRun = allChecks;
2440
2461
  if (component && componentMap[component]) {
@@ -1811,6 +1811,12 @@ const postTaskCommand = {
1811
1811
  }
1812
1812
  output.writeln();
1813
1813
  output.printSuccess(`Task outcome recorded: ${success ? 'SUCCESS' : 'FAILED'}`);
1814
+ // #3353: only show observed learning results; say so when degraded.
1815
+ const lu = result.learningUpdates;
1816
+ if (lu.available === false) {
1817
+ output.writeln();
1818
+ output.printWarning(`Learning degraded: ${lu.reason ?? 'feedback not recorded'} — no pattern updates were observed`);
1819
+ }
1814
1820
  output.writeln();
1815
1821
  output.writeln(output.bold('Learning Updates'));
1816
1822
  output.printTable({
@@ -1819,10 +1825,10 @@ const postTaskCommand = {
1819
1825
  { key: 'value', header: 'Value', width: 20, align: 'right' }
1820
1826
  ],
1821
1827
  data: [
1822
- { metric: 'Patterns Updated', value: result.learningUpdates.patternsUpdated },
1823
- { metric: 'New Patterns', value: result.learningUpdates.newPatterns },
1828
+ { metric: 'Patterns Updated', value: lu.patternsUpdated },
1829
+ { metric: 'New Patterns', value: lu.newPatterns ?? 'unknown' },
1830
+ { metric: 'Trajectory Recorded', value: result.trajectory ? (result.trajectory.recorded ? 'yes' : 'no') : 'unknown' },
1824
1831
  { metric: 'Duration', value: `${(result.duration / 1000).toFixed(1)}s` },
1825
- { metric: 'Trajectory ID', value: result.learningUpdates.trajectoryId }
1826
1832
  ]
1827
1833
  });
1828
1834
  return { success: true, data: result };
@@ -283,7 +283,7 @@ const AGENT_CAPABILITIES = {
283
283
  // the whitespace acts as a natural boundary.
284
284
  const TASK_PATTERNS = [
285
285
  { tokens: ['implement', 'create', 'build', 'add', 'write code', 'refactor', 'debug'], agent: 'coder' },
286
- { tokens: ['test', 'tests', 'spec', 'coverage', 'unit test', 'integration test'], agent: 'tester' },
286
+ { tokens: ['test', 'tests', 'testing', 'spec', 'specs', 'coverage', 'unit test', 'integration test'], agent: 'tester' },
287
287
  { tokens: ['review', 'audit', 'check', 'validate', 'security'], agent: 'reviewer' },
288
288
  { tokens: ['research', 'find', 'search', 'documentation', 'explore'], agent: 'researcher' },
289
289
  { tokens: ['design', 'architect', 'architecture', 'structure', 'plan'], agent: 'architect' },
@@ -329,15 +329,16 @@ function routeTask(task) {
329
329
  };
330
330
  }
331
331
 
332
- // CLI
333
- const task = process.argv.slice(2).join(' ');
334
-
335
- if (task) {
336
- const result = routeTask(task);
337
- console.log(JSON.stringify(result, null, 2));
338
- } else {
339
- console.log('Usage: router.js <task description>');
340
- console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
332
+ // CLI — only when executed directly, not when require()d by hook-handler.cjs
333
+ if (require.main === module) {
334
+ const task = process.argv.slice(2).join(' ');
335
+ if (task) {
336
+ const result = routeTask(task);
337
+ console.log(JSON.stringify(result, null, 2));
338
+ } else {
339
+ console.log('Usage: router.js <task description>');
340
+ console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
341
+ }
341
342
  }
342
343
 
343
344
  module.exports = { routeTask, AGENT_CAPABILITIES, TASK_PATTERNS, buildPattern };
@@ -125,7 +125,7 @@ export const CAPABILITY_DOMAINS = [
125
125
  {
126
126
  id: 'guidance',
127
127
  name: 'Capability Guidance',
128
- prefixes: ['guidance_'],
128
+ prefixes: ['guidance_', 'seraphina_'],
129
129
  description: 'Live capability inventory, task routing, workflow guidance, and system discovery.',
130
130
  taskSignals: ['guidance', 'discover', 'capability', 'what can ruflo do'],
131
131
  commands: ['guidance compile', 'guidance retrieve', 'guidance gates', 'guidance optimize'],
@@ -430,7 +430,7 @@ export const CAPABILITY_DOMAINS = [
430
430
  id: 'business-collaboration',
431
431
  name: 'AgentBBS & Business Pods',
432
432
  // Metadata classification only; execution remains behind the existing loadAgentbbs guard.
433
- prefixes: ['agentbbs_', 'business_pod_', 'federation_bbs_'],
433
+ prefixes: ['agentbbs_', 'business_pod_', 'federation_bbs_', 'x_federation_'],
434
434
  description: 'Federated business rooms, domain-affinity routing, and business-pod validation.',
435
435
  taskSignals: ['business pod', 'bbs', 'room', 'domain affinity'],
436
436
  commands: [],
@@ -551,6 +551,8 @@ const TOOL_OWNERSHIP = [
551
551
  { prefixes: ['testgen_'], packageOwner: '@claude-flow/cli', pluginOwner: 'ruflo-testgen' },
552
552
  { prefixes: ['managed_agent_', 'wasm_agent_', 'wasm_gallery_'], packageOwner: '@claude-flow/cli', pluginOwner: 'ruflo-agent' },
553
553
  { prefixes: ['guidance_'], packageOwner: '@claude-flow/guidance', pluginOwner: 'ruflo-core' },
554
+ { prefixes: ['seraphina_'], packageOwner: '@claude-flow/cli', pluginOwner: 'ruflo-core' },
555
+ { prefixes: ['x_federation_'], packageOwner: '@claude-flow/cli', pluginOwner: 'ruflo-x-gateway' },
554
556
  ];
555
557
  function ownershipForTool(toolName, definition) {
556
558
  const explicit = TOOL_OWNERSHIP.find((entry) => entry.prefixes.some((prefix) => toolName.startsWith(prefix)));
@@ -13,6 +13,11 @@ import { type MCPTool } from './types.js';
13
13
  * the tag names untouched.
14
14
  */
15
15
  export declare function scrubReasoningBlocks(text: string): string;
16
+ /** Exported for tests. */
17
+ export declare function suggestAgentsForTask(task: string): {
18
+ agents: string[];
19
+ confidence: number;
20
+ };
16
21
  export declare const hooksPreEdit: MCPTool;
17
22
  export declare const hooksPostEdit: MCPTool;
18
23
  export declare const hooksPreCommand: MCPTool;
@@ -9,6 +9,7 @@ import { getProjectCwd } from './types.js';
9
9
  import { validateIdentifier, validateText, validatePath } from './validate-input.js';
10
10
  import { checkCommandLoop, recordCommandOutcome } from './tool-loop-guardrail.js';
11
11
  import { buildLearnedRoutingPatterns, } from '../services/learned-routing.js';
12
+ import { applyTypesafeRouting, getTypesafeRouter } from '../ruvector/typesafe-router.js';
12
13
  // Real vector search functions - lazy loaded to avoid circular imports
13
14
  let searchEntriesFn = null;
14
15
  /**
@@ -269,6 +270,10 @@ const TASK_PATTERNS = {
269
270
  agents: ['memory-specialist', 'architect', 'coder'],
270
271
  },
271
272
  };
273
+ /** Wrap hooks_route so the opt-in typesafe router (src/ruvector/typesafe-router.ts) can override the legacy pick. */
274
+ function withTypesafeRouting(legacy) {
275
+ return async (params) => applyTypesafeRouting(params, (await legacy(params)), TASK_PATTERNS, getTypesafeRouter());
276
+ }
272
277
  /**
273
278
  * Get the semantic router with environment detection.
274
279
  * Tries native VectorDb first (HNSW, 16k routes/s), falls back to pure JS (47k routes/s cosine).
@@ -621,11 +626,21 @@ function suggestAgentsForFile(filePath) {
621
626
  }
622
627
  return AGENT_PATTERNS[ext] || ['coder', 'architect'];
623
628
  }
624
- function suggestAgentsForTask(task) {
625
- const taskLower = task.toLowerCase();
629
+ // Whole-word matchers for KEYWORD_PATTERNS. A bare `includes()` matched
630
+ // substrings: 'test' hit "latest" (tester @ 0.95), 'auth' hit "author",
631
+ // 'fix' hit "prefix", 'api' hit "capitalize". Single words get \b anchors plus
632
+ // simple inflections (tests, testing, fixes, deployed); phrases containing
633
+ // whitespace or '/' (e.g. 'ci/cd') match literally between word boundaries.
634
+ const KEYWORD_MATCHERS = Object.entries(KEYWORD_PATTERNS).map(([keyword, result]) => {
635
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
636
+ const body = /[\s/]/.test(keyword) ? escaped : `${escaped}(?:s|es|ing|ed)?`;
637
+ return { regex: new RegExp(`\\b${body}\\b`, 'i'), result };
638
+ });
639
+ /** Exported for tests. */
640
+ export function suggestAgentsForTask(task) {
626
641
  // Check static keyword patterns first
627
- for (const [pattern, result] of Object.entries(KEYWORD_PATTERNS)) {
628
- if (taskLower.includes(pattern)) {
642
+ for (const { regex, result } of KEYWORD_MATCHERS) {
643
+ if (regex.test(task)) {
629
644
  return result;
630
645
  }
631
646
  }
@@ -950,7 +965,8 @@ export const hooksRoute = {
950
965
  },
951
966
  required: ['task'],
952
967
  },
953
- handler: async (params) => {
968
+ // Opt-in @ruvector/typesafe augmentation (CLAUDE_FLOW_ROUTER_TYPESAFE=1); returns the legacy result unchanged when unset.
969
+ handler: withTypesafeRouting(async (params) => {
954
970
  const task = params.task;
955
971
  const context = params.context;
956
972
  const useSemanticRouter = params.useSemanticRouter !== false;
@@ -1131,7 +1147,7 @@ export const hooksRoute = {
1131
1147
  coordination: 'queen-led',
1132
1148
  } : null,
1133
1149
  };
1134
- },
1150
+ }),
1135
1151
  };
1136
1152
  export const hooksMetrics = {
1137
1153
  name: 'hooks_metrics',
@@ -1439,10 +1455,12 @@ export const hooksPostTask = {
1439
1455
  catch {
1440
1456
  // Non-fatal
1441
1457
  }
1442
- // Record trajectory via intelligence module (SONA + ReasoningBank)
1458
+ // Record trajectory via intelligence module (SONA + ReasoningBank).
1459
+ // #3353: keep the observed result instead of discarding it.
1460
+ let trajectoryRecorded = false;
1443
1461
  try {
1444
1462
  const intelligence = await import('../memory/intelligence.js');
1445
- await intelligence.recordTrajectory([{ type: 'result', content: params.task || taskId, metadata: { success, agent, quality }, timestamp: Date.now() }], success ? 'success' : 'failure');
1463
+ trajectoryRecorded = (await intelligence.recordTrajectory([{ type: 'result', content: params.task || taskId, metadata: { success, agent, quality }, timestamp: Date.now() }], success ? 'success' : 'failure')) === true;
1446
1464
  }
1447
1465
  catch {
1448
1466
  // Intelligence module not available — non-fatal
@@ -1582,17 +1600,32 @@ export const hooksPostTask = {
1582
1600
  writeFileSync(storePath, JSON.stringify(store, null, 2), 'utf-8');
1583
1601
  }
1584
1602
  catch { /* non-critical */ }
1603
+ // #3353: report only observed learning results. The previous
1604
+ // `feedbackResult?.updated || (success ? 2 : 1)` / `newPatterns: success ? 1 : 0`
1605
+ // invented counts whenever the feedback controller was unavailable (and the
1606
+ // `||` turned an observed 0 into 2). No path reports pattern *creation*, so
1607
+ // newPatterns is null (unknown) rather than a guess; the trajectory has no
1608
+ // real id to surface, so trajectoryId is null.
1609
+ const feedbackRecorded = feedbackResult?.success === true;
1610
+ const learningAvailable = feedbackRecorded;
1585
1611
  return {
1586
1612
  taskId,
1587
1613
  success,
1588
1614
  duration,
1589
1615
  learningUpdates: {
1590
- patternsUpdated: feedbackResult?.updated || (success ? 2 : 1),
1591
- newPatterns: success ? 1 : 0,
1592
- trajectoryId: `traj-${Date.now()}`,
1616
+ patternsUpdated: feedbackRecorded ? (feedbackResult?.updated ?? 0) : 0,
1617
+ newPatterns: null,
1618
+ trajectoryId: null,
1593
1619
  controller: feedbackResult?.controller || 'none',
1594
1620
  outcomePersisted,
1621
+ available: learningAvailable,
1622
+ ...(learningAvailable ? {} : {
1623
+ reason: feedbackResult
1624
+ ? `feedback controller '${feedbackResult.controller}' did not record the outcome`
1625
+ : 'feedback controller unavailable',
1626
+ }),
1595
1627
  },
1628
+ trajectory: { recorded: trajectoryRecorded },
1596
1629
  quality,
1597
1630
  pheromone,
1598
1631
  feedback: feedbackResult ? {
@@ -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.