From 26de90c413ab325bc7a6f329fcc6d05521f65a5f Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:05:53 +0900 Subject: [PATCH] fix: surface status --wait timeouts instead of looking successful status --wait already set waitTimedOut on the JSON snapshot, but text output still rendered a normal running job and the process exited 0. Callers that poll with /codex:status --wait therefore treated a timed-out wait as a finished status check. Print the timeout and exit non-zero while keeping waitTimedOut in the JSON payload. --- plugins/codex/scripts/codex-companion.mjs | 12 ++++- plugins/codex/scripts/lib/render.mjs | 7 ++- tests/runtime.test.mjs | 59 ++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 83df468ad..7a34eafa5 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -895,7 +895,17 @@ async function handleStatus(argv) { pollIntervalMs: options["poll-interval-ms"] }) : buildSingleJobSnapshot(cwd, reference); - outputCommandResult(snapshot, renderJobStatusReport(snapshot.job), options.json); + if (snapshot.waitTimedOut) { + process.exitCode = 1; + } + outputCommandResult( + snapshot, + renderJobStatusReport(snapshot.job, { + waitTimedOut: snapshot.waitTimedOut, + timeoutMs: snapshot.timeoutMs + }), + options.json + ); return; } diff --git a/plugins/codex/scripts/lib/render.mjs b/plugins/codex/scripts/lib/render.mjs index 2ec185236..42b6d75c6 100644 --- a/plugins/codex/scripts/lib/render.mjs +++ b/plugins/codex/scripts/lib/render.mjs @@ -374,8 +374,13 @@ export function renderStatusReport(report) { return `${lines.join("\n").trimEnd()}\n`; } -export function renderJobStatusReport(job) { +export function renderJobStatusReport(job, options = {}) { const lines = ["# Codex Job Status", ""]; + if (options.waitTimedOut) { + const timeoutSeconds = Math.max(1, Math.round((Number(options.timeoutMs) || 0) / 1000)); + lines.push(`Timed out after ${timeoutSeconds}s while the job was still ${job.status || "active"}.`); + lines.push(""); + } pushJobDetails(lines, job, { showElapsed: job.status === "queued" || job.status === "running", showDuration: job.status !== "queued" && job.status !== "running", diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8f276835b..db156ad17 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -1345,13 +1345,70 @@ test("status --wait times out cleanly when a job is still active", () => { cwd: workspace }); - assert.equal(result.status, 0, result.stderr); + assert.equal(result.status > 0, true, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.job.id, "task-live"); assert.equal(payload.job.status, "running"); assert.equal(payload.waitTimedOut, true); }); +test("status --wait reports a timeout in text output when the job is still active", () => { + const workspace = makeTempDir(); + const stateDir = resolveStateDir(workspace); + const jobsDir = path.join(stateDir, "jobs"); + fs.mkdirSync(jobsDir, { recursive: true }); + + const logFile = path.join(jobsDir, "task-live.log"); + fs.writeFileSync(logFile, "[2026-03-18T15:30:00.000Z] Starting Codex Task.\n", "utf8"); + fs.writeFileSync( + path.join(jobsDir, "task-live.json"), + JSON.stringify( + { + id: "task-live", + status: "running", + title: "Codex Task", + logFile + }, + null, + 2 + ), + "utf8" + ); + + fs.writeFileSync( + path.join(stateDir, "state.json"), + `${JSON.stringify( + { + version: 1, + config: { stopReviewGate: false }, + jobs: [ + { + id: "task-live", + status: "running", + title: "Codex Task", + jobClass: "task", + summary: "Investigate flaky test", + logFile, + createdAt: "2026-03-18T15:30:00.000Z", + startedAt: "2026-03-18T15:30:01.000Z", + updatedAt: "2026-03-18T15:30:02.000Z" + } + ] + }, + null, + 2 + )}\n`, + "utf8" + ); + + const result = run("node", [SCRIPT, "status", "task-live", "--wait", "--timeout-ms", "25"], { + cwd: workspace + }); + + assert.match(result.stdout, /Timed out after \d+s while the job was still running/i); + assert.equal(result.status > 0, true); +}); + test("result returns the stored output for the latest finished job by default", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace);