fix: harden workflow execution recovery

This commit is contained in:
liujing
2026-07-17 15:56:58 +08:00
parent 90551414b1
commit fb39ab289a
14 changed files with 355 additions and 30 deletions
+69 -1
View File
@@ -490,6 +490,7 @@ class ReasonixAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
});
@@ -541,6 +542,7 @@ class ReasonixAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
});
@@ -585,6 +587,68 @@ function extractClaudeSessionId(output: string): string | undefined {
return m?.[1];
}
export function validateClaudeSessionForWorkflow(
sessionId: string,
repoRoot: string,
planTitle: string,
): 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}`);
}
const projectsDir = path.join(process.env.HOME || os.homedir(), ".claude", "projects");
if (!fs.existsSync(projectsDir)) {
throw new Error(`Claude projects directory not found: ${projectsDir}`);
}
const candidates = fs.readdirSync(projectsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(projectsDir, entry.name, `${sessionId}.jsonl`))
.filter((candidate) => fs.existsSync(candidate));
if (candidates.length !== 1) {
throw new Error(
candidates.length === 0
? `Claude session file not found for ID: ${sessionId}`
: `Multiple Claude session files found for ID: ${sessionId}`,
);
}
const sessionFile = candidates[0];
const fd = fs.openSync(sessionFile, "r");
const maxBytes = 4 * 1024 * 1024;
const buffer = Buffer.alloc(maxBytes);
let content: string;
try {
const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
content = buffer.toString("utf8", 0, bytesRead);
} finally {
fs.closeSync(fd);
}
let repoMatches = false;
let sessionMatches = false;
for (const line of content.split("\n")) {
if (!line.trim()) continue;
try {
const entry = JSON.parse(line) as Record<string, unknown>;
if (entry.sessionId === sessionId) sessionMatches = true;
if (entry.cwd === repoRoot) repoMatches = true;
} catch {
// Ignore a trailing partial line at the read boundary.
}
}
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;
}
function extractClaudeFailureReason(output: string): string | undefined {
for (const line of output.split("\n")) {
const trimmed = line.trim();
@@ -654,6 +718,7 @@ class ClaudeAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
onStdoutData: (chunk: string) => {
@@ -727,6 +792,7 @@ class ClaudeAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
onStdoutData: (chunk: string) => {
@@ -837,7 +903,7 @@ function agyCommonArgs(config: ExecutorConfig, timeoutSeconds?: number): string[
return [
"--dangerously-skip-permissions",
"--mode", "accept-edits",
...(timeoutSeconds ? ["--print-timeout", `${timeoutSeconds}s`] : []),
...(timeoutSeconds ? ["--print-timeout", "24h"] : []),
...(config.agent ? ["--agent", config.agent] : []),
...(config.model ? ["--model", config.model] : []),
];
@@ -882,6 +948,7 @@ class AgyAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
});
@@ -952,6 +1019,7 @@ class AgyAdapter implements ExecutorAdapter {
processGroup: true,
logFilePath: logFile,
onProgress: ({ pid, bytesLogged }) => {
opts.onActivity?.();
updateSidecar(opts.runDir, { pid, logBytes: bytesLogged, lastActivityAt: nowIso() });
},
});