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.
@@ -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 { /* no outbox yet */ }
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', '--provenance', 'system_observation', '--path', db]);
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: `ruflo-cli (${fast.reason})` };
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; const result = store({ ruflo, db: journal.db, key: rec.key, value });
418
- const back = readBack({ ruflo, db: journal.db, key: rec.key });
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: result.status !== 0 || content !== value, ...(attempts > 1 ? { attempts, lastError: last?.reason } : {}) }]);
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 (event === 'Stop') {
522
+ if (['Stop', 'SessionEnd', 'PreCompact', 'SubagentStop'].includes(event)) {
517
523
  let lines = null;
518
- if (host === 'claude' && typeof payload.transcript_path === 'string' && payload.transcript_path) {
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 owner = fs.realpathSync(file), stage = { owner: file, originalTarget: owner, state: 'running', commands: [] }; result.stages.push(stage);
70
- const before = commandOutput(run, file, ['--version'], { timeout: 30_000 }, stage); stage.before = before;
71
- if (!dryRun) commandOutput(run, file, args, { timeout: 900_000 }, stage);
72
- const after = commandOutput(run, file, ['--version'], { timeout: 30_000 }, stage);
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
- const versionVerified = !!version(before) && !!version(after) && compare(version(after), version(before)) >= 0;
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
- atomic(paths.receipt, receipt);
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
- atomic(paths.receipt, receipt);
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
- atomic(paths.receipt, receipt);
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; atomic(paths.receipt, receipt); } });
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 { atomic(paths.receipt, receipt); } finally { lock.release(); }
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 file = path.join(brainHome, 'nightly-suite-update.json');
256
- if (!fs.existsSync(file)) return { state: 'never-ran', evidence: 'No coordinated update receipt exists yet.', receipt: null };
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 { receipt = JSON.parse(fs.readFileSync(file, 'utf8')); }
259
- catch (error) { return { state: 'failed', evidence: `Coordinated update receipt unreadable: ${error.message}`, receipt: null }; }
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
- if (!bound || !Number.isFinite(ageHours)) return { state: 'failed', evidence: 'Coordinated update receipt is not bound to registered source.', receipt };
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)