From dcba830d0e725227271cfc268fc9775292ce27c9 Mon Sep 17 00:00:00 2001 From: JS van Dijk <267467744+hogeheer499-commits@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:01:52 +0200 Subject: [PATCH] fix: preserve long foreground task recovery --- plugins/codex/agents/codex-rescue.md | 2 + plugins/codex/scripts/codex-companion.mjs | 45 ++++++++++++++++++- .../codex/skills/codex-cli-runtime/SKILL.md | 2 + tests/runtime.test.mjs | 29 ++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md index 7009ec86..083b051a 100644 --- a/plugins/codex/agents/codex-rescue.md +++ b/plugins/codex/agents/codex-rescue.md @@ -22,6 +22,8 @@ Forwarding rules: - Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ...`. - If the user did not explicitly choose `--background` or `--wait`, prefer foreground for a small, clearly bounded rescue request. - If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution. +- Claude Code stops a foreground Bash call after 600 seconds. Prefer background execution whenever a task could plausibly run longer than roughly eight minutes. +- A foreground task that is still running after eight minutes prints a `codex resume ` recovery command before the host timeout. - You may use the `gpt-5-4-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it. - Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work beyond shaping the forwarded prompt text. - Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own. diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 83df468a..c2752449 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -68,6 +68,7 @@ const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url))); const REVIEW_SCHEMA = path.join(ROOT_DIR, "schemas", "review-output.schema.json"); const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000; const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000; +const DEFAULT_FOREGROUND_RECOVERY_HINT_MS = 8 * 60 * 1000; const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]); const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]); const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn."; @@ -660,7 +661,47 @@ async function runForegroundCommand(job, runner, options = {}) { logFile: options.logFile, stderr: !options.json }); - const execution = await runTrackedJob(job, () => runner(progress), { logFile }); + let recoveryTimer = null; + let recoveryThreadId = null; + const configuredDelay = Number(process.env.CODEX_COMPANION_FOREGROUND_RECOVERY_HINT_MS); + const recoveryHintDelayMs = + Number.isFinite(configuredDelay) && configuredDelay >= 0 + ? configuredDelay + : DEFAULT_FOREGROUND_RECOVERY_HINT_MS; + const progressWithRecovery = (event) => { + progress?.(event); + const threadId = + event && typeof event === "object" && typeof event.threadId === "string" + ? event.threadId + : null; + if ( + !options.recoveryHint || + options.json || + recoveryTimer || + !threadId || + threadId === recoveryThreadId + ) { + return; + } + recoveryThreadId = threadId; + recoveryTimer = setTimeout(() => { + fs.writeSync( + 1, + `Codex task is still running. If the host stops it, resume with: codex resume ${threadId}\n` + ); + recoveryTimer = null; + }, recoveryHintDelayMs); + recoveryTimer.unref?.(); + }; + + let execution; + try { + execution = await runTrackedJob(job, () => runner(progressWithRecovery), { logFile }); + } finally { + if (recoveryTimer) { + clearTimeout(recoveryTimer); + } + } outputResult(options.json ? execution.payload : execution.rendered, options.json); if (execution.exitStatus !== 0) { process.exitCode = execution.exitStatus; @@ -818,7 +859,7 @@ async function handleTask(argv) { jobId: job.id, onProgress: progress }), - { json: options.json } + { json: options.json, recoveryHint: true } ); } diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md index 0e91bfb5..50cc2956 100644 --- a/plugins/codex/skills/codex-cli-runtime/SKILL.md +++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md @@ -26,6 +26,8 @@ Execution rules: Command selection: - Use exactly one `task` invocation per rescue handoff. - If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text. +- Claude Code enforces a 600-second ceiling on foreground Bash calls. Route work that could plausibly exceed roughly eight minutes through background execution. +- If a foreground task reaches eight minutes, preserve the recovery line containing `codex resume ` in the returned output. - If the forwarded request includes `--model`, normalize `spark` to `gpt-5.3-codex-spark` and pass it through to `task`. - If the forwarded request includes `--effort`, pass it through to `task`. - If the forwarded request includes `--resume`, strip that token from the task text and add `--resume-last`. diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8f276835..f2e0b266 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -920,6 +920,35 @@ test("task using the shared broker still completes when Codex spawns subagents", assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); +test("long foreground tasks print a recovery command before completion", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir, "slow-task"); + 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", "investigate the long-running failure"], { + cwd: repo, + env: { + ...buildEnv(binDir), + CODEX_COMPANION_FOREGROUND_RECOVERY_HINT_MS: "25" + } + }); + + assert.equal(result.status, 0, result.stderr); + assert.match( + result.stdout, + /Codex task is still running\. If the host stops it, resume with: codex resume thr_1/ + ); + assert.match(result.stdout, /Handled the requested task/); + assert.ok( + result.stdout.indexOf("codex resume thr_1") < + result.stdout.indexOf("Handled the requested task") + ); +}); + test("task --background enqueues a detached worker and exposes per-job status", async () => { const repo = makeTempDir(); const binDir = makeTempDir();