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:
121
src/utils.ts
121
src/utils.ts
@@ -13,6 +13,7 @@ import {
|
||||
DefaultResourceLoader,
|
||||
getAgentDir,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// ─── Directory Helpers ───────────────────────────────────────────────────────
|
||||
@@ -152,6 +153,70 @@ export function findProgressFile(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all PRDs from a ProgressState, sorted by lastUpdatedAt descending
|
||||
* (most recent first). Used by resume to offer a selection when multiple
|
||||
* loops have progress simultaneously.
|
||||
*/
|
||||
export function listPRDsSorted(
|
||||
state: ProgressState,
|
||||
): Array<{ key: string; prd: PRDProgress }> {
|
||||
const entries: Array<{ key: string; prd: PRDProgress }> = [];
|
||||
|
||||
if (state.prds) {
|
||||
for (const [key, prd] of Object.entries(state.prds)) {
|
||||
entries.push({ key, prd });
|
||||
}
|
||||
} else {
|
||||
// Legacy flat mode — single PRD
|
||||
entries.push({
|
||||
key: "legacy",
|
||||
prd: {
|
||||
sourcePath: state.sourcePath,
|
||||
tasks: state.tasks,
|
||||
startedAt: state.startedAt,
|
||||
lastUpdatedAt: state.lastUpdatedAt,
|
||||
paused: state.paused,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((a, b) => {
|
||||
return (
|
||||
new Date(b.prd.lastUpdatedAt).getTime() -
|
||||
new Date(a.prd.lastUpdatedAt).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ─── Model Resolution ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve a "<provider>/<model>" spec string via the model registry.
|
||||
* Returns undefined if spec is empty, malformed, or not found.
|
||||
*/
|
||||
export function resolveModelSpec(
|
||||
modelRegistry:
|
||||
| { find(provider: string, modelId: string): unknown }
|
||||
| undefined,
|
||||
spec: string,
|
||||
onWarning?: (msg: string) => void,
|
||||
): unknown | undefined {
|
||||
if (!spec) return undefined;
|
||||
const slashIdx = spec.indexOf("/");
|
||||
if (slashIdx === -1) {
|
||||
onWarning?.(
|
||||
`ralpi config: skipping model "${spec}" — expected <provider>/<model> format`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
const provider = spec.slice(0, slashIdx);
|
||||
const modelId = spec.slice(slashIdx + 1);
|
||||
return modelRegistry?.find(provider, modelId);
|
||||
}
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Try to use the `yaml` package (real dependency in package.json).
|
||||
@@ -239,6 +304,15 @@ export function loadConfig(projectDir: string): RalpiConfig {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const parsed = parseSimpleYaml(content);
|
||||
Object.assign(acc, mergeConfig(acc, parsed));
|
||||
// Track which execution keys were explicitly set in this YAML so the
|
||||
// loop-startup prompts can be skipped for fields the user already set.
|
||||
const exec = parsed?.execution;
|
||||
if (exec && typeof exec === "object" && !Array.isArray(exec)) {
|
||||
acc.execution.explicitKeys ??= new Set<string>();
|
||||
for (const key of Object.keys(exec)) {
|
||||
acc.execution.explicitKeys.add(key);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Malformed config — skip silently
|
||||
}
|
||||
@@ -434,6 +508,10 @@ export async function runAgentSession(
|
||||
signal?: AbortSignal,
|
||||
model?: unknown,
|
||||
thinkingLevel?: unknown,
|
||||
/** When true, skip loading the skills catalog for this session. Used by
|
||||
* focused follow-up sessions (commit/review) that don't need skills —
|
||||
* keeps the context lean and avoids dragging in unrelated overhead. */
|
||||
noSkills = false,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
text: string;
|
||||
@@ -466,7 +544,7 @@ export async function runAgentSession(
|
||||
cwd,
|
||||
agentDir: getAgentDir(),
|
||||
noExtensions: true,
|
||||
noSkills: false,
|
||||
noSkills,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
noContextFiles: true,
|
||||
@@ -477,6 +555,7 @@ export async function runAgentSession(
|
||||
cwd,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
resourceLoader: loader,
|
||||
settingsManager: SettingsManager.create(cwd, getAgentDir()),
|
||||
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
||||
model: model as any,
|
||||
thinkingLevel: thinkingLevel as any,
|
||||
@@ -675,3 +754,43 @@ export function captureGitCommits(projectDir: string): {
|
||||
|
||||
return { commitMessages, commitSummary };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the diff of the latest commit (HEAD).
|
||||
* Returns the short hash, subject, and full diff (stat + patch).
|
||||
* Used by the auto-review agent to review a commit against the task.
|
||||
*/
|
||||
export function getLatestCommitDiff(
|
||||
projectDir: string,
|
||||
): { hash: string; subject: string; diff: string } | null {
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
try {
|
||||
execSync("git rev-parse --git-dir", { cwd: projectDir, stdio: "pipe" });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = execSync("git rev-parse --short HEAD", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const subject = execSync("git log -1 --format=%s", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
// Full diff of the latest commit: stat overview + patch
|
||||
const diff = execSync("git show HEAD --stat --patch", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
maxBuffer: 1024 * 1024,
|
||||
}).trim();
|
||||
|
||||
return { hash, subject, diff };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user