diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index a99cfb5c4b2..db01c03d8c4 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -67,6 +67,7 @@ describe("failover-error", () => { expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe("rate_limit"); expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth"); expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout"); + expect(resolveFailoverReasonFromError({ status: 499 })).toBe("timeout"); expect(resolveFailoverReasonFromError({ status: 400 })).toBe("format"); // Keep the status-only path behavior-preserving and conservative. expect(resolveFailoverReasonFromError({ status: 500 })).toBeNull(); @@ -93,6 +94,12 @@ describe("failover-error", () => { message: ANTHROPIC_OVERLOADED_PAYLOAD, }), ).toBe("overloaded"); + expect( + resolveFailoverReasonFromError({ + status: 499, + message: ANTHROPIC_OVERLOADED_PAYLOAD, + }), + ).toBe("overloaded"); expect( resolveFailoverReasonFromError({ status: 429, diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 86fd90e7161..f60a127a0ab 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -443,6 +443,7 @@ describe("isLikelyContextOverflowError", () => { describe("isTransientHttpError", () => { it("returns true for retryable 5xx status codes", () => { + expect(isTransientHttpError("499 Client Closed Request")).toBe(true); expect(isTransientHttpError("500 Internal Server Error")).toBe(true); expect(isTransientHttpError("502 Bad Gateway")).toBe(true); expect(isTransientHttpError("503 Service Unavailable")).toBe(true); @@ -457,6 +458,19 @@ describe("isTransientHttpError", () => { }); }); +describe("classifyFailoverReasonFromHttpStatus", () => { + it("treats HTTP 499 as transient for structured errors", () => { + expect(classifyFailoverReasonFromHttpStatus(499)).toBe("timeout"); + expect(classifyFailoverReasonFromHttpStatus(499, "499 Client Closed Request")).toBe("timeout"); + expect( + classifyFailoverReasonFromHttpStatus( + 499, + '{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}', + ), + ).toBe("overloaded"); + }); +}); + describe("isFailoverErrorMessage", () => { it("matches auth/rate/billing/timeout", () => { const samples = [ diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 69203328c5e..9ab52c04355 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -1,6 +1,5 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import type { OpenClawConfig } from "../../config/config.js"; -import type { FailoverReason } from "./types.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { formatSandboxToolPolicyBlockedMessage } from "../sandbox.js"; import { stableStringify } from "../stable-stringify.js"; @@ -14,6 +13,7 @@ import { isTimeoutErrorMessage, matchesFormatErrorPattern, } from "./failover-matches.js"; +import type { FailoverReason } from "./types.js"; export { isAuthErrorMessage, @@ -375,6 +375,12 @@ export function classifyFailoverReasonFromHttpStatus( } return "timeout"; } + if (status === 499) { + if (message && isOverloadedErrorMessage(message)) { + return "overloaded"; + } + return "timeout"; + } if (status === 502 || status === 504) { return "timeout"; }