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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/agentdb-recall-process.mjs +118 -0
- package/plugin/scripts/agentdb-recall-reader.mjs +22 -0
- package/plugin/scripts/agentdb-recall.mjs +82 -99
- package/plugin/scripts/continuity-events.mjs +6 -5
- package/plugin/scripts/continuity-journal.mjs +22 -16
- package/plugin/scripts/native-turn-transcript.mjs +51 -0
- package/plugin/scripts/project-capture-queue.mjs +2 -0
- package/plugin/scripts/project-memory-enrollment.mjs +203 -0
- package/plugin/scripts/project-progression-reader.mjs +4 -4
- package/plugin/scripts/project-progression-session-start.mjs +7 -27
- package/plugin/scripts/project-progression-store.mjs +18 -6
- package/plugin/scripts/project-store-resolver.mjs +44 -6
- package/plugin/scripts/session-snapshot-hook.mjs +7 -0
- package/plugin/scripts/session-start-core.mjs +6 -0
- package/plugin/scripts/turn-outcome-capture.mjs +33 -19
- package/plugin/scripts/turn-transport-journal.mjs +3 -1
|
@@ -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:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
428
|
-
|
|
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');
|