From 11983d806952503849adce3d1e1f194191a640c1 Mon Sep 17 00:00:00 2001 From: VOIDXAI Date: Wed, 1 Apr 2026 16:09:06 +0800 Subject: [PATCH] codex: scope implicit resume-last selection to the current Claude session --- plugins/codex/scripts/codex-companion.mjs | 48 ++++++++--- tests/runtime.test.mjs | 99 +++++++++++++++++++++++ 2 files changed, 134 insertions(+), 13 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 201d1c7..4471834 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -290,6 +290,30 @@ function isActiveJobStatus(status) { return status === "queued" || status === "running"; } +function getCurrentClaudeSessionId() { + return process.env[SESSION_ID_ENV] ?? null; +} + +function filterJobsForCurrentClaudeSession(jobs) { + const sessionId = getCurrentClaudeSessionId(); + if (!sessionId) { + return jobs; + } + return jobs.filter((job) => job.sessionId === sessionId); +} + +function findLatestResumableTaskJob(jobs) { + return ( + jobs.find( + (job) => + job.jobClass === "task" && + job.threadId && + job.status !== "queued" && + job.status !== "running" + ) ?? null + ); +} + async function waitForSingleJobSnapshot(cwd, reference, options = {}) { const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS); const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS); @@ -310,17 +334,23 @@ async function waitForSingleJobSnapshot(cwd, reference, options = {}) { async function resolveLatestTrackedTaskThread(cwd, options = {}) { const workspaceRoot = resolveWorkspaceRoot(cwd); + const sessionId = getCurrentClaudeSessionId(); const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)).filter((job) => job.id !== options.excludeJobId); - const activeTask = jobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running")); + const visibleJobs = filterJobsForCurrentClaudeSession(jobs); + const activeTask = visibleJobs.find((job) => job.jobClass === "task" && (job.status === "queued" || job.status === "running")); if (activeTask) { throw new Error(`Task ${activeTask.id} is still running. Use /codex:status before continuing it.`); } - const trackedTask = jobs.find((job) => job.jobClass === "task" && job.status === "completed" && job.threadId); + const trackedTask = findLatestResumableTaskJob(visibleJobs); if (trackedTask) { return { id: trackedTask.threadId }; } + if (sessionId) { + return null; + } + return findLatestTaskThread(workspaceRoot); } @@ -862,17 +892,9 @@ function handleTaskResumeCandidate(argv) { const cwd = resolveCommandCwd(options); const workspaceRoot = resolveCommandWorkspace(options); - const sessionId = process.env[SESSION_ID_ENV] ?? null; - const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)); - const candidate = - jobs.find( - (job) => - job.jobClass === "task" && - job.threadId && - job.status !== "queued" && - job.status !== "running" && - (!sessionId || job.sessionId === sessionId) - ) ?? null; + const sessionId = getCurrentClaudeSessionId(); + const jobs = filterJobsForCurrentClaudeSession(sortJobsNewestFirst(listJobs(workspaceRoot))); + const candidate = findLatestResumableTaskJob(jobs); const payload = { available: Boolean(candidate), diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 6000c89..fea3fcf 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -284,6 +284,105 @@ test("task-resume-candidate returns the latest rescue thread from the current se assert.equal(payload.candidate.threadId, "thr_current"); }); +test("task --resume-last does not resume a task from another Claude session", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + 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 otherEnv = { + ...buildEnv(binDir), + CODEX_COMPANION_SESSION_ID: "sess-other" + }; + const currentEnv = { + ...buildEnv(binDir), + CODEX_COMPANION_SESSION_ID: "sess-current" + }; + + const firstRun = run("node", [SCRIPT, "task", "initial task"], { + cwd: repo, + env: otherEnv + }); + assert.equal(firstRun.status, 0, firstRun.stderr); + + const candidate = run("node", [SCRIPT, "task-resume-candidate", "--json"], { + cwd: repo, + env: currentEnv + }); + assert.equal(candidate.status, 0, candidate.stderr); + assert.equal(JSON.parse(candidate.stdout).available, false); + + const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], { + cwd: repo, + env: currentEnv + }); + assert.equal(resume.status, 1); + assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./); + + const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.equal(fakeState.lastTurnStart.threadId, "thr_1"); + assert.equal(fakeState.lastTurnStart.prompt, "initial task"); +}); + +test("task --resume-last ignores running tasks from other Claude sessions", () => { + 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 stateDir = resolveStateDir(repo); + fs.mkdirSync(path.join(stateDir, "jobs"), { recursive: true }); + fs.writeFileSync( + path.join(stateDir, "state.json"), + `${JSON.stringify( + { + version: 1, + config: { stopReviewGate: false }, + jobs: [ + { + id: "task-other-running", + status: "running", + title: "Codex Task", + jobClass: "task", + sessionId: "sess-other", + threadId: "thr_other", + summary: "Other session active task", + updatedAt: "2026-03-24T20:05:00.000Z" + } + ] + }, + null, + 2 + )}\n`, + "utf8" + ); + + const env = { + ...buildEnv(binDir), + CODEX_COMPANION_SESSION_ID: "sess-current" + }; + const status = run("node", [SCRIPT, "status", "--json"], { + cwd: repo, + env + }); + assert.equal(status.status, 0, status.stderr); + assert.deepEqual(JSON.parse(status.stdout).running, []); + + const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], { + cwd: repo, + env + }); + assert.equal(resume.status, 1); + assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./); +}); + test("session start hook exports the Claude session id and plugin data dir for later commands", () => { const repo = makeTempDir(); const envFile = path.join(makeTempDir(), "claude-env.sh");