fix: opt-in compaction precheck retry

Fix mid-turn compaction precheck retries so recovery continues from the current transcript instead of resubmitting the original user prompt.
This commit is contained in:
clawsweeper[bot]
2026-04-30 23:05:27 -07:00
committed by GitHub
parent ae07d57f9d
commit 0d2a201b27
5 changed files with 100 additions and 5 deletions

View File

@@ -253,6 +253,41 @@ describe("overflow compaction in run loop", () => {
expect(result.meta.error).toBeUndefined();
});
it("continues from the transcript after mid-turn precheck truncation handled the overflow", async () => {
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(
makeAttemptResult({
promptError: null,
preflightRecovery: {
route: "truncate_tool_results_only",
source: "mid-turn",
handled: true,
truncatedCount: 2,
},
}),
)
.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
const result = await runEmbeddedPiAgent(baseParams);
expect(mockedCompactDirect).not.toHaveBeenCalled();
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(mockedRunEmbeddedAttempt).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
prompt: expect.stringContaining("Continue from the current transcript"),
}),
);
expect(mockedRunEmbeddedAttempt).not.toHaveBeenNthCalledWith(
2,
expect.objectContaining({ prompt: baseParams.prompt }),
);
expect(mockedLog.info).toHaveBeenCalledWith(
expect.stringContaining("retrying from current transcript"),
);
expect(result.meta.error).toBeUndefined();
});
it("falls back to compaction when early truncate-only recovery does not help", async () => {
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(
@@ -286,6 +321,44 @@ describe("overflow compaction in run loop", () => {
expect(result.meta.error).toBeUndefined();
});
it("continues from the transcript after mid-turn precheck compaction", async () => {
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(
makeAttemptResult({
promptError: makeOverflowError(
"Context overflow: prompt too large for the model (mid-turn precheck).",
),
promptErrorSource: "precheck",
preflightRecovery: { route: "compact_only", source: "mid-turn" },
}),
)
.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
mockedCompactDirect.mockResolvedValueOnce(
makeCompactionSuccess({
summary: "Compacted after mid-turn precheck",
firstKeptEntryId: "entry-8",
tokensBefore: 155000,
}),
);
const result = await runEmbeddedPiAgent(baseParams);
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(mockedRunEmbeddedAttempt).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
prompt: expect.stringContaining("Continue from the current transcript"),
}),
);
expect(mockedRunEmbeddedAttempt).not.toHaveBeenNthCalledWith(
2,
expect.objectContaining({ prompt: baseParams.prompt }),
);
expect(result.meta.error).toBeUndefined();
});
it("runs post-compaction tool-result truncation before retry for mixed precheck routes", async () => {
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(

View File

@@ -163,6 +163,8 @@ type ApiKeyInfo = ResolvedProviderAuth;
const MAX_SAME_MODEL_IDLE_TIMEOUT_RETRIES = 1;
const EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS = 30_000;
const MID_TURN_PRECHECK_CONTINUATION_PROMPT =
"Continue from the current transcript after the latest tool result. Do not repeat the original user request, and do not rerun completed tools unless the transcript shows they are still needed.";
type EmbeddedRunAttemptForRunner = Awaited<ReturnType<typeof runEmbeddedAttemptWithBackend>>;
function resolveEmbeddedRunLaneTimeoutMs(timeoutMs: number): number | undefined {
@@ -759,6 +761,7 @@ export async function runEmbeddedPiAgent(
let planningOnlyRetryInstruction: string | null = null;
let reasoningOnlyRetryInstruction: string | null = null;
let emptyResponseRetryInstruction: string | null = null;
let nextAttemptPromptOverride: string | null = null;
const ackExecutionFastPathInstruction = resolveAckExecutionFastPathInstruction({
provider,
modelId,
@@ -963,7 +966,9 @@ export async function runEmbeddedPiAgent(
await fs.mkdir(resolvedWorkspace, { recursive: true });
const basePrompt =
provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
nextAttemptPromptOverride ??
(provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt);
nextAttemptPromptOverride = null;
const promptAdditions = [
ackExecutionFastPathInstruction,
planningOnlyRetryInstruction,
@@ -1201,10 +1206,15 @@ export async function runEmbeddedPiAgent(
(attempt.toolMetas?.length ?? 0) === 0 &&
(attempt.assistantTexts?.length ?? 0) === 0;
if (preflightRecovery?.handled) {
const retryingFromTranscript = preflightRecovery.source === "mid-turn";
log.info(
`[context-overflow-precheck] early recovery route=${preflightRecovery.route} ` +
`completed for ${provider}/${modelId}; retrying prompt`,
`completed for ${provider}/${modelId}; ` +
(retryingFromTranscript ? "retrying from current transcript" : "retrying prompt"),
);
if (retryingFromTranscript) {
nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT;
}
continue;
}
const requestedSelection = shouldSwitchToLiveModel({
@@ -1385,6 +1395,9 @@ export async function runEmbeddedPiAgent(
log.warn(
`context overflow persisted after in-attempt compaction (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); retrying prompt without additional compaction for ${provider}/${modelId}`,
);
if (preflightRecovery?.source === "mid-turn") {
nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT;
}
continue;
}
// Attempt explicit overflow compaction only when this attempt did not
@@ -1512,6 +1525,9 @@ export async function runEmbeddedPiAgent(
}
autoCompactionCount += 1;
log.info(`auto-compaction succeeded for ${provider}/${modelId}; retrying prompt`);
if (preflightRecovery?.source === "mid-turn") {
nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT;
}
continue;
}
log.warn(
@@ -1550,6 +1566,9 @@ export async function runEmbeddedPiAgent(
log.info(
`[context-overflow-recovery] Truncated ${truncResult.truncatedCount} tool result(s); retrying prompt`,
);
if (preflightRecovery?.source === "mid-turn") {
nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT;
}
continue;
}
log.warn(

View File

@@ -903,7 +903,7 @@ describe("runEmbeddedAttempt context engine mid-turn precheck integration", () =
});
expect(result.promptErrorSource).toBe("precheck");
expect(result.preflightRecovery).toEqual({ route: "compact_only" });
expect(result.preflightRecovery).toEqual({ route: "compact_only", source: "mid-turn" });
expect(result.messagesSnapshot).toEqual([seedMessage]);
});
});

View File

@@ -2357,6 +2357,7 @@ export async function runEmbeddedAttempt(
if (truncationResult.truncated) {
preflightRecovery = {
route: "truncate_tool_results_only",
source: "mid-turn",
handled: true,
truncatedCount: truncationResult.truncatedCount,
};
@@ -2367,7 +2368,7 @@ export async function runEmbeddedAttempt(
`handled=true truncatedCount=${truncationResult.truncatedCount}`,
);
} else {
preflightRecovery = { route: "compact_only" };
preflightRecovery = { route: "compact_only", source: "mid-turn" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
logMidTurnPrecheck(
@@ -2376,7 +2377,7 @@ export async function runEmbeddedAttempt(
);
}
} else {
preflightRecovery = { route: request.route };
preflightRecovery = { route: request.route, source: "mid-turn" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
logMidTurnPrecheck(request.route);

View File

@@ -68,11 +68,13 @@ export type EmbeddedRunAttemptResult = {
preflightRecovery?:
| {
route: Exclude<PreemptiveCompactionRoute, "fits">;
source?: "mid-turn";
handled: true;
truncatedCount?: number;
}
| {
route: Exclude<PreemptiveCompactionRoute, "fits">;
source?: "mid-turn";
handled?: false;
};
sessionIdUsed: string;