ruvnet-brain 4.6.1 → 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 +4 -3
- package/bin/nightly-refresh.mjs +5 -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/developer-update-maintenance.mjs +133 -6
- package/plugin/scripts/developer-update.mjs +37 -7
- package/plugin/scripts/native-turn-transcript.mjs +51 -0
- package/plugin/scripts/nightly-scheduler.mjs +27 -6
- 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
- package/scripts/release-qualification-contract.mjs +3 -3
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* RETAIN_COMMITTED_MS, ages quarantine/corrupt notices after QUARANTINE_REPORT_MS, and bounds committed
|
|
13
13
|
* history at MAX_EVENT_RECORDS. Pending events are never dropped; prolonged outages report pressure.
|
|
14
14
|
*/
|
|
15
|
+
import { nativeTurnLines } from './native-turn-transcript.mjs';
|
|
15
16
|
import fs from 'node:fs';
|
|
16
17
|
import os from 'node:os';
|
|
17
18
|
import path from 'node:path';
|
|
@@ -137,7 +138,9 @@ export class ContinuityJournal {
|
|
|
137
138
|
scan() {
|
|
138
139
|
this.requireBudget();
|
|
139
140
|
let text = '';
|
|
140
|
-
try { text = fs.readFileSync(this.path, 'utf8'); } catch {
|
|
141
|
+
try { text = fs.readFileSync(this.path, 'utf8'); } catch (error) {
|
|
142
|
+
if (error.code !== 'ENOENT') throw new Error(`continuity outbox unreadable: ${error.code || 'I/O error'}`);
|
|
143
|
+
}
|
|
141
144
|
const events = new Map();
|
|
142
145
|
const committed = new Map();
|
|
143
146
|
const failures = new Map();
|
|
@@ -367,23 +370,23 @@ export function stopNotice({ journal, status, session }) {
|
|
|
367
370
|
}
|
|
368
371
|
function defaultStore({ ruflo, db, key, value }) {
|
|
369
372
|
const cwd = rufloRunDir(db);
|
|
373
|
+
const source = JSON.parse(value).source;
|
|
374
|
+
const provenance = ['assistant-detected', 'owner-correction-detected', 'agent-result'].includes(source) ? 'agent_output' : 'system_observation';
|
|
370
375
|
try {
|
|
371
376
|
const { executable, args } = rufloInvocation(ruflo, ['memory', 'store', '--key', key, '--value', value,
|
|
372
|
-
'--namespace', CONTINUITY_NAMESPACE, '--no-upsert', '--
|
|
377
|
+
'--namespace', CONTINUITY_NAMESPACE, '--no-upsert', '--require-native', '--append-only', '--provenance', provenance, '--path', db]);
|
|
373
378
|
const r = spawnSync(executable, args, { cwd, encoding: 'utf8', timeout: 60_000, windowsHide: true,
|
|
374
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
379
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined } });
|
|
375
380
|
return { status: Number.isInteger(r.status) ? r.status : 1, output: `${r.stderr || ''}\n${r.stdout || ''}${r.error ? `\n${r.error.message}` : ''}` };
|
|
376
381
|
} finally { fs.rmSync(cwd, { recursive: true, force: true }); }
|
|
377
382
|
}
|
|
378
383
|
|
|
379
384
|
function defaultReadBack({ ruflo, db, key }) {
|
|
380
|
-
const fast = withProgressionReader(db, (reader) => reader.readContent(CONTINUITY_NAMESPACE, key));
|
|
381
|
-
if (fast.ok) return { key, content: fast.value, readPath: 'node:sqlite' };
|
|
382
385
|
const cwd = rufloRunDir(db);
|
|
383
386
|
try {
|
|
384
387
|
const { executable, args } = rufloInvocation(ruflo, ['memory', 'retrieve', '--key', key, '--namespace', CONTINUITY_NAMESPACE, '--value-only', '--path', db]);
|
|
385
|
-
const r = spawnSync(executable, args, { cwd, encoding: 'utf8', timeout: 60_000, windowsHide: true, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
386
|
-
return { key, content: r.status === 0 ? String(r.stdout || '') : null, readPath:
|
|
388
|
+
const r = spawnSync(executable, args, { cwd, encoding: 'utf8', timeout: 60_000, windowsHide: true, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined } });
|
|
389
|
+
return { key, content: r.status === 0 ? String(r.stdout || '') : null, readPath: 'ruflo-cli' };
|
|
387
390
|
} finally { fs.rmSync(cwd, { recursive: true, force: true }); }
|
|
388
391
|
}
|
|
389
392
|
|
|
@@ -414,15 +417,18 @@ export function drain(journal, {
|
|
|
414
417
|
if (now() >= deadline) break;
|
|
415
418
|
const value = JSON.stringify(privateContinuityEvent(rec.event, consent.contentPathExcludes, consent.capturePath));
|
|
416
419
|
if (value !== JSON.stringify(rec.event)) return { committed, failed, remaining: journal.pending().length, skipped: 'content exclusions changed; immutable continuity retained' };
|
|
417
|
-
attempts += 1;
|
|
418
|
-
const
|
|
420
|
+
attempts += 1;
|
|
421
|
+
const prior = readBack({ ruflo, db: journal.db, key: rec.key });
|
|
422
|
+
const alreadyStored = typeof prior.content === 'string' && prior.content.trim() === value && (prior.key === undefined || prior.key === rec.key);
|
|
423
|
+
const result = alreadyStored ? { status: 0 } : store({ ruflo, db: journal.db, key: rec.key, value });
|
|
424
|
+
const back = alreadyStored ? prior : readBack({ ruflo, db: journal.db, key: rec.key });
|
|
419
425
|
const content = typeof back.content === 'string' ? back.content.trim() : '';
|
|
420
|
-
if (content === value && digestCanonical(rec.event) === rec.digest && (back.key === undefined || back.key === rec.key)) {
|
|
426
|
+
if (result.status === 0 && content === value && digestCanonical(rec.event) === rec.digest && (back.key === undefined || back.key === rec.key)) {
|
|
421
427
|
journal.appendRecords([{ type: 'commit', key: rec.key, digest: rec.digest, committedAt: new Date(now()).toISOString(), readPath: back.readPath,
|
|
422
|
-
alreadyStored
|
|
428
|
+
alreadyStored, ...(attempts > 1 ? { attempts, lastError: last?.reason } : {}) }]);
|
|
423
429
|
committed += 1;
|
|
424
430
|
done = true;
|
|
425
|
-
} else if (content) {
|
|
431
|
+
} else if (content && (content !== value || digestCanonical(rec.event) !== rec.digest || (back.key !== undefined && back.key !== rec.key))) {
|
|
426
432
|
// A DIFFERENT row holds our key: never overwritten (--no-upsert), never retried, reported.
|
|
427
433
|
journal.appendRecords([{ type: 'quarantine', key: rec.key, at: new Date(now()).toISOString(), reason: 'a different stored row holds this key' }]);
|
|
428
434
|
failed += 1;
|
|
@@ -466,7 +472,7 @@ export function takeLock(journal, { now = Date.now } = {}) {
|
|
|
466
472
|
export function launchDrain({ projectRoot, spawnFn = spawn, env = process.env } = {}) {
|
|
467
473
|
try {
|
|
468
474
|
const child = spawnFn(process.execPath, [fileURLToPath(import.meta.url), '--drain', projectRoot], {
|
|
469
|
-
cwd: os.tmpdir(), detached: true, stdio: 'ignore', windowsHide: true, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
475
|
+
cwd: os.tmpdir(), detached: true, stdio: 'ignore', windowsHide: true, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined },
|
|
470
476
|
});
|
|
471
477
|
child.unref?.();
|
|
472
478
|
return true;
|
|
@@ -513,13 +519,13 @@ export function captureContinuityEvents({
|
|
|
513
519
|
events.push(...collectCommits({ checkoutRoot: resolution.checkoutRoot, sinceMs, host, session, project }));
|
|
514
520
|
events.push(...collectReleases({ checkoutRoot: resolution.checkoutRoot, sinceMs, host, session, project }));
|
|
515
521
|
}
|
|
516
|
-
if (
|
|
522
|
+
if (['Stop', 'SessionEnd', 'PreCompact', 'SubagentStop'].includes(event)) {
|
|
517
523
|
let lines = null;
|
|
518
|
-
if (
|
|
524
|
+
if (typeof payload.transcript_path === 'string' && payload.transcript_path) {
|
|
519
525
|
try { lines = readTranscript(payload.transcript_path); } catch { /* unreadable transcript: git events still count */ }
|
|
520
526
|
}
|
|
521
527
|
const last = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message : ''; const policy = journal.captureConsent();
|
|
522
|
-
if (!policy.skipped && !(lines && turnReferencesExcludedResource(claudeTurn(lines), policy.contentPathExcludes, projectDir))) events.push(...collectTurnEvents({ lines, lastAssistantMessage: last, host, session, project, env, at: now() }));
|
|
528
|
+
if (!policy.skipped && !(lines && turnReferencesExcludedResource(claudeTurn(nativeTurnLines(lines, host)), policy.contentPathExcludes, projectDir))) events.push(...collectTurnEvents({ lines, lastAssistantMessage: last, host, session, project, env, at: now() }));
|
|
523
529
|
}
|
|
524
530
|
try { report.recorded = journal.record(events).length; } catch (error) { return { ...report, skipped: `outbox write failed: ${error.message}` }; }
|
|
525
531
|
try { if (journal.needsCompaction()) journal.compact(); } catch { /* bounded next time */ }
|
|
@@ -3,6 +3,114 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { cmpVersion as compare } from './developer-update-policy.mjs';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
const quote = text => `'${String(text).replaceAll("'", "'\\''")}'`;
|
|
10
|
+
function regular(file) {
|
|
11
|
+
const stat = fs.lstatSync(file);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.()
|
|
13
|
+
|| (stat.mode & 0o022)) throw Error('Codex launcher requires owned regular files');
|
|
14
|
+
return fs.readFileSync(file);
|
|
15
|
+
}
|
|
16
|
+
function runtimeIdentity(root) {
|
|
17
|
+
const entries = [];
|
|
18
|
+
function visit(relative) {
|
|
19
|
+
const file = path.join(root, relative), stat = fs.lstatSync(file);
|
|
20
|
+
if (stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === process.getuid?.()) {
|
|
21
|
+
for (const name of fs.readdirSync(file)) visit(path.join(relative, name));
|
|
22
|
+
} else entries.push([relative.split(path.sep).join('/'), sha(regular(file))]);
|
|
23
|
+
}
|
|
24
|
+
visit('');
|
|
25
|
+
return sha(JSON.stringify(entries.sort(([a], [b]) => a.localeCompare(b))));
|
|
26
|
+
}
|
|
27
|
+
function nativeOwner(file, home) {
|
|
28
|
+
const real = fs.realpathSync(file), root = path.join(home, '.codex/packages/standalone/releases') + path.sep;
|
|
29
|
+
if (!real.startsWith(root) || path.basename(real) !== 'codex') throw Error('Codex native owner outside standalone releases');
|
|
30
|
+
const bytes = regular(real), magic = bytes.subarray(0, 4).toString('hex');
|
|
31
|
+
if (!['7f454c46', 'cffaedfe', 'feedfacf', 'cafebabe', 'bebafeca'].includes(magic)) throw Error('Codex native executable identity unavailable');
|
|
32
|
+
fs.accessSync(real, fs.constants.X_OK);
|
|
33
|
+
return real;
|
|
34
|
+
}
|
|
35
|
+
export function inspectCodexLauncher(file, home) {
|
|
36
|
+
if (file !== path.join(home, '.local/bin/codex') || fs.lstatSync(file).isSymbolicLink()) return null;
|
|
37
|
+
const bytes = regular(file);
|
|
38
|
+
// A direct native binary is handled by the ordinary owner gate; unknown scripts never run.
|
|
39
|
+
if (!bytes.subarray(0, 2).equals(Buffer.from('#!'))) {
|
|
40
|
+
if (!['7f454c46', 'cffaedfe', 'feedfacf', 'cafebabe', 'bebafeca'].includes(bytes.subarray(0, 4).toString('hex'))) throw Error('Unknown Codex native launcher refused');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const configPath = path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json');
|
|
44
|
+
const configBytes = regular(configPath), config = JSON.parse(configBytes);
|
|
45
|
+
const runtimeRoot = path.join(home, '.cache/ruvnet-brain/model-routing/versions', config.runtimeDigest || '');
|
|
46
|
+
const keys = ['schemaVersion', 'managedBy', 'runtimeDigest', 'runtimeRoot', 'nodeBinary', 'runner', 'realCodex', 'gatewayPath',
|
|
47
|
+
...(config.realClaude ? ['realClaude', 'claudeHelperPath', 'enginePath'] : [])];
|
|
48
|
+
if (Object.keys(config).sort().join(',') !== keys.sort().join(',')) throw Error('Codex launcher config shape mismatch');
|
|
49
|
+
if (config.schemaVersion !== 1 || config.managedBy !== 'ruvnet-brain-terminal-launchers'
|
|
50
|
+
|| !/^[a-f0-9]{64}$/.test(config.runtimeDigest || '') || config.runtimeRoot !== runtimeRoot
|
|
51
|
+
|| fs.realpathSync(runtimeRoot) !== runtimeRoot || config.runner !== path.join(runtimeRoot, 'scripts/model-terminal-launchers.mjs')
|
|
52
|
+
|| config.gatewayPath !== path.join(runtimeRoot, 'scripts/model-terminal-gateway.mjs')
|
|
53
|
+
|| (config.realClaude && (config.claudeHelperPath !== path.join(runtimeRoot, 'scripts/claude-terminal-mod.mjs')
|
|
54
|
+
|| config.enginePath !== path.join(runtimeRoot, 'scripts/model-router-engine.mjs')))
|
|
55
|
+
|| runtimeIdentity(runtimeRoot) !== config.runtimeDigest) throw Error('Codex launcher runtime/config identity mismatch');
|
|
56
|
+
const expected = `#!/bin/sh\n# Managed RuvNet Brain terminal launcher\nexec ${quote(config.nodeBinary)} ${quote(config.runner)} --launch 'codex' --config ${quote(configPath)} -- "$@"\n`;
|
|
57
|
+
if (!bytes.equals(Buffer.from(expected))) throw Error('Preserving unknown Codex terminal wrapper');
|
|
58
|
+
regular(config.nodeBinary); fs.accessSync(config.nodeBinary, fs.constants.X_OK);
|
|
59
|
+
// The shared installer also visits registered Claude launchers; require exact existing bytes.
|
|
60
|
+
const claudeEntries = [];
|
|
61
|
+
if (config.realClaude) {
|
|
62
|
+
const claudeBytes = Buffer.from(expected.replace("--launch 'codex'", "--launch 'claude'"));
|
|
63
|
+
for (const entry of ['claude', 'ruvnet-brain-claude-terminal']) {
|
|
64
|
+
const claudeFile = path.join(home, '.local/bin', entry);
|
|
65
|
+
if (!regular(claudeFile).equals(claudeBytes)) throw Error('Preserving changed Claude terminal wrapper');
|
|
66
|
+
claudeEntries.push({ file: claudeFile, bytes: claudeBytes });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { file, home, configPath, configBytes, config, bytes, claudeEntries, native: nativeOwner(config.realCodex, home) };
|
|
70
|
+
}
|
|
71
|
+
export function updatedCodexNative(proof) {
|
|
72
|
+
if (fs.lstatSync(proof.file).uid !== process.getuid?.()) throw Error('Codex canonical owner changed during update');
|
|
73
|
+
if (!fs.lstatSync(proof.file).isSymbolicLink() && regular(proof.file).equals(proof.bytes)) {
|
|
74
|
+
return nativeOwner(path.join(proof.home, '.codex/packages/standalone/current/bin/codex'), proof.home);
|
|
75
|
+
}
|
|
76
|
+
return nativeOwner(proof.file, proof.home);
|
|
77
|
+
}
|
|
78
|
+
export async function restoreCodexLauncher(proof, { retainOriginalNative = false } = {}) {
|
|
79
|
+
const { file, home, config, configPath } = proof;
|
|
80
|
+
if (!regular(configPath).equals(proof.configBytes) || runtimeIdentity(config.runtimeRoot) !== config.runtimeDigest) {
|
|
81
|
+
throw Error('Codex launcher identity changed during native update');
|
|
82
|
+
}
|
|
83
|
+
for (const entry of proof.claudeEntries) if (!regular(entry.file).equals(entry.bytes)) throw Error('Claude wrapper changed during Codex update');
|
|
84
|
+
const candidate = updatedCodexNative(proof);
|
|
85
|
+
const native = retainOriginalNative ? nativeOwner(proof.native, home) : candidate;
|
|
86
|
+
const installed = await import(pathToFileURL(config.runner).href);
|
|
87
|
+
if (typeof installed.installTerminalLaunchers !== 'function') throw Error('Codex launcher installer unavailable');
|
|
88
|
+
// Installer accepts only same-owner native links. On failure retain the known original
|
|
89
|
+
// wrapper first, backing up the verified vendor link, then let its installer reconcile config.
|
|
90
|
+
if (retainOriginalNative && fs.lstatSync(file).isSymbolicLink()) {
|
|
91
|
+
const link = fs.readlinkSync(file), backup = `${file}.${sha(link)}.original`;
|
|
92
|
+
try { fs.symlinkSync(link, backup); }
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== 'EEXIST' || !fs.lstatSync(backup).isSymbolicLink()
|
|
95
|
+
|| fs.lstatSync(backup).uid !== process.getuid?.() || fs.readlinkSync(backup) !== link) throw Error('Codex vendor link backup mismatch');
|
|
96
|
+
}
|
|
97
|
+
const temporary = `${file}.${crypto.randomUUID()}.tmp`;
|
|
98
|
+
try { fs.writeFileSync(temporary, proof.bytes, { flag: 'wx', mode: 0o755 }); fs.renameSync(temporary, file); }
|
|
99
|
+
finally { fs.rmSync(temporary, { force: true }); }
|
|
100
|
+
}
|
|
101
|
+
installed.installTerminalLaunchers({ home, runtimeRoot: config.runtimeRoot, runtimeDigest: config.runtimeDigest,
|
|
102
|
+
nodeBinary: config.nodeBinary, realCodex: native, realClaude: config.realClaude, apply: true, manageZsh: false });
|
|
103
|
+
if (!regular(file).equals(proof.bytes)) throw Error('Codex wrapper restoration verification failed');
|
|
104
|
+
const after = JSON.parse(regular(configPath));
|
|
105
|
+
const expectedConfig = { ...config, realCodex: native };
|
|
106
|
+
const sameConfig = Object.keys(after).sort().join(',') === Object.keys(expectedConfig).sort().join(',')
|
|
107
|
+
&& Object.entries(expectedConfig).every(([key, value]) => after[key] === value);
|
|
108
|
+
if (!sameConfig || nativeOwner(after.realCodex, home) !== native) {
|
|
109
|
+
throw Error('Codex restored owner verification failed');
|
|
110
|
+
}
|
|
111
|
+
return native;
|
|
112
|
+
}
|
|
113
|
+
|
|
6
114
|
const HOME = os.homedir();
|
|
7
115
|
const read = p => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
8
116
|
function required(p) { fs.accessSync(p, fs.constants.X_OK); return p; }
|
|
@@ -66,14 +174,33 @@ export async function maintenance(config, run, dryRun, { home = HOME, brainHome,
|
|
|
66
174
|
const resolved = fs.realpathSync(file);
|
|
67
175
|
if (resolved.split(path.sep).includes('node_modules')) { result.exclusions.push(`npm owns native alias: ${file}`); continue; }
|
|
68
176
|
required(file);
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
177
|
+
const launcher = inspectCodexLauncher(file, home);
|
|
178
|
+
const command = launcher?.native || file;
|
|
179
|
+
const owner = launcher?.native || fs.realpathSync(file), stage = { owner: file, originalTarget: owner, state: 'running', commands: [] }; result.stages.push(stage);
|
|
180
|
+
if (launcher) stage.launcherProof = { kind: 'brain-terminal-wrapper', runtimeDigest: launcher.config.runtimeDigest, wrapperSha256: sha(launcher.bytes) };
|
|
181
|
+
const before = commandOutput(run, command, ['--version'], { timeout: 30_000 }, stage); stage.before = before;
|
|
73
182
|
const version = text => text.match(/\b(\d+\.\d+\.\d+(?:-[\w.-]+)?)/)?.[1];
|
|
74
|
-
|
|
183
|
+
let after, afterOwner, versionVerified, restorationAttempted = false;
|
|
184
|
+
try {
|
|
185
|
+
if (!dryRun) commandOutput(run, command, args, { timeout: 900_000 }, stage);
|
|
186
|
+
const afterCommand = launcher && !dryRun ? updatedCodexNative(launcher) : command;
|
|
187
|
+
after = commandOutput(run, afterCommand, ['--version'], { timeout: 30_000 }, stage);
|
|
188
|
+
versionVerified = !!version(before) && !!version(after) && compare(version(after), version(before)) >= 0;
|
|
189
|
+
if (launcher && !dryRun) {
|
|
190
|
+
restorationAttempted = true;
|
|
191
|
+
if (!versionVerified) stage.rejectedNativeTarget = afterCommand;
|
|
192
|
+
afterOwner = await restoreCodexLauncher(launcher, { retainOriginalNative: !versionVerified });
|
|
193
|
+
}
|
|
194
|
+
else afterOwner = launcher?.native || fs.realpathSync(file);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
stage.state = 'failed';
|
|
197
|
+
if (launcher && !dryRun && !restorationAttempted) {
|
|
198
|
+
try { stage.restoredTarget = await restoreCodexLauncher(launcher, { retainOriginalNative: true }); }
|
|
199
|
+
catch (restoreError) { stage.restorationError = restoreError.message; }
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
75
203
|
// Native launchers may re-point to a newer version, but stay in the same established root.
|
|
76
|
-
const afterOwner = fs.realpathSync(file);
|
|
77
204
|
const ownerPreserved = nativeOwnerPreserved(file, owner, afterOwner, home);
|
|
78
205
|
settleStage(stage, after, { versionVerified, ownerPreserved }, { currentTarget: afterOwner });
|
|
79
206
|
}
|
|
@@ -310,7 +310,7 @@ function knowledge(run, dryRun, runId, { brainHome, root, node }) {
|
|
|
310
310
|
}
|
|
311
311
|
export function developerUpdatePaths({ home = os.homedir(), brainHome = process.env.RUVNET_BRAIN_HOME || path.join(home, '.cache/ruvnet-brain') } = {}) {
|
|
312
312
|
return { home, brainHome, config: path.join(brainHome, 'developer-update-config.json'),
|
|
313
|
-
receipt: path.join(brainHome, 'nightly-suite-update.json'), lock: path.join(brainHome, 'developer-update.lock') };
|
|
313
|
+
receipt: path.join(brainHome, 'nightly-suite-update.json'), scheduledReceipt: path.join(brainHome, 'scheduler/last-suite-attempt.json'), lock: path.join(brainHome, 'developer-update.lock') };
|
|
314
314
|
}
|
|
315
315
|
export function validateDeveloperUpdateConfig(value = {}) {
|
|
316
316
|
const defaults = { schemaVersion: 1, channel: 'latest', scope: 'ruvnet', homebrew: false, uv: false, cargo: false, native: false, managedCallback: null, cleanup: false, preservePackages: [] };
|
|
@@ -371,12 +371,42 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
371
371
|
run.receipt = (command, args, options = {}) => dispatch(command, args, { ...options, capture: true });
|
|
372
372
|
const locate = name => locateExecutable(name, { env: childEnv });
|
|
373
373
|
const receipt = { schemaVersion: 1, kind: 'nightly-suite-update', runId: crypto.randomUUID(), pid: process.pid,
|
|
374
|
-
startedAt: new Date().toISOString(), mode, state: 'running', ok: false, config, steps: [], notes: [],
|
|
374
|
+
startedAt: new Date().toISOString(), mode, scheduled: mode === 'apply' && env.RUVNET_NIGHTLY === '1' && !!env.RUVNET_NIGHTLY_IDENTITY, state: 'running', ok: false, config, steps: [], notes: [],
|
|
375
375
|
sourceSha256: null, ownerToken: lock.token, ownerPid: lock.ownerPid, schedulerIdentity: env.RUVNET_NIGHTLY_IDENTITY || null };
|
|
376
|
+
const scheduledAttempt = value => value?.mode === 'apply' && value.scheduled !== false && !!value.schedulerIdentity;
|
|
377
|
+
const readAttempt = file => {
|
|
378
|
+
try {
|
|
379
|
+
const stat = fs.lstatSync(file);
|
|
380
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Error('scheduled receipt must be a regular file');
|
|
381
|
+
return read(file);
|
|
382
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
383
|
+
};
|
|
384
|
+
const persistReceipt = () => {
|
|
385
|
+
// Write Activity first: if the second write fails, health still sees this newer failed attempt.
|
|
386
|
+
atomic(paths.receipt, receipt);
|
|
387
|
+
if (receipt.scheduled) {
|
|
388
|
+
try { atomic(paths.scheduledReceipt, receipt); }
|
|
389
|
+
catch (error) {
|
|
390
|
+
receipt.state = 'failed'; receipt.ok = false; receipt.error = `Scheduled receipt persistence failed: ${error.message}`;
|
|
391
|
+
atomic(paths.receipt, receipt);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
let latestMayBeReplaced = receipt.scheduled;
|
|
376
397
|
try {
|
|
398
|
+
if (!receipt.scheduled) {
|
|
399
|
+
const latest = readAttempt(paths.receipt), retained = readAttempt(paths.scheduledReceipt);
|
|
400
|
+
const latestTime = Date.parse(latest?.startedAt || latest?.finishedAt || ''), retainedTime = Date.parse(retained?.startedAt || retained?.finishedAt || '');
|
|
401
|
+
if (scheduledAttempt(latest) && (!retained || !Number.isFinite(latestTime) || !Number.isFinite(retainedTime) || latestTime >= retainedTime)) {
|
|
402
|
+
// Preserve real legacy/failed evidence before a check or manual run replaces Activity.
|
|
403
|
+
atomic(paths.scheduledReceipt, latest);
|
|
404
|
+
}
|
|
405
|
+
latestMayBeReplaced = true;
|
|
406
|
+
}
|
|
377
407
|
receipt.sourceSha256 = hash(fs.readFileSync(new URL(import.meta.url)));
|
|
378
408
|
receipt.sourceSnapshot = Object.fromEntries(EXECUTION_MODULES.map(name => [name, hash(fs.readFileSync(new URL(name, import.meta.url)))]));
|
|
379
|
-
|
|
409
|
+
persistReceipt();
|
|
380
410
|
const npm = locate('npm');
|
|
381
411
|
if (!npm) throw Error('existing npm owner absent; fresh install refused');
|
|
382
412
|
const prefix = run(npm, ['prefix', '-g'], { timeout: 30_000 }).trim();
|
|
@@ -386,16 +416,16 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
386
416
|
childEnv.npm_config_prefix = prefix;
|
|
387
417
|
receipt.npmIdentity = { npm, prefix, root, node: process.execPath };
|
|
388
418
|
receipt.before = discover(root, prefix, config.scope);
|
|
389
|
-
|
|
419
|
+
persistReceipt();
|
|
390
420
|
for (const before of receipt.before) {
|
|
391
421
|
const tags = before.localSource ? {} : JSON.parse(run(npm, ['view', before.name, 'dist-tags', '--json'], { timeout: 90_000 }));
|
|
392
422
|
receipt.steps.push(upgradePackage(before, tags, REVIEWED_INSTALL_SCRIPTS, { run, dryRun: mode === 'check', channel: config.channel, npm, preservePackages: config.preservePackages }));
|
|
393
|
-
|
|
423
|
+
persistReceipt();
|
|
394
424
|
}
|
|
395
425
|
receipt.plugins = await synchronizePlugins(run, mode === 'check', receipt.notes, { home, prefix, scope: config.scope, locate, unattended: !!env.RUVNET_NIGHTLY_IDENTITY });
|
|
396
426
|
if (exists(path.join(paths.brainHome, 'kb/forge-update.mjs'))) receipt.knowledge = knowledge(run, mode === 'check', receipt.runId, { brainHome: paths.brainHome, root, node: process.execPath });
|
|
397
427
|
else receipt.notes.push('Brain corpus absent; fresh knowledge install excluded');
|
|
398
|
-
receipt.maintenance = await maintenance(config, run, mode === 'check', { home, brainHome: paths.brainHome, locate, node: process.execPath, progress: result => { receipt.maintenance = result;
|
|
428
|
+
receipt.maintenance = await maintenance(config, run, mode === 'check', { home, brainHome: paths.brainHome, locate, node: process.execPath, progress: result => { receipt.maintenance = result; persistReceipt(); } });
|
|
399
429
|
receipt.cleanup = cleanupNpxDuplicates({ home, globalRoot: root, enabled: config.cleanup && mode === 'apply', run });
|
|
400
430
|
receipt.after = discover(root, prefix, config.scope);
|
|
401
431
|
receipt.state = mode === 'check' ? 'checked' : 'completed'; receipt.ok = true;
|
|
@@ -412,7 +442,7 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
412
442
|
throw error;
|
|
413
443
|
} finally {
|
|
414
444
|
receipt.finishedAt = new Date().toISOString();
|
|
415
|
-
try {
|
|
445
|
+
try { if (latestMayBeReplaced) persistReceipt(); } finally { lock.release(); }
|
|
416
446
|
}
|
|
417
447
|
}
|
|
418
448
|
export async function developerUpdateCli(args = process.argv.slice(2)) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Bounded, mechanical Codex JSONL normalization. No model inference or user-text persistence. */
|
|
2
|
+
const LIMIT = 2 * 1024 * 1024;
|
|
3
|
+
const text = content => typeof content === 'string' ? content : Array.isArray(content)
|
|
4
|
+
? content.filter(part => ['text', 'input_text', 'output_text'].includes(part?.type)).map(part => part.text || '').join('\n') : '';
|
|
5
|
+
const message = (role, content) => ({ message: { role, content } });
|
|
6
|
+
const parse = value => { try { return JSON.parse(value); } catch { return null; } };
|
|
7
|
+
|
|
8
|
+
/** Keep call IDs intact; torn/unknown records cannot manufacture successful tool evidence. */
|
|
9
|
+
export function nativeTurnLines(lines, host) {
|
|
10
|
+
if (host !== 'codex') return lines;
|
|
11
|
+
const records = []; let bytes = 0;
|
|
12
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
13
|
+
bytes += Buffer.byteLength(String(lines[i]));
|
|
14
|
+
if (bytes > LIMIT) break;
|
|
15
|
+
const record = parse(lines[i]); if (record) records.push(record);
|
|
16
|
+
}
|
|
17
|
+
records.reverse();
|
|
18
|
+
const out = []; const sessions = new Map(); const calls = new Map();
|
|
19
|
+
for (const record of records) {
|
|
20
|
+
const p = record.payload;
|
|
21
|
+
if (record.type === 'event_msg' && p?.type === 'user_message') {
|
|
22
|
+
out.push(message('user', p.message || '')); continue;
|
|
23
|
+
}
|
|
24
|
+
if (record.type === 'event_msg' && p?.type === 'agent_message' && typeof p.message === 'string') {
|
|
25
|
+
out.push(message('assistant', p.message)); continue;
|
|
26
|
+
}
|
|
27
|
+
if (record.type !== 'response_item' || !p) continue;
|
|
28
|
+
if (p.type === 'message' && ['user', 'assistant'].includes(p.role)) {
|
|
29
|
+
out.push(message(p.role, text(p.content))); continue;
|
|
30
|
+
}
|
|
31
|
+
if (['function_call', 'custom_tool_call'].includes(p.type) && typeof p.call_id === 'string') {
|
|
32
|
+
const args = typeof p.arguments === 'string' ? parse(p.arguments) : p.arguments;
|
|
33
|
+
let name = String(p.name || '').split('.').at(-1); let input = args || {};
|
|
34
|
+
if (name === 'exec_command' && typeof args?.cmd === 'string') { name = 'Bash'; input = { command: args.cmd }; }
|
|
35
|
+
if (name === 'apply_patch') input = { input: p.input || '' };
|
|
36
|
+
if (name === 'write_stdin' && sessions.has(args?.session_id)) {
|
|
37
|
+
name = 'Bash'; input = { command: sessions.get(args.session_id) };
|
|
38
|
+
}
|
|
39
|
+
calls.set(p.call_id, { name, input });
|
|
40
|
+
out.push(message('assistant', [{ type: 'tool_use', id: p.call_id, name, input }]));
|
|
41
|
+
} else if (['function_call_output', 'custom_tool_call_output'].includes(p.type) && typeof p.call_id === 'string') {
|
|
42
|
+
const result = typeof p.output === 'string' ? parse(p.output) : p.output;
|
|
43
|
+
const call = calls.get(p.call_id);
|
|
44
|
+
if (Number.isSafeInteger(result?.session_id) && call?.name === 'Bash') sessions.set(result.session_id, call.input.command);
|
|
45
|
+
// Native structured exit fields are retained; opaque text is never guessed to be a pass.
|
|
46
|
+
out.push(message('user', [{ ...(result && typeof result === 'object' ? result : {}),
|
|
47
|
+
type: 'tool_result', tool_use_id: p.call_id, content: typeof p.output === 'string' ? p.output : JSON.stringify(p.output ?? '') }]));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out.map(record => JSON.stringify(record));
|
|
51
|
+
}
|
|
@@ -252,24 +252,45 @@ export function resolveNightlyProofBundle({ brainHome, env = process.env } = {})
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export function developerRunHealth({ brainHome, registration, now = Date.now(), maxAgeHours = 30 }) {
|
|
255
|
-
const
|
|
256
|
-
|
|
255
|
+
const scheduled = value => value?.mode === 'apply' && value.scheduled !== false && !!value.schedulerIdentity;
|
|
256
|
+
const readReceipt = file => {
|
|
257
|
+
try {
|
|
258
|
+
const stat = fs.lstatSync(file);
|
|
259
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Error('receipt is not a regular file');
|
|
260
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
261
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
262
|
+
};
|
|
257
263
|
let receipt;
|
|
258
|
-
try {
|
|
259
|
-
|
|
264
|
+
try {
|
|
265
|
+
const retained = readReceipt(path.join(brainHome, 'scheduler/last-suite-attempt.json'));
|
|
266
|
+
const latest = readReceipt(path.join(brainHome, 'nightly-suite-update.json'));
|
|
267
|
+
if (retained && !scheduled(retained)) throw Error('retained receipt is not a scheduled apply attempt');
|
|
268
|
+
receipt = retained || latest;
|
|
269
|
+
if (scheduled(latest)) {
|
|
270
|
+
const latestTime = Date.parse(latest.startedAt || latest.finishedAt || '');
|
|
271
|
+
const retainedTime = Date.parse(retained?.startedAt || retained?.finishedAt || '');
|
|
272
|
+
if (!retained || !Number.isFinite(latestTime) || !Number.isFinite(retainedTime) || latestTime >= retainedTime) receipt = latest;
|
|
273
|
+
}
|
|
274
|
+
} catch (error) { return { state: 'failed', evidence: `Coordinated update receipt unreadable or refused: ${error.message}`, receipt: null }; }
|
|
275
|
+
if (!receipt) return { state: 'never-ran', evidence: 'No coordinated update receipt exists yet.', receipt: null };
|
|
260
276
|
const validDigest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
261
277
|
const bound = validDigest(receipt.sourceSha256) && receipt.kind === 'nightly-suite-update' && receipt.schemaVersion === 1
|
|
262
278
|
&& receipt.sourceSha256 === registration.updateModules?.['developer-update.mjs']?.sha256
|
|
279
|
+
&& Object.entries(receipt.sourceSnapshot || {}).every(([name, digest]) => validDigest(digest) && digest === registration.updateModules?.[name]?.sha256)
|
|
263
280
|
&& EXECUTION_MODULES.filter(name => name !== 'plugin-artifact-proof.mjs' || registration.updateModules?.[name]).every(name => validDigest(receipt.sourceSnapshot?.[name]) && receipt.sourceSnapshot[name] === registration.updateModules?.[name]?.sha256);
|
|
264
281
|
const ageHours = (now - Date.parse(receipt.finishedAt || receipt.startedAt || '')) / 3_600_000;
|
|
265
|
-
|
|
282
|
+
const times = [receipt.startedAt, receipt.finishedAt].filter(value => value !== undefined).map(value => Date.parse(value));
|
|
283
|
+
const timestampsValid = times.length > 0 && times.every(value => Number.isFinite(value) && value <= now)
|
|
284
|
+
&& !(receipt.startedAt && receipt.finishedAt && Date.parse(receipt.finishedAt) < Date.parse(receipt.startedAt));
|
|
285
|
+
if (!bound) return { state: 'failed', evidence: 'Coordinated update receipt is not bound to registered source.', receipt };
|
|
286
|
+
if (!Number.isFinite(ageHours) || !timestampsValid) return { state: 'failed', evidence: 'Coordinated update receipt timestamps are invalid or future-dated.', receipt };
|
|
287
|
+
if (!scheduled(receipt) || receipt.schedulerIdentity !== registration.identity) return { state: 'never-ran', ageHours, receipt, evidence: 'No scheduled apply attempt is bound to the current registration.' };
|
|
266
288
|
if (receipt.state === 'running') {
|
|
267
289
|
const lock = sharedLockStatus({ brainHome });
|
|
268
290
|
const live = lock.state === 'running' && lock.owner.pid === (receipt.ownerPid || receipt.pid) && lock.owner.token === receipt.ownerToken;
|
|
269
291
|
return { state: live ? 'running' : 'failed', ageHours, receipt, evidence: live ? 'Coordinated updates have a live exact owner.' : 'Coordinated update owner is absent or unverified.' };
|
|
270
292
|
}
|
|
271
293
|
if (receipt.state === 'failed' || receipt.ok !== true) return { state: 'failed', ageHours, receipt, evidence: receipt.error || 'Coordinated updates failed.' };
|
|
272
|
-
if (receipt.mode !== 'apply' || receipt.schedulerIdentity !== registration.identity) return { state: 'never-ran', ageHours, receipt, evidence: 'Latest receipt is manual or a currency check; nightly execution remains unproven.' };
|
|
273
294
|
if (receipt.state !== 'completed') return { state: 'failed', ageHours, receipt, evidence: 'Coordinated update terminal state is unverified.' };
|
|
274
295
|
if (ageHours > maxAgeHours) return { state: 'stale', ageHours, receipt, evidence: `Last coordinated update is ${ageHours.toFixed(1)}h old.` };
|
|
275
296
|
const verification = { ok: bound && receipt.mode === 'apply' && receipt.schedulerIdentity === registration.identity && receipt.state === 'completed' && receipt.ok === true && ageHours >= 0 && ageHours <= maxAgeHours };
|
|
@@ -318,6 +318,8 @@ export function runOutboxReplay({ projectDir, token = process.env.RUVNET_REPLAY_
|
|
|
318
318
|
const options = { rawInput: JSON.stringify(job.payload), host: job.host, env,
|
|
319
319
|
budgetMs: Math.max(0, deadlineAt - now()), deadlineAt, signal,
|
|
320
320
|
makeStoreFactory: () => makeStoreFactory(deadlineAt), now, ordered: held, writeMetadata: false,
|
|
321
|
+
// A queued job was admitted at its live boundary; replay must not re-run (or be suspended by) enrollment.
|
|
322
|
+
enrollMemory: () => ({ state: 'existing' }),
|
|
321
323
|
captureTurn: () => ({ recorded: false, skipped: 'detached replay' }),
|
|
322
324
|
captureEvents: () => ({ recorded: 0, skipped: 'detached replay' }) };
|
|
323
325
|
const result = job.payload?.normalizedTransition ? captureNormalized(job, options)
|