claude-flow 3.47.0 → 3.48.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/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +6 -3
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +0 -2
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.js +242 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +24 -41
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +73 -53
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
- package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
- package/v3/@claude-flow/cli/package.json +9 -9
- package/v3/@claude-flow/guidance/package.json +5 -6
- package/v3/@claude-flow/shared/package.json +6 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
|
|
1
2
|
/**
|
|
2
3
|
* Hooks MCP Tools
|
|
3
4
|
* Provides intelligent hooks functionality via MCP protocol
|
|
@@ -410,8 +411,75 @@ async function getLoRAAdapter() {
|
|
|
410
411
|
}
|
|
411
412
|
return loraAdapter;
|
|
412
413
|
}
|
|
413
|
-
//
|
|
414
|
+
// Cache pending trajectories; the existing memory store also persists checkpoints.
|
|
414
415
|
const activeTrajectories = new Map();
|
|
416
|
+
const dirtyTrajectories = new Set();
|
|
417
|
+
const trajectoryOperations = new Map();
|
|
418
|
+
/** Serialize read/modify/checkpoint operations for a trajectory in this process. */
|
|
419
|
+
async function withTrajectoryOperation(id, operation) {
|
|
420
|
+
const previous = trajectoryOperations.get(id) ?? Promise.resolve();
|
|
421
|
+
let release;
|
|
422
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
423
|
+
const current = previous.then(() => gate);
|
|
424
|
+
trajectoryOperations.set(id, current);
|
|
425
|
+
await previous;
|
|
426
|
+
try {
|
|
427
|
+
return await operation();
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
release();
|
|
431
|
+
if (trajectoryOperations.get(id) === current)
|
|
432
|
+
trajectoryOperations.delete(id);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function persistPendingTrajectory(trajectory) {
|
|
436
|
+
dirtyTrajectories.add(trajectory.id);
|
|
437
|
+
try {
|
|
438
|
+
const store = await getRealStoreFunction();
|
|
439
|
+
const result = await store?.({
|
|
440
|
+
key: `trajectory-pending-${trajectory.id}`, value: JSON.stringify(trajectory),
|
|
441
|
+
namespace: 'trajectories', tags: [trajectory.agent, 'pending', 'sona-trajectory'], upsert: true,
|
|
442
|
+
});
|
|
443
|
+
if (result?.success === true)
|
|
444
|
+
dirtyTrajectories.delete(trajectory.id);
|
|
445
|
+
return result?.success === true;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function loadPendingTrajectory(id) {
|
|
452
|
+
try {
|
|
453
|
+
const { getEntry } = await import('../memory/memory-initializer.js');
|
|
454
|
+
// A completed record is authoritative even if pending-record cleanup failed
|
|
455
|
+
// or another process still has an old cache entry.
|
|
456
|
+
const completed = await getEntry({ key: `trajectory-${id}`, namespace: 'trajectories' });
|
|
457
|
+
if (completed.success && completed.found) {
|
|
458
|
+
activeTrajectories.delete(id);
|
|
459
|
+
dirtyTrajectories.delete(id);
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
// Failed checkpoints must not erase already-acknowledged live steps.
|
|
463
|
+
// Clean caches still reload so another process's later checkpoint is seen.
|
|
464
|
+
if (dirtyTrajectories.has(id) && activeTrajectories.has(id))
|
|
465
|
+
return activeTrajectories.get(id);
|
|
466
|
+
const pending = await getEntry({ key: `trajectory-pending-${id}`, namespace: 'trajectories' });
|
|
467
|
+
if (pending.success && pending.found) {
|
|
468
|
+
const value = JSON.parse(pending.entry?.content ?? 'null');
|
|
469
|
+
if (!value || value.id !== id || typeof value.task !== 'string' || typeof value.agent !== 'string'
|
|
470
|
+
|| typeof value.startedAt !== 'string' || !Number.isFinite(Date.parse(value.startedAt))
|
|
471
|
+
|| !Array.isArray(value.steps) || !value.steps.every((step) => step
|
|
472
|
+
&& typeof step.action === 'string' && typeof step.result === 'string'
|
|
473
|
+
&& typeof step.quality === 'number' && Number.isFinite(step.quality)
|
|
474
|
+
&& typeof step.timestamp === 'string'))
|
|
475
|
+
return undefined;
|
|
476
|
+
activeTrajectories.set(id, value);
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch { /* store unavailable: retain same-process best-effort tracking */ }
|
|
481
|
+
return activeTrajectories.get(id);
|
|
482
|
+
}
|
|
415
483
|
const MEMORY_DIR = '.claude-flow/memory';
|
|
416
484
|
const MEMORY_FILE = 'store.json';
|
|
417
485
|
function getMemoryPath() {
|
|
@@ -897,13 +965,19 @@ export const hooksPostCommand = {
|
|
|
897
965
|
properties: {
|
|
898
966
|
command: { type: 'string', description: 'Executed command' },
|
|
899
967
|
exitCode: { type: 'number', description: 'Command exit code' },
|
|
968
|
+
success: { type: 'boolean', description: 'Explicit execution outcome; false records a failure even without a nonzero exit code' },
|
|
969
|
+
ttl: { type: 'integer', minimum: 1, maximum: 2147483647, description: 'Command history lifetime in seconds (default: 30 days)' },
|
|
900
970
|
},
|
|
901
971
|
required: ['command'],
|
|
902
972
|
},
|
|
903
973
|
handler: async (params) => {
|
|
904
974
|
const command = params.command;
|
|
905
975
|
const exitCode = params.exitCode || 0;
|
|
906
|
-
const success = exitCode === 0;
|
|
976
|
+
const success = exitCode === 0 && params.success !== false;
|
|
977
|
+
const ttl = params.ttl ?? 30 * 24 * 60 * 60;
|
|
978
|
+
if (typeof ttl !== 'number' || !Number.isSafeInteger(ttl) || ttl < 1 || ttl > 2147483647) {
|
|
979
|
+
return { success: false, recorded: false, error: 'ttl must be a positive integer number of seconds (at most 2147483647)' };
|
|
980
|
+
}
|
|
907
981
|
{
|
|
908
982
|
const v = validateText(command, 'command');
|
|
909
983
|
if (!v.valid)
|
|
@@ -916,21 +990,35 @@ export const hooksPostCommand = {
|
|
|
916
990
|
let _storedIn = 'none';
|
|
917
991
|
try {
|
|
918
992
|
const bridge = await import('../memory/memory-bridge.js');
|
|
919
|
-
await bridge.bridgeStoreEntry({
|
|
993
|
+
const stored = await bridge.bridgeStoreEntry({
|
|
920
994
|
key: `cmd-${Date.now()}`,
|
|
921
995
|
value: JSON.stringify({ command, exitCode, success }),
|
|
922
996
|
namespace: 'commands',
|
|
923
997
|
tags: [success ? 'success' : 'error'],
|
|
998
|
+
ttl,
|
|
999
|
+
generateEmbeddingFlag: false,
|
|
924
1000
|
});
|
|
1001
|
+
if (!stored?.success)
|
|
1002
|
+
throw new Error(stored?.error || 'Native bridge did not store the command');
|
|
925
1003
|
_storedIn = 'agentdb';
|
|
926
1004
|
}
|
|
927
1005
|
catch {
|
|
928
1006
|
// AgentDB not available — store in JSON
|
|
929
1007
|
try {
|
|
930
1008
|
const store = loadMemoryStore();
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
1009
|
+
const now = Date.now();
|
|
1010
|
+
for (const [entryKey, entry] of Object.entries(store.entries)) {
|
|
1011
|
+
if (entry.namespace === 'commands' && entry.expiresAt && Date.parse(entry.expiresAt) <= now) {
|
|
1012
|
+
delete store.entries[entryKey];
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const key = `cmd-${now}`;
|
|
1016
|
+
store.entries[key] = {
|
|
1017
|
+
key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands',
|
|
1018
|
+
storedAt: new Date(now).toISOString(), lastAccessed: new Date(now).toISOString(), accessCount: 0,
|
|
1019
|
+
expiresAt: new Date(now + ttl * 1000).toISOString(),
|
|
1020
|
+
};
|
|
1021
|
+
const memDir = dirname(getMemoryPath());
|
|
934
1022
|
if (!existsSync(memDir))
|
|
935
1023
|
mkdirSync(memDir, { recursive: true });
|
|
936
1024
|
writeFileSync(getMemoryPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
@@ -1444,9 +1532,10 @@ export const hooksPostTask = {
|
|
|
1444
1532
|
type: 'object',
|
|
1445
1533
|
properties: {
|
|
1446
1534
|
taskId: { type: 'string', description: 'Task identifier' },
|
|
1535
|
+
patterns: feedbackPatternsSchema,
|
|
1447
1536
|
success: { type: 'boolean', description: 'Whether task was successful' },
|
|
1448
1537
|
agent: { type: 'string', description: 'Agent that completed the task' },
|
|
1449
|
-
quality: { type: 'number', description: 'Quality score (0-1)' },
|
|
1538
|
+
quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
|
|
1450
1539
|
task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
|
|
1451
1540
|
duration: { type: 'number', description: 'Observed task duration in milliseconds (used by pheromone-adaptive topology)' },
|
|
1452
1541
|
latencyBudgetMs: { type: 'number', description: 'Latency budget used to normalize duration (default 60000ms)' },
|
|
@@ -1463,8 +1552,19 @@ export const hooksPostTask = {
|
|
|
1463
1552
|
const taskId = params.taskId;
|
|
1464
1553
|
const success = params.success !== false;
|
|
1465
1554
|
const agent = params.agent;
|
|
1466
|
-
|
|
1555
|
+
if (params.quality !== undefined && (typeof params.quality !== 'number'
|
|
1556
|
+
|| !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
|
|
1557
|
+
return { success: false, error: 'quality must be a finite number between 0 and 1' };
|
|
1558
|
+
}
|
|
1559
|
+
const quality = params.quality === undefined ? (success ? 0.85 : 0.3) : params.quality;
|
|
1467
1560
|
const startTime = Date.now();
|
|
1561
|
+
let patterns;
|
|
1562
|
+
try {
|
|
1563
|
+
patterns = validateFeedbackPatterns(params.patterns);
|
|
1564
|
+
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
return { success: false, error: error.message };
|
|
1567
|
+
}
|
|
1468
1568
|
{
|
|
1469
1569
|
const v = validateIdentifier(taskId, 'taskId');
|
|
1470
1570
|
if (!v.valid)
|
|
@@ -1502,7 +1602,7 @@ export const hooksPostTask = {
|
|
|
1502
1602
|
quality,
|
|
1503
1603
|
agent,
|
|
1504
1604
|
duration: params.duration || undefined,
|
|
1505
|
-
patterns
|
|
1605
|
+
patterns,
|
|
1506
1606
|
// ADR-147 P2: forward spawn-tree lineage so it lands in feedback + memory
|
|
1507
1607
|
parentAgentId,
|
|
1508
1608
|
depth,
|
|
@@ -2754,9 +2854,8 @@ export const hooksIntelligenceReset = {
|
|
|
2754
2854
|
// Directory read failed
|
|
2755
2855
|
}
|
|
2756
2856
|
}
|
|
2757
|
-
//
|
|
2758
|
-
cleared.trajectories =
|
|
2759
|
-
activeTrajectories.clear();
|
|
2857
|
+
// Reset learned state without abandoning tasks still recording outcomes.
|
|
2858
|
+
cleared.trajectories = 0;
|
|
2760
2859
|
return {
|
|
2761
2860
|
reset: true,
|
|
2762
2861
|
cleared,
|
|
@@ -2815,27 +2914,15 @@ export const hooksTrajectoryStart = {
|
|
|
2815
2914
|
getTrajectoryTree().openTrajectory({ sessionId, trajectoryId, task, agent });
|
|
2816
2915
|
}
|
|
2817
2916
|
catch { /* prototype path — never blocks trajectory recording */ }
|
|
2818
|
-
//
|
|
2819
|
-
const
|
|
2820
|
-
if (storeFn) {
|
|
2821
|
-
try {
|
|
2822
|
-
await storeFn({
|
|
2823
|
-
key: `trajectory-pending-${trajectoryId}`,
|
|
2824
|
-
value: JSON.stringify(trajectory),
|
|
2825
|
-
namespace: 'trajectories',
|
|
2826
|
-
tags: [agent, 'pending', 'sona-trajectory'],
|
|
2827
|
-
});
|
|
2828
|
-
}
|
|
2829
|
-
catch {
|
|
2830
|
-
// Best-effort persistence — trajectory still lives in-memory
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2917
|
+
// Checkpoint before handing the id to another process.
|
|
2918
|
+
const persisted = await persistPendingTrajectory(trajectory);
|
|
2833
2919
|
return {
|
|
2834
2920
|
trajectoryId,
|
|
2835
2921
|
task,
|
|
2836
2922
|
agent,
|
|
2837
2923
|
started: startedAt,
|
|
2838
2924
|
status: 'recording',
|
|
2925
|
+
persisted,
|
|
2839
2926
|
implementation: 'real-trajectory-tracking',
|
|
2840
2927
|
activeCount: activeTrajectories.size,
|
|
2841
2928
|
};
|
|
@@ -2850,17 +2937,21 @@ export const hooksTrajectoryStep = {
|
|
|
2850
2937
|
trajectoryId: { type: 'string', description: 'Trajectory ID' },
|
|
2851
2938
|
action: { type: 'string', description: 'Action taken' },
|
|
2852
2939
|
result: { type: 'string', description: 'Action result' },
|
|
2853
|
-
quality: { type: 'number', description: 'Quality score (0-1)' },
|
|
2940
|
+
quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
|
|
2854
2941
|
},
|
|
2855
2942
|
required: ['trajectoryId', 'action'],
|
|
2856
2943
|
},
|
|
2857
|
-
handler: async (params) => {
|
|
2944
|
+
handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
|
|
2858
2945
|
const trajectoryId = params.trajectoryId;
|
|
2859
2946
|
// #14: scrub extended-thinking blocks so reasoning tokens don't contaminate
|
|
2860
2947
|
// the learning signal (DISTILL embeds this text).
|
|
2861
2948
|
const action = scrubReasoningBlocks(params.action);
|
|
2862
2949
|
const result = scrubReasoningBlocks(params.result || 'success');
|
|
2863
|
-
|
|
2950
|
+
if (params.quality !== undefined && (typeof params.quality !== 'number'
|
|
2951
|
+
|| !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
|
|
2952
|
+
return { success: false, error: 'quality must be a finite number between 0 and 1' };
|
|
2953
|
+
}
|
|
2954
|
+
const quality = params.quality === undefined ? 0.85 : params.quality;
|
|
2864
2955
|
const timestamp = new Date().toISOString();
|
|
2865
2956
|
const stepId = `step-${Date.now()}`;
|
|
2866
2957
|
{
|
|
@@ -2874,7 +2965,7 @@ export const hooksTrajectoryStep = {
|
|
|
2874
2965
|
return { success: false, error: v.error };
|
|
2875
2966
|
}
|
|
2876
2967
|
// Add step to real trajectory if it exists
|
|
2877
|
-
const trajectory =
|
|
2968
|
+
const trajectory = await loadPendingTrajectory(trajectoryId);
|
|
2878
2969
|
if (trajectory) {
|
|
2879
2970
|
trajectory.steps.push({
|
|
2880
2971
|
action,
|
|
@@ -2883,6 +2974,7 @@ export const hooksTrajectoryStep = {
|
|
|
2883
2974
|
timestamp,
|
|
2884
2975
|
});
|
|
2885
2976
|
}
|
|
2977
|
+
const persisted = trajectory ? await persistPendingTrajectory(trajectory) : false;
|
|
2886
2978
|
// MAGE-style execution-state-tree mirror (prototype, non-fatal)
|
|
2887
2979
|
try {
|
|
2888
2980
|
const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
|
|
@@ -2914,11 +3006,12 @@ export const hooksTrajectoryStep = {
|
|
|
2914
3006
|
result,
|
|
2915
3007
|
quality,
|
|
2916
3008
|
recorded: !!trajectory,
|
|
3009
|
+
persisted,
|
|
2917
3010
|
timestamp,
|
|
2918
3011
|
totalSteps: trajectory?.steps.length || 0,
|
|
2919
3012
|
implementation: trajectory ? 'real-step-recording' : 'trajectory-not-found',
|
|
2920
3013
|
};
|
|
2921
|
-
},
|
|
3014
|
+
}),
|
|
2922
3015
|
};
|
|
2923
3016
|
export const hooksTrajectoryEnd = {
|
|
2924
3017
|
name: 'hooks_intelligence_trajectory-end',
|
|
@@ -2932,7 +3025,7 @@ export const hooksTrajectoryEnd = {
|
|
|
2932
3025
|
},
|
|
2933
3026
|
required: ['trajectoryId'],
|
|
2934
3027
|
},
|
|
2935
|
-
handler: async (params) => {
|
|
3028
|
+
handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
|
|
2936
3029
|
const trajectoryId = params.trajectoryId;
|
|
2937
3030
|
{
|
|
2938
3031
|
const v = validateIdentifier(trajectoryId, 'trajectoryId');
|
|
@@ -2944,7 +3037,7 @@ export const hooksTrajectoryEnd = {
|
|
|
2944
3037
|
const endedAt = new Date().toISOString();
|
|
2945
3038
|
const startTime = Date.now();
|
|
2946
3039
|
// Get and finalize real trajectory
|
|
2947
|
-
const trajectory =
|
|
3040
|
+
const trajectory = await loadPendingTrajectory(trajectoryId);
|
|
2948
3041
|
let persistResult = { success: false };
|
|
2949
3042
|
if (trajectory) {
|
|
2950
3043
|
trajectory.success = success;
|
|
@@ -2970,8 +3063,17 @@ export const hooksTrajectoryEnd = {
|
|
|
2970
3063
|
persistResult = { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
2971
3064
|
}
|
|
2972
3065
|
}
|
|
2973
|
-
//
|
|
2974
|
-
|
|
3066
|
+
// Failed writes remain retryable. A completed row prevents stale pending
|
|
3067
|
+
// data from being replayed even if cleanup itself fails.
|
|
3068
|
+
if (persistResult.success) {
|
|
3069
|
+
activeTrajectories.delete(trajectoryId);
|
|
3070
|
+
dirtyTrajectories.delete(trajectoryId);
|
|
3071
|
+
try {
|
|
3072
|
+
const { deleteEntry } = await import('../memory/memory-initializer.js');
|
|
3073
|
+
await deleteEntry({ key: `trajectory-pending-${trajectoryId}`, namespace: 'trajectories' });
|
|
3074
|
+
}
|
|
3075
|
+
catch { /* completed record is the replay guard */ }
|
|
3076
|
+
}
|
|
2975
3077
|
}
|
|
2976
3078
|
// MAGE-style execution-state-tree mirror (prototype, non-fatal)
|
|
2977
3079
|
try {
|
|
@@ -3187,7 +3289,7 @@ export const hooksTrajectoryEnd = {
|
|
|
3187
3289
|
? `SONA learned pattern "${sonaResult.patternKey}" with ${(sonaResult.confidence * 100).toFixed(1)}% confidence`
|
|
3188
3290
|
: (persistResult.success ? 'Trajectory persisted for future learning' : (persistResult.error || 'Trajectory not found')),
|
|
3189
3291
|
};
|
|
3190
|
-
},
|
|
3292
|
+
}),
|
|
3191
3293
|
};
|
|
3192
3294
|
// Pattern store/search hooks - REAL implementation using storeEntry
|
|
3193
3295
|
export const hooksPatternStore = {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module v3/cli/mcp-tools/memory-tools
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join, resolve } from 'path';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
@@ -273,11 +273,11 @@ const MEMORY_STATS_MAX_PAGES = 200;
|
|
|
273
273
|
* breakdown and embedding coverage computed from a prefix, and a listing
|
|
274
274
|
* that reported `success: false` was read as an empty one.
|
|
275
275
|
*/
|
|
276
|
-
async function collectAllEntries(listEntries) {
|
|
276
|
+
async function collectAllEntries(listEntries, dbPath) {
|
|
277
277
|
const entries = [];
|
|
278
278
|
let total = 0;
|
|
279
279
|
for (let page = 0; page < MEMORY_STATS_MAX_PAGES; page++) {
|
|
280
|
-
const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length });
|
|
280
|
+
const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length, dbPath });
|
|
281
281
|
if (!result.success) {
|
|
282
282
|
return { success: false, entries, total: result.total ?? total, error: result.error };
|
|
283
283
|
}
|
|
@@ -300,10 +300,10 @@ function memoryStatsUnavailable(error) {
|
|
|
300
300
|
* WAL frames, so it is allowed to fail without that meaning the store is
|
|
301
301
|
* missing — which is exactly the conflation #3311 reports.
|
|
302
302
|
*/
|
|
303
|
-
async function readMemoryStatusLabels() {
|
|
303
|
+
async function readMemoryStatusLabels(dbPath) {
|
|
304
304
|
try {
|
|
305
305
|
const { checkMemoryInitialization } = await getMemoryFunctions();
|
|
306
|
-
const status = await checkMemoryInitialization();
|
|
306
|
+
const status = await checkMemoryInitialization(dbPath);
|
|
307
307
|
return { version: status.version, features: status.features };
|
|
308
308
|
}
|
|
309
309
|
catch {
|
|
@@ -315,37 +315,43 @@ async function readMemoryStatusLabels() {
|
|
|
315
315
|
* #1606: Wrapped in try/catch to prevent process-level crashes that kill
|
|
316
316
|
* the stdio MCP transport on Windows/Codex.
|
|
317
317
|
*/
|
|
318
|
-
async function ensureInitialized() {
|
|
318
|
+
async function ensureInitialized(dbPath) {
|
|
319
319
|
try {
|
|
320
320
|
const { initializeMemoryDatabase, checkMemoryInitialization, storeEntry } = await getMemoryFunctions();
|
|
321
321
|
// Check if already initialized
|
|
322
|
-
const status = await checkMemoryInitialization();
|
|
322
|
+
const status = await checkMemoryInitialization(dbPath);
|
|
323
323
|
if (!status.initialized) {
|
|
324
|
-
await initializeMemoryDatabase({ force: false, verbose: false });
|
|
324
|
+
await initializeMemoryDatabase({ force: false, verbose: false, dbPath });
|
|
325
325
|
}
|
|
326
326
|
// Migrate legacy JSON data if exists (from old .claude-flow/memory/ location)
|
|
327
|
+
// A custom import target must not consume the one-time migration marker
|
|
328
|
+
// for the project's normal memory store.
|
|
327
329
|
if (hasLegacyStore()) {
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
330
|
+
const { resolveDbPath } = await import('../memory/memory-initializer.js');
|
|
331
|
+
if (!dbPath || dbPath === resolveDbPath()) {
|
|
332
|
+
const legacyStore = loadLegacyStore();
|
|
333
|
+
if (legacyStore && Object.keys(legacyStore.entries).length > 0) {
|
|
334
|
+
console.error('[MCP Memory] Migrating legacy JSON store to sql.js...');
|
|
335
|
+
let migrated = 0;
|
|
336
|
+
for (const [key, entry] of Object.entries(legacyStore.entries)) {
|
|
337
|
+
try {
|
|
338
|
+
const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value);
|
|
339
|
+
await storeEntry({
|
|
340
|
+
key,
|
|
341
|
+
value,
|
|
342
|
+
namespace: 'default',
|
|
343
|
+
generateEmbeddingFlag: true,
|
|
344
|
+
dbPath,
|
|
345
|
+
});
|
|
346
|
+
migrated++;
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
console.error(`[MCP Memory] Failed to migrate key "${key}":`, e);
|
|
350
|
+
}
|
|
345
351
|
}
|
|
352
|
+
console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
|
|
353
|
+
markMigrationComplete();
|
|
346
354
|
}
|
|
347
|
-
console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
|
|
348
|
-
markMigrationComplete();
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
357
|
}
|
|
@@ -792,10 +798,18 @@ export const memoryTools = [
|
|
|
792
798
|
category: 'memory',
|
|
793
799
|
inputSchema: {
|
|
794
800
|
type: 'object',
|
|
795
|
-
properties: {
|
|
801
|
+
properties: {
|
|
802
|
+
dbPath: { type: 'string', description: 'Database file to inspect; omitted uses the MCP store default' },
|
|
803
|
+
},
|
|
796
804
|
},
|
|
797
|
-
handler: async () => {
|
|
798
|
-
|
|
805
|
+
handler: async (input) => {
|
|
806
|
+
if (input.dbPath !== undefined && (typeof input.dbPath !== 'string' || !input.dbPath.trim())) {
|
|
807
|
+
return memoryStatsUnavailable('dbPath must be a non-empty string');
|
|
808
|
+
}
|
|
809
|
+
const dbPath = typeof input.dbPath === 'string' ? resolve(input.dbPath) : undefined;
|
|
810
|
+
// An explicit read must not initialize or migrate the unrelated default store.
|
|
811
|
+
if (!dbPath)
|
|
812
|
+
await ensureInitialized();
|
|
799
813
|
const { listEntries } = await getMemoryFunctions();
|
|
800
814
|
// #3311: the store's own listing decides whether memory is there.
|
|
801
815
|
// `checkMemoryInitialization` opens a whole-image sql.js snapshot of
|
|
@@ -806,7 +820,7 @@ export const memoryTools = [
|
|
|
806
820
|
// but it no longer gets to overrule a working store.
|
|
807
821
|
let listing;
|
|
808
822
|
try {
|
|
809
|
-
listing = await collectAllEntries(listEntries);
|
|
823
|
+
listing = await collectAllEntries(listEntries, dbPath);
|
|
810
824
|
}
|
|
811
825
|
catch (error) {
|
|
812
826
|
return memoryStatsUnavailable(error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -822,16 +836,49 @@ export const memoryTools = [
|
|
|
822
836
|
// from the breakdown while still being counted in the total.
|
|
823
837
|
const namespaces = Object.create(null);
|
|
824
838
|
let withEmbeddings = 0;
|
|
839
|
+
let oldest = Infinity;
|
|
840
|
+
let newest = -Infinity;
|
|
825
841
|
for (const entry of listing.entries) {
|
|
826
842
|
namespaces[entry.namespace] = (namespaces[entry.namespace] || 0) + 1;
|
|
827
843
|
if (entry.hasEmbedding)
|
|
828
844
|
withEmbeddings++;
|
|
845
|
+
// AgentDB returns INTEGER epoch milliseconds; legacy sql.js rows
|
|
846
|
+
// may expose ISO strings or numeric strings from SQLite TEXT affinity.
|
|
847
|
+
const rawCreated = entry.createdAt;
|
|
848
|
+
const millis = typeof rawCreated === 'number'
|
|
849
|
+
? rawCreated
|
|
850
|
+
: typeof rawCreated === 'string' && /^-?\d+$/.test(rawCreated)
|
|
851
|
+
? Number(rawCreated)
|
|
852
|
+
: Date.parse(rawCreated ?? '');
|
|
853
|
+
const created = new Date(millis).getTime();
|
|
854
|
+
if (Number.isFinite(created)) {
|
|
855
|
+
oldest = Math.min(oldest, created);
|
|
856
|
+
newest = Math.max(newest, created);
|
|
857
|
+
}
|
|
829
858
|
}
|
|
830
859
|
const counted = listing.entries.length;
|
|
831
|
-
const status = await readMemoryStatusLabels();
|
|
860
|
+
const status = await readMemoryStatusLabels(dbPath);
|
|
861
|
+
let totalSize = null;
|
|
862
|
+
if (dbPath) {
|
|
863
|
+
try {
|
|
864
|
+
let bytes = statSync(dbPath).size;
|
|
865
|
+
try {
|
|
866
|
+
bytes += statSync(dbPath + '-wal').size;
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
if (error.code !== 'ENOENT')
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
totalSize = `${bytes} B`;
|
|
873
|
+
}
|
|
874
|
+
catch { /* unavailable file metadata is unknown, not zero */ }
|
|
875
|
+
}
|
|
832
876
|
return {
|
|
833
877
|
initialized: true,
|
|
834
878
|
totalEntries: listing.total,
|
|
879
|
+
...(dbPath ? { location: dbPath, totalSize } : {}),
|
|
880
|
+
oldestEntry: counted >= listing.total && Number.isFinite(oldest) ? new Date(oldest).toISOString() : null,
|
|
881
|
+
newestEntry: counted >= listing.total && Number.isFinite(newest) ? new Date(newest).toISOString() : null,
|
|
835
882
|
entriesCounted: counted,
|
|
836
883
|
// The breakdown below covers `entriesCounted` rows, which is every
|
|
837
884
|
// row unless the listing was truncated; say so rather than letting
|
|
@@ -1484,11 +1531,15 @@ export const memoryTools = [
|
|
|
1484
1531
|
inputPath: { type: 'string', description: 'Path to the JSON export file' },
|
|
1485
1532
|
merge: { type: 'boolean', description: 'Merge into existing entries (upsert) vs. fail on conflict (default true)' },
|
|
1486
1533
|
namespace: { type: 'string', description: 'Override the namespace for all imported entries' },
|
|
1534
|
+
dbPath: { type: 'string', description: 'Database file to import into (defaults to the MCP memory store)' },
|
|
1487
1535
|
},
|
|
1488
1536
|
required: ['inputPath'],
|
|
1489
1537
|
},
|
|
1490
1538
|
handler: async (input) => {
|
|
1491
|
-
|
|
1539
|
+
const dbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
|
|
1540
|
+
? resolve(input.dbPath)
|
|
1541
|
+
: undefined;
|
|
1542
|
+
await ensureInitialized(dbPath);
|
|
1492
1543
|
const { storeEntry } = await getMemoryFunctions();
|
|
1493
1544
|
const t0 = Date.now();
|
|
1494
1545
|
const inputPath = String(input.inputPath ?? '');
|
|
@@ -1517,8 +1568,11 @@ export const memoryTools = [
|
|
|
1517
1568
|
}
|
|
1518
1569
|
const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
|
|
1519
1570
|
try {
|
|
1520
|
-
await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false });
|
|
1521
|
-
|
|
1571
|
+
const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
|
|
1572
|
+
if (result.success)
|
|
1573
|
+
imported++;
|
|
1574
|
+
else
|
|
1575
|
+
skipped++;
|
|
1522
1576
|
}
|
|
1523
1577
|
catch {
|
|
1524
1578
|
skipped++;
|
|
@@ -62,6 +62,12 @@ export interface McpPolicy {
|
|
|
62
62
|
harnessId?: string;
|
|
63
63
|
defaultDeny?: boolean;
|
|
64
64
|
auditLog?: boolean;
|
|
65
|
+
/** Explicit destination; takes precedence over the environment and project default. */
|
|
66
|
+
auditLogPath?: string;
|
|
67
|
+
/** Maximum bytes per audit segment. Default 10 MiB; oversized records fail closed. */
|
|
68
|
+
auditLogMaxBytes?: number;
|
|
69
|
+
/** Rotated segments retained in addition to the active log. Default 5, maximum 100. */
|
|
70
|
+
auditLogMaxFiles?: number;
|
|
65
71
|
requireApprovalForDangerous?: boolean;
|
|
66
72
|
toolTimeoutMs?: number;
|
|
67
73
|
/**
|
|
@@ -103,10 +109,11 @@ export interface AuditLogEntry {
|
|
|
103
109
|
toolName: string;
|
|
104
110
|
allowed: boolean;
|
|
105
111
|
reason?: string;
|
|
112
|
+
projectPath?: string;
|
|
106
113
|
}
|
|
107
114
|
/** Test-only: redirect the audit log to a temp file instead of the default path. */
|
|
108
115
|
export declare function setAuditLogPathForTesting(p: string | null): void;
|
|
109
|
-
export declare function getAuditLogPath(): string;
|
|
116
|
+
export declare function getAuditLogPath(options?: Pick<McpPolicy, 'auditLogPath'>): string;
|
|
110
117
|
/**
|
|
111
118
|
* Appends one JSONL audit record. Returns `true` if `policy.auditLog` is not
|
|
112
119
|
* set (nothing was required) or the write succeeded; `false` only when
|