claude-flow 3.56.8 → 3.56.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/hook-handler.cjs +109 -23
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/proven-config.json +42 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  8. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  9. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  10. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  11. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  12. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  13. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  14. package/node_modules/@claude-flow/codex/package.json +1 -1
  15. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  16. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  17. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  18. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  19. package/node_modules/@claude-flow/security/package.json +1 -1
  20. package/package.json +5 -5
  21. package/v3/@claude-flow/cli/dist/src/autopilot-state.d.ts +2 -0
  22. package/v3/@claude-flow/cli/dist/src/autopilot-state.js +36 -8
  23. package/v3/@claude-flow/cli/dist/src/command-resolution.d.ts +15 -0
  24. package/v3/@claude-flow/cli/dist/src/command-resolution.js +25 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/agent.js +17 -17
  26. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +40 -29
  27. package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
  28. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +86 -31
  29. package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
  30. package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.d.ts +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.js +61 -3
  32. package/v3/@claude-flow/cli/dist/src/commands/security.d.ts +24 -0
  33. package/v3/@claude-flow/cli/dist/src/commands/security.js +81 -35
  34. package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +9 -0
  35. package/v3/@claude-flow/cli/dist/src/commands/tui.js +29 -3
  36. package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +9 -0
  37. package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +24 -3
  38. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +2 -1
  39. package/v3/@claude-flow/cli/dist/src/index.js +6 -42
  40. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +109 -23
  41. package/v3/@claude-flow/cli/dist/src/init/types.js +1 -1
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +15 -0
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +45 -22
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  48. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  49. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +25 -2
  50. package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  51. package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  52. package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +10 -11
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +2 -2
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +29 -7
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +4 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +70 -20
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +28 -4
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +55 -17
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +66 -17
  62. package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +5 -1
  63. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +105 -29
  64. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +10 -3
  65. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +2 -1
  66. package/v3/@claude-flow/cli/dist/src/memory/search-defaults.d.ts +54 -0
  67. package/v3/@claude-flow/cli/dist/src/memory/search-defaults.js +37 -0
  68. package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +28 -0
  69. package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +59 -2
  70. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +24 -24
  71. package/v3/@claude-flow/cli/dist/src/parser.js +32 -22
  72. package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +1 -0
  73. package/v3/@claude-flow/cli/dist/src/production/monitoring.js +19 -6
  74. package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +5 -4
  75. package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -0
  76. package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.d.ts +10 -0
  77. package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +27 -1
  78. package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.d.ts +2 -4
  79. package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +202 -69
  80. package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  81. package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.js +12 -1
  82. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +3 -3
  83. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -1
  84. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +7 -1
  85. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
  86. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +37 -0
  87. package/v3/@claude-flow/cli/dist/src/transfer/store/discovery.js +16 -17
  88. package/v3/@claude-flow/cli/dist/src/transfer/store/types.d.ts +4 -0
  89. package/v3/@claude-flow/cli/package.json +7 -7
  90. package/v3/@claude-flow/shared/dist/resilience/rate-limiter.js +3 -3
  91. package/v3/@claude-flow/shared/dist/resilience/retry.js +3 -1
  92. package/v3/@claude-flow/shared/package.json +1 -1
@@ -12,6 +12,7 @@
12
12
  import { validateText, validateIdentifier } from './validate-input.js';
13
13
  import { autoInstallPackage } from './auto-install.js';
14
14
  import { createRequire } from 'module';
15
+ import { findKnownMiss, recordKnownMiss } from './aidefence-known-misses.js';
15
16
  // Create require for resolving module paths
16
17
  const require = createRequire(import.meta.url);
17
18
  // Lazy-loaded AIDefence instance
@@ -152,7 +153,8 @@ const aidefenceScanTool = {
152
153
  piiPresent = !!defender.hasPII(input);
153
154
  }
154
155
  catch { /* hasPII unavailable */ }
155
- const threatDetected = result.threat || piiPresent;
156
+ const knownMiss = findKnownMiss(input);
157
+ const threatDetected = result.threat || piiPresent || !!knownMiss;
156
158
  wrapperStats.quickScanCalls++;
157
159
  if (threatDetected)
158
160
  wrapperStats.quickScanThreats++;
@@ -164,24 +166,29 @@ const aidefenceScanTool = {
164
166
  threatDetected,
165
167
  confidence: result.confidence,
166
168
  piiDetected: piiPresent,
167
- promptInjectionDetected: result.threat,
169
+ promptInjectionDetected: result.threat || !!knownMiss,
170
+ ...(knownMiss ? { knownMiss: { threatType: knownMiss.threatType, reportedAt: knownMiss.reportedAt } } : {}),
168
171
  mode: 'quick',
169
172
  }, null, 2),
170
173
  }],
171
174
  };
172
175
  }
173
176
  const result = await defender.detect(input);
177
+ const knownMiss = findKnownMiss(input);
174
178
  return {
175
179
  content: [{
176
180
  type: 'text',
177
181
  text: JSON.stringify({
178
- safe: result.safe,
179
- threats: result.threats.map(t => ({
180
- type: t.type,
181
- severity: t.severity,
182
- confidence: t.confidence,
183
- description: t.description,
184
- })),
182
+ safe: result.safe && !knownMiss,
183
+ threats: [
184
+ ...result.threats.map(t => ({
185
+ type: t.type,
186
+ severity: t.severity,
187
+ confidence: t.confidence,
188
+ description: t.description,
189
+ })),
190
+ ...(knownMiss ? [{ type: knownMiss.threatType, severity: 'high', confidence: 1, description: 'Matches an input previously reported as a missed threat (aidefence_learn)' }] : []),
191
+ ],
185
192
  piiFound: result.piiFound,
186
193
  detectionTimeMs: result.detectionTimeMs,
187
194
  }, null, 2),
@@ -398,11 +405,33 @@ const aidefenceLearnTool = {
398
405
  const defender = await getAIDefence();
399
406
  // Re-detect to get result for learning
400
407
  const result = await defender.detect(input);
401
- // Learn from detection
402
- await defender.learnFromDetection(input, result, {
403
- wasAccurate,
404
- userVerdict: verdict,
405
- });
408
+ // A false negative (wasAccurate=false, nothing detected) gives
409
+ // learnFromDetection() no threat to store, so record the miss itself.
410
+ const falseNegative = !wasAccurate && result.threats.length === 0;
411
+ const missType = threatType || 'prompt_injection';
412
+ let learned = result.threats.length;
413
+ let newlyRecorded = false;
414
+ if (falseNegative) {
415
+ newlyRecorded = recordKnownMiss(input, missType);
416
+ if (newlyRecorded) {
417
+ const synthetic = {
418
+ ...result,
419
+ safe: false,
420
+ threats: [{
421
+ type: missType,
422
+ severity: 'high',
423
+ confidence: 1,
424
+ pattern: 'user-reported-miss',
425
+ description: verdict || 'Reported by user as a missed threat',
426
+ }],
427
+ };
428
+ await defender.learnFromDetection(input, synthetic, { wasAccurate, userVerdict: verdict });
429
+ }
430
+ learned = 1;
431
+ }
432
+ else {
433
+ await defender.learnFromDetection(input, result, { wasAccurate, userVerdict: verdict });
434
+ }
406
435
  // Record mitigation if provided
407
436
  if (threatType && mitigationStrategy && mitigationSuccess !== undefined) {
408
437
  await defender.recordMitigation(threatType, mitigationStrategy, mitigationSuccess);
@@ -412,7 +441,14 @@ const aidefenceLearnTool = {
412
441
  type: 'text',
413
442
  text: JSON.stringify({
414
443
  success: true,
415
- message: 'Feedback recorded for pattern learning',
444
+ message: falseNegative
445
+ ? (newlyRecorded
446
+ ? 'Missed threat recorded: this exact input (normalised for case/whitespace) will now be flagged by aidefence_scan and aidefence_is_safe. Detection is pattern-based and does not generalise from it.'
447
+ : 'This input was already recorded as a missed threat')
448
+ : (learned === 0
449
+ ? 'Feedback noted, but no threat was detected so no pattern was learned. To teach a missed threat, pass wasAccurate=false.'
450
+ : 'Feedback recorded for pattern learning'),
451
+ learned,
416
452
  learnedFrom: {
417
453
  input: input.slice(0, 50) + (input.length > 50 ? '...' : ''),
418
454
  wasAccurate,
@@ -468,7 +504,8 @@ const aidefenceIsSafeTool = {
468
504
  piiPresent = !!defender.hasPII(input);
469
505
  }
470
506
  catch { /* hasPII unavailable */ }
471
- const safe = promptSafe && !piiPresent;
507
+ const knownMiss = findKnownMiss(input);
508
+ const safe = promptSafe && !piiPresent && !knownMiss;
472
509
  wrapperStats.isSafeCalls++;
473
510
  if (!safe)
474
511
  wrapperStats.isSafeUnsafeVerdicts++;
@@ -477,8 +514,9 @@ const aidefenceIsSafeTool = {
477
514
  type: 'text',
478
515
  text: JSON.stringify({
479
516
  safe,
480
- promptSafe,
517
+ promptSafe: promptSafe && !knownMiss,
481
518
  piiPresent,
519
+ ...(knownMiss ? { knownMiss: { threatType: knownMiss.threatType, reportedAt: knownMiss.reportedAt } } : {}),
482
520
  }, null, 2),
483
521
  }],
484
522
  };
@@ -12,6 +12,35 @@ import { executeAgentTask } from './agent-execute-core.js';
12
12
  const STORAGE_DIR = '.claude-flow';
13
13
  const WORKFLOW_DIR = 'workflows';
14
14
  const WORKFLOW_FILE = 'store.json';
15
+ function parseCondition(when) {
16
+ if (typeof when !== 'string' || !when.trim()) {
17
+ return new Error('condition step requires a non-empty config.when');
18
+ }
19
+ const expression = when.trim();
20
+ if (expression === 'true' || expression === 'false') {
21
+ return { constant: expression === 'true' };
22
+ }
23
+ const match = expression.match(/^([a-zA-Z_]\w*)\s*===?\s*(?:'([^']*)'|"([^"]*)"|(-?\d+(?:\.\d+)?)|(true|false))$/);
24
+ if (!match) {
25
+ return new Error('condition config.when must compare a variable to a quoted string, number, or boolean using == or ===');
26
+ }
27
+ const expected = match[2] ?? match[3] ?? (match[4] !== undefined ? Number(match[4]) : match[5] === 'true');
28
+ return { variable: match[1], expected };
29
+ }
30
+ function validateExecutableStep(step) {
31
+ if (step.type === 'parallel' || step.type === 'loop') {
32
+ return `step type '${step.type}' is not implemented by the workflow runtime`;
33
+ }
34
+ if (step.type === 'condition') {
35
+ const parsed = parseCondition(step.config?.when);
36
+ if (parsed instanceof Error)
37
+ return parsed.message;
38
+ }
39
+ else if (step.type !== 'task' && step.type !== 'wait') {
40
+ return `unsupported workflow step type '${step.type}'`;
41
+ }
42
+ return undefined;
43
+ }
15
44
  function getWorkflowDir() {
16
45
  return join(getProjectCwd(), STORAGE_DIR, WORKFLOW_DIR);
17
46
  }
@@ -181,8 +210,8 @@ export const workflowTools = [
181
210
  type: 'object',
182
211
  properties: {
183
212
  name: { type: 'string' },
184
- type: { type: 'string', enum: ['task', 'condition', 'parallel', 'loop', 'wait'] },
185
- config: { type: 'object' },
213
+ type: { type: 'string', enum: ['task', 'condition', 'wait'] },
214
+ config: { type: 'object', description: 'Condition steps require config.when; parallel and loop steps are not yet supported' },
186
215
  },
187
216
  },
188
217
  },
@@ -209,6 +238,11 @@ export const workflowTools = [
209
238
  config: s.config || {},
210
239
  status: 'pending',
211
240
  }));
241
+ for (const step of steps) {
242
+ const error = validateExecutableStep(step);
243
+ if (error)
244
+ return { success: false, error: `${step.stepId} (${step.name}): ${error}` };
245
+ }
212
246
  const workflow = {
213
247
  workflowId,
214
248
  name: input.name,
@@ -257,6 +291,20 @@ export const workflowTools = [
257
291
  if (workflow.status === 'running') {
258
292
  return { workflowId, error: 'Workflow already running' };
259
293
  }
294
+ // Older stored workflows may predate creation-time validation. Reject
295
+ // unsupported steps before dispatching any task with side effects.
296
+ for (const step of workflow.steps) {
297
+ const error = validateExecutableStep(step);
298
+ if (error) {
299
+ workflow.status = 'failed';
300
+ workflow.error = `${step.stepId} (${step.name}): ${error}`;
301
+ step.status = 'failed';
302
+ step.result = { error: workflow.error };
303
+ workflow.completedAt = new Date().toISOString();
304
+ saveWorkflowStore(store);
305
+ return { workflowId, status: 'failed', failedStep: step.stepId, error: workflow.error };
306
+ }
307
+ }
260
308
  // Indices may name a step or the end boundary (used when resuming
261
309
  // after the final step). Reject invalid control flow before any writes.
262
310
  const validIndex = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= workflow.steps.length;
@@ -382,19 +430,14 @@ export const workflowTools = [
382
430
  stepEntry = { stepId: step.stepId, type: 'wait', status: 'completed', durationMs: ms };
383
431
  }
384
432
  else if (step.type === 'condition') {
385
- // Simple condition: config.when is a JS expression evaluated against workflow.variables.
386
- // For safety, we only support `var === 'value'` or `var === number`.
387
433
  const cfg = step.config;
388
- const expr = String(cfg.when || 'true').trim();
389
- const m = expr.match(/^([a-zA-Z_][\w]*)\s*===?\s*(['\"])?([^'\"]*)\2?$/);
390
- let truthy = false;
391
- if (m) {
392
- const v = workflow.variables[m[1]];
393
- const expected = m[2] ? m[3] : Number(m[3]);
394
- truthy = v === expected;
395
- }
396
- else if (expr === 'true')
397
- truthy = true;
434
+ const parsed = parseCondition(cfg.when);
435
+ if (parsed instanceof Error)
436
+ throw parsed;
437
+ const truthy = 'constant' in parsed
438
+ ? parsed.constant
439
+ : workflow.variables[parsed.variable] === parsed.expected;
440
+ const expr = String(cfg.when).trim();
398
441
  step.result = { conditionExpr: expr, truthy };
399
442
  // condition can declare a target step index to jump to via cfg.thenStep / cfg.elseStep
400
443
  if (typeof cfg.thenStep === 'number' && truthy)
@@ -404,9 +447,7 @@ export const workflowTools = [
404
447
  stepEntry = { stepId: step.stepId, type: 'condition', status: 'completed' };
405
448
  }
406
449
  else {
407
- // parallel/loop are deferred — mark skipped honestly rather than mock-completing.
408
- step.result = { _note: `step type '${step.type}' not yet implemented in runtime` };
409
- stepEntry = { stepId: step.stepId, type: step.type, status: 'skipped' };
450
+ throw new Error(`step type '${step.type}' is not implemented by the workflow runtime`);
410
451
  }
411
452
  step.status = stepEntry.status === 'skipped' ? 'skipped' : 'completed';
412
453
  step.completedAt = new Date().toISOString();
@@ -906,6 +947,14 @@ export const workflowTools = [
906
947
  const agentSet = new Set();
907
948
  steps.forEach((s, i) => {
908
949
  const step = (s ?? {});
950
+ if (typeof step.type === 'string') {
951
+ const error = validateExecutableStep({
952
+ type: step.type,
953
+ config: (step.config ?? {}),
954
+ });
955
+ if (error)
956
+ errors.push({ line: i + 1, message: `step ${i + 1}: ${error}`, severity: 'error' });
957
+ }
909
958
  // Match workflow_execute: control steps do not dispatch agents,
910
959
  // and task steps use config.agentId or the workflow default.
911
960
  const config = step.config;
@@ -175,8 +175,12 @@ declare class LocalReasoningBank {
175
175
  * Deduplicates by content: if a pattern with the same content already
176
176
  * exists, the existing entry is updated (bumped usageCount, higher
177
177
  * confidence wins, refreshed lastUsedAt) instead of adding a duplicate.
178
+ *
179
+ * Returns true only when a NEW pattern was added (#3979 item 4): callers that
180
+ * keep a "patterns learned" counter must not count a merge or an update of an
181
+ * existing pattern as a newly learned one.
178
182
  */
179
- store(pattern: Omit<StoredPattern, 'usageCount' | 'createdAt' | 'lastUsedAt'> & Partial<StoredPattern>): void;
183
+ store(pattern: Omit<StoredPattern, 'usageCount' | 'createdAt' | 'lastUsedAt'> & Partial<StoredPattern>): boolean;
180
184
  /**
181
185
  * Find similar patterns by embedding.
182
186
  *
@@ -10,11 +10,12 @@
10
10
  *
11
11
  * @module v3/cli/intelligence
12
12
  */
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { createRequire } from 'node:module';
16
16
  import { join } from 'node:path';
17
17
  import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
18
+ import { getProjectCwd } from '../mcp-tools/types.js';
18
19
  // ============================================================================
19
20
  // Persistence Configuration
20
21
  // ============================================================================
@@ -24,7 +25,11 @@ import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
24
25
  * falling back to home directory
25
26
  */
26
27
  function getDataDir() {
27
- const cwd = process.cwd();
28
+ // Every MCP tool resolves `.claude-flow/` against the project root
29
+ // (CLAUDE_FLOW_CWD when set), so the neural stats/patterns must too —
30
+ // otherwise an MCP server whose process cwd differs from the project
31
+ // writes learning state where nothing reads it.
32
+ const cwd = getProjectCwd();
28
33
  const localDir = join(cwd, '.claude-flow', 'neural');
29
34
  const homeDir = join(homedir(), '.claude-flow', 'neural');
30
35
  // Prefer local directory if .claude-flow exists
@@ -429,6 +434,10 @@ class LocalReasoningBank {
429
434
  * Deduplicates by content: if a pattern with the same content already
430
435
  * exists, the existing entry is updated (bumped usageCount, higher
431
436
  * confidence wins, refreshed lastUsedAt) instead of adding a duplicate.
437
+ *
438
+ * Returns true only when a NEW pattern was added (#3979 item 4): callers that
439
+ * keep a "patterns learned" counter must not count a merge or an update of an
440
+ * existing pattern as a newly learned one.
432
441
  */
433
442
  store(pattern) {
434
443
  const now = Date.now();
@@ -448,6 +457,9 @@ class LocalReasoningBank {
448
457
  if (idx >= 0) {
449
458
  this.patternList[idx] = stored;
450
459
  }
460
+ this.patterns.set(pattern.id, stored);
461
+ this.saveToDisk();
462
+ return false;
451
463
  }
452
464
  else {
453
465
  // Check for content-duplicate before inserting a new entry
@@ -462,7 +474,7 @@ class LocalReasoningBank {
462
474
  // Keep the Map in sync with the mutated object
463
475
  this.patterns.set(contentDupe.id, contentDupe);
464
476
  this.saveToDisk();
465
- return;
477
+ return false;
466
478
  }
467
479
  // Evict oldest if at capacity
468
480
  if (this.patterns.size >= this.maxSize) {
@@ -476,6 +488,7 @@ class LocalReasoningBank {
476
488
  this.patterns.set(pattern.id, stored);
477
489
  // Trigger persistence (debounced)
478
490
  this.saveToDisk();
491
+ return true;
479
492
  }
480
493
  /**
481
494
  * Find similar patterns by embedding.
@@ -629,43 +642,92 @@ let globalStats = {
629
642
  signalsProcessed: 0,
630
643
  lastAdaptation: null
631
644
  };
632
- // ============================================================================
633
- // Stats Persistence
634
- // ============================================================================
635
645
  /**
636
- * Load persisted stats from disk
646
+ * Increments made by THIS process that have not yet been merged into
647
+ * stats.json. Persistence is delta-based: several processes (MCP server,
648
+ * hook one-shots, the worker daemon) share one stats.json, and each holds
649
+ * its own module-level globalStats. Writing the local snapshot verbatim let
650
+ * any writer erase every other writer's progress — notably the
651
+ * memory-bridge `recordSignalProcessed()` path, which persists without ever
652
+ * loading the file, reset `trajectoriesRecorded` to 0 on disk every 16
653
+ * writes. Merging deltas makes the counters monotonic across processes.
637
654
  */
638
- function loadPersistedStats() {
655
+ let pendingDelta = {
656
+ trajectoriesRecorded: 0,
657
+ patternsLearned: 0,
658
+ signalsProcessed: 0,
659
+ };
660
+ let persistedStatsLoaded = false;
661
+ function bumpStat(key, n = 1) {
662
+ globalStats[key] = (globalStats[key] ?? 0) + n;
663
+ pendingDelta[key] += n;
664
+ }
665
+ function readStatsFile() {
639
666
  try {
640
667
  const path = getStatsPath();
641
- if (existsSync(path)) {
642
- const data = JSON.parse(readFileSync(path, 'utf-8'));
643
- if (data && typeof data === 'object') {
644
- // #2245: previously only restored trajectoriesRecorded — patternsLearned
645
- // and signalsProcessed reset to zero on every restart, masking real
646
- // learning progress in the dashboards.
647
- globalStats.trajectoriesRecorded = data.trajectoriesRecorded ?? 0;
648
- globalStats.patternsLearned = data.patternsLearned ?? 0;
649
- globalStats.signalsProcessed = data.signalsProcessed ?? 0;
650
- globalStats.lastAdaptation = data.lastAdaptation ?? null;
651
- }
652
- }
668
+ if (!existsSync(path))
669
+ return null;
670
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
671
+ return data && typeof data === 'object' ? data : null;
653
672
  }
654
673
  catch {
655
- // Ignore load errors, start fresh
674
+ return null;
656
675
  }
657
676
  }
677
+ // ============================================================================
678
+ // Stats Persistence
679
+ // ============================================================================
680
+ /**
681
+ * Load persisted stats from disk
682
+ */
683
+ function loadPersistedStats() {
684
+ persistedStatsLoaded = true;
685
+ const data = readStatsFile();
686
+ if (!data)
687
+ return;
688
+ // #2245: previously only restored trajectoriesRecorded — patternsLearned
689
+ // and signalsProcessed reset to zero on every restart, masking real
690
+ // learning progress in the dashboards. Unsaved local increments are
691
+ // re-applied on top so a late load never drops them.
692
+ globalStats.trajectoriesRecorded = (data.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded;
693
+ globalStats.patternsLearned = (data.patternsLearned ?? 0) + pendingDelta.patternsLearned;
694
+ globalStats.signalsProcessed = (data.signalsProcessed ?? 0) + pendingDelta.signalsProcessed;
695
+ globalStats.lastAdaptation = maxTimestamp(data.lastAdaptation ?? null, globalStats.lastAdaptation);
696
+ }
697
+ function maxTimestamp(a, b) {
698
+ if (a == null)
699
+ return b;
700
+ if (b == null)
701
+ return a;
702
+ return Math.max(a, b);
703
+ }
658
704
  /**
659
- * Save stats to disk
705
+ * Save stats to disk: merge this process's unsaved deltas into whatever is
706
+ * currently on disk (read-merge-write, atomic rename), then adopt the merged
707
+ * totals locally so this process also sees other writers' progress.
660
708
  */
661
709
  function savePersistedStats() {
662
710
  try {
663
711
  ensureDataDir();
664
712
  const path = getStatsPath();
665
- writeFileSync(path, JSON.stringify(globalStats, null, 2), 'utf-8');
713
+ const disk = readStatsFile();
714
+ const merged = disk
715
+ ? {
716
+ trajectoriesRecorded: (disk.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded,
717
+ patternsLearned: (disk.patternsLearned ?? 0) + pendingDelta.patternsLearned,
718
+ signalsProcessed: (disk.signalsProcessed ?? 0) + pendingDelta.signalsProcessed,
719
+ lastAdaptation: maxTimestamp(disk.lastAdaptation ?? null, globalStats.lastAdaptation),
720
+ }
721
+ : { ...globalStats };
722
+ const tmp = `${path}.${process.pid}.tmp`;
723
+ writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf-8');
724
+ renameSync(tmp, path);
725
+ globalStats = merged;
726
+ pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
727
+ persistedStatsLoaded = true;
666
728
  }
667
729
  catch {
668
- // Ignore save errors
730
+ // Ignore save errors (deltas are retained for the next save)
669
731
  }
670
732
  }
671
733
  /**
@@ -679,7 +741,7 @@ function savePersistedStats() {
679
741
  let signalsSinceLastSave = 0;
680
742
  const SIGNAL_PERSIST_EVERY = 16;
681
743
  export function recordSignalProcessed() {
682
- globalStats.signalsProcessed = (globalStats.signalsProcessed ?? 0) + 1;
744
+ bumpStat('signalsProcessed');
683
745
  signalsSinceLastSave++;
684
746
  if (signalsSinceLastSave >= SIGNAL_PERSIST_EVERY) {
685
747
  savePersistedStats();
@@ -881,7 +943,7 @@ export async function recordStep(step) {
881
943
  await sonaCoordinator.distillLearning(reasoningBank);
882
944
  globalStats.lastAdaptation = Date.now();
883
945
  }
884
- globalStats.trajectoriesRecorded++;
946
+ bumpStat('trajectoriesRecorded');
885
947
  savePersistedStats();
886
948
  return true;
887
949
  }
@@ -928,7 +990,7 @@ export async function recordTrajectory(steps, verdict) {
928
990
  if (verdict === 'success') {
929
991
  for (const step of enrichedSteps) {
930
992
  if (step.embedding && step.embedding.length > 0) {
931
- reasoningBank.store({
993
+ const added = reasoningBank.store({
932
994
  id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
933
995
  type: step.type,
934
996
  content: step.content,
@@ -937,7 +999,11 @@ export async function recordTrajectory(steps, verdict) {
937
999
  metadata: step.metadata || {},
938
1000
  createdAt: Date.now(),
939
1001
  });
940
- globalStats.patternsLearned++;
1002
+ // #3979 item 4: count only patterns that were actually added. A step
1003
+ // whose content already exists is merged into that pattern, so the
1004
+ // counter no longer climbs once per successful step.
1005
+ if (added)
1006
+ bumpStat('patternsLearned');
941
1007
  }
942
1008
  }
943
1009
  }
@@ -962,7 +1028,7 @@ export async function recordTrajectory(steps, verdict) {
962
1028
  // ruvllm recording failed silently
963
1029
  }
964
1030
  }
965
- globalStats.trajectoriesRecorded++;
1031
+ bumpStat('trajectoriesRecorded');
966
1032
  globalStats.lastAdaptation = Date.now();
967
1033
  savePersistedStats();
968
1034
  return true;
@@ -1034,6 +1100,12 @@ export function getIntelligenceStats() {
1034
1100
  if (!ruvllmLoaded) {
1035
1101
  loadRuvllmCoordinatorSync();
1036
1102
  }
1103
+ // A read-only status process (ruvllm_status, `neural status`, a fresh MCP
1104
+ // server) has never run initializeIntelligence(); without this it reported
1105
+ // 0 trajectories no matter what other processes had persisted.
1106
+ if (!persistedStatsLoaded) {
1107
+ loadPersistedStats();
1108
+ }
1037
1109
  const ruvllmStats = ruvllmCoordinator?.stats?.() || null;
1038
1110
  // Fetch cross-module stats for unified reporting.
1039
1111
  //
@@ -1145,6 +1217,10 @@ export function clearIntelligence() {
1145
1217
  signalsProcessed: 0,
1146
1218
  lastAdaptation: null
1147
1219
  };
1220
+ pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
1221
+ // In-memory reset only: stay "loaded" so the next stats read does not
1222
+ // immediately re-hydrate the counters this call just cleared.
1223
+ persistedStatsLoaded = true;
1148
1224
  }
1149
1225
  /**
1150
1226
  * Benchmark SONA adaptation time
@@ -19,10 +19,11 @@
19
19
  import { AppendConditionFailed, assertAppendConditions, validateAppendConditions } from './append-conditions.js';
20
20
  import { liveMemoryRowSql } from './live-memory-row.js';
21
21
  import { resolveAgentdbBetterSqlite3 } from './shared-sqlite.js';
22
+ import { DEFAULT_SEARCH_THRESHOLD } from './search-defaults.js';
22
23
  import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
23
24
  import * as path from 'path';
24
25
  import * as crypto from 'crypto';
25
- import { realpathSync } from 'node:fs';
26
+ import { mkdirSync, realpathSync } from 'node:fs';
26
27
  import { createRequire } from 'node:module';
27
28
  import { AsyncLocalStorage } from 'node:async_hooks';
28
29
  import { validateFeedbackPatterns } from './feedback-patterns.js';
@@ -265,7 +266,13 @@ async function getRegistry(dbPath) {
265
266
  return;
266
267
  origLog.apply(console, args);
267
268
  };
269
+ // #4132: better-sqlite3 cannot create a missing parent directory. In a
270
+ // fresh checkout or worktree `.swarm/` does not exist yet, so AgentDB
271
+ // failed to open, the degraded registry was cached for the server's
272
+ // lifetime and hierarchical memory silently ran volatile.
268
273
  try {
274
+ if (resolvedPath !== ':memory:')
275
+ mkdirSync(path.dirname(resolvedPath), { recursive: true });
269
276
  await registry.initialize({
270
277
  // #2786: use agentdb-memory.db (plaintext) so native better-sqlite3 doesn't hit the encrypted memory.db.
271
278
  dbPath: resolvedPath,
@@ -1299,7 +1306,7 @@ export async function bridgeSearchEntries(options) {
1299
1306
  if (!ctx)
1300
1307
  return null;
1301
1308
  try {
1302
- const { query: queryStr, namespace, limit = 10, threshold = 0.3, provenanceFilter } = options;
1309
+ const { query: queryStr, namespace, limit = 10, threshold = DEFAULT_SEARCH_THRESHOLD, provenanceFilter } = options;
1303
1310
  const effectiveNamespace = namespace || 'all';
1304
1311
  const startTime = Date.now();
1305
1312
  // Generate query embedding — same agentdb-then-local chain as the write
@@ -1870,7 +1877,7 @@ export async function bridgeSearchBruteForceCosine(queryEmbedding, options, dbPa
1870
1877
  return null;
1871
1878
  try {
1872
1879
  const k = options?.k ?? 10;
1873
- const threshold = options?.threshold ?? 0.3;
1880
+ const threshold = options?.threshold ?? DEFAULT_SEARCH_THRESHOLD;
1874
1881
  const nsFilter = options?.namespace && options.namespace !== 'all'
1875
1882
  ? `AND namespace = ?`
1876
1883
  : '';
@@ -1,5 +1,6 @@
1
1
  import { loadBetterSqlite3 } from './shared-sqlite.js';
2
2
  import { resolveMemoryRoot } from './memory-root.js';
3
+ import { DEFAULT_SEARCH_THRESHOLD } from './search-defaults.js';
3
4
  import { liveMemoryRowSql } from './live-memory-row.js';
4
5
  import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
5
6
  import * as fs from 'fs';
@@ -2745,7 +2746,7 @@ export async function searchEntries(options) {
2745
2746
  return bridgeResult;
2746
2747
  }
2747
2748
  // Fallback: raw sql.js
2748
- const { query, namespace, limit = 10, threshold = 0.3, dbPath: customPath, provenanceFilter } = options;
2749
+ const { query, namespace, limit = 10, threshold = DEFAULT_SEARCH_THRESHOLD, dbPath: customPath, provenanceFilter } = options;
2749
2750
  const effectiveNamespace = namespace || 'all';
2750
2751
  const swarmDir = getMemoryRoot();
2751
2752
  const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Default relevance floor for memory search (#4129).
3
+ *
4
+ * The bundled 384-dim embedding model scores a correct paraphrase match at
5
+ * roughly 0.25-0.5 and unrelated text at 0.0-0.15, so the old 0.3 floor sent
6
+ * back an empty list for natural-language queries whose right answer was
7
+ * clearly the best match (0.28 in the report). 0.2 sits between the two
8
+ * populations. An explicit `threshold` always wins, including 0.
9
+ *
10
+ * #2558 constraint stays: the fused recall score of a full-coverage exact
11
+ * keyword hit with semantic cosine <= 0 tops out at 0.40, so this must stay
12
+ * well below 0.4.
13
+ */
14
+ export declare const DEFAULT_SEARCH_THRESHOLD = 0.2;
15
+ /** How many below-threshold matches an empty default search reports as hints. */
16
+ export declare const NEAR_MISS_LIMIT = 3;
17
+ interface NearMissSearch {
18
+ (req: {
19
+ query: string;
20
+ namespace?: string;
21
+ limit?: number;
22
+ threshold?: number;
23
+ provenanceFilter?: string[];
24
+ dbPath?: string;
25
+ }): Promise<{
26
+ success: boolean;
27
+ results?: Array<{
28
+ key: string;
29
+ namespace: string;
30
+ score: number;
31
+ }>;
32
+ }>;
33
+ }
34
+ /**
35
+ * When a search that used the DEFAULT threshold returns nothing, say what was
36
+ * close instead of leaving the caller to assume the store has no answer
37
+ * (#4129). One extra, cheap query at threshold 0; never throws; absent when
38
+ * nothing at all matched.
39
+ */
40
+ export declare function nearMisses(search: NearMissSearch, req: {
41
+ query: string;
42
+ namespace?: string;
43
+ provenanceFilter?: string[];
44
+ dbPath?: string;
45
+ }): Promise<{
46
+ nearMisses: Array<{
47
+ key: string;
48
+ namespace: string;
49
+ similarity: number;
50
+ }>;
51
+ hint: string;
52
+ } | undefined>;
53
+ export {};
54
+ //# sourceMappingURL=search-defaults.d.ts.map