From 282a70ccfeb022365663dde88e0d0c8d8a6aad34 Mon Sep 17 00:00:00 2001 From: Un-tiong Lim Date: Mon, 10 Aug 2026 17:55:52 +0800 Subject: [PATCH] suppress dynamic tool progress in stderr --- plugins/codex/scripts/lib/codex.mjs | 12 ++++++++---- plugins/codex/scripts/lib/tracked-jobs.mjs | 4 +++- tests/fake-codex-fixture.mjs | 16 ++++++++++++++++ tests/runtime.test.mjs | 22 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/plugins/codex/scripts/lib/codex.mjs b/plugins/codex/scripts/lib/codex.mjs index fead00cc4..9ba0113e9 100644 --- a/plugins/codex/scripts/lib/codex.mjs +++ b/plugins/codex/scripts/lib/codex.mjs @@ -252,7 +252,7 @@ function describeStartedItem(state, item) { case "mcpToolCall": return { message: `Calling ${item.server}/${item.tool}.`, phase: "investigating" }; case "dynamicToolCall": - return { message: `Running tool: ${item.tool}.`, phase: "investigating" }; + return { message: `Running tool: ${item.tool}.`, phase: "investigating", hideFromStderr: true }; case "collabAgentToolCall": { const subagents = (item.receiverThreadIds ?? []).map((threadId) => labelForThread(state, threadId) ?? threadId); const summary = @@ -283,7 +283,7 @@ function describeCompletedItem(state, item) { case "mcpToolCall": return { message: `Tool ${item.server}/${item.tool} ${item.status}.`, phase: "investigating" }; case "dynamicToolCall": - return { message: `Tool ${item.tool} ${item.status}.`, phase: "investigating" }; + return { message: `Tool ${item.tool} ${item.status}.`, phase: "investigating", hideFromStderr: true }; case "collabAgentToolCall": { const subagents = (item.receiverThreadIds ?? []).map((threadId) => labelForThread(state, threadId) ?? threadId); const summary = @@ -524,14 +524,18 @@ function applyTurnNotification(state, message) { recordItem(state, message.params.item, "started", message.params.threadId ?? null); { const update = describeStartedItem(state, message.params.item); - emitProgress(state.onProgress, update?.message, update?.phase ?? null); + emitProgress(state.onProgress, update?.message, update?.phase ?? null, { + hideFromStderr: update?.hideFromStderr === true + }); } break; case "item/completed": recordItem(state, message.params.item, "completed", message.params.threadId ?? null); { const update = describeCompletedItem(state, message.params.item); - emitProgress(state.onProgress, update?.message, update?.phase ?? null); + emitProgress(state.onProgress, update?.message, update?.phase ?? null, { + hideFromStderr: update?.hideFromStderr === true + }); } break; case "error": diff --git a/plugins/codex/scripts/lib/tracked-jobs.mjs b/plugins/codex/scripts/lib/tracked-jobs.mjs index 902869012..c2dc69ef8 100644 --- a/plugins/codex/scripts/lib/tracked-jobs.mjs +++ b/plugins/codex/scripts/lib/tracked-jobs.mjs @@ -16,6 +16,7 @@ function normalizeProgressEvent(value) { phase: typeof value.phase === "string" && value.phase.trim() ? value.phase.trim() : null, threadId: typeof value.threadId === "string" && value.threadId.trim() ? value.threadId.trim() : null, turnId: typeof value.turnId === "string" && value.turnId.trim() ? value.turnId.trim() : null, + hideFromStderr: value.hideFromStderr === true, stderrMessage: value.stderrMessage == null ? null : String(value.stderrMessage).trim(), logTitle: typeof value.logTitle === "string" && value.logTitle.trim() ? value.logTitle.trim() : null, logBody: value.logBody == null ? null : String(value.logBody).trimEnd() @@ -27,6 +28,7 @@ function normalizeProgressEvent(value) { phase: null, threadId: null, turnId: null, + hideFromStderr: false, stderrMessage: String(value ?? "").trim(), logTitle: null, logBody: null @@ -122,7 +124,7 @@ export function createProgressReporter({ stderr = false, logFile = null, onEvent return (eventOrMessage) => { const event = normalizeProgressEvent(eventOrMessage); const stderrMessage = event.stderrMessage ?? event.message; - if (stderr && stderrMessage) { + if (stderr && !event.hideFromStderr && stderrMessage) { process.stderr.write(`[codex] ${stderrMessage}\n`); } appendLogLine(logFile, event.message); diff --git a/tests/fake-codex-fixture.mjs b/tests/fake-codex-fixture.mjs index f83c96a0d..a2a8b5fbc 100644 --- a/tests/fake-codex-fixture.mjs +++ b/tests/fake-codex-fixture.mjs @@ -568,6 +568,22 @@ rl.on("line", (line) => { } const items = [ + ...(BEHAVIOR === "noisy-tool-progress" + ? Array.from({ length: 100 }, (_, index) => ({ + started: { + type: "dynamicToolCall", + id: "tool_started_" + turnId + "_" + index, + tool: index % 2 === 0 ? "Read" : "Bash", + status: "inProgress" + }, + completed: { + type: "dynamicToolCall", + id: "tool_completed_" + turnId + "_" + index, + tool: index % 2 === 0 ? "Read" : "Bash", + status: "completed" + } + })) + : []), ...(BEHAVIOR === "with-reasoning" ? [ { diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8f276835b..8e9f25493 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -175,6 +175,28 @@ test("task runs when the active provider does not require OpenAI login", () => { assert.match(result.stdout, /Handled the requested task/); }); +test("task keeps high-frequency dynamic tool progress out of foreground stderr and in the job log", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir, "noisy-tool-progress"); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "exercise progress reporting"], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + assert.doesNotMatch(result.stderr, /Running tool:|Tool (Read|Bash) completed/); + const state = JSON.parse(fs.readFileSync(path.join(resolveStateDir(repo), "state.json"), "utf8")); + const log = fs.readFileSync(state.jobs[0].logFile, "utf8"); + assert.equal((log.match(/Running tool:/g) ?? []).length, 100); + assert.equal((log.match(/Tool (Read|Bash) completed/g) ?? []).length, 100); +}); + test("task runs without auth preflight so Codex can refresh an expired session", () => { const repo = makeTempDir(); const binDir = makeTempDir();