@claude-flow/cli 3.56.9 → 3.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/helpers/router.cjs +0 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/autopilot-state.d.ts +2 -0
  9. package/dist/src/autopilot-state.js +36 -8
  10. package/dist/src/command-resolution.d.ts +15 -0
  11. package/dist/src/command-resolution.js +25 -0
  12. package/dist/src/commands/agent.js +17 -17
  13. package/dist/src/commands/claims.js +6 -8
  14. package/dist/src/commands/daemon.js +94 -33
  15. package/dist/src/commands/doctor.js +95 -18
  16. package/dist/src/commands/memory.js +14 -2
  17. package/dist/src/commands/policy.js +6 -1
  18. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  19. package/dist/src/commands/security-code-patterns.js +61 -3
  20. package/dist/src/commands/security.d.ts +24 -0
  21. package/dist/src/commands/security.js +81 -35
  22. package/dist/src/commands/tui.d.ts +9 -0
  23. package/dist/src/commands/tui.js +29 -3
  24. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  25. package/dist/src/config/proven-config-refresh.js +24 -3
  26. package/dist/src/funnel/local-signals.js +2 -1
  27. package/dist/src/index.js +11 -47
  28. package/dist/src/init/settings-generator.js +4 -58
  29. package/dist/src/init/types.js +1 -1
  30. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  31. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  32. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  33. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  34. package/dist/src/mcp-tools/agent-tools.js +45 -22
  35. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  36. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  37. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  38. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  39. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  40. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  41. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  42. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  43. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  44. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  45. package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
  46. package/dist/src/mcp-tools/memory-tools.js +100 -25
  47. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  48. package/dist/src/mcp-tools/neural-tools.js +48 -54
  49. package/dist/src/mcp-tools/policy-tools.js +5 -3
  50. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  51. package/dist/src/mcp-tools/security-tools.js +55 -17
  52. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  53. package/dist/src/memory/intelligence.d.ts +5 -1
  54. package/dist/src/memory/intelligence.js +158 -35
  55. package/dist/src/memory/memory-bridge.js +10 -3
  56. package/dist/src/memory/memory-initializer.js +2 -1
  57. package/dist/src/memory/search-defaults.d.ts +54 -0
  58. package/dist/src/memory/search-defaults.js +37 -0
  59. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  60. package/dist/src/memory/sona-optimizer.js +59 -2
  61. package/dist/src/memory/stats-file-lock.d.ts +7 -0
  62. package/dist/src/memory/stats-file-lock.js +53 -0
  63. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  64. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  65. package/dist/src/ruvector/agent-wasm.js +27 -1
  66. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  67. package/dist/src/ruvector/diff-classifier.js +202 -69
  68. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  69. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  70. package/dist/src/services/distill-oracle.d.ts +1 -1
  71. package/dist/src/services/distill-oracle.js +2 -2
  72. package/dist/src/services/policy-anchor-log.d.ts +13 -0
  73. package/dist/src/services/policy-anchor-log.js +242 -0
  74. package/dist/src/services/policy-archive-scan.d.ts +7 -0
  75. package/dist/src/services/policy-archive-scan.js +50 -0
  76. package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
  77. package/dist/src/services/policy-ledger-anchor.js +356 -40
  78. package/dist/src/services/policy-ledger-audit.d.ts +22 -0
  79. package/dist/src/services/policy-ledger-audit.js +260 -0
  80. package/dist/src/services/policy-ledger-record.d.ts +3 -0
  81. package/dist/src/services/policy-ledger-record.js +22 -0
  82. package/dist/src/services/policy-runtime.d.ts +2 -1
  83. package/dist/src/services/policy-runtime.js +267 -40
  84. package/dist/src/services/worker-daemon.d.ts +11 -0
  85. package/dist/src/services/worker-daemon.js +37 -0
  86. package/dist/src/transfer/store/discovery.js +16 -17
  87. package/dist/src/transfer/store/types.d.ts +4 -0
  88. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  89. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  90. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
  91. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  92. package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
  93. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  94. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
  95. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  96. package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
  97. package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
  98. package/package.json +5 -5
  99. package/plugins/ruflo-console/dist/tui.mjs +162 -161
  100. package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
  101. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
@@ -10,11 +10,13 @@
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';
19
+ import { withStatsFileLock } from './stats-file-lock.js';
18
20
  // ============================================================================
19
21
  // Persistence Configuration
20
22
  // ============================================================================
@@ -24,7 +26,11 @@ import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
24
26
  * falling back to home directory
25
27
  */
26
28
  function getDataDir() {
27
- const cwd = process.cwd();
29
+ // Every MCP tool resolves `.claude-flow/` against the project root
30
+ // (CLAUDE_FLOW_CWD when set), so the neural stats/patterns must too —
31
+ // otherwise an MCP server whose process cwd differs from the project
32
+ // writes learning state where nothing reads it.
33
+ const cwd = getProjectCwd();
28
34
  const localDir = join(cwd, '.claude-flow', 'neural');
29
35
  const homeDir = join(homedir(), '.claude-flow', 'neural');
30
36
  // Prefer local directory if .claude-flow exists
@@ -429,6 +435,10 @@ class LocalReasoningBank {
429
435
  * Deduplicates by content: if a pattern with the same content already
430
436
  * exists, the existing entry is updated (bumped usageCount, higher
431
437
  * confidence wins, refreshed lastUsedAt) instead of adding a duplicate.
438
+ *
439
+ * Returns true only when a NEW pattern was added (#3979 item 4): callers that
440
+ * keep a "patterns learned" counter must not count a merge or an update of an
441
+ * existing pattern as a newly learned one.
432
442
  */
433
443
  store(pattern) {
434
444
  const now = Date.now();
@@ -448,6 +458,9 @@ class LocalReasoningBank {
448
458
  if (idx >= 0) {
449
459
  this.patternList[idx] = stored;
450
460
  }
461
+ this.patterns.set(pattern.id, stored);
462
+ this.saveToDisk();
463
+ return false;
451
464
  }
452
465
  else {
453
466
  // Check for content-duplicate before inserting a new entry
@@ -462,7 +475,7 @@ class LocalReasoningBank {
462
475
  // Keep the Map in sync with the mutated object
463
476
  this.patterns.set(contentDupe.id, contentDupe);
464
477
  this.saveToDisk();
465
- return;
478
+ return false;
466
479
  }
467
480
  // Evict oldest if at capacity
468
481
  if (this.patterns.size >= this.maxSize) {
@@ -476,6 +489,7 @@ class LocalReasoningBank {
476
489
  this.patterns.set(pattern.id, stored);
477
490
  // Trigger persistence (debounced)
478
491
  this.saveToDisk();
492
+ return true;
479
493
  }
480
494
  /**
481
495
  * Find similar patterns by embedding.
@@ -629,6 +643,36 @@ let globalStats = {
629
643
  signalsProcessed: 0,
630
644
  lastAdaptation: null
631
645
  };
646
+ /**
647
+ * Increments made by THIS process that have not yet been merged into
648
+ * stats.json. Persistence is delta-based: several processes (MCP server,
649
+ * hook one-shots, the worker daemon) share one stats.json, and each holds
650
+ * its own module-level globalStats. Writing the local snapshot verbatim let
651
+ * any writer erase every other writer's progress — notably the
652
+ * memory-bridge `recordSignalProcessed()` path, which persists without ever
653
+ * loading the file, reset `trajectoriesRecorded` to 0 on disk every 16
654
+ * writes. Merging deltas makes the counters monotonic across processes.
655
+ */
656
+ let pendingDelta = {
657
+ trajectoriesRecorded: 0,
658
+ patternsLearned: 0,
659
+ signalsProcessed: 0,
660
+ };
661
+ let persistedStatsLoaded = false;
662
+ function bumpStat(key, n = 1) {
663
+ globalStats[key] = (globalStats[key] ?? 0) + n;
664
+ pendingDelta[key] += n;
665
+ }
666
+ function readStatsFile() {
667
+ try {
668
+ // Read-only callers keep their unavailable-on-error contract, while invalid
669
+ // disk values cannot poison local state and a later repaired-file flush.
670
+ return readStatsForWrite(getStatsPath());
671
+ }
672
+ catch {
673
+ return null;
674
+ }
675
+ }
632
676
  // ============================================================================
633
677
  // Stats Persistence
634
678
  // ============================================================================
@@ -636,36 +680,99 @@ let globalStats = {
636
680
  * Load persisted stats from disk
637
681
  */
638
682
  function loadPersistedStats() {
683
+ persistedStatsLoaded = true;
684
+ const data = readStatsFile();
685
+ if (!data)
686
+ return;
687
+ // #2245: previously only restored trajectoriesRecorded — patternsLearned
688
+ // and signalsProcessed reset to zero on every restart, masking real
689
+ // learning progress in the dashboards. Unsaved local increments are
690
+ // re-applied on top so a late load never drops them.
691
+ globalStats.trajectoriesRecorded = (data.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded;
692
+ globalStats.patternsLearned = (data.patternsLearned ?? 0) + pendingDelta.patternsLearned;
693
+ globalStats.signalsProcessed = (data.signalsProcessed ?? 0) + pendingDelta.signalsProcessed;
694
+ globalStats.lastAdaptation = maxTimestamp(data.lastAdaptation ?? null, globalStats.lastAdaptation);
695
+ }
696
+ function maxTimestamp(a, b) {
697
+ if (a == null)
698
+ return b;
699
+ if (b == null)
700
+ return a;
701
+ return Math.max(a, b);
702
+ }
703
+ /** Missing files are first use; unreadable or invalid files must not be replaced. */
704
+ function readStatsForWrite(path) {
705
+ let text;
639
706
  try {
640
- 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
- }
707
+ text = readFileSync(path, 'utf-8');
708
+ }
709
+ catch (error) {
710
+ if (error.code === 'ENOENT')
711
+ return null;
712
+ throw error;
713
+ }
714
+ let data;
715
+ try {
716
+ data = JSON.parse(text);
653
717
  }
654
718
  catch {
655
- // Ignore load errors, start fresh
719
+ throw Object.assign(new Error('Invalid learning stats JSON'), { code: 'EINVALIDSTATS' });
720
+ }
721
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
722
+ throw Object.assign(new Error('Invalid learning stats object'), { code: 'EINVALIDSTATS' });
656
723
  }
724
+ for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
725
+ const value = data[key];
726
+ // Older files may omit a counter, but present counters must be exact counts.
727
+ if (value === undefined)
728
+ continue;
729
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
730
+ throw Object.assign(new Error(`Invalid learning stats field: ${key}`), { code: 'EINVALIDSTATS' });
731
+ }
732
+ }
733
+ const timestamp = data.lastAdaptation;
734
+ if (timestamp != null && (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0)) {
735
+ throw Object.assign(new Error('Invalid learning stats timestamp'), { code: 'EINVALIDSTATS' });
736
+ }
737
+ return data;
657
738
  }
658
- /**
659
- * Save stats to disk
660
- */
739
+ /** Merge pending deltas under the cooperating-writer lock, then adopt committed totals. */
661
740
  function savePersistedStats() {
741
+ let statsPath;
662
742
  try {
663
- ensureDataDir();
664
- const path = getStatsPath();
665
- writeFileSync(path, JSON.stringify(globalStats, null, 2), 'utf-8');
743
+ statsPath = join(ensureDataDir(), 'stats.json');
744
+ withStatsFileLock(statsPath, (path) => {
745
+ const disk = readStatsForWrite(path);
746
+ const merged = disk
747
+ ? {
748
+ trajectoriesRecorded: (disk.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded,
749
+ patternsLearned: (disk.patternsLearned ?? 0) + pendingDelta.patternsLearned,
750
+ signalsProcessed: (disk.signalsProcessed ?? 0) + pendingDelta.signalsProcessed,
751
+ lastAdaptation: maxTimestamp(disk.lastAdaptation ?? null, globalStats.lastAdaptation),
752
+ }
753
+ : { ...globalStats };
754
+ for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
755
+ if (!Number.isSafeInteger(merged[key]) || merged[key] < 0) {
756
+ throw Object.assign(new Error('Learning stats counter overflow'), { code: 'EINVALIDSTATS' });
757
+ }
758
+ }
759
+ if (merged.lastAdaptation != null &&
760
+ (!Number.isFinite(merged.lastAdaptation) || merged.lastAdaptation < 0)) {
761
+ throw Object.assign(new Error('Invalid merged learning stats timestamp'), { code: 'EINVALIDSTATS' });
762
+ }
763
+ const tmp = `${path}.${process.pid}.tmp`;
764
+ writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf-8');
765
+ renameSync(tmp, path);
766
+ // Commit local state before release: a failed release must not replay it.
767
+ globalStats = merged;
768
+ pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
769
+ persistedStatsLoaded = true;
770
+ });
666
771
  }
667
- catch {
668
- // Ignore save errors
772
+ catch (error) {
773
+ const { code = 'UNKNOWN', path = statsPath || 'neural stats directory' } = error;
774
+ // Do not print file contents, which may be malformed or user-controlled.
775
+ console.warn(`[intelligence] Could not persist learning stats at ${path} (${code}); pending increments retained`);
669
776
  }
670
777
  }
671
778
  /**
@@ -679,7 +786,7 @@ function savePersistedStats() {
679
786
  let signalsSinceLastSave = 0;
680
787
  const SIGNAL_PERSIST_EVERY = 16;
681
788
  export function recordSignalProcessed() {
682
- globalStats.signalsProcessed = (globalStats.signalsProcessed ?? 0) + 1;
789
+ bumpStat('signalsProcessed');
683
790
  signalsSinceLastSave++;
684
791
  if (signalsSinceLastSave >= SIGNAL_PERSIST_EVERY) {
685
792
  savePersistedStats();
@@ -881,7 +988,7 @@ export async function recordStep(step) {
881
988
  await sonaCoordinator.distillLearning(reasoningBank);
882
989
  globalStats.lastAdaptation = Date.now();
883
990
  }
884
- globalStats.trajectoriesRecorded++;
991
+ bumpStat('trajectoriesRecorded');
885
992
  savePersistedStats();
886
993
  return true;
887
994
  }
@@ -928,7 +1035,7 @@ export async function recordTrajectory(steps, verdict) {
928
1035
  if (verdict === 'success') {
929
1036
  for (const step of enrichedSteps) {
930
1037
  if (step.embedding && step.embedding.length > 0) {
931
- reasoningBank.store({
1038
+ const added = reasoningBank.store({
932
1039
  id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
933
1040
  type: step.type,
934
1041
  content: step.content,
@@ -937,7 +1044,11 @@ export async function recordTrajectory(steps, verdict) {
937
1044
  metadata: step.metadata || {},
938
1045
  createdAt: Date.now(),
939
1046
  });
940
- globalStats.patternsLearned++;
1047
+ // #3979 item 4: count only patterns that were actually added. A step
1048
+ // whose content already exists is merged into that pattern, so the
1049
+ // counter no longer climbs once per successful step.
1050
+ if (added)
1051
+ bumpStat('patternsLearned');
941
1052
  }
942
1053
  }
943
1054
  }
@@ -949,20 +1060,22 @@ export async function recordTrajectory(steps, verdict) {
949
1060
  const avgQuality = verdict === 'success' ? 1.0 : verdict === 'partial' ? 0.5 : 0.0;
950
1061
  ruvllmCoord.recordTrajectory({
951
1062
  steps: enrichedSteps.map(s => ({
952
- state: s.content,
953
- action: s.type,
954
- reward: avgQuality,
955
- embedding: s.embedding || []
1063
+ type: ['query', 'route', 'generate', 'memory', 'feedback'].includes(s.type)
1064
+ ? s.type : 'generate',
1065
+ input: s.content,
1066
+ output: '',
1067
+ durationMs: 0,
1068
+ confidence: avgQuality
956
1069
  })),
957
1070
  totalReward: avgQuality,
958
- success: verdict === 'success'
1071
+ outcome: verdict
959
1072
  });
960
1073
  }
961
1074
  catch {
962
1075
  // ruvllm recording failed silently
963
1076
  }
964
1077
  }
965
- globalStats.trajectoriesRecorded++;
1078
+ bumpStat('trajectoriesRecorded');
966
1079
  globalStats.lastAdaptation = Date.now();
967
1080
  savePersistedStats();
968
1081
  return true;
@@ -1034,6 +1147,12 @@ export function getIntelligenceStats() {
1034
1147
  if (!ruvllmLoaded) {
1035
1148
  loadRuvllmCoordinatorSync();
1036
1149
  }
1150
+ // A read-only status process (ruvllm_status, `neural status`, a fresh MCP
1151
+ // server) has never run initializeIntelligence(); without this it reported
1152
+ // 0 trajectories no matter what other processes had persisted.
1153
+ if (!persistedStatsLoaded) {
1154
+ loadPersistedStats();
1155
+ }
1037
1156
  const ruvllmStats = ruvllmCoordinator?.stats?.() || null;
1038
1157
  // Fetch cross-module stats for unified reporting.
1039
1158
  //
@@ -1145,6 +1264,10 @@ export function clearIntelligence() {
1145
1264
  signalsProcessed: 0,
1146
1265
  lastAdaptation: null
1147
1266
  };
1267
+ pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
1268
+ // In-memory reset only: stay "loaded" so the next stats read does not
1269
+ // immediately re-hydrate the counters this call just cleared.
1270
+ persistedStatsLoaded = true;
1148
1271
  }
1149
1272
  /**
1150
1273
  * 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
@@ -0,0 +1,37 @@
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 const DEFAULT_SEARCH_THRESHOLD = 0.2;
15
+ /** How many below-threshold matches an empty default search reports as hints. */
16
+ export const NEAR_MISS_LIMIT = 3;
17
+ /**
18
+ * When a search that used the DEFAULT threshold returns nothing, say what was
19
+ * close instead of leaving the caller to assume the store has no answer
20
+ * (#4129). One extra, cheap query at threshold 0; never throws; absent when
21
+ * nothing at all matched.
22
+ */
23
+ export async function nearMisses(search, req) {
24
+ try {
25
+ const r = await search({ ...req, limit: NEAR_MISS_LIMIT, threshold: 0 });
26
+ if (!r?.success || !r.results?.length)
27
+ return undefined;
28
+ return {
29
+ nearMisses: r.results.map(e => ({ key: e.key, namespace: e.namespace, similarity: e.score })),
30
+ hint: `No result reached the default threshold (${DEFAULT_SEARCH_THRESHOLD}); the closest matches are in nearMisses. Pass threshold: 0 to include them in results.`,
31
+ };
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ //# sourceMappingURL=search-defaults.js.map
@@ -93,6 +93,8 @@ export interface SONAStats {
93
93
  agents: number;
94
94
  } | 'unavailable';
95
95
  }
96
+ /** The routing gate in getRoutingSuggestion: confidence x matchRatio must reach this for a learned pattern to be used. */
97
+ export declare const SONA_ROUTING_GATE = 0.6;
96
98
  /** Agent roles supported by the SONA routing optimizer. */
97
99
  export declare function isSupportedSonaAgent(agent: string): boolean;
98
100
  /**
@@ -101,6 +103,17 @@ export declare function isSupportedSonaAgent(agent: string): boolean;
101
103
  * Learns from past task outcomes to improve future routing decisions.
102
104
  * Integrates with Q-learning router for hybrid routing strategy.
103
105
  */
106
+ /**
107
+ * How close a pattern's confidence is to the routing gate. `successesNeeded`
108
+ * assumes a perfect keyword match (matchRatio 1) and the 0.1 success increment;
109
+ * a partial match needs more.
110
+ */
111
+ export declare function routingGateStatus(confidence: number): {
112
+ threshold: number;
113
+ confidence: number;
114
+ reached: boolean;
115
+ successesNeeded: number;
116
+ };
104
117
  export declare class SONAOptimizer {
105
118
  private patterns;
106
119
  private trajectoriesProcessed;
@@ -140,6 +153,15 @@ export declare class SONAOptimizer {
140
153
  patternKey: string;
141
154
  confidence: number;
142
155
  keywordsExtracted: string[];
156
+ /** True when this trajectory reinforced an existing near-identical pattern instead of creating a new one (#4134). */
157
+ reinforcedNeighbour?: boolean;
158
+ /** How far the pattern is from influencing routing, so a user can see learning has not kicked in yet (#4134). */
159
+ routingGate?: {
160
+ threshold: number;
161
+ confidence: number;
162
+ reached: boolean;
163
+ successesNeeded: number;
164
+ };
143
165
  };
144
166
  /**
145
167
  * Get routing suggestion based on learned patterns.
@@ -202,6 +224,12 @@ export declare class SONAOptimizer {
202
224
  * Create a unique pattern key from keywords and agent
203
225
  */
204
226
  private createPatternKey;
227
+ /**
228
+ * The key of the existing pattern for `agent` whose keyword set overlaps
229
+ * `keywords` the most, if that overlap (Jaccard) reaches NEIGHBOUR_MIN_JACCARD.
230
+ * Ties go to the more confident pattern. Linear in the pattern count (<= 1000).
231
+ */
232
+ private findNeighbourPattern;
205
233
  /**
206
234
  * Find the best matching pattern for given keywords
207
235
  */
@@ -26,6 +26,10 @@ const CONFIDENCE_INCREMENT = 0.1;
26
26
  const CONFIDENCE_DECREMENT = 0.15;
27
27
  const DECAY_RATE = 0.01; // Per day
28
28
  const MAX_PATTERNS = 1000;
29
+ /** #4134: a trajectory reinforces an existing pattern of the same agent whose keyword set overlaps at least this much (Jaccard). */
30
+ const NEIGHBOUR_MIN_JACCARD = 0.6;
31
+ /** The routing gate in getRoutingSuggestion: confidence x matchRatio must reach this for a learned pattern to be used. */
32
+ export const SONA_ROUTING_GATE = 0.6;
29
33
  // ============================================================================
30
34
  // Contrastive Trainer (lazy-loaded from @ruvector/ruvllm)
31
35
  // ============================================================================
@@ -119,6 +123,18 @@ const KEYWORD_CATEGORIES = {
119
123
  * Learns from past task outcomes to improve future routing decisions.
120
124
  * Integrates with Q-learning router for hybrid routing strategy.
121
125
  */
126
+ /**
127
+ * How close a pattern's confidence is to the routing gate. `successesNeeded`
128
+ * assumes a perfect keyword match (matchRatio 1) and the 0.1 success increment;
129
+ * a partial match needs more.
130
+ */
131
+ export function routingGateStatus(confidence) {
132
+ const reached = confidence >= SONA_ROUTING_GATE;
133
+ const successesNeeded = reached || confidence >= MAX_CONFIDENCE
134
+ ? 0
135
+ : Math.ceil(Math.log((1 - SONA_ROUTING_GATE) / (1 - confidence)) / Math.log(1 - CONFIDENCE_INCREMENT));
136
+ return { threshold: SONA_ROUTING_GATE, confidence, reached, successesNeeded };
137
+ }
122
138
  export class SONAOptimizer {
123
139
  patterns = new Map();
124
140
  trajectoriesProcessed = 0;
@@ -210,7 +226,18 @@ export class SONAOptimizer {
210
226
  };
211
227
  }
212
228
  // Create pattern key from sorted keywords
213
- const patternKey = this.createPatternKey(keywords, agent);
229
+ let patternKey = this.createPatternKey(keywords, agent);
230
+ // #4134: near-identical tasks ("Verify the X plugin ..." / "Verify the Y plugin ...")
231
+ // used to create a separate pattern each at confidence 0.5, so none ever reached
232
+ // the routing gate. Reinforce the closest existing pattern of the same agent instead.
233
+ let reinforcedNeighbour = false;
234
+ if (!this.patterns.has(patternKey)) {
235
+ const neighbour = this.findNeighbourPattern(keywords, agent);
236
+ if (neighbour) {
237
+ patternKey = neighbour;
238
+ reinforcedNeighbour = true;
239
+ }
240
+ }
214
241
  // Get or create pattern
215
242
  let pattern = this.patterns.get(patternKey);
216
243
  if (!pattern) {
@@ -264,6 +291,8 @@ export class SONAOptimizer {
264
291
  patternKey,
265
292
  confidence: pattern.confidence,
266
293
  keywordsExtracted: keywords,
294
+ reinforcedNeighbour,
295
+ routingGate: routingGateStatus(pattern.confidence),
267
296
  };
268
297
  }
269
298
  /**
@@ -305,7 +334,7 @@ export class SONAOptimizer {
305
334
  const keywords = this.extractKeywords(task);
306
335
  // Priority 2: Try SONA learned pattern matching
307
336
  const sonaResult = this.findBestPatternMatch(keywords);
308
- if (sonaResult && sonaResult.confidence >= 0.6) {
337
+ if (sonaResult && sonaResult.confidence >= SONA_ROUTING_GATE) {
309
338
  return {
310
339
  agent: sonaResult.agent,
311
340
  confidence: sonaResult.confidence,
@@ -532,6 +561,34 @@ export class SONAOptimizer {
532
561
  const sortedKeywords = [...keywords].sort();
533
562
  return `${agent}:${sortedKeywords.join('+')}`;
534
563
  }
564
+ /**
565
+ * The key of the existing pattern for `agent` whose keyword set overlaps
566
+ * `keywords` the most, if that overlap (Jaccard) reaches NEIGHBOUR_MIN_JACCARD.
567
+ * Ties go to the more confident pattern. Linear in the pattern count (<= 1000).
568
+ */
569
+ findNeighbourPattern(keywords, agent) {
570
+ const wanted = new Set(keywords);
571
+ let bestKey = null;
572
+ let bestJaccard = 0;
573
+ let bestConfidence = -1;
574
+ for (const [key, pattern] of this.patterns) {
575
+ if (pattern.agent !== agent)
576
+ continue;
577
+ let shared = 0;
578
+ for (const k of pattern.keywords)
579
+ if (wanted.has(k))
580
+ shared++;
581
+ const union = wanted.size + pattern.keywords.length - shared;
582
+ const jaccard = union === 0 ? 0 : shared / union;
583
+ if (jaccard >= NEIGHBOUR_MIN_JACCARD &&
584
+ (jaccard > bestJaccard || (jaccard === bestJaccard && pattern.confidence > bestConfidence))) {
585
+ bestKey = key;
586
+ bestJaccard = jaccard;
587
+ bestConfidence = pattern.confidence;
588
+ }
589
+ }
590
+ return bestKey;
591
+ }
535
592
  /**
536
593
  * Find the best matching pattern for given keywords
537
594
  */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Serialize cooperating local writers without stealing a paused writer's lock.
3
+ * Abnormal termination can leave an orphan: recovery requires confirming that
4
+ * no writer still owns the directory before removing it. Never age-reap it.
5
+ */
6
+ export declare function withStatsFileLock<T>(file: string, write: (canonicalFile: string) => T): T;
7
+ //# sourceMappingURL=stats-file-lock.d.ts.map