Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ they already have.
| --- | --- | --- |
| [`/codex:review`](#codexreview) | read-only Codex review of your current work | `--wait`, `--background`, `--base <ref>`, `--scope <auto\|working-tree\|branch>`, `--model <model\|spark>`, `--effort <level>` |
| [`/codex:adversarial-review`](#codexadversarial-review) | steerable review that challenges the approach | same as `/codex:review`, plus free-form focus text |
| [`/codex:rescue`](#codexrescue) | delegate investigation or a fix to Codex | `--background`, `--wait`, `--resume`, `--resume-thread <id>`, `--fresh`, `--model`, `--effort`, `--write`, `--sandbox <mode>`, `--read-root <dir>` |
| [`/codex:rescue`](#codexrescue) | delegate investigation or a fix to Codex | `--background`, `--wait`, `--resume`, `--resume-thread <id>`, `--fresh`, `--ephemeral`, `--model`, `--effort`, `--write`, `--sandbox <mode>`, `--read-root <dir>` |
| [`/codex:transfer`](#codextransfer) | turn this Claude session into a resumable Codex thread | `--source <claude-jsonl>` |
| [`/codex:status`](#codexstatus) | show active and recent Codex jobs | `[job-id]`, `--wait`, `--timeout-ms <ms>`, `--all` |
| [`/codex:result`](#codexresult) | show the stored output of a finished job | `[job-id]` |
Expand Down Expand Up @@ -192,7 +192,7 @@ Use it when you want Codex to:
> [!NOTE]
> Depending on the task and the model you choose these tasks might take a long time and it's generally recommended to force the task to be in the background or move the agent to the background.

It supports `--background`, `--wait`, `--resume`, `--resume-thread <id>`, `--fresh`, `--model <model|spark>`, `--effort <level>`, `--write`, `--sandbox <read-only|workspace-write|danger-full-access>`, and repeatable `--read-root <directory>`. If you omit the resume flags, the plugin can offer to continue the latest rescue thread for this repo.
It supports `--background`, `--wait`, `--resume`, `--resume-thread <id>`, `--fresh`, `--ephemeral`, `--model <model|spark>`, `--effort <level>`, `--write`, `--sandbox <read-only|workspace-write|danger-full-access>`, and repeatable `--read-root <directory>`. If you omit the resume flags, the plugin can offer to continue the latest rescue thread for this repo.

Examples:

Expand Down Expand Up @@ -221,6 +221,7 @@ Ask Codex to redesign the database connection to be more resilient.
- if you say `spark`, the plugin maps that to `gpt-5.3-codex-spark`
- follow-up rescue requests can continue the latest Codex task in the repo
- `--resume`/`--resume-last` continues the newest thread for this repository; `--resume-thread <id>` continues one specific thread (the id is printed by `/codex:status` and `/codex:result`). `--resume`, `--resume-thread`, and `--fresh` are mutually exclusive.
- `--ephemeral` runs without persisting the Codex thread: nothing is added to Codex's Recent list, and there is no thread to come back to. Useful for disposable, fire-and-forget work — many parallel subtasks from an orchestrating agent, say — where the persistent threads are only noise. It is refused together with `--resume`, `--resume-last` and `--resume-thread`, an ephemeral run is never offered as a `--resume-last` candidate, and `/codex:status` and `/codex:result` stop printing a `codex resume` line for it. Without the flag nothing changes: threads persist exactly as before.
- `--sandbox` applies to `/codex:rescue` only; the review commands stay read-only. It takes precedence over `--write` and counts only before the task text. Rescue runs edit files inside the repository by default (`workspace-write`); `read-only` blocks edits, and `danger-full-access` disables the Codex sandbox entirely, so Codex can write outside the repository and use the network without asking. Reserve it for tasks the sandbox blocks.
- a resumed thread keeps the sandbox it was started with while the plugin's shared app-server still holds it, which is the normal case inside one Claude Code session (Codex CLI 0.153.2 applies a new mode only when it loads the thread again from disk). `task` refuses a resume whose sandbox differs from what the app-server reports; resume with the same `--sandbox`, or start a new thread with `--fresh`.
- each `--read-root <directory>` must name an existing directory and opts into an OS-enforced permission profile that denies local command reads outside the listed directories and Codex's minimal runtime paths
Expand Down Expand Up @@ -428,6 +429,7 @@ Broker and background-job lifecycle:
| [#774](https://github.com/openai/codex-plugin-cc/pull/774) | `status --wait` prints its timeout and exits non-zero, instead of looking like a finished status check |
| [#773](https://github.com/openai/codex-plugin-cc/pull/773) | a broker connect that never completes is given up on after 2s and falls back to a direct app-server (the probe half of that PR is not taken: ours already bounds each attempt *and* reports why it failed) |
| [#776](https://github.com/openai/codex-plugin-cc/pull/776) | Windows teardown decides on the root's liveness instead of taskkill's message: a process already gone costs no `taskkill` at all, and a `taskkill` that reports failure only because a short-lived descendant exited mid-walk no longer throws at the caller (its broker-endpoint and shutdown-timeout changes are not taken — one is a no-op here, the other is behind what this fork already does) |
| [#779](https://github.com/openai/codex-plugin-cc/pull/779) | `--ephemeral` on `task`, so a disposable run does not leave a persistent Codex thread behind (extended here to refuse this fork's `--resume-thread` as well) |

Commands and flags:

Expand Down
3 changes: 2 additions & 1 deletion plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ Forwarding rules:
- Preserve every `--read-root <directory>` pair as a runtime control and do not include either token in the task text you pass through.
- When forwarding both scoped roots and a write-capable sandbox (`--write` or `--sandbox workspace-write`), include an approved read root that covers the workspace directory.
- Otherwise default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
- Treat `--resume`, `--resume-thread <id>`, and `--fresh` as routing controls and do not include them in the task text you pass through.
- Treat `--resume`, `--resume-thread <id>`, `--fresh`, and `--ephemeral` as routing controls and do not include them in the task text you pass through.
- `--resume` means add `--resume-last`.
- `--resume-thread <id>` means pass that exact routing pair through and do not add `--resume-last`.
- `--fresh` means do not add `--resume-last`.
- `--ephemeral` means pass the flag through and do not add `--resume-last`: the run is not persisted, so there is no thread to resume and `task` refuses the combination.
- If the user is clearly asking to continue prior Codex work in this repository, such as "continue", "keep going", "resume", "apply the top fix", or "dig deeper", add `--resume-last` unless `--fresh` or `--resume-thread <id>` is present.
- Otherwise forward the task as a fresh `task` run.
- Preserve the user's task text as-is apart from stripping routing flags.
Expand Down
3 changes: 2 additions & 1 deletion plugins/codex/commands/rescue.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
argument-hint: "[--background|--wait] [--resume|--resume-thread <id>|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [--sandbox <read-only|workspace-write|danger-full-access>] [--read-root <directory> ...] [what Codex should investigate, solve, or continue]"
argument-hint: "[--background|--wait] [--resume|--resume-thread <id>|--fresh|--ephemeral] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [--sandbox <read-only|workspace-write|danger-full-access>] [--read-root <directory> ...] [what Codex should investigate, solve, or continue]"
allowed-tools: Bash(node:*), AskUserQuestion, Agent
---

Expand All @@ -24,6 +24,7 @@ Execution mode:
- If the request includes `--resume`, do not ask whether to continue. The user already chose.
- If the request includes `--fresh`, do not ask whether to continue. The user already chose.
- If the request includes `--resume-thread <id>`, do not ask whether to continue. The user already chose the exact Codex thread.
- If the request includes `--ephemeral`, do not ask whether to continue: an ephemeral run cannot resume anything. Preserve the flag for the forwarded `task` call and keep it out of the natural-language task text.
- Otherwise, before starting Codex, check for a resumable rescue thread from this Claude session by running:

```bash
Expand Down
43 changes: 35 additions & 8 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ function printUsage() {
" 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>] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [focus text]",
" node scripts/codex-companion.mjs task [--background] [--write] [--sandbox <read-only|workspace-write|danger-full-access>] [--read-root <directory> ...] [--resume-last|--resume|--resume-thread <id>|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs task [--background] [--write] [--ephemeral] [--sandbox <read-only|workspace-write|danger-full-access>] [--read-root <directory> ...] [--resume-last|--resume|--resume-thread <id>|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" --ephemeral: run without persisting the Codex thread. Ephemeral tasks cannot be resumed and do not appear in Codex Recent. Cannot be combined with --resume/--resume-last/--resume-thread.",
" 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]",
Expand Down Expand Up @@ -368,6 +369,7 @@ function findLatestResumableTaskJob(jobs) {
(job) =>
job.jobClass === "task" &&
job.threadId &&
!job.ephemeral &&
job.status !== "queued" &&
job.status !== "running"
) ?? null
Expand Down Expand Up @@ -547,6 +549,7 @@ async function executeTaskRun(request) {
throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last / --resume-thread <id>.");
}

const persistThread = resumeThreadId ? true : !request.ephemeral;
const result = await runAppServerTurn(workspaceRoot, {
resumeThreadId,
prompt: request.prompt,
Expand All @@ -557,8 +560,8 @@ async function executeTaskRun(request) {
readRoots: request.readRoots,
write: request.write,
onProgress: request.onProgress,
persistThread: true,
threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT)
persistThread,
threadName: persistThread ? buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT) : null
});

const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
Expand Down Expand Up @@ -597,7 +600,8 @@ async function executeTaskRun(request) {
errorMessage: failureMessage || null,
jobTitle: taskMetadata.title,
jobClass: "task",
write: Boolean(request.write)
write: Boolean(request.write),
ephemeral: !persistThread
};
}

Expand Down Expand Up @@ -673,7 +677,19 @@ function buildTaskJob(workspaceRoot, taskMetadata, write) {
});
}

function buildTaskRequest({ cwd, model, effort, prompt, write, sandbox, readRoots, resumeLast, resumeThread = null, jobId }) {
function buildTaskRequest({
cwd,
model,
effort,
prompt,
write,
sandbox,
readRoots,
resumeLast,
resumeThread = null,
jobId,
ephemeral
}) {
return {
cwd,
model,
Expand All @@ -684,7 +700,8 @@ function buildTaskRequest({ cwd, model, effort, prompt, write, sandbox, readRoot
readRoots,
resumeLast,
resumeThread,
jobId
jobId,
ephemeral: Boolean(ephemeral)
};
}

Expand Down Expand Up @@ -879,7 +896,7 @@ async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file", "sandbox", "resume-thread"],
multiValueOptions: ["read-root"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background", "ephemeral"],
leadingOnlyOptions: ["sandbox"],
aliasMap: {
m: "model"
Expand Down Expand Up @@ -915,6 +932,14 @@ async function handleTask(argv) {
"--write requires an approved --read-root that covers the workspace directory; the same applies to --sandbox workspace-write."
);
}
const ephemeral = Boolean(options.ephemeral);
// This fork also has --resume-thread, which resumes by id: an ephemeral thread is never
// persisted, so there is nothing for either form of resume to come back to.
if (ephemeral && (resumeLast || resumeThread)) {
throw new Error(
"--ephemeral cannot be combined with --resume/--resume-last/--resume-thread. Ephemeral tasks are not persisted and cannot be resumed."
);
}
const taskMetadata = buildTaskRunMetadata({
prompt,
resumeLast: resumeLast || Boolean(resumeThread)
Expand All @@ -935,7 +960,8 @@ async function handleTask(argv) {
readRoots,
resumeLast,
resumeThread,
jobId: job.id
jobId: job.id,
ephemeral
});
const { payload } = enqueueBackgroundTask(cwd, job, request);
outputCommandResult(payload, renderQueuedTaskLaunch(payload), options.json);
Expand All @@ -957,6 +983,7 @@ async function handleTask(argv) {
resumeLast,
resumeThread,
jobId: job.id,
ephemeral,
onProgress: progress
}),
{ json: options.json }
Expand Down
5 changes: 3 additions & 2 deletions plugins/codex/scripts/lib/render.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function escapeMarkdownCell(value) {
}

function formatCodexResumeCommand(job) {
if (!job?.threadId) {
if (!job?.threadId || job.ephemeral) {
return null;
}
return `codex resume ${job.threadId}`;
Expand Down Expand Up @@ -393,7 +393,8 @@ export function renderJobStatusReport(job, options = {}) {
}

export function renderStoredJobResult(job, storedJob) {
const threadId = storedJob?.threadId ?? job.threadId ?? null;
const isEphemeral = Boolean(storedJob?.ephemeral ?? job.ephemeral);
const threadId = isEphemeral ? null : (storedJob?.threadId ?? job.threadId ?? null);
const resumeCommand = threadId ? `codex resume ${threadId}` : null;
if (isStructuredReviewStoredResult(storedJob) && storedJob?.rendered) {
const output = storedJob.rendered.endsWith("\n") ? storedJob.rendered : `${storedJob.rendered}\n`;
Expand Down
2 changes: 2 additions & 0 deletions plugins/codex/scripts/lib/tracked-jobs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ export async function runTrackedJob(job, runner, options = {}) {
status: completionStatus,
threadId: execution.threadId ?? null,
turnId: execution.turnId ?? null,
ephemeral: Boolean(execution.ephemeral),
pid: null,
phase: completionStatus === "completed" ? "done" : "failed",
completedAt,
Expand All @@ -386,6 +387,7 @@ export async function runTrackedJob(job, runner, options = {}) {
status: completionStatus,
threadId: execution.threadId ?? null,
turnId: execution.turnId ?? null,
ephemeral: Boolean(execution.ephemeral),
summary: execution.summary,
phase: completionStatus === "completed" ? "done" : "failed",
pid: null,
Expand Down
8 changes: 7 additions & 1 deletion tests/commands.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,13 @@ test("rescue command absorbs continue semantics", () => {
assert.match(agent, /--resume-thread <id>/);
assert.match(agent, /Preserve every `--read-root <directory>`/i);
assert.match(agent, /thin forwarding wrapper/i);
assert.match(agent, /Treat `--resume`, `--resume-thread <id>`, and `--fresh` as routing controls/i);
assert.match(
agent,
/Treat `--resume`, `--resume-thread <id>`, `--fresh`, and `--ephemeral` as routing controls/i
);
// The flag is worthless to anyone who only ever types /codex:rescue unless the wrapper forwards
// it, so the contract is asserted on both sides.
assert.match(agent, /`--ephemeral` means pass the flag through and do not add `--resume-last`/i);
assert.match(agent, /`--resume-thread <id>` means pass that exact routing pair through and do not add `--resume-last`/i);
assert.match(agent, /prefer foreground for a small, clearly bounded rescue request/i);
assert.match(agent, /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/i);
Expand Down
Loading
Loading