feat: reuse executor sessions within Codex tasks

This commit is contained in:
liujing
2026-07-19 21:51:31 +08:00
parent 717d4344ee
commit 71b8025666
13 changed files with 702 additions and 13 deletions
+171
View File
@@ -0,0 +1,171 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { abortWorkflow, startWorkflow } from "./workflow-engine.js";
import { readState, runDir } from "./workflow-state.js";
import { readTaskBinding } from "./workflow-task-binding.js";
describe("task-level executor session reuse", () => {
let tmpDir: string;
let repoRoot: string;
let stateRoot: string;
let homeDir: string;
let argsFile: string;
let previousHome: string | undefined;
let previousWorkflowDir: string | undefined;
const threadId = "019f6e2d-333c-7900-bbf4-8ad8a9ee161f";
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-task-reuse-"));
repoRoot = path.join(tmpDir, "repo");
stateRoot = path.join(tmpDir, "state");
homeDir = path.join(tmpDir, "home");
argsFile = path.join(tmpDir, "reasonix-args.bin");
fs.mkdirSync(repoRoot);
fs.mkdirSync(homeDir);
previousHome = process.env.HOME;
previousWorkflowDir = process.env.AGENT_WORKFLOW_DIR;
process.env.HOME = homeDir;
process.env.AGENT_WORKFLOW_DIR = stateRoot;
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test"], { cwd: repoRoot });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot });
const projectKey = path.resolve(repoRoot).replaceAll(path.sep, "-");
const sessionsDir = path.join(homeDir, ".reasonix", "projects", projectKey, "sessions");
const sessionFile = path.join(sessionsDir, "task-session.jsonl");
const fakeBin = path.join(repoRoot, "fake-reasonix.sh");
fs.writeFileSync(fakeBin, `#!/bin/bash
printf '%s\\0' "$@" >> '${argsFile}'
if [[ "$1" == "--version" ]]; then exit 0; fi
if [[ "$*" == *"--resume"* ]]; then echo 'resumed'; exit 0; fi
mkdir -p '${sessionsDir}'
printf '%s\\n' '{"role":"system","content":"Current workspace: \\"${repoRoot}\\""}' > '${sessionFile}'
touch '${sessionFile}.meta'
echo 'started'
`, { mode: 0o755 });
fs.writeFileSync(path.join(repoRoot, "agent-workflow.json"), JSON.stringify({
version: "1",
executors: { reasonix: { binary: fakeBin } },
}));
fs.writeFileSync(path.join(repoRoot, "tracked.txt"), "unchanged\n");
execFileSync("git", ["add", "."], { cwd: repoRoot });
execFileSync("git", ["commit", "-m", "init"], { cwd: repoRoot, stdio: "ignore" });
});
afterEach(() => {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousWorkflowDir === undefined) delete process.env.AGENT_WORKFLOW_DIR;
else process.env.AGENT_WORKFLOW_DIR = previousWorkflowDir;
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writePlan(name: string): string {
const planPath = path.join(tmpDir, `${name}.json`);
fs.writeFileSync(planPath, JSON.stringify({
version: "1",
title: name,
planMarkdown: `Implement ${name}.`,
scope: ["tracked.txt"],
acceptanceCriteria: [`${name} is complete`],
verificationCommands: [["git", "status", "--short"]],
}));
return planPath;
}
it("resumes the same executor when a later plan belongs to the current Codex task", async () => {
const first = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("First plan"),
codexThreadId: threadId,
});
const firstState = readState(runDir(repoRoot, first.runId))!;
const firstTaskId = firstState.taskId;
expect(firstState.taskSessionReused).toBe(false);
expect(readTaskBinding(repoRoot, threadId)?.executors.reasonix?.handle).toEqual(firstState.sessionHandle);
abortWorkflow({ runId: first.runId, reason: "test boundary" });
const second = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("Second plan"),
codexThreadId: threadId,
});
const secondState = readState(runDir(repoRoot, second.runId))!;
expect(secondState.taskId).toBe(firstTaskId);
expect(secondState.taskSessionReused).toBe(true);
expect(secondState.sessionHandle).toEqual(firstState.sessionHandle);
const args = fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean);
expect(args).toContain("--resume");
expect(args).toContain((firstState.sessionHandle as { sessionFilePath: string }).sessionFilePath);
expect(args.some((arg) => arg.includes("# Continued Task Plan: Second plan"))).toBe(true);
const events = fs.readFileSync(path.join(runDir(repoRoot, second.runId), "events.jsonl"), "utf8");
expect(events).toContain('"kind":"executor_session_reused"');
abortWorkflow({ runId: second.runId, reason: "test cleanup" });
});
it("starts a fresh task and session when newTask is explicit", async () => {
const first = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("First task"),
codexThreadId: threadId,
});
const firstTaskId = readState(runDir(repoRoot, first.runId))?.taskId;
abortWorkflow({ runId: first.runId, reason: "test boundary" });
fs.rmSync(argsFile, { force: true });
const second = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("New task"),
codexThreadId: threadId,
newTask: true,
});
const secondState = readState(runDir(repoRoot, second.runId))!;
expect(secondState.taskId).not.toBe(firstTaskId);
expect(secondState.taskSessionReused).toBe(false);
expect(fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean)).not.toContain("--resume");
abortWorkflow({ runId: second.runId, reason: "test cleanup" });
});
it("blocks a missing bound session instead of silently replacing it", async () => {
const first = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("Recoverable task"),
codexThreadId: threadId,
});
const handle = readState(runDir(repoRoot, first.runId))?.sessionHandle;
abortWorkflow({ runId: first.runId, reason: "test boundary" });
expect(handle?.kind).toBe("reasonix");
if (handle?.kind === "reasonix") {
fs.rmSync(handle.sessionFilePath, { force: true });
fs.rmSync(`${handle.sessionFilePath}.meta`, { force: true });
}
await expect(startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("Follow-up plan"),
codexThreadId: threadId,
})).rejects.toThrow(/Cannot reuse the current task's reasonix session/);
const fresh = await startWorkflow({
cwd: repoRoot,
executor: "reasonix",
planInput: writePlan("Intentional new task"),
codexThreadId: threadId,
newTask: true,
});
expect(readState(runDir(repoRoot, fresh.runId))?.taskSessionReused).toBe(false);
abortWorkflow({ runId: fresh.runId, reason: "test cleanup" });
});
});