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
+63 -5
View File
@@ -264,6 +264,36 @@ ${getSafetyConstraints(opts.vcsKind)}`;
function buildFixPrompt(opts: ExecutorResumeOpts): string {
const { plan, acceptedFindings } = opts;
if (opts.purpose === "task_plan") {
const scopeList = plan.scope.map((s) => ` - ${s}`).join("\n");
const criteriaList = plan.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
const cmdsText = plan.verificationCommands.map((cmd) => ` ${cmd.join(" ")}`).join("\n");
return `# Continued Task Plan: ${plan.title}
This is a new frozen plan for the same task. Use the repository's current state and your existing task context.
## Plan
${plan.planMarkdown}
## Allowed Scope
Only modify files within these paths (relative to repository root):
${scopeList}
## Acceptance Criteria
${criteriaList}
## Verification Commands
After implementation, ensure these commands pass:
${cmdsText}
${getSafetyConstraints(opts.vcsKind)}`;
}
if (acceptedFindings.startsWith("# Scope Remediation")) {
return `# Scope Fix Request: ${plan.title}
@@ -395,6 +425,22 @@ function isSessionInReasonixProject(jsonlPath: string, projectsDir: string, repo
return relative !== "" && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
}
export function validateReasonixSessionForRepository(sessionFilePath: string, repoRoot: string): string {
const projectsDir = path.join(process.env.HOME || os.homedir(), ".reasonix", "projects");
const resolved = path.resolve(sessionFilePath);
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile() || !resolved.endsWith(".jsonl")) {
throw new Error(`Reasonix session file not found: ${resolved}`);
}
if (!fs.existsSync(`${resolved}.meta`)) {
throw new Error(`Reasonix session metadata file not found: ${resolved}.meta`);
}
const workspaceMatches = getWorkspaceFromJsonl(resolved) === repoRoot;
if (!workspaceMatches && !isSessionInReasonixProject(resolved, projectsDir, repoRoot)) {
throw new Error(`Reasonix session does not belong to repository ${repoRoot}: ${resolved}`);
}
return resolved;
}
function listJsonlFiles(dir: string, repoRoot?: string): string[] {
if (!fs.existsSync(dir)) return [];
const result: string[] = [];
@@ -638,6 +684,22 @@ export function validateClaudeSessionForWorkflow(
repoRoot: string,
planTitle: string,
): string {
const { sessionFile, content } = readClaudeSessionForRepository(sessionId, repoRoot);
if (!content.includes(planTitle)) {
throw new Error(`Claude session ${sessionId} does not contain frozen plan title: ${planTitle}`);
}
return sessionFile;
}
export function validateClaudeSessionForRepository(sessionId: string, repoRoot: string): string {
return readClaudeSessionForRepository(sessionId, repoRoot).sessionFile;
}
function readClaudeSessionForRepository(
sessionId: string,
repoRoot: string,
): { sessionFile: string; content: string } {
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
throw new Error(`Invalid Claude session ID: ${sessionId}`);
}
@@ -688,11 +750,7 @@ export function validateClaudeSessionForWorkflow(
if (!sessionMatches || !repoMatches) {
throw new Error(`Claude session ${sessionId} does not belong to repository ${repoRoot}`);
}
if (!content.includes(planTitle)) {
throw new Error(`Claude session ${sessionId} does not contain frozen plan title: ${planTitle}`);
}
return sessionFile;
return { sessionFile, content };
}
function extractClaudeFailureReason(output: string): string | undefined {