diff --git a/packages/coding-agent/src/session/session-maintenance.ts b/packages/coding-agent/src/session/session-maintenance.ts index d3a83d903..e0c3a9f86 100644 --- a/packages/coding-agent/src/session/session-maintenance.ts +++ b/packages/coding-agent/src/session/session-maintenance.ts @@ -2537,16 +2537,14 @@ export class SessionMaintenance { } const message = error instanceof Error ? error.message : String(error); - const id = AIError.classify(error, candidate.api); + const id = AIError.classify( + error instanceof NativeCompactionError ? error.cause : error, + candidate.api, + ); if (AIError.is(id, AIError.Flag.AuthFailed)) { lastError = this.#buildCompactionAuthError(); break; } - if (error instanceof NativeCompactionError) { - nativeCompactionFailure ??= { error, provider: candidate.provider }; - lastError = nativeCompactionFailure.error; - break; - } if (AIError.is(id, AIError.Flag.Timeout)) { logger.warn( hasMoreCandidates @@ -2569,7 +2567,12 @@ export class SessionMaintenance { AIError.is(id, AIError.Flag.Transient) || AIError.is(id, AIError.Flag.UsageLimit)); if (!shouldRetry) { - lastError = error; + if (error instanceof NativeCompactionError) { + nativeCompactionFailure ??= { error, provider: candidate.provider }; + lastError = nativeCompactionFailure.error; + } else { + lastError = error; + } break; } @@ -2579,6 +2582,11 @@ export class SessionMaintenance { // If retry delay is too long (>30s), try next candidate instead of waiting const maxAcceptableDelayMs = 30_000; if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) { + if (error instanceof NativeCompactionError) { + nativeCompactionFailure ??= { error, provider: candidate.provider }; + lastError = nativeCompactionFailure.error; + break; + } logger.warn("Auto-compaction retry delay too long, trying next model", { delayMs, retryAfterMs, diff --git a/packages/coding-agent/test/issue-986-compaction-auth-fallback.test.ts b/packages/coding-agent/test/issue-986-compaction-auth-fallback.test.ts index 8423cbe8c..a05649a3b 100644 --- a/packages/coding-agent/test/issue-986-compaction-auth-fallback.test.ts +++ b/packages/coding-agent/test/issue-986-compaction-auth-fallback.test.ts @@ -1,7 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; +import { scheduler } from "node:timers/promises"; import { Agent } from "@oh-my-pi/pi-agent-core"; import * as compactionModule from "@oh-my-pi/pi-agent-core/compaction"; +import * as AIError from "@oh-my-pi/pi-ai/error"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; @@ -188,6 +190,39 @@ describe("issue #986 compaction auth fallback", () => { ]); }); + it("retries a transient native compaction failure on the same candidate", async () => { + const { currentModel, triggerAutoCompaction } = await createAutoNativeFallbackSession(); + session.settings.set("retry.enabled", true); + session.settings.set("retry.baseDelayMs", 1); + session.settings.set("retry.maxRetries", 1); + const waitSpy = vi.spyOn(scheduler, "wait").mockResolvedValue(undefined); + const attemptedModels: string[] = []; + vi.spyOn(compactionModule, "compact").mockImplementation(async (preparation, model) => { + attemptedModels.push(`${model.provider}/${model.id}`); + if (model.provider !== currentModel.provider || model.id !== currentModel.id) { + throw new Error(`Unexpected compaction model ${model.provider}/${model.id}`); + } + if (attemptedModels.length === 1) { + throw new compactionModule.NativeCompactionError( + new AIError.ProviderHttpError("native compaction temporarily unavailable", 503), + ); + } + return { + summary: "native retry summary", + shortSummary: "native retry", + firstKeptEntryId: preparation.firstKeptEntryId, + tokensBefore: 42, + }; + }); + + await triggerAutoCompaction(); + + expect(attemptedModels).toEqual([ + `${currentModel.provider}/${currentModel.id}`, + `${currentModel.provider}/${currentModel.id}`, + ]); + expect(waitSpy).toHaveBeenCalledTimes(1); + }); it("stops auto-compaction before a same-provider candidate with native compaction disabled", async () => { const { currentModel, sameProviderModel, triggerAutoCompaction } = await createAutoNativeFallbackSession({ sameProviderNativeEnabled: false,