feat(FN-1877): add auto-compaction on context window errors to promptWithFallback

- Detect context-window-limit errors from AI sessions and automatically compact
  the session conversation history before retrying (one attempt per session)
- Centralize auto-compaction in promptWithFallback (pi.ts) so executor, merger,
  and step-session-executor all benefit from the same mechanism
- Remove scattered context-limit error handling from executor.ts, merger.ts, and
  step-session-executor.ts in favor of the centralized approach
- Add comprehensive tests for auto-compaction retry behavior in pi.test.ts
- Remove unused compactSessionContext import from step-session-executor.ts
- Add memory note documenting the centralized auto-compaction design
This commit is contained in:
Fusion
2026-04-15 10:42:20 -07:00
committed by gsxdsm
parent bd7ada7533
commit ddf0fc7303
7 changed files with 335 additions and 244 deletions

View File

@@ -2358,71 +2358,45 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
},
});
} catch (err: unknown) {
// Context-limit error: try compact-and-retry recovery.
// Context-limit error after promptWithFallback's auto-compaction already attempted recovery.
// Try truncated prompt retry as second-level fallback.
// This detects when the LLM rejects the prompt due to context-window overflow.
const errorMessage = err instanceof Error ? err.message : String(err);
if (isContextLimitError(errorMessage)) {
mergerLog.warn(`${taskId}: context limit hit in merge agent — attempting compaction recovery`);
await store.logEntry(taskId, "Context limit reached during merge — compacting session and retrying");
mergerLog.warn(`${taskId}: context limit hit after auto-compaction — retrying with minimal merge prompt`);
await store.logEntry(taskId, "Context limit reached during merge after auto-compaction — retrying with reduced prompt");
// Attempt to compress conversation history to free context space.
const compactResult = await compactSessionContext(session);
if (compactResult) {
// Compaction succeeded: retry with the compressed session.
mergerLog.log(`${taskId}: context compacted at ${compactResult.tokensBefore} tokens — retrying`);
await store.logEntry(taskId, `Session compacted at ${compactResult.tokensBefore} tokens — retrying merge`);
// Build minimal prompt: omit diff stat, use placeholder for commit log
const truncatedPrompt = buildMergePrompt({
taskId,
branch,
commitLog: "(see git log)", // Minimal placeholder instead of full commit log
diffStat: "", // Omit diff stat entirely
hasConflicts,
simplifiedContext: true, // Also skip detailed context
testCommand,
buildCommand,
authorArg,
});
// Retry the prompting after compaction
try {
await withRateLimitRetry(async () => {
await promptWithFallback(session, prompt);
await promptWithFallback(session, truncatedPrompt);
checkSessionError(session);
}, {
onRetry: (attempt, delayMs, error) => {
const delaySec = Math.round(delayMs / 1000);
mergerLog.warn(`${taskId} rate limited after compaction — retry ${attempt} in ${delaySec}s: ${error.message}`);
mergerLog.warn(`${taskId} rate limited during truncated retry — retry ${attempt} in ${delaySec}s: ${error.message}`);
},
});
} else {
// Compaction unavailable or failed (fresh session has nothing to compact):
// Retry with a minimal prompt that omits diff stat and uses placeholder commit log.
// Use truncatedRetryAttempted flag to prevent infinite loops.
let truncatedRetryAttempted = false;
mergerLog.warn(`${taskId}: compaction unavailable — retrying with minimal merge prompt`);
await store.logEntry(taskId, "Context limit reached — retrying with reduced prompt");
// Build minimal prompt: omit diff stat, use placeholder for commit log
const truncatedPrompt = buildMergePrompt({
taskId,
branch,
commitLog: "(see git log)", // Minimal placeholder instead of full commit log
diffStat: "", // Omit diff stat entirely
hasConflicts,
simplifiedContext: true, // Also skip detailed context
testCommand,
buildCommand,
authorArg,
});
try {
await withRateLimitRetry(async () => {
await promptWithFallback(session, truncatedPrompt);
checkSessionError(session);
truncatedRetryAttempted = true;
}, {
onRetry: (attempt, delayMs, error) => {
const delaySec = Math.round(delayMs / 1000);
mergerLog.warn(`${taskId} rate limited during truncated retry — retry ${attempt} in ${delaySec}s: ${error.message}`);
},
});
} catch (retryErr: unknown) {
// Truncated retry also failed: propagate original error
const retryErrorMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
if (isContextLimitError(retryErrorMessage) && truncatedRetryAttempted) {
mergerLog.error(`${taskId}: truncated retry also hit context limit — propagating original error`);
throw err; // Throw original error with original context
}
throw retryErr; // Non-context error or other failure
} catch (retryErr: unknown) {
// Truncated retry also failed: propagate original error
const retryErrorMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
if (isContextLimitError(retryErrorMessage)) {
mergerLog.error(`${taskId}: truncated retry also hit context limit — propagating original error`);
throw err; // Throw original error with original context
}
throw retryErr; // Non-context error or other failure
}
} else {
// Non-context error (network, rate limit, build failure): propagate immediately.