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
@@ -362,4 +362,83 @@ describe("executePrompt", () => {
});
assert.strictEqual(debugInfos[0].noisySuccess, false);
});
it("filters codex ReasoningSummary noise from stderr on exit code 0", async () => {
const noisyStderr = [
'2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryPartAdded without active item',
'2026-05-20T18:33:03.281713Z ERROR codex_core::util: ReasoningSummaryDelta without active item',
'2026-05-20T18:33:03.348247Z ERROR codex_core::util: ReasoningSummaryDelta without active item',
].join('\n');
const scenarios = new Map<string, MockScenario>([
["codex exec --yolo hello", { stdout: "Hello world!", stderr: noisyStderr, exitCode: 0 }],
]);
const result = await executePrompt("codex", "hello", {
spawn: mockSpawn(scenarios),
existsSync: () => true,
});
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stderr, "");
assert.strictEqual(result.stdout, "Hello world!");
});
it("preserves real error stderr from codex on non-zero exit code", async () => {
const noisyStderr = [
'2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item',
'Error: something actually went wrong',
].join('\n');
const scenarios = new Map<string, MockScenario>([
["codex exec --yolo fail", { stdout: "", stderr: noisyStderr, exitCode: 1 }],
]);
const result = await executePrompt("codex", "fail", {
spawn: mockSpawn(scenarios),
existsSync: () => true,
});
assert.strictEqual(result.exitCode, 1);
assert.ok(result.stderr.includes("ReasoningSummaryDelta"));
assert.ok(result.stderr.includes("something actually went wrong"));
});
it("provides rawStderr in debug info when noise is filtered", async () => {
const noisyStderr = '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item\n';
const scenarios = new Map<string, MockScenario>([
["codex exec --yolo hello", { stdout: "ok", stderr: noisyStderr, exitCode: 0 }],
]);
const debugInfos: any[] = [];
const result = await executePrompt("codex", "hello", {
spawn: mockSpawn(scenarios),
existsSync: () => true,
debug: true,
onDebug: (info) => debugInfos.push(info),
});
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stderr, "");
assert.strictEqual(debugInfos[0].rawStderr, noisyStderr);
});
it("does not set rawStderr when no noise filtering occurred", async () => {
const scenarios = new Map<string, MockScenario>([
["codex exec --yolo hello", { stdout: "ok", stderr: "", exitCode: 0 }],
]);
const debugInfos: any[] = [];
await executePrompt("codex", "hello", {
spawn: mockSpawn(scenarios),
existsSync: () => true,
debug: true,
onDebug: (info) => debugInfos.push(info),
});
assert.strictEqual(debugInfos[0].rawStderr, undefined);
});
it("does not filter stderr for non-codex clients", async () => {
const noisyStderr = '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item\n';
const scenarios = new Map<string, MockScenario>([
["claude -p hello --dangerously-skip-permissions", { stdout: "ok", stderr: noisyStderr, exitCode: 0 }],
]);
const result = await executePrompt("claude", "hello", {
spawn: mockSpawn(scenarios),
existsSync: () => true,
});
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stderr, noisyStderr);
});
});