Files
rea/scripts/lib/agent-evaluation-commands.mjs
gensokyo-dev 8716b52411 fix(evaluation): prepare and retain native ownership helpers (#5615)
Prepare native process ownership outside short command deadlines. Reuse the shared cleanup adapter and resource scope for direct/nested preparation failures and command cleanup.

Fixes #5094.
2026-10-20 16:29:46 +00:00

192 lines
5.6 KiB
JavaScript

import { randomUUID } from "node:crypto";
import { createInterface } from "node:readline";
import { PassThrough, Writable } from "node:stream";
import {
OwnedCommandFailure,
prepareOwnedCommand,
runOwnedCommand,
} from "../../dist/process/OwnedCommand.js";
import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../../dist/artifacts/AnalysisCleanupLease.js";
import { retainOwnedCommandCleanup } from "../../dist/providers/OwnedCommandCleanup.js";
/** Keep command ownership through failure and join cleanup before removing a run. */
export function createAgentEvaluationCommands({
codex,
evaluationRoot,
evaluationEnvironment,
optionalModel,
timeoutMs,
repositoryRoot,
}) {
const resources = new ArtifactResourceScope();
const resourceOwner = {
retain(owner, resource) {
resources.retain({
kind: "lease",
resource,
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
});
},
run: (work) => resources.run(work),
};
const diagnosticBytes = 32_768;
async function execute(
command,
arguments_,
cwd,
env,
limits,
timeoutMessage,
) {
return resourceOwner.run(async () => {
try {
// Fresh Darwin helper compilation has its own bounded deadline.
await prepareOwnedCommand();
return await runOwnedCommand(
{
command,
arguments: arguments_,
// CLI shims may execute through a shebang interpreter. Ownership
// still uses the launch generation and token, not an argv prefix.
expectedCommand: null,
cwd,
hostEnvironment: env,
runId: randomUUID(),
sweepTokenOwnedProcesses: true,
},
{ diagnosticBytes, acceptNonZeroExit: true, ...limits },
);
} catch (cause) {
retainOwnedCommandCleanup(
cause,
resourceOwner,
"agent-evaluation",
command,
);
if (cause instanceof OwnedCommandFailure && cause.reason === "timeout")
throw new Error(timeoutMessage, { cause });
throw cause;
}
});
}
return {
async runCodex(prompt, approvedTools) {
const arguments_ = [
"--no-daemon",
"exec",
"--ephemeral",
"--skip-git-repo-check",
"--sandbox",
"read-only",
"--json",
"--color",
"never",
"-C",
evaluationRoot,
"-c",
'approval_policy="never"',
...approvedTools.flatMap((name) => [
"-c",
`mcp_servers.rea.tools.${name}.approval_mode="approve"`,
]),
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
prompt,
];
const events = [];
const nonJson = [];
let nonJsonBytes = 0;
const stdout = new PassThrough();
const lines = createInterface({ input: stdout });
const linesClosed = new Promise((resolve) =>
lines.once("close", resolve),
);
let lineError;
lines.on("error", (cause) => {
// The owned pipeline reports stream failure or cancellation. Join this
// consumer too, without turning its forwarded error into a process crash.
lineError = cause;
lines.close();
});
lines.on("line", (line) => {
try {
events.push(JSON.parse(line));
} catch {
if (nonJsonBytes >= diagnosticBytes) return;
const bytes = Buffer.from(
`non-json stdout: ${line.slice(0, diagnosticBytes - nonJsonBytes)}`,
);
const retained = bytes.subarray(0, diagnosticBytes - nonJsonBytes);
if (retained.length > 0) nonJson.push(retained);
nonJsonBytes += retained.length;
}
});
try {
const result = await execute(
codex,
arguments_,
evaluationRoot,
evaluationEnvironment,
{ timeoutMs, stdout },
`Codex evaluation timed out after ${String(timeoutMs)}ms`,
);
await linesClosed;
if (lineError !== undefined) throw lineError;
return {
events,
exitCode: result.exitCode ?? 1,
stderr: Buffer.concat([...nonJson, Buffer.from(result.stderr.text)])
.subarray(0, diagnosticBytes)
.toString("utf8"),
};
} finally {
stdout.destroy();
lines.close();
await linesClosed;
}
},
async runProcess(command, arguments_, cwd, timeout, env = process.env) {
const result = await execute(
command,
arguments_,
cwd,
env,
{
timeoutMs: timeout,
stdout: new Writable({ write: (_chunk, _encoding, done) => done() }),
},
`${command} timed out`,
);
if (result.exitCode !== 0)
throw new Error(
`${command} failed with ${String(result.exitCode)}: ${result.stderr.text}`,
);
},
async readCommandVersion(command) {
const result = await execute(
command,
["--version"],
repositoryRoot,
process.env,
{ timeoutMs: 10_000, diagnosticBytes: 8192 },
`${command} --version timed out`,
);
if (result.exitCode !== 0)
throw new Error(
`${command} --version failed with ${String(result.exitCode)}: ${result.stderr.text}`,
);
const version = result.stdout.text.trim();
if (version.length === 0)
throw new Error(`${command} --version returned empty output`);
return version;
},
close: () => resources.close(),
};
}