@claude-flow/cli 3.55.0 → 3.56.1

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 (71) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.js +40 -24
  5. package/dist/src/commands/agent.js +12 -0
  6. package/dist/src/commands/analyze.js +18 -14
  7. package/dist/src/commands/config.js +1 -1
  8. package/dist/src/commands/daemon.js +11 -19
  9. package/dist/src/commands/doctor.js +65 -58
  10. package/dist/src/commands/memory-backup.d.ts +1 -1
  11. package/dist/src/commands/memory-backup.js +23 -9
  12. package/dist/src/commands/memory.d.ts +0 -4
  13. package/dist/src/commands/memory.js +10 -2
  14. package/dist/src/commands/plugins.js +13 -1
  15. package/dist/src/commands/task.js +12 -0
  16. package/dist/src/init/settings-generator.js +3 -3
  17. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  18. package/dist/src/mcp-tools/agent-tools.js +15 -16
  19. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  20. package/dist/src/mcp-tools/claims-tools.js +3 -11
  21. package/dist/src/mcp-tools/config-tools.js +118 -13
  22. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  23. package/dist/src/mcp-tools/memory-tools.js +29 -10
  24. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  25. package/dist/src/mcp-tools/record-store.js +21 -0
  26. package/dist/src/mcp-tools/session-tools.js +47 -13
  27. package/dist/src/mcp-tools/task-tools.js +5 -11
  28. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  29. package/dist/src/memory/append-conditions.d.ts +28 -0
  30. package/dist/src/memory/append-conditions.js +56 -0
  31. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  32. package/dist/src/memory/graph-edge-writer.js +90 -0
  33. package/dist/src/memory/memory-bridge.d.ts +4 -0
  34. package/dist/src/memory/memory-bridge.js +50 -9
  35. package/dist/src/memory/memory-initializer.d.ts +4 -0
  36. package/dist/src/memory/memory-initializer.js +100 -62
  37. package/dist/src/memory/memory-root.d.ts +2 -0
  38. package/dist/src/memory/memory-root.js +28 -0
  39. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  40. package/dist/src/memory/sona-optimizer.js +4 -0
  41. package/dist/src/parser.js +7 -0
  42. package/dist/src/plugins/store/discovery.d.ts +6 -0
  43. package/dist/src/plugins/store/discovery.js +7 -3
  44. package/dist/src/ruvector/neural-router.js +7 -2
  45. package/dist/src/services/claim-service.js +39 -18
  46. package/dist/src/services/claims-store.d.ts +3 -0
  47. package/dist/src/services/claims-store.js +40 -0
  48. package/dist/src/services/config-file-manager.d.ts +2 -2
  49. package/dist/src/services/config-file-manager.js +22 -3
  50. package/dist/src/services/distill-tuning.js +17 -10
  51. package/dist/src/services/memory-backup.d.ts +7 -0
  52. package/dist/src/services/memory-backup.js +29 -2
  53. package/dist/src/services/memory-distillation.js +2 -2
  54. package/dist/src/services/policy-ledger-anchor.d.ts +8 -0
  55. package/dist/src/services/policy-ledger-anchor.js +18 -3
  56. package/dist/src/services/policy-runtime.js +2 -2
  57. package/dist/src/services/worker-daemon.d.ts +4 -2
  58. package/dist/src/services/worker-daemon.js +14 -6
  59. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  60. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  61. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  62. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  63. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  64. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  65. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  66. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  67. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  68. package/node_modules/@claude-flow/security/package.json +1 -1
  69. package/package.json +4 -4
  70. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  71. package/.claude/agents/tmp.json +0 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
+ import { readRecordStore } from './record-store.js';
9
10
  import { getProjectCwd } from './types.js';
10
11
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
11
12
  import { executeAgentTask } from './agent-execute-core.js';
@@ -32,17 +33,7 @@ function ensureAgentDir() {
32
33
  }
33
34
  }
34
35
  function loadAgentStore() {
35
- try {
36
- const path = getAgentPath();
37
- if (existsSync(path)) {
38
- const data = readFileSync(path, 'utf-8');
39
- return JSON.parse(data);
40
- }
41
- }
42
- catch {
43
- // Return empty store on error
44
- }
45
- return { agents: {}, version: '3.0.0' };
36
+ return readRecordStore(getAgentPath(), 'agents', () => ({ agents: {}, version: '3.0.0' }));
46
37
  }
47
38
  function saveAgentStore(store) {
48
39
  ensureAgentDir();
@@ -247,7 +238,6 @@ export const agentTools = [
247
238
  if (!validation.valid) {
248
239
  return { success: false, error: `Input validation failed: ${validation.errors.join('; ')}` };
249
240
  }
250
- const store = loadAgentStore();
251
241
  const agentId = input.agentId || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
252
242
  const agentType = input.agentType;
253
243
  const config = input.config || {};
@@ -285,6 +275,7 @@ export const agentTools = [
285
275
  : routingResult.provider ? { provider: routingResult.provider } : {}),
286
276
  ...(routingResult.openrouterModel ? { openrouterModel: routingResult.openrouterModel } : {}),
287
277
  };
278
+ const store = loadAgentStore();
288
279
  store.agents[agentId] = agent;
289
280
  saveAgentStore(store);
290
281
  // #2085 — also push to the swarm store's agents array so that
@@ -337,8 +328,13 @@ export const agentTools = [
337
328
  memoryBranch = br.branchPath;
338
329
  agent.memoryBranch = br.branchPath;
339
330
  agent.memoryBase = br.basePath;
340
- store.agents[agentId] = agent;
341
- saveAgentStore(store);
331
+ const latest = loadAgentStore();
332
+ const current = latest.agents[agentId];
333
+ if (current && current.status !== 'terminated') {
334
+ current.memoryBranch = br.branchPath;
335
+ current.memoryBase = br.basePath;
336
+ saveAgentStore(latest);
337
+ }
342
338
  }
343
339
  // else: degraded (agenticow missing / kill-switched) — agent stands
344
340
  // without an isolated branch; callers see no memoryBranch field.
@@ -629,7 +625,10 @@ export const agentTools = [
629
625
  };
630
626
  }
631
627
  if (action === 'scale') {
632
- const targetSize = input.targetSize || 5;
628
+ const targetSize = input.targetSize === undefined ? 5 : input.targetSize;
629
+ if (typeof targetSize !== 'number' || !Number.isSafeInteger(targetSize) || targetSize < 0) {
630
+ return { action, error: 'targetSize must be a non-negative safe integer' };
631
+ }
633
632
  const agentType = input.agentType || 'worker';
634
633
  const currentSize = agents.filter(a => a.agentType === agentType).length;
635
634
  const delta = targetSize - currentSize;
@@ -663,7 +662,7 @@ export const agentTools = [
663
662
  agentType,
664
663
  previousSize: currentSize,
665
664
  targetSize,
666
- newSize: currentSize + delta,
665
+ newSize: currentSize + added.length - removed.length,
667
666
  added,
668
667
  removed,
669
668
  };
@@ -61,6 +61,25 @@ async function getGraphEdgeWriter() {
61
61
  graphEdgeWriterMod = await import('../memory/graph-edge-writer.js');
62
62
  return graphEdgeWriterMod;
63
63
  }
64
+ // #2889 — a null graph_edges DB handle can mean several unrelated things
65
+ // (missing native module, permissions, genuine corruption, or an RFE1
66
+ // encrypted-at-rest store). Recommending `memory init` for the encrypted
67
+ // case is actively harmful — it creates a fresh EMPTY store instead of
68
+ // using the existing encrypted data — so distinguish it before choosing
69
+ // a hint.
70
+ async function bridgeUnavailableResult() {
71
+ const { isBridgeDbEncryptedAtRest } = await getGraphEdgeWriter();
72
+ if (isBridgeDbEncryptedAtRest()) {
73
+ return {
74
+ error: 'graph_edges DB is encrypted at rest (RFE1) — the native bridge cannot open it directly.',
75
+ hint: 'Your existing data is intact; this is a known limitation, not corruption. Do NOT run `memory init` — see https://github.com/ruvnet/ruflo/issues/2889.',
76
+ };
77
+ }
78
+ return {
79
+ error: 'graph_edges DB unavailable (sql.js could not load)',
80
+ hint: 'Check Node version + try `ruflo memory init` to initialize manually.',
81
+ };
82
+ }
64
83
  let memInitMod = null;
65
84
  async function getMemInit() {
66
85
  if (!memInitMod)
@@ -1152,7 +1171,7 @@ export const agentdbGraphQuery = {
1152
1171
  // fresh environments work without a pre-existing memory init.
1153
1172
  const db = await getBridgeDb(undefined, { createIfMissing: true });
1154
1173
  if (!db)
1155
- return { success: false, error: 'graph_edges DB unavailable (sql.js could not load)', hint: 'Check Node version + try `ruflo memory init` to initialize manually.', mode, nodeId };
1174
+ return { success: false, ...(await bridgeUnavailableResult()), mode, nodeId };
1156
1175
  // Load all rows with embedding_ref and score by cosine.
1157
1176
  // better-sqlite3 API — `db.exec(sql, params)` (sql.js) silently
1158
1177
  // throws "datatype mismatch" because exec ignores params, so `?`
@@ -1194,7 +1213,7 @@ export const agentdbGraphQuery = {
1194
1213
  // fresh environments work without a pre-existing memory init.
1195
1214
  const db = await getBridgeDb(undefined, { createIfMissing: true });
1196
1215
  if (!db)
1197
- return { success: false, error: 'graph_edges DB unavailable (sql.js could not load)', hint: 'Check Node version + try `ruflo memory init` to initialize manually.', mode, nodeId };
1216
+ return { success: false, ...(await bridgeUnavailableResult()), mode, nodeId };
1198
1217
  // better-sqlite3 API — see semantic-mode comment above.
1199
1218
  const edges = db.prepare(`SELECT source_id, target_id, weight FROM graph_edges LIMIT ?`).raw().all(budget.maxNodesVisited);
1200
1219
  if (edges.length === 0) {
@@ -1380,7 +1399,7 @@ export const agentdbGraphPathfinder = {
1380
1399
  // #2246 fix: lazy-create memory.db on first pathfinder call.
1381
1400
  const db = await getBridgeDb(undefined, { createIfMissing: true });
1382
1401
  if (!db)
1383
- return { success: false, error: 'graph_edges DB unavailable (sql.js could not load)', hint: 'Check Node version + try `ruflo memory init` to initialize manually.', seedNodeId };
1402
+ return { success: false, ...(await bridgeUnavailableResult()), seedNodeId };
1384
1403
  const colsSql = algorithm === 'witness-chain-divergence'
1385
1404
  ? 'source_id, target_id, weight, last_reinforced, witness_id'
1386
1405
  : algorithm === 'temporal-centrality'
@@ -6,9 +6,10 @@
6
6
  *
7
7
  * @module @claude-flow/cli/mcp-tools/claims
8
8
  */
9
+ import { readClaimsStore } from '../services/claims-store.js';
9
10
  import { validateIdentifier, validateText } from './validate-input.js';
10
11
  // File-based persistence
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
12
13
  import { join, resolve } from 'path';
13
14
  const CLAIMS_DIR = '.claude-flow/claims';
14
15
  const CLAIMS_FILE = 'claims.json';
@@ -22,16 +23,7 @@ function ensureClaimsDir() {
22
23
  }
23
24
  }
24
25
  function loadClaims() {
25
- try {
26
- const path = getClaimsPath();
27
- if (existsSync(path)) {
28
- return JSON.parse(readFileSync(path, 'utf-8'));
29
- }
30
- }
31
- catch {
32
- // Return empty store on error
33
- }
34
- return { claims: {}, stealable: {}, contests: {} };
26
+ return readClaimsStore(getClaimsPath());
35
27
  }
36
28
  function saveClaims(store) {
37
29
  ensureClaimsDir();
@@ -35,12 +35,110 @@ function ensureConfigDir() {
35
35
  mkdirSync(dir, { recursive: true, mode: 0o700 });
36
36
  }
37
37
  }
38
+ // Plain CLI files and legacy MCP envelopes share the same path. Keep the
39
+ // original plain document so MCP writes remain readable by the CLI.
40
+ const plainDocuments = new WeakMap();
41
+ const STORE_METADATA = new Set(['scopes', 'version', 'updatedAt']);
42
+ function isRecord(value) {
43
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
44
+ }
45
+ function flattenValues(document) {
46
+ const values = {};
47
+ function visit(value, key) {
48
+ if (isRecord(value) && Object.keys(value).length > 0) {
49
+ for (const [child, entry] of Object.entries(value))
50
+ visit(entry, `${key}.${child}`);
51
+ }
52
+ else {
53
+ Object.defineProperty(values, key, { value, enumerable: true, configurable: true, writable: true });
54
+ }
55
+ }
56
+ for (const [key, value] of Object.entries(document)) {
57
+ if (!STORE_METADATA.has(key))
58
+ visit(value, key);
59
+ }
60
+ // CLI reads literal dotted keys before traversing nested objects.
61
+ for (const [key, value] of Object.entries(document)) {
62
+ if (key.includes('.'))
63
+ Object.defineProperty(values, key, { value, enumerable: true, configurable: true, writable: true });
64
+ }
65
+ return values;
66
+ }
67
+ function readDefaultValue(store, key) {
68
+ const document = plainDocuments.get(store);
69
+ if (!document)
70
+ return store.values[key];
71
+ return Object.hasOwn(document, key) ? document[key] : getNestedValue(document, key);
72
+ }
73
+ function writeDefaultValue(store, key, value) {
74
+ const document = plainDocuments.get(store);
75
+ if (!document) {
76
+ store.values[key] = value;
77
+ return;
78
+ }
79
+ // Keep existing literal dotted keys; otherwise use the CLI's nested shape.
80
+ if (Object.hasOwn(document, key)) {
81
+ // Reuse the same segment validation as nested writes.
82
+ setNestedValue({}, key, value);
83
+ document[key] = value;
84
+ }
85
+ else {
86
+ setNestedValue(document, key, value);
87
+ }
88
+ store.values = flattenValues(document);
89
+ }
90
+ function deleteDefaultValue(store, key) {
91
+ const document = plainDocuments.get(store);
92
+ if (!document)
93
+ return Object.hasOwn(store.values, key) && delete store.values[key];
94
+ let target = document;
95
+ let leaf = key;
96
+ if (!Object.hasOwn(document, key)) {
97
+ const parts = key.split('.');
98
+ leaf = parts.pop();
99
+ for (const part of parts) {
100
+ if (!Object.hasOwn(target, part) || !isRecord(target[part]))
101
+ return false;
102
+ target = target[part];
103
+ }
104
+ }
105
+ const deleted = Object.hasOwn(target, leaf) && delete target[leaf];
106
+ store.values = flattenValues(document);
107
+ return deleted;
108
+ }
109
+ function replaceDefaultValues(store, values) {
110
+ const document = plainDocuments.get(store);
111
+ if (!document) {
112
+ store.values = values;
113
+ return;
114
+ }
115
+ for (const key of Object.keys(document)) {
116
+ if (!STORE_METADATA.has(key))
117
+ delete document[key];
118
+ }
119
+ for (const [key, value] of Object.entries(values))
120
+ writeDefaultValue(store, key, value);
121
+ }
38
122
  function loadConfigStore() {
39
123
  try {
40
124
  const path = getConfigPath();
41
125
  if (existsSync(path)) {
42
126
  const data = readFileSync(path, 'utf-8');
43
- return JSON.parse(data);
127
+ const document = JSON.parse(data);
128
+ const isEnvelope = isRecord(document) && isRecord(document.values)
129
+ && isRecord(document.scopes) && typeof document.version === 'string'
130
+ && typeof document.updatedAt === 'string';
131
+ if (isRecord(document) && !isEnvelope) {
132
+ const store = {
133
+ values: flattenValues(document),
134
+ scopes: isRecord(document.scopes) ? document.scopes : {},
135
+ version: typeof document.version === 'string' ? document.version : '3.0.0',
136
+ updatedAt: typeof document.updatedAt === 'string' ? document.updatedAt : '',
137
+ };
138
+ plainDocuments.set(store, document);
139
+ return store;
140
+ }
141
+ return document;
44
142
  }
45
143
  }
46
144
  catch {
@@ -56,13 +154,20 @@ function loadConfigStore() {
56
154
  function saveConfigStore(store) {
57
155
  ensureConfigDir();
58
156
  store.updatedAt = new Date().toISOString();
59
- writeFileSync(getConfigPath(), JSON.stringify(store, null, 2), 'utf-8');
157
+ const document = plainDocuments.get(store);
158
+ if (document) {
159
+ document.updatedAt = store.updatedAt;
160
+ if (Object.keys(store.scopes).length > 0 || Object.hasOwn(document, 'scopes')) {
161
+ document.scopes = store.scopes;
162
+ }
163
+ }
164
+ writeFileSync(getConfigPath(), JSON.stringify(document ?? store, null, 2), 'utf-8');
60
165
  }
61
166
  function getNestedValue(obj, key) {
62
167
  const parts = key.split('.');
63
168
  let current = obj;
64
169
  for (const part of parts) {
65
- if (current && typeof current === 'object' && part in current) {
170
+ if (current && typeof current === 'object' && Object.hasOwn(current, part)) {
66
171
  current = current[part];
67
172
  }
68
173
  else {
@@ -95,7 +200,7 @@ function setNestedValue(obj, key, value) {
95
200
  let current = obj;
96
201
  for (let i = 0; i < parts.length - 1; i++) {
97
202
  const part = parts[i];
98
- if (!(part in current) || typeof current[part] !== 'object') {
203
+ if (!Object.hasOwn(current, part) || current[part] === null || typeof current[part] !== 'object') {
99
204
  current[part] = {};
100
205
  }
101
206
  current = current[part];
@@ -134,7 +239,7 @@ export const configTools = [
134
239
  value = store.scopes[scope][key];
135
240
  }
136
241
  if (value === undefined) {
137
- value = store.values[key];
242
+ value = readDefaultValue(store, key);
138
243
  }
139
244
  if (value === undefined) {
140
245
  value = DEFAULT_CONFIG[key];
@@ -144,7 +249,7 @@ export const configTools = [
144
249
  value,
145
250
  scope,
146
251
  exists: value !== undefined,
147
- source: value !== undefined ? (store.values[key] !== undefined ? 'stored' : 'default') : 'none',
252
+ source: value !== undefined ? (readDefaultValue(store, key) !== undefined ? 'stored' : 'default') : 'none',
148
253
  };
149
254
  },
150
255
  },
@@ -175,9 +280,9 @@ export const configTools = [
175
280
  const key = input.key;
176
281
  const value = input.value;
177
282
  const scope = input.scope || 'default';
178
- const previousValue = store.values[key];
283
+ const previousValue = readDefaultValue(store, key);
179
284
  if (scope === 'default') {
180
- store.values[key] = value;
285
+ writeDefaultValue(store, key, value);
181
286
  }
182
287
  else {
183
288
  if (!store.scopes[scope]) {
@@ -289,8 +394,7 @@ export const configTools = [
289
394
  if (key) {
290
395
  // Reset specific key
291
396
  if (scope === 'default') {
292
- if (key in store.values) {
293
- delete store.values[key];
397
+ if (deleteDefaultValue(store, key)) {
294
398
  resetKeys.push(key);
295
399
  }
296
400
  }
@@ -303,7 +407,7 @@ export const configTools = [
303
407
  // Reset all keys in scope
304
408
  if (scope === 'default') {
305
409
  resetKeys = Object.keys(store.values);
306
- store.values = { ...DEFAULT_CONFIG };
410
+ replaceDefaultValues(store, { ...DEFAULT_CONFIG });
307
411
  }
308
412
  else if (store.scopes[scope]) {
309
413
  resetKeys = Object.keys(store.scopes[scope]);
@@ -385,10 +489,11 @@ export const configTools = [
385
489
  const importedKeys = Object.keys(config);
386
490
  if (scope === 'default') {
387
491
  if (merge) {
388
- Object.assign(store.values, config);
492
+ for (const [key, value] of Object.entries(config))
493
+ writeDefaultValue(store, key, value);
389
494
  }
390
495
  else {
391
- store.values = { ...DEFAULT_CONFIG, ...config };
496
+ replaceDefaultValues(store, { ...DEFAULT_CONFIG, ...config });
392
497
  }
393
498
  }
394
499
  else {
@@ -1154,14 +1154,59 @@ export const hooksRoute = {
1154
1154
  catch {
1155
1155
  // AgentDB router not available — fall through to local routing
1156
1156
  }
1157
+ // Trajectory learning persists SONA outcomes separately from the local
1158
+ // semantic index. Only consume an eligible learned pattern here; native,
1159
+ // Q-learning and keyword suggestions retain their existing routing paths.
1160
+ try {
1161
+ const sonaStart = performance.now();
1162
+ const sona = await getSONAOptimizer();
1163
+ const suggestion = await sona?.getRoutingSuggestion(task);
1164
+ const { isSupportedSonaAgent } = await import('../memory/sona-optimizer.js');
1165
+ if (suggestion?.source === 'sona-pattern'
1166
+ && Number.isFinite(suggestion.confidence)
1167
+ && suggestion.confidence >= 0.6 && suggestion.confidence <= 1
1168
+ && isSupportedSonaAgent(suggestion.agent)) {
1169
+ const confidence = Math.round(suggestion.confidence * 100) / 100;
1170
+ const complexity = task.length > 200 ? 'high' : task.length < 50 ? 'low' : 'medium';
1171
+ return {
1172
+ task,
1173
+ routing: { method: 'sona-pattern', backend: 'SONA persisted keyword patterns', latencyMs: performance.now() - sonaStart, throughput: 'N/A' },
1174
+ matched: true,
1175
+ matchedPattern: 'sona-pattern',
1176
+ semanticMatches: [],
1177
+ primaryAgent: {
1178
+ type: suggestion.agent,
1179
+ confidence,
1180
+ reason: `Persisted SONA outcome pattern (${Math.round(suggestion.confidence * 100)}% confidence)`,
1181
+ },
1182
+ alternativeAgents: suggestion.alternatives
1183
+ .filter(alternative => isSupportedSonaAgent(alternative.agent) && Number.isFinite(alternative.score))
1184
+ .map(alternative => ({
1185
+ type: alternative.agent,
1186
+ confidence: Math.round(alternative.score * 100) / 100,
1187
+ reason: 'Alternative from persisted SONA patterns',
1188
+ })),
1189
+ estimatedMetrics: {
1190
+ successProbability: confidence,
1191
+ estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
1192
+ complexity,
1193
+ },
1194
+ swarmRecommendation: null,
1195
+ learning: { source: suggestion.source, matchedKeywords: suggestion.matchedKeywords },
1196
+ };
1197
+ }
1198
+ }
1199
+ catch {
1200
+ // Optional SONA state unavailable — preserve the existing local route.
1201
+ }
1157
1202
  }
1158
1203
  return routeTaskLocal(task, context, useSemanticRouter);
1159
1204
  }),
1160
1205
  };
1161
1206
  /**
1162
1207
  * hooks_route's local routing (semantic index + keyword fallback), run after the
1163
- * AgentDB pre-route. Shared with {@link routeTaskForBench} so the benchmark
1164
- * measures exactly the path hooks_route takes. (Body kept at its original
1208
+ * AgentDB and eligible SONA pre-routes. Shared with {@link routeTaskForBench}
1209
+ * so the benchmark measures this local path. (Body kept at its original
1165
1210
  * indentation to keep this refactor's diff small.)
1166
1211
  */
1167
1212
  async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride) {
@@ -1500,7 +1500,21 @@ export const memoryTools = [
1500
1500
  // entry body. Without this, `value` is always null because listEntries
1501
1501
  // strips content by default (callers pay for the JSON parse only when
1502
1502
  // they need it).
1503
- const all = await listEntries({ limit: 100000, namespace, includeContent: true });
1503
+ // A successful export must cover the whole reported store. Read all
1504
+ // pages before touching an existing snapshot, including on read failure.
1505
+ const entries = [];
1506
+ let total;
1507
+ do {
1508
+ const page = await listEntries({ limit: 10000, offset: entries.length, namespace, includeContent: true });
1509
+ if (!page.success)
1510
+ return { error: page.error || 'Could not read memory for export' };
1511
+ total ??= page.total;
1512
+ if (page.entries.length === 0 && entries.length < total) {
1513
+ return { error: `Incomplete memory export: read ${entries.length} of ${total} entries` };
1514
+ }
1515
+ entries.push(...page.entries);
1516
+ } while (entries.length < total);
1517
+ const all = { entries };
1504
1518
  const payload = {
1505
1519
  schema: 'ruflo-memory-export/v1',
1506
1520
  exportedAt: new Date().toISOString(),
@@ -1595,6 +1609,8 @@ export const memoryTools = [
1595
1609
  }
1596
1610
  let imported = 0;
1597
1611
  let skipped = 0;
1612
+ let failed = 0;
1613
+ const errors = [];
1598
1614
  for (const e of entries) {
1599
1615
  if (!e || typeof e.key !== 'string') {
1600
1616
  skipped++;
@@ -1603,19 +1619,22 @@ export const memoryTools = [
1603
1619
  const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
1604
1620
  try {
1605
1621
  const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
1606
- if (result.success) {
1607
- imported++;
1608
- if (result.embedding)
1609
- vectors++;
1610
- }
1611
- else
1612
- skipped++;
1622
+ if (!result.success)
1623
+ throw new Error(result.error || 'Memory write was rejected');
1624
+ imported++;
1625
+ if (result.embedding)
1626
+ vectors++;
1613
1627
  }
1614
- catch {
1615
- skipped++;
1628
+ catch (error) {
1629
+ failed++;
1630
+ if (errors.length < 10)
1631
+ errors.push(`${e.key}: ${error instanceof Error ? error.message : String(error)}`);
1616
1632
  }
1617
1633
  }
1618
1634
  return {
1635
+ success: failed === 0,
1636
+ error: failed > 0 ? `${failed} memory write(s) failed: ${errors.join('; ')}` : undefined,
1637
+ failed,
1619
1638
  inputPath,
1620
1639
  imported: { entries: imported, vectors, patterns: 0 },
1621
1640
  skipped,
@@ -0,0 +1,3 @@
1
+ /** Only an absent store is empty. Failed reads must never authorize replacement. */
2
+ export declare function readRecordStore<T>(path: string, collection: string, empty: () => T): T;
3
+ //# sourceMappingURL=record-store.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** Only an absent store is empty. Failed reads must never authorize replacement. */
3
+ export function readRecordStore(path, collection, empty) {
4
+ let text;
5
+ try {
6
+ text = readFileSync(path, 'utf-8');
7
+ }
8
+ catch (error) {
9
+ if (error.code === 'ENOENT')
10
+ return empty();
11
+ throw error;
12
+ }
13
+ const store = JSON.parse(text);
14
+ const records = store?.[collection];
15
+ if (!store || typeof store !== 'object' || Array.isArray(store)
16
+ || !records || typeof records !== 'object' || Array.isArray(records)) {
17
+ throw new Error(`Invalid ${collection} store at ${path}; existing data was preserved`);
18
+ }
19
+ return store;
20
+ }
21
+ //# sourceMappingURL=record-store.js.map
@@ -10,6 +10,33 @@ import { join } from 'node:path';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
12
12
  import { validateIdentifier, validateText } from './validate-input.js';
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ // Imported snapshots become live stores on restore. Validate their containers
17
+ // before registering them, while allowing older metadata-only snapshots.
18
+ function validateImportedSession(value) {
19
+ if (!isRecord(value) || (typeof value.name !== 'string' && !isSessionRecordLike(value))) {
20
+ return 'Invalid session: Not a session record; expected a named snapshot or snapshot data';
21
+ }
22
+ if (value.name !== undefined && typeof value.name !== 'string') {
23
+ return 'Invalid session: name must be a string';
24
+ }
25
+ if (value.data === undefined)
26
+ return undefined;
27
+ if (!isRecord(value.data))
28
+ return 'Invalid session: data must be an object';
29
+ for (const [component, collection] of [['tasks', 'tasks'], ['agents', 'agents'], ['memory', 'entries']]) {
30
+ const store = value.data[component];
31
+ if (store === undefined)
32
+ continue;
33
+ if (!isRecord(store) || !isRecord(store[collection]) ||
34
+ Object.values(store[collection]).some(record => !isRecord(record))) {
35
+ return `Invalid session: data.${component}.${collection} must be a record map`;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
13
40
  // Storage paths
14
41
  const STORAGE_DIR = '.claude-flow';
15
42
  const SESSION_DIR = 'sessions';
@@ -306,9 +333,9 @@ export const sessionTools = [
306
333
  properties: {
307
334
  name: { type: 'string', description: 'Session name' },
308
335
  description: { type: 'string', description: 'Session description' },
309
- includeMemory: { type: 'boolean', description: 'Include memory in session' },
310
- includeTasks: { type: 'boolean', description: 'Include tasks in session' },
311
- includeAgents: { type: 'boolean', description: 'Include agents in session' },
336
+ includeMemory: { type: 'boolean', default: true, description: 'Include memory in session' },
337
+ includeTasks: { type: 'boolean', default: true, description: 'Include tasks in session' },
338
+ includeAgents: { type: 'boolean', default: true, description: 'Include agents in session' },
312
339
  },
313
340
  required: ['name'],
314
341
  },
@@ -325,9 +352,9 @@ export const sessionTools = [
325
352
  const sessionId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
326
353
  // Load related data based on options
327
354
  const { data, memoryCapture } = await loadRelatedStores({
328
- includeMemory: input.includeMemory,
329
- includeTasks: input.includeTasks,
330
- includeAgents: input.includeAgents,
355
+ includeMemory: input.includeMemory !== false,
356
+ includeTasks: input.includeTasks !== false,
357
+ includeAgents: input.includeAgents !== false,
331
358
  });
332
359
  // Calculate stats
333
360
  const stats = {
@@ -692,19 +719,26 @@ export const sessionTools = [
692
719
  return { error: `Invalid session JSON: ${e.message}` };
693
720
  }
694
721
  }
695
- if (!isSessionRecordLike(parsed)) {
696
- return { error: 'Not a session record: expected an object produced by session export' };
697
- }
722
+ const validationError = validateImportedSession(parsed);
723
+ if (validationError)
724
+ return { error: validationError };
725
+ const record = parsed;
698
726
  const newId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
699
- const stats = { tasks: 0, agents: 0, memoryEntries: 0, totalSize: 0, ...(parsed.stats || {}) };
727
+ const stats = {
728
+ tasks: Object.keys(record.data?.tasks?.tasks || {}).length,
729
+ agents: Object.keys(record.data?.agents?.agents || {}).length,
730
+ memoryEntries: countMemoryEntries(record.data?.memory),
731
+ totalSize: 0,
732
+ };
700
733
  const session = {
701
734
  sessionId: newId,
702
- name: input.name ? String(input.name) : (parsed.name || 'imported-session'),
703
- description: parsed.description,
735
+ name: input.name ? String(input.name) : (record.name || 'imported-session'),
736
+ description: record.description,
704
737
  savedAt: new Date().toISOString(),
705
738
  stats,
706
- data: parsed.data,
739
+ data: record.data,
707
740
  };
741
+ session.stats.totalSize = Buffer.byteLength(JSON.stringify(session), 'utf-8');
708
742
  saveSession(session);
709
743
  let activated = false;
710
744
  if (input.activate === true) {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ import { readRecordStore } from './record-store.js';
8
9
  import { getProjectCwd } from './types.js';
9
10
  import { validateIdentifier, validateText } from './validate-input.js';
10
11
  // Storage paths
@@ -24,17 +25,7 @@ function ensureTaskDir() {
24
25
  }
25
26
  }
26
27
  function loadTaskStore() {
27
- try {
28
- const path = getTaskPath();
29
- if (existsSync(path)) {
30
- const data = readFileSync(path, 'utf-8');
31
- return JSON.parse(data);
32
- }
33
- }
34
- catch {
35
- // Return empty store on error
36
- }
37
- return { tasks: {}, version: '3.0.0' };
28
+ return readRecordStore(getTaskPath(), 'tasks', () => ({ tasks: {}, version: '3.0.0' }));
38
29
  }
39
30
  function saveTaskStore(store) {
40
31
  ensureTaskDir();
@@ -378,6 +369,9 @@ export const taskTools = [
378
369
  return { taskId, error: 'Task not found' };
379
370
  }
380
371
  const previouslyAssigned = [...task.assignedTo];
372
+ // Load agent store to sync worker state
373
+ const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json');
374
+ readRecordStore(agentStorePath, 'agents', () => ({ agents: {} }));
381
375
  if (input.unassign) {
382
376
  // Revert previously assigned agents to idle
383
377
  releaseAgents(previouslyAssigned, taskId);