claude-flow 3.47.1 → 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.
Files changed (48) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  2. package/.claude/helpers/learning-service.mjs +4 -2
  3. package/.claude/helpers/metrics-db.mjs +4 -2
  4. package/node_modules/@claude-flow/codex/package.json +1 -1
  5. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  6. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  7. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  8. package/node_modules/@claude-flow/mcp/package.json +6 -3
  9. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  10. package/node_modules/@claude-flow/security/package.json +2 -3
  11. package/package.json +7 -7
  12. package/v3/@claude-flow/cli/README.md +0 -2
  13. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  14. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  15. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  16. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  17. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
  18. package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
  19. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  20. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  21. package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
  22. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  23. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
  24. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  30. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
  32. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  33. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  34. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  35. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  36. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  37. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  38. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  39. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
  40. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  41. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  42. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  43. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  44. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  45. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  46. package/v3/@claude-flow/cli/package.json +9 -9
  47. package/v3/@claude-flow/guidance/package.json +5 -6
  48. package/v3/@claude-flow/shared/package.json +6 -3
@@ -1,15 +1,4 @@
1
- /**
2
- * AgentDB MCP Tools — Phase 6 of ADR-053
3
- *
4
- * Exposes AgentDB v3 controller operations as MCP tools.
5
- * Provides direct access to ReasoningBank, CausalGraph, SkillLibrary,
6
- * AttestationLog, and bridge health through the MCP protocol.
7
- *
8
- * Security: All handlers validate input types, enforce length bounds,
9
- * and sanitize error messages before returning to MCP callers.
10
- *
11
- * @module v3/cli/mcp-tools/agentdb-tools
12
- */
1
+ import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
13
2
  import { validateIdentifier, validateText } from './validate-input.js';
14
3
  // ===== Shared validation helpers =====
15
4
  const MAX_STRING_LENGTH = 100_000; // 100KB max for any string input
@@ -66,12 +55,6 @@ async function getBridge() {
66
55
  // Lazy-cached modules used by graph-query / graph-pathfinder dispatch.
67
56
  // Caching the resolved namespace avoids per-call dynamic-import overhead
68
57
  // in the ADR-130 hot path (smoke harness measures elapsedMs of every call).
69
- let graphBackendMod = null;
70
- async function getGraphBackend() {
71
- if (!graphBackendMod)
72
- graphBackendMod = await import('../ruvector/graph-backend.js');
73
- return graphBackendMod;
74
- }
75
58
  let graphEdgeWriterMod = null;
76
59
  async function getGraphEdgeWriter() {
77
60
  if (!graphEdgeWriterMod)
@@ -396,6 +379,7 @@ export const agentdbFeedback = {
396
379
  type: 'object',
397
380
  properties: {
398
381
  taskId: { type: 'string', description: 'Task identifier' },
382
+ patterns: feedbackPatternsSchema,
399
383
  success: { type: 'boolean', description: 'Whether task succeeded' },
400
384
  quality: { type: 'number', description: 'Quality score (0-1)' },
401
385
  agent: { type: 'string', description: 'Agent that performed the task' },
@@ -415,9 +399,11 @@ export const agentdbFeedback = {
415
399
  const taskId = validateString(params.taskId, 'taskId', 500);
416
400
  if (!taskId)
417
401
  return { success: false, error: 'taskId is required (non-empty string, max 500 chars)' };
402
+ const patterns = validateFeedbackPatterns(params.patterns);
418
403
  const bridge = await getBridge();
419
404
  const result = await bridge.bridgeRecordFeedback({
420
405
  taskId,
406
+ patterns,
421
407
  success: params.success === true,
422
408
  quality: validateScore(params.quality, 0.85),
423
409
  agent: validateString(params.agent, 'agent', 200) ?? undefined,
@@ -802,21 +788,18 @@ export const agentdbHierarchicalRecall = {
802
788
  // ===== agentdb_consolidate — Run memory consolidation =====
803
789
  export const agentdbConsolidate = {
804
790
  name: 'agentdb_consolidate',
805
- description: 'Run memory consolidation to promote entries across tiers and compress old data Use when generic memory_* tools are wrong because you need AgentDB-specific controllers (HNSW vector search, hierarchical tiers, causal-graph links, pattern store/recall, RaBitQ quantization). For simple key-value persistence, memory_store/memory_retrieve are simpler. For unrelated file work, native Read/Write are fine.',
791
+ description: 'Request memory consolidation. Use when checking whether AgentDB can consolidate retained memories; native file edits cannot perform controller consolidation. Currently returns unsupported when the installed controller is a no-op stub; no entries are promoted or compressed in that case.',
806
792
  inputSchema: {
807
793
  type: 'object',
808
- properties: {
809
- minAge: { type: 'number', description: 'Minimum age in hours since store (optional)' },
810
- maxEntries: { type: 'number', description: 'Maximum entries to consolidate (optional)' },
811
- },
794
+ properties: {},
812
795
  },
813
796
  handler: async (params) => {
814
797
  try {
798
+ if (Object.keys(params).length > 0) {
799
+ return { success: false, status: 'unsupported', error: 'agentdb_consolidate does not support options' };
800
+ }
815
801
  const bridge = await getBridge();
816
- const result = await bridge.bridgeConsolidate({
817
- minAge: typeof params.minAge === 'number' ? Math.max(0, params.minAge) : undefined,
818
- maxEntries: validatePositiveInt(params.maxEntries, 1000, 10_000),
819
- });
802
+ const result = await bridge.bridgeConsolidate({});
820
803
  return result ?? { success: false, error: 'AgentDB bridge not available. Use memory_store/memory_search instead.' };
821
804
  }
822
805
  catch (error) {
@@ -1060,7 +1043,7 @@ export const agentdbCausalNodeDelete = {
1060
1043
  };
1061
1044
  export const agentdbGraphQuery = {
1062
1045
  name: 'agentdb_graph-query',
1063
- description: 'Unified graph traversal across the knowledge graph (ADR-130). Native graph-node k-hop cannot filter by relation and returns an explicit unsupported error for that request; the SQL fallback can filter by relation. Inspect appliedDepth/truncated because SQL is bounded at 3 hops. Semantic and PageRank modes use their own backends. Use when you need structured graph traversal beyond flat memory search.',
1046
+ description: 'Unified graph traversal across the knowledge graph (ADR-130). K-hop queries read committed graph_edges relationships through SQL; a separately populated native graph is not evidence of retained-history coverage. Inspect appliedDepth/truncated because SQL is bounded at 3 hops. maxNodesVisited bounds returned rows, not traversal work or elapsed time. Semantic and PageRank modes use their own backends. Use when you need relationship traversal beyond flat memory search.',
1064
1047
  inputSchema: {
1065
1048
  type: 'object',
1066
1049
  properties: {
@@ -1101,53 +1084,38 @@ export const agentdbGraphQuery = {
1101
1084
  }
1102
1085
  const budgetRaw = (params.complexityBudget ?? {});
1103
1086
  const budget = {
1104
- maxNodesVisited: budgetRaw.maxNodesVisited ?? 10_000,
1105
- maxDepth: budgetRaw.maxDepth ?? 5,
1087
+ maxNodesVisited: validatePositiveInt(budgetRaw.maxNodesVisited, 10_000, 10_000),
1088
+ maxDepth: validatePositiveInt(budgetRaw.maxDepth, 5, 5),
1106
1089
  maxMillis: budgetRaw.maxMillis ?? 50,
1107
1090
  maxMemoryMB: budgetRaw.maxMemoryMB ?? 32,
1108
1091
  };
1109
- const depth = Math.min(validatePositiveInt(params.depth, 2, budget.maxDepth), budget.maxDepth);
1092
+ const requestedDepth = validatePositiveInt(params.depth, 2, 5);
1093
+ const depth = Math.min(requestedDepth, budget.maxDepth);
1110
1094
  const topK = validatePositiveInt(params.topK, 10, MAX_TOP_K);
1111
1095
  const relation = validateString(params.relation, 'relation', 200) ?? undefined;
1112
1096
  // ── k-hop mode ──────────────────────────────────────────────────────────
1113
1097
  if (mode === 'k-hop') {
1114
- // graph-node's kHopNeighbors API has no relation parameter. Its graph
1115
- // and the SQL graph are separate stores, and SQL writes can be async;
1116
- // switching stores here could report a false negative. Refuse the
1117
- // unsupported native query instead of silently widening it.
1118
- try {
1119
- const graphBackend = await getGraphBackend();
1120
- if (await graphBackend.isGraphBackendAvailable()) {
1121
- if (relation) {
1122
- return { success: false, error: 'Native graph-node k-hop does not support relation filtering', unsupported: 'relation', mode, nodeId, relation, backend: 'graph-node' };
1123
- }
1124
- const neighbors = await graphBackend.getNeighbors(nodeId, depth);
1125
- return {
1126
- success: true, mode, nodeId, depth, appliedDepth: depth,
1127
- results: neighbors.map(id => ({ nodeId: id })),
1128
- count: neighbors.length,
1129
- backend: 'graph-node',
1130
- elapsedMs: Date.now() - t0,
1131
- };
1132
- }
1133
- }
1134
- catch { /* fall through to SQL */ }
1135
- // SQL CTE fallback is bounded at depth 3. Report that bound so callers
1136
- // cannot mistake an incomplete depth-5 cycle check for a complete one.
1098
+ // #3315: graph-node and retained SQL are populated independently.
1099
+ // Native availability does not establish historical coverage. Query
1100
+ // the committed relationship source, and fail if it cannot be read.
1137
1101
  try {
1138
1102
  const { getBridgeDb } = await getGraphEdgeWriter();
1139
1103
  const db = await getBridgeDb();
1140
1104
  if (db) {
1141
1105
  const appliedDepth = Math.min(depth, 3);
1142
- const cteSql = buildKHopCTE(nodeId, appliedDepth, relation, budget.maxNodesVisited);
1106
+ const cteSql = buildKHopCTE(nodeId, appliedDepth, relation, budget.maxNodesVisited + 1);
1143
1107
  // graph-edge-writer returns a better-sqlite3 Database after #2431.
1144
1108
  // `db.exec(sql, params)` (sql.js style) is a runner with no result
1145
1109
  // on better-sqlite3 — use `prepare(sql).raw().all(...)` to get the
1146
1110
  // same array-of-arrays shape the downstream code expects.
1147
- const rows = db.prepare(cteSql).raw().all();
1111
+ const allRows = db.prepare(cteSql).raw().all();
1112
+ const resultLimitReached = allRows.length > budget.maxNodesVisited;
1113
+ const rows = allRows.slice(0, budget.maxNodesVisited);
1114
+ const depthLimited = appliedDepth < requestedDepth;
1148
1115
  return {
1149
- success: true, mode, nodeId, depth, appliedDepth,
1150
- ...(appliedDepth < depth ? { truncated: true } : {}),
1116
+ success: true, mode, nodeId, depth, requestedDepth, appliedDepth,
1117
+ depthLimited, resultLimitReached, truncated: depthLimited || resultLimitReached,
1118
+ resultLimit: budget.maxNodesVisited,
1151
1119
  results: rows.map((r) => ({ nodeId: r[0], depth: r[1] })),
1152
1120
  count: rows.length,
1153
1121
  backend: 'sql-cte',
@@ -1155,11 +1123,10 @@ export const agentdbGraphQuery = {
1155
1123
  };
1156
1124
  }
1157
1125
  }
1158
- catch { /* db unavailable */ }
1159
- if (relation) {
1160
- return { success: false, error: 'Relation-filtered k-hop query requires the SQL graph_edges backend', mode, nodeId, relation };
1126
+ catch (error) {
1127
+ return { success: false, error: `Retained SQL graph query failed: ${sanitizeError(error)}`, mode, nodeId };
1161
1128
  }
1162
- return { success: false, error: 'No graph backend available for k-hop query', mode, nodeId };
1129
+ return { success: false, error: 'Retained SQL graph_edges backend is unavailable for k-hop query', mode, nodeId };
1163
1130
  }
1164
1131
  // ── semantic mode ────────────────────────────────────────────────────────
1165
1132
  if (mode === 'semantic') {
@@ -1,7 +1,3 @@
1
- /**
2
- * Hooks MCP Tools
3
- * Provides intelligent hooks functionality via MCP protocol
4
- */
5
1
  import { type MCPTool } from './types.js';
6
2
  import { type RouterEmbedderKind } from '../ruvector/router-embedder.js';
7
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
- // In-memory trajectory tracking (persisted on end)
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 key = `cmd-${Date.now()}`;
932
- store.entries[key] = { key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', createdAt: new Date().toISOString() };
933
- const memDir = resolve(MEMORY_DIR);
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
- const quality = params.quality || (success ? 0.85 : 0.3);
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: params.patterns || undefined,
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
- // Clear in-memory trajectories
2758
- cleared.trajectories = activeTrajectories.size;
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
- // Persist pending trajectory to disk so it survives MCP restarts
2819
- const storeFn = await getRealStoreFunction();
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
- const quality = params.quality || 0.85;
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 = activeTrajectories.get(trajectoryId);
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 = activeTrajectories.get(trajectoryId);
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
- // Remove from active trajectories
2974
- activeTrajectories.delete(trajectoryId);
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 = {