fix: cap timeout-compaction retries to prevent rotation bypass
When timeout-triggered compaction succeeds, the run loop retries the same model via `continue`. Without a cap, repeated timeouts with successful compaction (e.g. force: true on long sessions) create an infinite compact→retry→timeout→compact loop that never falls through to the failover rotation path (shouldRotate). Add a timeoutCompactionAttempts counter (max 1) so after one successful timeout compaction, subsequent timeouts skip compaction and fall through to profile/model rotation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ada811e9d
commit
0fa1aea357
@ -231,6 +231,45 @@ describe("timeout-triggered compaction", () => {
|
||||
expect(mockedCompactDirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls through to failover rotation after max timeout compaction attempts", async () => {
|
||||
// First attempt: timeout with high usage (150k / 200k = 75%)
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||
makeAttemptResult({
|
||||
timedOut: true,
|
||||
lastAssistant: {
|
||||
usage: { total: 150000 },
|
||||
} as never,
|
||||
}),
|
||||
);
|
||||
// Compaction succeeds on first timeout
|
||||
mockedCompactDirect.mockResolvedValueOnce(
|
||||
makeCompactionSuccess({
|
||||
summary: "timeout recovery compaction",
|
||||
tokensBefore: 150000,
|
||||
tokensAfter: 80000,
|
||||
}),
|
||||
);
|
||||
// Second attempt after compaction: also times out with high usage
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||
makeAttemptResult({
|
||||
timedOut: true,
|
||||
lastAssistant: {
|
||||
usage: { total: 140000 },
|
||||
} as never,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
|
||||
|
||||
// Compaction was only attempted once (first timeout); second timeout
|
||||
// should NOT trigger compaction because the counter is exhausted.
|
||||
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
// Falls through to timeout error payload (failover rotation path)
|
||||
expect(result.payloads?.[0]?.isError).toBe(true);
|
||||
expect(result.payloads?.[0]?.text).toContain("timed out");
|
||||
});
|
||||
|
||||
it("catches thrown errors from contextEngine.compact during timeout recovery", async () => {
|
||||
// Timeout with high usage
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
|
||||
|
||||
@ -815,6 +815,7 @@ export async function runEmbeddedPiAgent(
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_TIMEOUT_COMPACTION_ATTEMPTS = 1;
|
||||
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
|
||||
const MAX_RUN_LOOP_ITERATIONS = resolveMaxRunRetryIterations(profileCandidates.length);
|
||||
let overflowCompactionAttempts = 0;
|
||||
@ -827,6 +828,7 @@ export async function runEmbeddedPiAgent(
|
||||
let autoCompactionCount = 0;
|
||||
let runLoopIterations = 0;
|
||||
let overloadFailoverAttempts = 0;
|
||||
let timeoutCompactionAttempts = 0;
|
||||
const maybeMarkAuthProfileFailure = async (failure: {
|
||||
profileId?: string;
|
||||
reason?: AuthProfileFailureReason | null;
|
||||
@ -1098,7 +1100,11 @@ export async function runEmbeddedPiAgent(
|
||||
if (timedOut && !aborted && !timedOutDuringCompaction) {
|
||||
const tokenUsedRatio =
|
||||
lastTurnTotal != null && ctxInfo.tokens > 0 ? lastTurnTotal / ctxInfo.tokens : 0;
|
||||
if (tokenUsedRatio > 0.65) {
|
||||
if (timeoutCompactionAttempts >= MAX_TIMEOUT_COMPACTION_ATTEMPTS) {
|
||||
log.warn(
|
||||
`[timeout-compaction] already compacted ${timeoutCompactionAttempts} time(s) for timeouts; falling through to failover rotation`,
|
||||
);
|
||||
} else if (tokenUsedRatio > 0.65) {
|
||||
const timeoutDiagId = createCompactionDiagId();
|
||||
log.warn(
|
||||
`[timeout-compaction] LLM timed out with high context usage (${Math.round(tokenUsedRatio * 100)}%); ` +
|
||||
@ -1148,6 +1154,7 @@ export async function runEmbeddedPiAgent(
|
||||
await runOwnsCompactionAfterHook("timeout recovery", timeoutCompactResult);
|
||||
if (timeoutCompactResult.compacted) {
|
||||
autoCompactionCount += 1;
|
||||
timeoutCompactionAttempts += 1;
|
||||
log.info(
|
||||
`[timeout-compaction] compaction succeeded for ${provider}/${modelId}; retrying prompt`,
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user