fix(compaction): retry transient native failures

(cherry picked from commit 293c96dfa479de13995d581caec33b3c90b5b3ca)
This commit is contained in:
usr-bin-roygbiv
2026-07-28 13:16:41 +00:00
committed by can1357
parent 792e8a06b1
commit d084697fae
2 changed files with 50 additions and 7 deletions
@@ -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,
@@ -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,