ruvnet-brain 4.6.2 → 4.6.3

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.
@@ -23,11 +23,9 @@
23
23
  * • Content = assistant outcome text, files changed, Bash descriptions. NEVER raw user text (a
24
24
  * 2026-07-13 measurement: prompt echoes made 87% of a store noise). Trivial turns are skipped.
25
25
  *
26
- * CODEX: codex-cli 0.158.0's own `stop.command.input` schema (read from the installed binary
27
- * 2026-09-29) carries `last_assistant_message` (nullable) and `transcript_path` (nullable).
28
- * The Codex rollout format is NOT parsed: project-progression-sources.mjs already declares it
29
- * unknown, and the rollout records observed locally (custom_tool_call / function_call with free-form
30
- * inputs) give no stable file-change shape. Codex records therefore carry the outcome text only.
26
+ * CODEX: native response_item JSONL is normalized mechanically through native-turn-transcript.
27
+ * SessionEnd and PreCompact recover a closing outcome when Stop payload text is absent. Tool IDs
28
+ * correlate observable gate outcomes; opaque custom orchestration inputs remain unclassified.
31
29
  *
32
30
  * LATENCY: Stop runs synchronously in the host's turn, and one `ruflo memory store` costs ~0.7s
33
31
  * (measured). So the writes run in ONE detached worker (this file, `--run-steps`), store then
@@ -40,9 +38,11 @@ import path from 'node:path';
40
38
  import crypto from 'node:crypto';
41
39
  import { spawn, spawnSync } from 'node:child_process';
42
40
  import { fileURLToPath } from 'node:url';
41
+ import { nativeTurnLines } from './native-turn-transcript.mjs';
43
42
  import { resolveRuflo, rufloInvocation } from './ruflo-bin.mjs';
43
+ import { rufloCwdFor, rufloScratchRoot } from './project-progression-store.mjs';
44
44
  import { redactText, userLevelAgentdbHooks } from './continuity-events.mjs';
45
- import { resolveProjectStore } from './project-store-resolver.mjs';
45
+ import { resolveProjectStore, globalEnrollmentAllowed } from './project-store-resolver.mjs';
46
46
  import { digest, journalTurn, readJournal, pendingTurnFiles, acknowledgeJournal, appendReceipt, storeData } from './turn-transport-journal.mjs';
47
47
  import { contentPathExcludes, pathIsExcluded, privateTurn, maskExcludedPaths, captureFailureReason, firstTurnCaptureNotice } from './turn-capture-privacy.mjs';
48
48
 
@@ -162,7 +162,7 @@ const consentMap = (value) => value !== null && typeof value === 'object'
162
162
  && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype
163
163
  && Object.values(value).every((setting) => setting === 'on' || setting === 'off');
164
164
 
165
- export function resolveTurnDb({ projectDir, brainHome, requestedStorePath, gitTimeoutMs = 1000, unknownOriginalPath = false, deadlineAt = Infinity, signal } = {}) {
165
+ export function resolveTurnDb({ projectDir, brainHome, requestedStorePath, gitTimeoutMs = 1000, unknownOriginalPath = false, deadlineAt = Infinity, signal, enrollmentRoots = {} } = {}) {
166
166
  if (signal?.aborted || Date.now() >= deadlineAt) throw new Error('restore deadline exceeded');
167
167
  const resolved = resolveProjectStore({ projectDir, requestedStorePath, gitTimeoutMs, deadlineAt });
168
168
  if (signal?.aborted || Date.now() >= deadlineAt) throw new Error('restore deadline exceeded');
@@ -173,12 +173,14 @@ export function resolveTurnDb({ projectDir, brainHome, requestedStorePath, gitTi
173
173
  try {
174
174
  policy = JSON.parse(fs.readFileSync(file, 'utf8'));
175
175
  if (policy?.schemaVersion !== 1 || !consentMap(policy.projects)
176
+ || (Object.hasOwn(policy, 'default') && !['on', 'off'].includes(policy.default))
176
177
  || (Object.hasOwn(policy, 'paths') && !consentMap(policy.paths))) throw new Error('invalid policy');
177
178
  contentPathExcludes(policy.contentPathExcludes);
178
179
  } catch { return { skipped: 'turn capture policy unreadable or invalid', projectRoot: resolved.projectRoot }; }
179
180
  }
180
181
  // A path rule wins over a project rule, allowing a linked checkout/subdirectory to opt out.
181
- const setting = policy.paths?.[capturePath] ?? policy.projects?.[resolved.projectRoot];
182
+ const explicitSetting = policy.paths?.[capturePath] ?? policy.projects?.[resolved.projectRoot];
183
+ const setting = explicitSetting ?? policy.default;
182
184
  if (setting !== undefined && !['on', 'off'].includes(setting)) return { skipped: 'invalid turn capture consent', projectRoot: resolved.projectRoot };
183
185
  const db = resolved.canonicalAgentDbPath;
184
186
  const exclusions = contentPathExcludes(policy.contentPathExcludes);
@@ -191,6 +193,9 @@ export function resolveTurnDb({ projectDir, brainHome, requestedStorePath, gitTi
191
193
  if (setting === 'off') return { db, scope: 'project', capturePath, projectRoot: resolved.projectRoot, skipped: 'persisted turn capture opt-out' };
192
194
  let exists = false;
193
195
  try { exists = fs.statSync(db).isFile(); } catch { /* absent */ }
196
+ if (!exists && setting === 'on' && explicitSetting !== 'on' && !globalEnrollmentAllowed({ resolution: resolved, brainHome, ...enrollmentRoots })) {
197
+ return { db, scope: 'project', capturePath, projectRoot: resolved.projectRoot, skipped: 'global capture consent requires a safe Git project root; explicit project/path opt-in required' };
198
+ }
194
199
  if (!exists && setting !== 'on') return { db, scope: 'project', capturePath, projectRoot: resolved.projectRoot, skipped: 'no project memory db; persisted opt-in required' };
195
200
  return { db, scope: 'project', capturePath, projectRoot: resolved.projectRoot, optedIn: setting === 'on',
196
201
  contentPathExcludes: exclusions };
@@ -292,14 +297,14 @@ export function captureTurnOutcome({
292
297
  if (deferTo) {
293
298
  report.skipped = 'deferred: an enabled user-level Stop writer explicitly targets the canonical turn store; actual native delivery is unproven';
294
299
  report.deferredToUserLevel = true;
295
- } else if (event === 'Stop') {
300
+ } else if (['Stop', 'SessionEnd', 'PreCompact'].includes(event)) {
296
301
  const sessionKey = String(payload.session_id || payload.transcript_path || '');
297
302
  const message = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message.trim() : '';
298
303
  let turn = { finalText: '', files: [], actions: [] };
299
- if (host === 'claude' && typeof payload.transcript_path === 'string' && payload.transcript_path) {
304
+ if (typeof payload.transcript_path === 'string' && payload.transcript_path) {
300
305
  // With the closing message in hand, the transcript is read immediately (tool calls are already
301
306
  // flushed); without it, wait for the closing message to land, bounded.
302
- try { turn = claudeTurn(readTranscript(payload.transcript_path, { maxMs: message ? 0 : settleMs })); } catch { /* unreadable */ }
307
+ try { turn = claudeTurn(nativeTurnLines(readTranscript(payload.transcript_path, { maxMs: message || event !== 'Stop' ? 0 : settleMs }), host)); } catch { /* unreadable */ }
303
308
  }
304
309
  if (message && (message.length >= MIN_OUTCOME_CHARS || message.length >= turn.finalText.length)) turn.finalText = message;
305
310
  turn = privateTurn(turn, target.contentPathExcludes, target.capturePath);
@@ -323,12 +328,12 @@ export function captureTurnOutcome({
323
328
  catch (error) { return { ...report, skipped: `durable turn queue invalid: ${redactText(error.message)}` }; }
324
329
  }
325
330
  steps.push({ ...binding, kind: 'store', ruflo, args: ['memory', 'store', '-k', key, '--value', value, '-n', TURN_NAMESPACE,
326
- '--path', db, '--no-upsert', '--tags', `project=${project},host=${host}`, '--provenance', 'agent_output'] });
331
+ '--path', db, '--no-upsert', '--require-native', '--append-only', '--tags', `project=${project},host=${host}`, '--provenance', 'agent_output'] });
327
332
  // This synchronous boundary proves only queuing; the worker's exact receipt proves recording.
328
333
  Object.assign(report, { queued: true, key, value });
329
334
  }
330
335
  }
331
- } else report.skipped = `turn outcomes are recorded at Stop, not ${event}`;
336
+ } else report.skipped = `no turn outcome capture at ${event}`;
332
337
 
333
338
  if (event === 'SessionEnd' || event === 'PreCompact') {
334
339
  if (!fs.existsSync(db)) report.distill = { queued: false, skipped: 'no memory db to distill yet' };
@@ -376,7 +381,7 @@ export function runSteps({ steps = [], receipts } = {}, { run: suppliedRun = spa
376
381
  };
377
382
  const results = [];
378
383
  for (const queued of steps) {
379
- let status = 1; let error = null; let verified = false; let db = ''; let key; let privacyPatterns = [];
384
+ let status = 1; let error = null; let skipped = null; let verified = false; let db = ''; let key; let privacyPatterns = [];
380
385
  const kind = queued.kind === 'journal' ? 'store' : queued.kind;
381
386
  try {
382
387
  if (String(env.RUVNET_TURN_CAPTURE || '').toLowerCase() === 'off') throw new Error('RUVNET_TURN_CAPTURE=off');
@@ -396,7 +401,7 @@ export function runSteps({ steps = [], receipts } = {}, { run: suppliedRun = spa
396
401
  binding = queued.kind === 'journal' ? data.binding : queued;
397
402
  key = data.key; value = maskExcludedPaths(redactText(data.value), target.contentPathExcludes, target.capturePath);
398
403
  if (value !== redactText(data.value)) throw new Error('content exclusions changed; immutable turn retained');
399
- args = ['memory', 'store', '-k', key, '--value', value, '-n', TURN_NAMESPACE, '--path', db, '--no-upsert', '--provenance', 'agent_output'];
404
+ args = ['memory', 'store', '-k', key, '--value', value, '-n', TURN_NAMESPACE, '--path', db, '--no-upsert', '--require-native', '--append-only', '--provenance', 'agent_output'];
400
405
  } else if (kind === 'distill') {
401
406
  if (queued.brainHome !== brainHome) throw new Error('queued consent settings differ from trusted caller');
402
407
  const recipe = queued.args;
@@ -424,21 +429,30 @@ export function runSteps({ steps = [], receipts } = {}, { run: suppliedRun = spa
424
429
  const invocation = rufloInvocation(ruflo, args);
425
430
  const remaining = deadlineMs === undefined ? STEP_TIMEOUT_MS : deadlineMs - Date.now();
426
431
  if (remaining <= 0) throw new Error('turn replay deadline exhausted');
427
- const options = { encoding: 'utf8', timeout: Math.min(STEP_TIMEOUT_MS, remaining), cwd: path.dirname(db), windowsHide: true,
428
- maxBuffer: 1024 * 1024, env: { ...env, ...RUFLO_ENV, CLAUDE_FLOW_MEMORY_PATH: path.dirname(db) } };
432
+ // Measured 2026-10-10 with global Ruflo 3.56.3: cwd=<project>/.swarm litters ruvector.db/.claude-flow there, and
433
+ // CLAUDE_FLOW_MEMORY_PATH=<.swarm> makes Ruflo create a second agentdb-memory.db beside the canonical store.
434
+ // The explicit --path already binds the store, so run from private scratch with neither.
435
+ const options = { encoding: 'utf8', timeout: Math.min(STEP_TIMEOUT_MS, remaining), windowsHide: true,
436
+ cwd: rufloCwdFor(db, { root: rufloScratchRoot({ ...env, RUVNET_BRAIN_HOME: brainHome }) }),
437
+ maxBuffer: 1024 * 1024, env: { ...env, ...RUFLO_ENV } };
438
+ delete options.env.CLAUDE_FLOW_MEMORY_PATH;
429
439
  const existing = kind === 'store' && queued.journalFile ? read({ ruflo, db, key, run, options }) : null;
430
440
  if (existing !== null && existing !== value) throw new Error('existing turn key content differs; no upsert permitted');
431
441
  verified = kind === 'store' && existing === value;
432
442
  const r = verified ? { status: 0 } : run(invocation.executable, invocation.args, options);
433
443
  status = Number.isInteger(r.status) ? r.status : 1;
434
- if (kind === 'store' && !verified) verified = read({ ruflo, db, key, run, options }) === value;
444
+ if (kind === 'store' && !verified && status === 0) verified = read({ ruflo, db, key, run, options }) === value;
435
445
  if (verified) status = 0;
446
+ if (kind === 'distill' && status === 0) {
447
+ const match = /(?:^|\n)\s*skipped:\s*([^\n]+)/i.exec(String(r.stdout || '').replace(/\x1b\[[0-9;]*m/g, ''));
448
+ if (match) skipped = redactText(match[1]).slice(0, 300);
449
+ }
436
450
  if (!verified && (r.error || status !== 0)) error = captureFailureReason(r, status);
437
451
  else if (kind === 'store' && !verified) { status = 1; error = 'exact turn key/content readback failed'; }
438
452
  } catch (e) { status = 1; error = redactText(e.message).slice(0, 300); }
439
453
  if (error) error = maskExcludedPaths(error, privacyPatterns, projectDir);
440
454
  const row = { at: new Date().toISOString(), kind, db: redactText(db), storeIdentity: digest(db), key, status, error,
441
- ...(kind === 'store' ? { verified } : {}) };
455
+ ...(kind === 'store' ? { verified } : {}), ...(skipped ? { skipped } : {}) };
442
456
  results.push(row);
443
457
  if (receipts) {
444
458
  try { appendReceipt(receipts, row); if (verified && queued.journalFile) acknowledgeJournal(queued.journalFile, db); }
@@ -26,9 +26,11 @@ export function syncTurnDirectory(dir, { platform = process.platform, io = fs }
26
26
  export function storeData(step) {
27
27
  if (step?.kind !== 'store' || !Array.isArray(step.args) || step.args[0] !== 'memory' || step.args[1] !== 'store') throw new Error('invalid turn store operation');
28
28
  const fields = {}; const aliases = { '-k': 'key', '--key': 'key', '--value': 'value', '-n': 'namespace', '--namespace': 'namespace', '--path': 'db', '--tags': 'tags', '--provenance': 'provenance' };
29
- let strict = false;
29
+ let strict = false; let native = false; let immutable = false;
30
30
  for (let i = 2; i < step.args.length; i++) {
31
31
  const flag = step.args[i];
32
+ if (flag === '--append-only') { if (immutable) throw new Error('duplicate turn store flag'); immutable = true; continue; }
33
+ if (flag === '--require-native') { if (native) throw new Error('duplicate turn store flag'); native = true; continue; }
32
34
  if (flag === '--no-upsert') { if (strict) throw new Error('duplicate turn store flag'); strict = true; continue; }
33
35
  const field = aliases[flag];
34
36
  if (!field || Object.hasOwn(fields, field) || typeof step.args[i + 1] !== 'string') throw new Error('unknown or duplicate turn store flag');