diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 35ae8a4a92..1b9bc1db25 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -12,7 +12,10 @@ import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; -const MAX_BUFFER = 1024 * 100; // 100 KB +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; /** * This function should never return a rejected promise: errors should be @@ -143,37 +146,17 @@ export function exec( // resolve the promise and translate the failure into a regular // ExecResult object so the rest of the agent loop can carry on gracefully. - const stdoutChunks: Array = []; - const stderrChunks: Array = []; - let numStdoutBytes = 0; - let numStderrBytes = 0; - let hitMaxStdout = false; - let hitMaxStderr = false; + // Collect stdout and stderr up to configured limits + const stdoutCollector = createTruncatingCollector(); + const stderrCollector = createTruncatingCollector(); return new Promise((resolve) => { - child.stdout?.on("data", (data: Buffer) => { - if (!hitMaxStdout) { - numStdoutBytes += data.length; - if (numStdoutBytes <= MAX_BUFFER) { - stdoutChunks.push(data); - } else { - hitMaxStdout = true; - } - } - }); - child.stderr?.on("data", (data: Buffer) => { - if (!hitMaxStderr) { - numStderrBytes += data.length; - if (numStderrBytes <= MAX_BUFFER) { - stderrChunks.push(data); - } else { - hitMaxStderr = true; - } - } - }); + stdoutCollector.attach(child.stdout!); + stderrCollector.attach(child.stderr!); + child.on("exit", (code, signal) => { - const stdout = Buffer.concat(stdoutChunks).toString("utf8"); - const stderr = Buffer.concat(stderrChunks).toString("utf8"); + const stdout = stdoutCollector.getString(); + const stderr = stderrCollector.getString(); // Map (code, signal) to an exit code. We expect exactly one of the two // values to be non-null, but we code defensively to handle the case where @@ -192,19 +175,100 @@ export function exec( log( `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, ); - resolve({ + + const execResult = { stdout, stderr, exitCode, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); child.on("error", (err) => { - resolve({ + const execResult = { stdout: "", stderr: String(err), exitCode: 1, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); }); } + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +function createTruncatingCollector( + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + return { + attach(stream?: NodeJS.ReadableStream) { + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + totalBytes += data.length; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0x0a) { + totalLines++; + } + } + if (totalBytes <= byteLimit && totalLines <= lineLimit) { + chunks.push(data); + } else { + hitLimit = true; + } + }); + }, + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} + +/** + * Adds a truncation warnings to stdout and stderr, if appropriate. + */ +function addTruncationWarningsIfNecessary( + execResult: ExecResult, + hitMaxStdout: boolean, + hitMaxStderr: boolean, +): ExecResult { + if (!hitMaxStdout && !hitMaxStderr) { + return execResult; + } else { + const { stdout, stderr, exitCode } = execResult; + return { + stdout: hitMaxStdout + ? stdout + "\n\n[Output truncated: too many lines or bytes]" + : stdout, + stderr: hitMaxStderr + ? stderr + "\n\n[Output truncated: too many lines or bytes]" + : stderr, + exitCode, + }; + } +}