fix: harden workflow execution recovery
This commit is contained in:
@@ -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() });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user