Claude Code Codex plugin: fresh tasks fail with "no rollout found for thread id" — thread/name/set is called before the first turn

Open 💬 2 comments Opened Jul 5, 2026 by T2hhbmEK

Summary

The Claude Code Codex plugin (codex@openai-codex v1.0.5) fails every fresh delegated task with:

no rollout found for thread id <uuid>

Root cause is an ordering bug against current app-server semantics: the plugin calls thread/name/set immediately after thread/start, before the first turn runs. In codex-cli 0.142.5 a non-ephemeral thread's rollout file is created lazily at the first turn/start, not at thread/start. thread/name/set is a disk/thread-store path that requires the rollout to already exist, so it returns -32600 no rollout found for thread id …. The plugin's startThread() catch only swallows unknown variant / unknown method, so it rethrows this error and the whole task fails.

Because persistThread: true (which sets a thread name) is used for every task run, this is deterministic — the plugin's task path is effectively unusable on this CLI version, forcing users to fall back to codex exec.

Environment

  • codex-cli 0.142.5 (Homebrew cask), macOS (Darwin arm64)
  • Claude Code Codex plugin 1.0.5 (codex@openai-codex, gitCommitSha 80c31f9)
  • Transport: codex app-server over newline-delimited JSON-RPC 2.0

Where it happens (plugin source)

scripts/lib/codex.mjsstartThread():

async function startThread(client, cwd, options = {}) {
  const response = await client.request("thread/start", buildThreadParams(cwd, options));
  const threadId = response.thread.id;
  if (options.threadName) {
    try {
      await client.request("thread/name/set", { threadId, name: options.threadName });
    } catch (err) {
      // Only suppresses "unknown variant/method" from older CLIs.
      const msg = String(err?.message ?? err ?? "");
      if (!msg.includes("unknown variant") && !msg.includes("unknown method")) {
        throw err;  // <-- "no rollout found" is rethrown here → task fails
      }
    }
  }
  return response;
}
  • runAppServerTurn() starts a non-ephemeral thread for tasks (ephemeral: options.persistThread ? false : true) and always passes a threadName, then runs turn/start after startThread() returns.
  • executeTaskRun() (scripts/codex-companion.mjs) always calls with persistThread: true and a threadName, so thread/name/set is always attempted before any turn.

app-server side (matches source at tag rust-v0.142.5): a plain thread/start inserts the thread into the in-memory map but writes no rollout to $CODEX_HOME/sessions/; the rollout + state-db row are created when the first turn begins. thread/name/set resolves the rollout from disk (resolve_rollout_pathno rollout found when absent), unlike turn/start, which uses the in-memory handle (and would report thread not found on a miss, not no rollout found).

Minimal reproduction (no plugin needed)

// repro.mjs — run: node repro.mjs   (from any dir, with codex 0.142.5 on PATH)
import { spawn } from "node:child_process";
import readline from "node:readline";

const p = spawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
p.stdout.setEncoding("utf8");
const pending = new Map(); let id = 0;
readline.createInterface({ input: p.stdout }).on("line", (l) => {
  if (!l.trim()) return; let m; try { m = JSON.parse(l); } catch { return; }
  if (m.id !== undefined && (m.result !== undefined || m.error !== undefined)) {
    const r = pending.get(m.id); if (r) { pending.delete(m.id); m.error ? r.reject(m.error) : r.resolve(m.result); }
  }
});
const req = (method, params) => new Promise((res, rej) => { const i = ++id; pending.set(i, { resolve: res, reject: rej }); p.stdin.write(JSON.stringify({ id: i, method, params }) + "\n"); });
const notify = (method, params) => p.stdin.write(JSON.stringify({ method, params }) + "\n");

await req("initialize", { clientInfo: { name: "repro", version: "0", title: "repro" }, capabilities: {} });
notify("initialized", {});
const st = await req("thread/start", { cwd: process.cwd(), model: null, approvalPolicy: "never", sandbox: "read-only", serviceName: "claude_code_codex_plugin", ephemeral: false });
const tid = st.thread.id;
try {
  await req("thread/name/set", { threadId: tid, name: "My Task" }); // BEFORE any turn — same as the plugin
  console.log("name/set OK");
} catch (e) {
  console.log("name/set FAILED:", JSON.stringify(e)); // -> {"code":-32600,"message":"no rollout found for thread id <tid>"}
}
p.kill("SIGTERM");

Observed: name/set FAILED: {"code":-32600,"message":"no rollout found for thread id …"}10/10 runs, and still fails even with a 2000 ms delay before name/set (the rollout is genuinely not written until the first turn, not merely a flush race). If you instead run turn/start first and only then thread/name/set, the no rollout found error disappears.

Expected vs actual

  • Expected: delegating a fresh task through the Claude Code plugin runs the turn; naming the thread is best-effort and never fails the task.
  • Actual: the task fails immediately with no rollout found for thread id … before the turn ever runs.

Suggested fix (plugin side)

Make thread naming non-fatal / correctly ordered. Any of:

  1. Treat thread/name/set failures as best-effort — extend the startThread() catch to also swallow no rollout found (and the post-turn thread metadata unavailable before name update variant). Naming is cosmetic here: --resume-last resolves the thread from the plugin's own tracked job.threadId (resolveLatestTrackedTaskThreadfindLatestResumableTaskJob), not the codex-side thread name.
  2. Reorder — apply thread/name/set after the first turn has produced a rollout (aligns with the desired behavior in #24289).

Option 1 is the smallest and most robust across CLI versions.

Related

  • #28496 — -32600 overloaded across no rollout found and config-load errors (same error surface; recommends fall-back-to-thread/start for the resume half).
  • #24289 — auto-name thread after the first prompt (confirms the correct ordering).
  • #22064 — resume of a missing thread should fall back to thread/start.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗