claude-flow 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.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/intelligence.cjs +51 -9
- package/.claude/proven-config.json +42 -0
- package/.claude/skills/verification-quality/SKILL.md +14 -11
- package/README.md +1 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
- package/package.json +2 -2
- package/v3/@claude-flow/cli/README.md +164 -62
- package/v3/@claude-flow/cli/catalog-manifest.json +5 -5
- package/v3/@claude-flow/cli/dist/src/autopilot-state.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/autopilot-state.js +36 -8
- package/v3/@claude-flow/cli/dist/src/command-resolution.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/command-resolution.js +25 -0
- package/v3/@claude-flow/cli/dist/src/commands/agent.js +17 -17
- package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +94 -33
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +95 -18
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +6 -1
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.js +61 -3
- package/v3/@claude-flow/cli/dist/src/commands/security.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +81 -35
- package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +29 -3
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +24 -3
- package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +2 -1
- package/v3/@claude-flow/cli/dist/src/index.js +11 -47
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +4 -58
- package/v3/@claude-flow/cli/dist/src/init/types.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.js +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +45 -22
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.js +7 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +25 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +10 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +6 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +29 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +100 -25
- package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +48 -54
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-tools.js +5 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +6 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +55 -17
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +66 -17
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +5 -1
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +158 -35
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +10 -3
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +2 -1
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.d.ts +54 -0
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.js +37 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +59 -2
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.js +53 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +24 -24
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.d.ts +10 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +27 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.d.ts +2 -4
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +202 -69
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.js +12 -1
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +2 -2
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.js +242 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.js +50 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +14 -8
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +356 -40
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.js +260 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.js +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +2 -1
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +267 -40
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +37 -0
- package/v3/@claude-flow/cli/dist/src/transfer/store/discovery.js +16 -17
- package/v3/@claude-flow/cli/dist/src/transfer/store/types.d.ts +4 -0
- package/v3/@claude-flow/cli/package.json +5 -5
|
@@ -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', '
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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>):
|
|
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,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
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
1071
|
+
outcome: verdict
|
|
959
1072
|
});
|
|
960
1073
|
}
|
|
961
1074
|
catch {
|
|
962
1075
|
// ruvllm recording failed silently
|
|
963
1076
|
}
|
|
964
1077
|
}
|
|
965
|
-
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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
|