Skip to content

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

Description

@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 Auto-generate and apply a concise thread name after the first prompt #24289).

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

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingskillsIssues related to skills

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions