codex: scope default cancel selection to the current Claude session (#84)

Co-authored-by: VOIDXAI <VOIDXAI@users.noreply.github.com>
This commit is contained in:
VOIDXAI 2026-04-08 11:38:58 +08:00 committed by GitHub
parent 40d213d13f
commit d216a5fdea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 5 deletions

View File

@ -924,7 +924,7 @@ async function handleCancel(argv) {
const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference);
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
const existing = readStoredJob(workspaceRoot, job.id) ?? {};
const threadId = existing.threadId ?? job.threadId ?? null;
const turnId = existing.turnId ?? job.turnId ?? null;

View File

@ -278,7 +278,7 @@ export function resolveResultJob(cwd, reference) {
throw new Error("No finished Codex jobs found for this repository yet.");
}
export function resolveCancelableJob(cwd, reference) {
export function resolveCancelableJob(cwd, reference, options = {}) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const jobs = sortJobsNewestFirst(listJobs(workspaceRoot));
const activeJobs = jobs.filter((job) => job.status === "queued" || job.status === "running");
@ -291,12 +291,18 @@ export function resolveCancelableJob(cwd, reference) {
return { workspaceRoot, job: selected };
}
if (activeJobs.length === 1) {
return { workspaceRoot, job: activeJobs[0] };
const sessionScopedActiveJobs = filterJobsForCurrentSession(activeJobs, options);
if (sessionScopedActiveJobs.length === 1) {
return { workspaceRoot, job: sessionScopedActiveJobs[0] };
}
if (activeJobs.length > 1) {
if (sessionScopedActiveJobs.length > 1) {
throw new Error("Multiple Codex jobs are active. Pass a job id to /codex:cancel.");
}
if (getCurrentSessionId(options)) {
throw new Error("No active Codex jobs to cancel for this session.");
}
throw new Error("No active Codex jobs to cancel.");
}

View File

@ -1471,6 +1471,109 @@ test("cancel stops an active background job and marks it cancelled", async (t) =
assert.match(fs.readFileSync(logFile, "utf8"), /Cancelled by user/);
});
test("cancel without a job id ignores active jobs from other Claude sessions", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
const jobsDir = path.join(stateDir, "jobs");
fs.mkdirSync(jobsDir, { recursive: true });
const logFile = path.join(jobsDir, "task-other.log");
fs.writeFileSync(logFile, "", "utf8");
fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: "task-other",
status: "running",
title: "Codex Task",
jobClass: "task",
sessionId: "sess-other",
summary: "Other session run",
updatedAt: "2026-03-24T20:05:00.000Z",
logFile
}
]
},
null,
2
)}\n`,
"utf8"
);
const env = {
...process.env,
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const status = run("node", [SCRIPT, "status", "--json"], {
cwd: workspace,
env
});
assert.equal(status.status, 0, status.stderr);
assert.deepEqual(JSON.parse(status.stdout).running, []);
const cancel = run("node", [SCRIPT, "cancel", "--json"], {
cwd: workspace,
env
});
assert.equal(cancel.status, 1);
assert.match(cancel.stderr, /No active Codex jobs to cancel for this session\./);
const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8"));
assert.equal(state.jobs[0].status, "running");
});
test("cancel with a job id can still target an active job from another Claude session", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
const jobsDir = path.join(stateDir, "jobs");
fs.mkdirSync(jobsDir, { recursive: true });
const logFile = path.join(jobsDir, "task-other.log");
fs.writeFileSync(logFile, "", "utf8");
fs.writeFileSync(
path.join(stateDir, "state.json"),
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs: [
{
id: "task-other",
status: "running",
title: "Codex Task",
jobClass: "task",
sessionId: "sess-other",
summary: "Other session run",
updatedAt: "2026-03-24T20:05:00.000Z",
logFile
}
]
},
null,
2
)}\n`,
"utf8"
);
const env = {
...process.env,
CODEX_COMPANION_SESSION_ID: "sess-current"
};
const cancel = run("node", [SCRIPT, "cancel", "task-other", "--json"], {
cwd: workspace,
env
});
assert.equal(cancel.status, 0, cancel.stderr);
assert.equal(JSON.parse(cancel.stdout).jobId, "task-other");
const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8"));
assert.equal(state.jobs[0].status, "cancelled");
});
test("cancel sends turn interrupt to the shared app-server before killing a brokered task", async () => {
const repo = makeTempDir();
const binDir = makeTempDir();