diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index e10d0ff4242..813cb8b7cb4 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -917,6 +917,11 @@ describe("classifyFailoverReasonFromHttpStatus – 402 temporary limits", () => expect(classifyFailoverReason(zenMuxMessage)).toBe("rate_limit"); }); + it("does not classify numeric references that merely start with 402", () => { + expect(classifyFailoverReason("402 items found in the database")).toBeNull(); + expect(classifyFailoverReason("402 records processed")).toBeNull(); + }); + it("keeps plan-upgrade 402 limit messages in billing", () => { const billingMessage = "Your usage limit has been reached. Please upgrade your plan."; expect(classifyFailoverReason(`HTTP 402 Payment Required: ${billingMessage}`)).toBe("billing"); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 0f0c0cec872..c9492f6ddbe 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -294,7 +294,8 @@ const RETRYABLE_402_SCOPED_RESULT_HINTS = [ "exhausted", ] as const; const RAW_402_MARKER_RE = - /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\b/i; + /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment required\b|^\s*402\s+.*used up your points\b/i; +const BARE_LEADING_402_RE = /^\s*402\b/i; const LEADING_402_WRAPPER_RE = /^(?:error[:\s-]+)?(?:(?:http\s*)?402(?:\s+payment required)?|payment required)(?:[:\s-]+|$)/i; const TIMEOUT_ERROR_CODES = new Set([ @@ -476,6 +477,15 @@ function hasRetryable402TransientSignal(text: string): boolean { ); } +function hasKnownBareLeading402Signal(text: string): boolean { + return ( + hasQuotaRefreshWindowSignal(text) || + hasExplicit402BillingSignal(text) || + isRateLimitErrorMessage(text) || + hasRetryable402TransientSignal(text) + ); +} + function normalize402Message(raw: string): string { return normalizeOptionalLowercaseString(raw)?.replace(LEADING_402_WRAPPER_RE, "").trim() ?? ""; } @@ -506,7 +516,14 @@ function classify402Message(message: string): PaymentRequiredFailoverReason { } function classifyFailoverReasonFrom402Text(raw: string): PaymentRequiredFailoverReason | null { - if (!RAW_402_MARKER_RE.test(raw)) { + if (RAW_402_MARKER_RE.test(raw)) { + return classify402Message(raw); + } + if (!BARE_LEADING_402_RE.test(raw)) { + return null; + } + const normalized = normalize402Message(raw); + if (!normalized || !hasKnownBareLeading402Signal(normalized)) { return null; } return classify402Message(raw);