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
17 changes: 11 additions & 6 deletions plugins/codex/scripts/lib/job-control.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -256,25 +256,30 @@ export function buildSingleJobSnapshot(cwd, reference, options = {}) {
export function resolveResultJob(cwd, reference) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const jobs = sortJobsNewestFirst(reference ? listJobs(workspaceRoot) : filterJobsForCurrentSession(listJobs(workspaceRoot)));

if (reference) {
const selected = matchJobReference(jobs, reference);
if (selected.status === "queued" || selected.status === "running") {
throw new Error(`Job ${selected.id} is still ${selected.status}. Check /codex:status and try again once it finishes.`);
}
return { workspaceRoot, job: selected };
}

const selected = matchJobReference(
jobs,
reference,
"",
(job) => job.status === "completed" || job.status === "failed" || job.status === "cancelled"
);

if (selected) {
return { workspaceRoot, job: selected };
}

const active = matchJobReference(jobs, reference, (job) => job.status === "queued" || job.status === "running");
const active = matchJobReference(jobs, "", (job) => job.status === "queued" || job.status === "running");
if (active) {
throw new Error(`Job ${active.id} is still ${active.status}. Check /codex:status and try again once it finishes.`);
}

if (reference) {
throw new Error(`No finished job found for "${reference}". Run /codex:status to inspect active jobs.`);
}

throw new Error("No finished Codex jobs found for this repository yet.");
}

Expand Down
41 changes: 41 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,47 @@ test("result returns the stored output for the latest finished job by default",
);
});

test("result identifies an explicitly referenced active job", async (t) => {
for (const status of ["queued", "running"]) {
await t.test(status, () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
fs.mkdirSync(stateDir, { recursive: true });

fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: `task-${status}`,
status,
title: "Codex Task",
jobClass: "task",
createdAt: "2026-03-18T15:30:00.000Z",
updatedAt: "2026-03-18T15:30:01.000Z"
}
]
},
null,
2
)}\n`,
"utf8"
);

const result = run("node", [SCRIPT, "result", `task-${status}`], {
cwd: workspace
});

assert.notEqual(result.status, 0);
assert.match(result.stderr, new RegExp(`Job task-${status} is still ${status}`));
assert.doesNotMatch(result.stderr, /No job found/);
});
}
});

test("result without a job id prefers the latest finished job from the current Claude session", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
Expand Down