codex: scope implicit resume-last selection to the current Claude session (#83)
Co-authored-by: VOIDXAI <VOIDXAI@users.noreply.github.com>
This commit is contained in:
parent
4bd783b7ce
commit
40d213d13f
@ -287,6 +287,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);
|
||||
@ -307,17 +331,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);
|
||||
}
|
||||
|
||||
@ -859,17 +889,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),
|
||||
|
||||
@ -409,6 +409,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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user