fix: filter codex ReasoningSummary stderr noise on exit code 0

Codex writes informational ERROR messages about ReasoningSummaryDelta
to stderr even on successful execution (exit code 0). The OpenClaw
agent misinterprets this non-empty stderr as a failure.

- Add filterStderrNoise() to strip known codex noise patterns from
  stderr when exit code is 0
- Preserve raw stderr in DebugInfo.rawStderr when --debug is active
- Add 5 new tests covering noise filtering, preservation on failure,
  debug raw output, and non-codex client passthrough
This commit is contained in:
2026-05-20 13:37:21 -05:00
parent edb6611b74
commit afac143cb3
3 changed files with 110 additions and 3 deletions
+29 -3
View File
@@ -9,6 +9,28 @@ export const CLIENT_ARGS: Record<ClientName, (prompt: string) => string[]> = {
opencode: (p) => ["run", "--dangerously-skip-permissions", p],
};
/**
* Known stderr noise patterns per client.
* When exit code is 0, lines matching these patterns are stripped from the
* returned stderr to prevent agents from misinterpreting informational
* diagnostics as errors. The raw (unfiltered) stderr is preserved in
* DebugInfo.rawStderr when --debug is active.
*/
const STDERR_NOISE_PATTERNS: Partial<Record<ClientName, RegExp[]>> = {
codex: [
/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+ERROR\s+codex_core::util:\s+ReasoningSummary\w*\s+/,
],
};
function filterStderrNoise(client: ClientName, stderr: string, exitCode: number): string {
if (exitCode !== 0) return stderr;
const patterns = STDERR_NOISE_PATTERNS[client];
if (!patterns) return stderr;
const lines = stderr.split("\n");
const filtered = lines.filter((line) => !patterns.some((p) => p.test(line)));
return filtered.join("\n").replace(/\n+$/, "");
}
export async function executePrompt(
client: ClientName,
prompt: string,
@@ -78,6 +100,8 @@ export async function executePrompt(
settled = true;
clearTimeout(timeout);
const durationMs = Date.now() - startMs;
const rawStderr = stderr;
const cleanedStderr = filterStderrNoise(client, rawStderr, result?.exitCode ?? -1);
if (options.debug || options.onDebug) {
const effectiveExitCode = result?.exitCode ?? (err instanceof ExecError ? err.result.exitCode : null);
const debugInfo: DebugInfo = {
@@ -87,20 +111,22 @@ export async function executePrompt(
exitCode: effectiveExitCode,
exitSignal,
durationMs,
stderrLength: stderr.length,
stderrLength: rawStderr.length,
stdoutLength: stdout.length,
noisySuccess: effectiveExitCode === 0 && stderr.length > 0,
noisySuccess: effectiveExitCode === 0 && rawStderr.length > 0,
rawStderr: rawStderr !== cleanedStderr ? rawStderr : undefined,
};
options.onDebug?.(debugInfo);
}
if (err) {
if (err instanceof ExecError) {
err.result.stderr = cleanedStderr;
err.result.client = client;
err.result.durationMs = durationMs;
}
reject(err);
} else {
resolve({ ...result!, client, durationMs });
resolve({ ...result!, client, durationMs, stderr: cleanedStderr });
}
}