From 3a096d7bedbf2ca62a8ca72f8495a2fbcf8e6ee8 Mon Sep 17 00:00:00 2001 From: Ray Orsini Date: Sun, 19 Apr 2026 13:54:56 -0400 Subject: [PATCH] feat: add --auto-poll default for task command Foreground task blocks the caller until Codex returns with no client-side timeout cap; in Claude Code a stuck job hangs the parent session indefinitely (observed 11+ min on a small plan review) and can exceed the Bash tool's 10-min ceiling. Adds --auto-poll mode that reuses existing background enqueue + status polling primitives. Default 5-minute cap (overridable via --auto-poll-timeout-ms). On completion: prints stored job result. On timeout: prints "still running, follow up via /codex:status " handoff so the parent can keep working. Updates codex-cli-runtime skill and codex-rescue agent docs: - bare task call now appends --auto-poll - --wait opts back to uncapped foreground (original behavior) - --background stays fire-and-forget --- plugins/codex/agents/codex-rescue.md | 5 +- plugins/codex/scripts/codex-companion.mjs | 52 ++++++++++++++++++- .../codex/skills/codex-cli-runtime/SKILL.md | 5 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md index 971bb29..024edeb 100644 --- a/plugins/codex/agents/codex-rescue.md +++ b/plugins/codex/agents/codex-rescue.md @@ -19,8 +19,9 @@ Selection guidance: 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. +- Default to `--auto-poll` on every `task` call. This runs the job in the background with a 5-minute polling cap so short rescues return inline and long ones surface a "still running" handoff (with the job id) instead of blocking the parent session indefinitely. +- If the user explicitly passes `--wait`, drop `--auto-poll` and run the foreground path (no cap). Use this when the user wants the call to block no matter how long Codex takes. +- If the user explicitly passes `--background`, drop `--auto-poll` and pass `--background` through. Use this when the user wants fire-and-forget enqueue and will follow up via `/codex:status` themselves. - 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 201d1c7..a82d23a 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -66,6 +66,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_TASK_AUTO_POLL_TIMEOUT_MS = 300000; 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."; @@ -703,8 +704,8 @@ async function handleReview(argv) { async function handleTask(argv) { const { options, positionals } = parseCommandInput(argv, { - valueOptions: ["model", "effort", "cwd", "prompt-file"], - booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"], + valueOptions: ["model", "effort", "cwd", "prompt-file", "auto-poll-timeout-ms"], + booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background", "auto-poll"], aliasMap: { m: "model" } @@ -746,6 +747,53 @@ async function handleTask(argv) { return; } + if (options["auto-poll"]) { + ensureCodexReady(cwd); + requireTaskRequest(prompt, resumeLast); + + const job = buildTaskJob(workspaceRoot, taskMetadata, write); + const request = buildTaskRequest({ + cwd, + model, + effort, + prompt, + write, + resumeLast, + jobId: job.id + }); + const { payload: queuedPayload } = enqueueBackgroundTask(cwd, job, request); + + const timeoutMs = Math.max( + 0, + Number(options["auto-poll-timeout-ms"]) || DEFAULT_TASK_AUTO_POLL_TIMEOUT_MS + ); + const startedAt = Date.now(); + const snapshot = await waitForSingleJobSnapshot(cwd, job.id, { timeoutMs }); + const elapsedSec = Math.round((Date.now() - startedAt) / 1000); + + if (snapshot.waitTimedOut) { + const stillRunning = `${queuedPayload.title} still running after ${elapsedSec}s — job ${job.id}. Check /codex:status ${job.id} or /codex:result ${job.id} once it finishes.\n`; + outputCommandResult( + { ...queuedPayload, waitTimedOut: true, elapsedSec, timeoutMs }, + renderQueuedTaskLaunch(queuedPayload) + stillRunning, + options.json + ); + return; + } + + const { workspaceRoot: resolvedWorkspaceRoot, job: resolvedJob } = resolveResultJob(cwd, job.id); + const storedJob = readStoredJob(resolvedWorkspaceRoot, resolvedJob.id); + outputCommandResult( + { job: resolvedJob, storedJob }, + renderStoredJobResult(resolvedJob, storedJob), + options.json + ); + if (resolvedJob.status === "failed" || resolvedJob.exitStatus === 1) { + process.exitCode = 1; + } + return; + } + const job = buildTaskJob(workspaceRoot, taskMetadata, write); await runForegroundCommand( job, diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md index 0e91bfb..d48d042 100644 --- a/plugins/codex/skills/codex-cli-runtime/SKILL.md +++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md @@ -25,7 +25,10 @@ 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. +- Default execution mode: append `--auto-poll` to every `task` call. This enqueues the job in the background and polls for completion with a 5-minute cap so short rescues stay synchronous and long ones return a "still running" handoff instead of hanging the parent session. +- If the forwarded request includes `--wait`, treat that as opt-out from auto-poll. Strip `--wait` from the task text and DO NOT add `--auto-poll` — let `task` run foreground with no client-side cap. +- If the forwarded request includes `--background`, treat that as fire-and-forget. Strip it from the task text, pass `--background` through to `task`, and DO NOT add `--auto-poll`. +- `--background` and `--wait` are mutually exclusive with `--auto-poll`. Never combine them. - 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`.