fix: context overflow, retry, and reflection isolation
- Cap commit diffs in review/commit prompts at 50KB to prevent context window overflow on follow-up sessions - Skip skills catalog (noSkills) in commit/review follow-up sessions for leaner context - Wire Pi's SettingsManager into runAgentSession so Pi's built-in retry (exponential backoff, provider retry) applies to ralpi sessions — removes ralpi's duplicate manual retry loop - Remove maxRetries/retryDelayMs from ralpi config; rely on Pi's retry.* settings (with manual override support) - Remove retries field from progress.json and incrementRetry() from ProgressTracker - Add model failover to follow-up sessions (commit/review cycle through the model pool on connection errors) - Namespace reflection files by PRD key under .ralpi/reflections/<prdKey>/ so task sets don't overwrite each other - Skip loop-startup prompts for config fields explicitly set in YAML - Remove (default) annotations from loop options prompts - Default commitTimeoutMs/reviewTimeoutMs to 0 (inherit Pi defaults)
This commit is contained in:
156
index.ts
156
index.ts
@@ -29,6 +29,8 @@ import {
|
||||
deleteLoopActive,
|
||||
readLoopActive,
|
||||
findRalpiDir,
|
||||
listPRDsSorted,
|
||||
formatDuration,
|
||||
} from "./src/utils";
|
||||
|
||||
const COMMANDS = ["plan", "resume", "reset"] as const;
|
||||
@@ -120,6 +122,96 @@ function buildPlanByMode(
|
||||
: buildSequentialPlan(project, completed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to select auto-commit and auto-review options for this loop.
|
||||
* Defaults are taken from config; the user can override at loop startup.
|
||||
* Fields explicitly set in the config YAML are skipped (no prompt).
|
||||
* Returns the selected options (or config defaults if cancelled).
|
||||
*/
|
||||
async function selectLoopOptions(
|
||||
ctx: ExtensionContext,
|
||||
config: import("./src/types").RalpiConfig,
|
||||
): Promise<{ autoCommit: boolean; autoReview: boolean }> {
|
||||
const explicit = config.execution.explicitKeys;
|
||||
|
||||
// Skip the commit prompt when the YAML explicitly sets it.
|
||||
let autoCommit: boolean;
|
||||
if (explicit?.has("autoCommit")) {
|
||||
autoCommit = config.execution.autoCommit;
|
||||
} else {
|
||||
const commitChoice = await ctx.ui.select("Auto-commit after each task?", [
|
||||
"Yes — stage and commit changes automatically",
|
||||
"No — skip auto-commit",
|
||||
]);
|
||||
autoCommit = commitChoice
|
||||
? commitChoice.startsWith("Yes")
|
||||
: config.execution.autoCommit;
|
||||
}
|
||||
|
||||
let autoReview = false;
|
||||
if (autoCommit) {
|
||||
// Skip the review prompt when the YAML explicitly sets it.
|
||||
if (explicit?.has("autoReview")) {
|
||||
autoReview = config.execution.autoReview;
|
||||
} else {
|
||||
const reviewChoice = await ctx.ui.select(
|
||||
"Auto-review each commit against the task?",
|
||||
["Yes — spawn a review agent after each commit", "No — skip review"],
|
||||
);
|
||||
autoReview = reviewChoice
|
||||
? reviewChoice.startsWith("Yes")
|
||||
: config.execution.autoReview;
|
||||
}
|
||||
}
|
||||
|
||||
return { autoCommit, autoReview };
|
||||
}
|
||||
|
||||
/**
|
||||
* When multiple PRD loops have progress, prompt the user to select which one
|
||||
* to resume. Returns the selected PRD key and sourcePath.
|
||||
* If only one PRD exists, returns it without prompting.
|
||||
* Returns null if no PRDs exist.
|
||||
*/
|
||||
async function selectPRDToResume(
|
||||
ctx: ExtensionContext,
|
||||
found: NonNullable<ReturnType<typeof findProgressFile>>,
|
||||
): Promise<{ prdKey: string; sourcePath: string } | null> {
|
||||
const prds = listPRDsSorted(found.state);
|
||||
if (prds.length === 0) return null;
|
||||
if (prds.length === 1) {
|
||||
return { prdKey: prds[0].key, sourcePath: prds[0].prd.sourcePath };
|
||||
}
|
||||
|
||||
// Multiple PRDs — show selection sorted by most recent first
|
||||
const options = prds.map((entry) => {
|
||||
const tasks = entry.prd.tasks;
|
||||
const total = Object.keys(tasks).length;
|
||||
const completed = Object.values(tasks).filter(
|
||||
(t) => t.status === "completed",
|
||||
).length;
|
||||
const failed = Object.values(tasks).filter(
|
||||
(t) => t.status === "failed",
|
||||
).length;
|
||||
const relPath = path.relative(process.cwd(), entry.prd.sourcePath);
|
||||
const updated = new Date(entry.prd.lastUpdatedAt).toLocaleString();
|
||||
return `${relPath} — ${completed}/${total} done${failed ? `, ${failed} failed` : ""} · ${updated}`;
|
||||
});
|
||||
|
||||
const selected = await ctx.ui.select(
|
||||
"Multiple loops found. Which to resume?",
|
||||
options,
|
||||
);
|
||||
if (!selected) return null;
|
||||
|
||||
const idx = options.indexOf(selected);
|
||||
if (idx === -1) return null;
|
||||
return {
|
||||
prdKey: prds[idx].key,
|
||||
sourcePath: prds[idx].prd.sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
/** Run all batches in a plan, updating the task file after each batch. */
|
||||
async function executePlanBatches(
|
||||
plan: ReturnType<typeof buildPlanByMode>,
|
||||
@@ -147,8 +239,20 @@ async function executePlanBatches(
|
||||
// Track failed task IDs across batches to block downstream tasks
|
||||
const failedTaskIds = new Set(progress.getFailedTaskIds());
|
||||
|
||||
// Loop-level execution timeout: stop starting new batches once elapsed.
|
||||
// In-progress tasks finish naturally; we just skip remaining batches.
|
||||
const loopStart = Date.now();
|
||||
const loopTimeoutMs = config.execution.loopTimeoutMs;
|
||||
let loopTimedOut = false;
|
||||
|
||||
try {
|
||||
for (const batch of plan.batches) {
|
||||
// Check loop timeout before starting a new batch
|
||||
if (loopTimeoutMs > 0 && Date.now() - loopStart > loopTimeoutMs) {
|
||||
loopTimedOut = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (progress.getState().paused) {
|
||||
ctx.ui.notify(
|
||||
"Execution paused. Use /ralpi resume to continue.",
|
||||
@@ -223,6 +327,13 @@ async function executePlanBatches(
|
||||
if (projectDir) {
|
||||
deleteLoopActive(projectDir);
|
||||
}
|
||||
if (loopTimedOut) {
|
||||
const elapsed = formatDuration(Date.now() - loopStart);
|
||||
ctx.ui.notify(
|
||||
`Loop execution timeout reached (${elapsed}). Remaining tasks skipped. Use /ralpi resume to continue.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,6 +895,9 @@ async function handleRun(
|
||||
|
||||
const completed = buildCompletedSet(progress, project);
|
||||
const mode = await selectExecutionMode(ctx, project, taskFile, config);
|
||||
const { autoCommit, autoReview } = await selectLoopOptions(ctx, config);
|
||||
config.execution.autoCommit = autoCommit;
|
||||
config.execution.autoReview = autoReview;
|
||||
const plan = buildPlanByMode(mode, project, completed);
|
||||
|
||||
// Show dependency chain + execution plan before starting
|
||||
@@ -839,11 +953,11 @@ async function handleResume(
|
||||
): Promise<void> {
|
||||
let taskFile: string;
|
||||
let projectDir: string;
|
||||
let found: ReturnType<typeof findProgressFile>;
|
||||
let prdKey: string | undefined;
|
||||
|
||||
if (args[0]) {
|
||||
taskFile = resolveTaskArg(args[0], process.cwd());
|
||||
found = findProgressFile(process.cwd(), taskFile);
|
||||
const found = findProgressFile(process.cwd(), taskFile);
|
||||
if (!found) {
|
||||
ctx.ui.notify(
|
||||
`No existing progress for ${args[0]}. Start with /ralpi run ${args[0]}`,
|
||||
@@ -852,8 +966,9 @@ async function handleResume(
|
||||
return;
|
||||
}
|
||||
projectDir = path.dirname(path.dirname(found.path));
|
||||
prdKey = found.prdKey;
|
||||
} else {
|
||||
found = findProgressFile(process.cwd());
|
||||
const found = findProgressFile(process.cwd());
|
||||
if (!found) {
|
||||
ctx.ui.notify(
|
||||
"No .ralpi/progress.json found. Start with /ralpi run [task-file]",
|
||||
@@ -862,10 +977,16 @@ async function handleResume(
|
||||
return;
|
||||
}
|
||||
projectDir = path.dirname(path.dirname(found.path));
|
||||
// For no-arg resume, use the first PRD's source path or legacy sourcePath
|
||||
taskFile = found.state.prds
|
||||
? Object.values(found.state.prds)[0].sourcePath
|
||||
: found.state.sourcePath;
|
||||
|
||||
// When no specific task file is given, let the user select which loop
|
||||
// to resume from multiple PRDs (sorted by most recent first).
|
||||
const selected = await selectPRDToResume(ctx, found);
|
||||
if (!selected) {
|
||||
ctx.ui.notify("Resume cancelled.", "info");
|
||||
return;
|
||||
}
|
||||
taskFile = selected.sourcePath;
|
||||
prdKey = selected.prdKey;
|
||||
}
|
||||
|
||||
const project = parseTaskFile(taskFile);
|
||||
@@ -877,21 +998,21 @@ async function handleResume(
|
||||
const config = loadConfig(projectDir);
|
||||
config.model = parentModel ?? ctx.model;
|
||||
config.thinkingLevel = parentThinkingLevel;
|
||||
const progress = new ProgressTracker(projectDir, taskFile, found.prdKey);
|
||||
const progress = new ProgressTracker(projectDir, taskFile, prdKey);
|
||||
|
||||
progress.setPaused(false);
|
||||
|
||||
const completed = buildCompletedSet(progress, project);
|
||||
const mode = await selectExecutionMode(ctx, project, taskFile, config);
|
||||
const { autoCommit, autoReview } = await selectLoopOptions(ctx, config);
|
||||
config.execution.autoCommit = autoCommit;
|
||||
config.execution.autoReview = autoReview;
|
||||
const plan = buildPlanByMode(mode, project, completed);
|
||||
|
||||
// Print remaining batches before executing
|
||||
const formattedPlan = formatExecutionPlan(plan);
|
||||
if (mode === "parallel") {
|
||||
ctx.ui.notify(
|
||||
`${formattedPlan}\n\nResuming parallel execution...`,
|
||||
"info",
|
||||
);
|
||||
ctx.ui.notify(`${formattedPlan}\n\nResuming parallel execution...`, "info");
|
||||
} else {
|
||||
ctx.ui.notify(
|
||||
`${formattedPlan}\n\nResuming sequential execution...`,
|
||||
@@ -941,12 +1062,11 @@ async function handleReset(
|
||||
return;
|
||||
}
|
||||
const projectDir = path.dirname(path.dirname(found.path));
|
||||
const progress = new ProgressTracker(
|
||||
projectDir,
|
||||
found.state.prds
|
||||
? Object.values(found.state.prds)[0].sourcePath
|
||||
: found.state.sourcePath,
|
||||
);
|
||||
// Use the most recently updated PRD (first in sorted order)
|
||||
const prds = listPRDsSorted(found.state);
|
||||
const sourcePath =
|
||||
prds.length > 0 ? prds[0].prd.sourcePath : found.state.sourcePath;
|
||||
const progress = new ProgressTracker(projectDir, sourcePath);
|
||||
progress.reset();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user