Skip to content

task: add --resume-thread <id> — jobs killed by a usage limit cannot be resumed from the plugin #700

Description

@RsMan-Dev

Summary

task can only resume the latest thread (--resume-last), and a background job killed by a usage limit is left as failed with no way to continue it from the plugin — even though /codex:result prints the thread's Codex session ID and the runtime already supports resumeThreadId.

Scenario (plugin 1.0.6, codex-cli 0.151.0, Linux)

  1. Launch five /codex:rescue --background audits in parallel (read-only, --effort high).
  2. The account's 5-hour window runs out mid-run. All five jobs end with:
    [codex] Codex error: You've hit your usage limit. ... try again at 3:40 PM.
    [codex] Turn failed.
    
    Each had already read a large part of the repo (the expensive part).
  3. After the reset, there is no plugin way to continue them: --resume-last only targets the newest thread, and /codex:result just prints Resume in Codex: codex resume <id> — i.e. it hands the id back and sends the user to the terminal.

Workaround that works today: codex exec --sandbox read-only resume <id> "<continue prompt>" per thread.

Proposed fix (small)

Add --resume-thread <id> to task, mutually exclusive with --resume-last / --fresh, and thread it through buildTaskRequestexecuteTaskRun so both the foreground and background paths honour it. Patch against 1.0.6 (companion script + the codex-cli-runtime skill so the rescue subagent knows the flag) is below — ~40 lines, no behaviour change for existing flags.

A natural follow-up: when a job fails on a usage-limit error (the message carries the reset time), keep the thread id in the job record and let /codex:status offer a one-liner to resume it after the reset, instead of marking it terminally failed.

Patch

--- a/scripts/codex-companion.mjs
+++ b/scripts/codex-companion.mjs
@@ -79,7 +79,7 @@
       "  node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
       "  node scripts/codex-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>]",
       "  node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
-      "  node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
+      "  node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--resume-thread <id>|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
       "  node scripts/codex-companion.mjs transfer [--source <claude-jsonl>] [--json]",
       "  node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
       "  node scripts/codex-companion.mjs result [job-id] [--json]",
@@ -464,11 +464,14 @@
 
   const taskMetadata = buildTaskRunMetadata({
     prompt: request.prompt,
-    resumeLast: request.resumeLast
+    resumeLast: request.resumeLast || Boolean(request.resumeThread)
   });
 
   let resumeThreadId = null;
-  if (request.resumeLast) {
+  if (request.resumeThread) {
+    // Explicit thread id (e.g. a job cut by a usage limit): skip the latest-thread lookup.
+    resumeThreadId = String(request.resumeThread).trim();
+  } else if (request.resumeLast) {
     const latestThread = await resolveLatestTrackedTaskThread(workspaceRoot, {
       excludeJobId: request.jobId
     });
@@ -479,7 +482,7 @@
   }
 
   if (!request.prompt && !resumeThreadId) {
-    throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
+    throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last / --resume-thread <id>.");
   }
 
   const result = await runAppServerTurn(workspaceRoot, {
@@ -601,7 +604,7 @@
   });
 }
 
-function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId }) {
+function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, resumeThread = null, jobId }) {
   return {
     cwd,
     model,
@@ -609,6 +612,7 @@
     prompt,
     write,
     resumeLast,
+    resumeThread,
     jobId
   };
 }
@@ -649,9 +653,9 @@
   return positionalPrompt || readStdinIfPiped();
 }
 
-function requireTaskRequest(prompt, resumeLast) {
-  if (!prompt && !resumeLast) {
-    throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last.");
+function requireTaskRequest(prompt, resumeLast, resumeThread = null) {
+  if (!prompt && !resumeLast && !resumeThread) {
+    throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last / --resume-thread <id>.");
   }
 }
 
@@ -761,7 +765,7 @@
 
 async function handleTask(argv) {
   const { options, positionals } = parseCommandInput(argv, {
-    valueOptions: ["model", "effort", "cwd", "prompt-file"],
+    valueOptions: ["model", "effort", "cwd", "prompt-file", "resume-thread"],
     booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
     aliasMap: {
       m: "model"
@@ -775,19 +779,23 @@
   const prompt = readTaskPrompt(cwd, options, positionals);
 
   const resumeLast = Boolean(options["resume-last"] || options.resume);
+  const resumeThread = options["resume-thread"] ? String(options["resume-thread"]).trim() : null;
   const fresh = Boolean(options.fresh);
-  if (resumeLast && fresh) {
-    throw new Error("Choose either --resume/--resume-last or --fresh.");
+  if ((resumeLast || resumeThread) && fresh) {
+    throw new Error("Choose either --resume/--resume-last/--resume-thread or --fresh.");
+  }
+  if (resumeLast && resumeThread) {
+    throw new Error("Choose either --resume-last or --resume-thread <id>, not both.");
   }
   const write = Boolean(options.write);
   const taskMetadata = buildTaskRunMetadata({
     prompt,
-    resumeLast
+    resumeLast: resumeLast || Boolean(resumeThread)
   });
 
   if (options.background) {
     ensureCodexAvailable(cwd);
-    requireTaskRequest(prompt, resumeLast);
+    requireTaskRequest(prompt, resumeLast, resumeThread);
 
     const job = buildTaskJob(workspaceRoot, taskMetadata, write);
     const request = buildTaskRequest({
@@ -797,6 +805,7 @@
       prompt,
       write,
       resumeLast,
+      resumeThread,
       jobId: job.id
     });
     const { payload } = enqueueBackgroundTask(cwd, job, request);
@@ -815,6 +824,7 @@
         prompt,
         write,
         resumeLast,
+        resumeThread,
         jobId: job.id,
         onProgress: progress
       }),
--- a/plugins/codex/skills/codex-cli-runtime/SKILL.md
+++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md
@@ -30,6 +30,7 @@
 - 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`.
 - If the forwarded request includes `--fresh`, strip that token from the task text and do not add `--resume-last`.
+- If the forwarded request includes `--resume-thread <id>` (a Codex session/thread UUID, e.g. from a `/codex:result` line "Codex session ID:" of a job that failed on a usage limit), strip both tokens from the task text and pass `--resume-thread <id>` to `task`. This resumes THAT thread with its context instead of the latest one.
 - `--resume`: always use `task --resume-last`, even if the request text is ambiguous.
 - `--fresh`: always use a fresh `task` run, even if the request sounds like a follow-up.
 - `--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions