fix(agent): skipped compaction timeout retries
Stopped auto context-full maintenance from retrying repeated summarization timeouts on the same model before fallback. Added a regression test for timeout fast-fallback behavior.\n\nFixes #2913
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
- Fixed the `tools.format` setting schema so `minimax` can be selected as an owned tool-calling dialect, and taught auto mode to route tool-less MiniMax-family models to the MiniMax owned dialect. ([#2759](https://github.com/can1357/oh-my-pi/issues/2759))
|
||||
- Fixed WSL2 TUI stutter by adding a `git.enabled` setting and skipping footer/status-line git probes when disabled or when no git-backed status segment is visible ([#2847](https://github.com/can1357/oh-my-pi/issues/2847)).
|
||||
- Fixed JSON-mode startup notices (export/resume/session-picker messages) writing to stdout before the JSON event stream; they now route to stderr so stdout remains newline-delimited JSON.
|
||||
- Fixed auto context-full maintenance to stop retrying the same summarization timeout before falling back to the next compaction model ([#2913](https://github.com/can1357/oh-my-pi/issues/2913)).
|
||||
|
||||
## [16.0.4] - 2026-06-17
|
||||
|
||||
|
||||
@@ -9225,7 +9225,9 @@ export class AgentSession {
|
||||
let compactResult: CompactionResult | undefined;
|
||||
let lastError: unknown;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
|
||||
const candidate = candidates[candidateIndex];
|
||||
const hasMoreCandidates = candidateIndex < candidates.length - 1;
|
||||
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
||||
if (!apiKey) continue;
|
||||
|
||||
@@ -9264,6 +9266,20 @@ export class AgentSession {
|
||||
lastError = this.#buildCompactionAuthError();
|
||||
break;
|
||||
}
|
||||
if (this.#isCompactionSummarizationTimeoutMessage(message)) {
|
||||
logger.warn(
|
||||
hasMoreCandidates
|
||||
? "Auto-compaction summarization timed out, trying next model"
|
||||
: "Auto-compaction summarization timed out, not retrying same model",
|
||||
{
|
||||
error: message,
|
||||
model: `${candidate.provider}/${candidate.id}`,
|
||||
},
|
||||
);
|
||||
lastError = error;
|
||||
break;
|
||||
}
|
||||
|
||||
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
||||
const shouldRetry =
|
||||
retrySettings.enabled &&
|
||||
@@ -9281,19 +9297,15 @@ export class AgentSession {
|
||||
|
||||
// If retry delay is too long (>30s), try next candidate instead of waiting
|
||||
const maxAcceptableDelayMs = 30_000;
|
||||
if (delayMs > maxAcceptableDelayMs) {
|
||||
const hasMoreCandidates = candidates.indexOf(candidate) < candidates.length - 1;
|
||||
if (hasMoreCandidates) {
|
||||
logger.warn("Auto-compaction retry delay too long, trying next model", {
|
||||
delayMs,
|
||||
retryAfterMs,
|
||||
error: message,
|
||||
model: `${candidate.provider}/${candidate.id}`,
|
||||
});
|
||||
lastError = error;
|
||||
break; // Exit retry loop, continue to next candidate
|
||||
}
|
||||
// No more candidates - we have to wait
|
||||
if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) {
|
||||
logger.warn("Auto-compaction retry delay too long, trying next model", {
|
||||
delayMs,
|
||||
retryAfterMs,
|
||||
error: message,
|
||||
model: `${candidate.provider}/${candidate.id}`,
|
||||
});
|
||||
lastError = error;
|
||||
break; // Exit retry loop, continue to next candidate
|
||||
}
|
||||
|
||||
attempt++;
|
||||
@@ -9709,6 +9721,10 @@ export class AgentSession {
|
||||
return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
|
||||
}
|
||||
|
||||
#isCompactionSummarizationTimeoutMessage(errorMessage: string): boolean {
|
||||
return /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i.test(errorMessage);
|
||||
}
|
||||
|
||||
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
||||
// service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
|
||||
|
||||
@@ -239,6 +239,47 @@ describe("AgentSession handoff", () => {
|
||||
expect(events).toContainEqual({ type: "auto_compaction_start", reason: "threshold", action: "context-full" });
|
||||
expect(events.some(event => event.type === "auto_compaction_end" && event.aborted === false)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back after one auto-compaction timeout instead of retrying the same model", async () => {
|
||||
session.settings.set("compaction.thresholdTokens", 50);
|
||||
session.settings.set("compaction.keepRecentTokens", 1);
|
||||
session.settings.set("contextPromotion.enabled", false);
|
||||
session.settings.set("retry.baseDelayMs", 1);
|
||||
|
||||
let firstCandidateKey: string | undefined;
|
||||
let fallbackCandidateKey: string | undefined;
|
||||
const compactSpy = vi.spyOn(compactionModule, "compact").mockImplementation(async (preparation, candidate) => {
|
||||
const candidateKey = `${candidate.provider}/${candidate.id}`;
|
||||
firstCandidateKey ??= candidateKey;
|
||||
if (candidateKey === firstCandidateKey) {
|
||||
throw new Error("Summarization failed: The operation timed out.");
|
||||
}
|
||||
fallbackCandidateKey = candidateKey;
|
||||
return {
|
||||
summary: "fallback compacted",
|
||||
shortSummary: undefined,
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
details: {},
|
||||
};
|
||||
});
|
||||
const promptSpy = vi.spyOn(session.agent, "prompt").mockImplementation(async () => {
|
||||
expect(sessionManager.getEntries().some(entry => entry.type === "compaction")).toBe(true);
|
||||
});
|
||||
|
||||
await session.prompt("pending prompt ".repeat(120));
|
||||
await waitFor(
|
||||
() =>
|
||||
fallbackCandidateKey !== undefined &&
|
||||
events.some(event => event.type === "auto_compaction_end" && event.aborted === false),
|
||||
);
|
||||
|
||||
expect(
|
||||
compactSpy.mock.calls.filter(call => `${call[1].provider}/${call[1].id}` === firstCandidateKey),
|
||||
).toHaveLength(1);
|
||||
expect(fallbackCandidateKey).toBeDefined();
|
||||
expect(promptSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("keeps pre-prompt context-full checks aligned with provider-anchored usage", async () => {
|
||||
await session.dispose();
|
||||
authStorage.setRuntimeApiKey("openai", "test-key");
|
||||
|
||||
Reference in New Issue
Block a user