Skip to content
Open
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
29 changes: 18 additions & 11 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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>] [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]",
Expand Down Expand Up @@ -464,11 +464,11 @@ async function executeTaskRun(request) {

const taskMetadata = buildTaskRunMetadata({
prompt: request.prompt,
resumeLast: request.resumeLast
resumeLast: Boolean(request.resumeLast || request.resumeThreadId)
});

let resumeThreadId = null;
if (request.resumeLast) {
let resumeThreadId = request.resumeThreadId ?? null;
if (!resumeThreadId && request.resumeLast) {
const latestThread = await resolveLatestTrackedTaskThread(workspaceRoot, {
excludeJobId: request.jobId
});
Expand Down Expand Up @@ -601,14 +601,15 @@ function buildTaskJob(workspaceRoot, taskMetadata, write) {
});
}

function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId }) {
function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, resumeThreadId, jobId }) {
return {
cwd,
model,
effort,
prompt,
write,
resumeLast,
resumeThreadId: resumeThreadId ?? null,
jobId
};
}
Expand Down Expand Up @@ -649,9 +650,9 @@ function readTaskPrompt(cwd, options, positionals) {
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, resume) {
if (!prompt && !resume) {
throw new Error("Provide a prompt, a prompt file, piped stdin, or use --resume-last/--resume-thread.");
}
}

Expand Down Expand Up @@ -761,7 +762,7 @@ async function handleReview(argv) {

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"
Expand All @@ -775,19 +776,23 @@ async function handleTask(argv) {
const prompt = readTaskPrompt(cwd, options, positionals);

const resumeLast = Boolean(options["resume-last"] || options.resume);
const resumeThreadId = options["resume-thread"] || null;
const fresh = Boolean(options.fresh);
if (resumeLast && fresh) {
throw new Error("Choose either --resume/--resume-last or --fresh.");
}
if (resumeThreadId && (resumeLast || fresh)) {
throw new Error("Choose either --resume-thread or --resume/--resume-last/--fresh.");
}
const write = Boolean(options.write);
const taskMetadata = buildTaskRunMetadata({
prompt,
resumeLast
resumeLast: resumeLast || Boolean(resumeThreadId)
});

if (options.background) {
ensureCodexAvailable(cwd);
requireTaskRequest(prompt, resumeLast);
requireTaskRequest(prompt, resumeLast || Boolean(resumeThreadId));

const job = buildTaskJob(workspaceRoot, taskMetadata, write);
const request = buildTaskRequest({
Expand All @@ -797,6 +802,7 @@ async function handleTask(argv) {
prompt,
write,
resumeLast,
resumeThreadId,
jobId: job.id
});
const { payload } = enqueueBackgroundTask(cwd, job, request);
Expand All @@ -815,6 +821,7 @@ async function handleTask(argv) {
prompt,
write,
resumeLast,
resumeThreadId,
jobId: job.id,
onProgress: progress
}),
Expand Down
52 changes: 52 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,58 @@ test("task --resume-last resumes the latest persisted task thread", () => {
assert.equal(result.stdout, "Resumed the prior run.\nFollow-up prompt accepted.\n");
});

test("task --resume-thread resumes the specified thread", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

const firstRun = run("node", [SCRIPT, "task", "--json", "initial task"], {
cwd: repo,
env: buildEnv(binDir)
});
assert.equal(firstRun.status, 0, firstRun.stderr);
const firstPayload = JSON.parse(firstRun.stdout);
assert.ok(firstPayload.threadId, "first run should report a threadId");

const result = run(
"node",
[SCRIPT, "task", "--resume-thread", firstPayload.threadId, "--json", "follow up"],
{
cwd: repo,
env: buildEnv(binDir)
}
);

assert.equal(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.equal(payload.threadId, firstPayload.threadId);
assert.equal(payload.rawOutput, "Resumed the prior run.\nFollow-up prompt accepted.");
});

test("task --resume-thread rejects --resume-last and --fresh", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir);
initGitRepo(repo);

for (const conflicting of ["--resume-last", "--fresh"]) {
const result = run(
"node",
[SCRIPT, "task", "--resume-thread", "thr_explicit", conflicting, "follow up"],
{
cwd: repo,
env: buildEnv(binDir)
}
);
assert.equal(result.status, 1);
assert.match(result.stderr, /Choose either --resume-thread/);
}
});

test("task-resume-candidate returns the latest rescue thread from the current session", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
Expand Down