feat: use stored review on resume if interrupted
This commit is contained in:
12
index.ts
12
index.ts
@@ -161,9 +161,9 @@ async function selectLoopOptions(
|
|||||||
saveReviews = config.execution.saveReviews;
|
saveReviews = config.execution.saveReviews;
|
||||||
} else {
|
} else {
|
||||||
const saveChoice = await ctx.ui.select(
|
const saveChoice = await ctx.ui.select(
|
||||||
"Save full review output to disk?",
|
"Save full review output to disk? (recommended — enables review feedback recovery when resuming interrupted loops)",
|
||||||
[
|
[
|
||||||
"Yes — write each review to .ralpi/reviews/<loop>/<task>.md",
|
"Yes — write each review to .ralpi/reviews/<loop>/<task>.json",
|
||||||
"No — keep reviews in-chat only",
|
"No — keep reviews in-chat only",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -223,7 +223,9 @@ async function selectPRDToResume(
|
|||||||
).length;
|
).length;
|
||||||
const relPath = path.relative(ctx.cwd, entry.prd.sourcePath);
|
const relPath = path.relative(ctx.cwd, entry.prd.sourcePath);
|
||||||
const updated = new Date(entry.prd.lastUpdatedAt).toLocaleString();
|
const updated = new Date(entry.prd.lastUpdatedAt).toLocaleString();
|
||||||
return `${relPath} — ${completed}/${total} done${failed ? `, ${failed} failed` : ""} · ${updated}`;
|
return `${relPath} — ${completed}/${total} done${
|
||||||
|
failed ? `, ${failed} failed` : ""
|
||||||
|
} · ${updated}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const selected = await ctx.ui.select(
|
const selected = await ctx.ui.select(
|
||||||
@@ -929,7 +931,9 @@ async function resumeLoop(
|
|||||||
.map((id) => `${id}: ${fin.conflicts[id].slice(0, 3).join(", ")}`)
|
.map((id) => `${id}: ${fin.conflicts[id].slice(0, 3).join(", ")}`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
sendChatMessage?.(
|
sendChatMessage?.(
|
||||||
`⚠ ${conflictIds.join(", ")} — merge conflict on resume-finalize; re-running (${detail})`,
|
`⚠ ${conflictIds.join(
|
||||||
|
", ",
|
||||||
|
)} — merge conflict on resume-finalize; re-running (${detail})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { extractReflection } from "./reflection";
|
|||||||
import {
|
import {
|
||||||
extractReview,
|
extractReview,
|
||||||
saveReviewToFile as saveReviewJson,
|
saveReviewToFile as saveReviewJson,
|
||||||
|
loadReview as loadReviewJson,
|
||||||
verdictGlyph,
|
verdictGlyph,
|
||||||
verdictSummary,
|
verdictSummary,
|
||||||
} from "./review";
|
} from "./review";
|
||||||
@@ -813,6 +814,27 @@ async function executeTask(
|
|||||||
? captureGitHead(worktreeDir)
|
? captureGitHead(worktreeDir)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Load a prior review from disk when resuming an interrupted loop.
|
||||||
|
// If the previous run's review rejected the task (verdict 'fail') and the
|
||||||
|
// re-execution was lost to a crash/connection error, the findings would
|
||||||
|
// otherwise be orphaned. Injecting them here gives the fresh run the
|
||||||
|
// reviewer's feedback so it doesn't reintroduce the same blockers.
|
||||||
|
let priorReview: ReviewResult | undefined;
|
||||||
|
if (config.execution.autoReview) {
|
||||||
|
const loaded = loadReviewJson(
|
||||||
|
projectDir,
|
||||||
|
config.paths.reviewsDir,
|
||||||
|
task.id,
|
||||||
|
progress.getKey(),
|
||||||
|
);
|
||||||
|
if (loaded && loaded.verdict === "fail") {
|
||||||
|
priorReview = loaded;
|
||||||
|
sendChatMessage?.(
|
||||||
|
`↻ ${task.id} · ${task.title} — resuming with prior review feedback (${loaded.findings.length} findings)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run the task
|
// Run the task
|
||||||
const result = await runTask(
|
const result = await runTask(
|
||||||
task,
|
task,
|
||||||
@@ -825,6 +847,7 @@ async function executeTask(
|
|||||||
parallelState,
|
parallelState,
|
||||||
currentModel,
|
currentModel,
|
||||||
batchRender,
|
batchRender,
|
||||||
|
priorReview,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -1014,8 +1037,18 @@ async function executeTask(
|
|||||||
`↻ review for ${task.id} · ${task.title} — verdict ${review?.verdict ?? "unknown"}, re-executing with feedback (${attempt}/${maxRetries})...`,
|
`↻ review for ${task.id} · ${task.title} — verdict ${review?.verdict ?? "unknown"}, re-executing with feedback (${attempt}/${maxRetries})...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-execute the task with review feedback injected.
|
// Re-execute the task with review feedback injected, cycling
|
||||||
const fixResult = await runTask(
|
// through failover models on connection errors so a flaky
|
||||||
|
// provider doesn't waste the review-fix attempt.
|
||||||
|
const fixModels = buildFailoverModels(currentModel, roundRobin);
|
||||||
|
let fixResult: Awaited<ReturnType<typeof runTask>> | undefined;
|
||||||
|
for (
|
||||||
|
let fixAttempt = 0;
|
||||||
|
fixAttempt < fixModels.length;
|
||||||
|
fixAttempt++
|
||||||
|
) {
|
||||||
|
const fixModel = fixModels[fixAttempt];
|
||||||
|
fixResult = await runTask(
|
||||||
task,
|
task,
|
||||||
project,
|
project,
|
||||||
config,
|
config,
|
||||||
@@ -1024,14 +1057,22 @@ async function executeTask(
|
|||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
worktreeDir,
|
worktreeDir,
|
||||||
parallelState,
|
parallelState,
|
||||||
currentModel,
|
fixModel,
|
||||||
batchRender,
|
batchRender,
|
||||||
review ?? undefined,
|
review ?? undefined,
|
||||||
);
|
);
|
||||||
|
if (fixResult.success) break;
|
||||||
if (!fixResult.success) {
|
// Connection/error failover — try the next model.
|
||||||
|
if (fixAttempt < fixModels.length - 1) {
|
||||||
sendChatMessage?.(
|
sendChatMessage?.(
|
||||||
`~ re-execution for ${task.id} · ${task.title} failed: ${fixResult.error}`,
|
`~ re-execution for ${task.id} · ${task.title} — cycling to model ${fixAttempt + 2}/${fixModels.length} (previous: ${fixResult.error})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fixResult || !fixResult.success) {
|
||||||
|
sendChatMessage?.(
|
||||||
|
`~ re-execution for ${task.id} · ${task.title} failed: ${fixResult?.error}`,
|
||||||
);
|
);
|
||||||
break; // proceed with what we have
|
break; // proceed with what we have
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user