fix(agents/failover): classify bare pi-ai stream wrapper as timeout for all providers#71647
Conversation
Greptile SummaryThis PR fixes a provider-scoping bug in the failover classifier where only Anthropic-primary agents would correctly rotate fallback chains on Confidence Score: 5/5Safe to merge — targeted one-line logic change with comprehensive regression tests, no security implications. The fix is minimal and correct: the anchored regex properly isolates the exact bare message from incidental substrings, the old Anthropic-only gate is cleanly removed, and tests cover positive variants (case, whitespace, trailing period), provider-agnostic paths, and negative anchoring cases. No regressions in related test suites reported. No files require special attention. Reviews (1): Last reviewed commit: "fix(agents/failover): classify bare pi-a..." | Re-trigger Greptile |
ea00507 to
6af743b
Compare
|
Codex review landed via rebase onto
Thanks @willamhou and @mattcproctor. |
Fixes #71620.
Root cause
@mariozechner/pi-aiproviders (anthropic, google, google-vertex, openai-completions, openai-responses, mistral, amazon-bedrock, azure-openai-responses, google-gemini-cli) all throw a literalError("An unknown error occurred")from a shared stream-wrapper pattern when the stream ends withstopReason: "aborted" | "error"and no specific error info is attached.OpenClaw classified that bare string as transient (timeout) for failover only when the active provider name contained
"anthropic", viaisAnthropicGenericUnknownError()insrc/agents/pi-embedded-helpers/errors.ts:756. For non-Anthropic primaries (the issue reporter is ongoogle/gemini-2.5-flash) the classifier returnednull, so:isFailoverAssistantError()→falsefailoverFailurebranch inpi-embedded-runner/run.ts:1301never firesagents.defaults.model.fallbackschain is silently bypassedAn unknown error occurredtext is surfaced to end usersThis is provider-agnostic upstream, but the gate was provider-scoped.
Fix
src/agents/pi-embedded-helpers/errors.ts:756: renameisAnthropicGenericUnknownError(raw, provider)toisGenericUnknownStreamError(raw)and drop theisProvider(provider, "anthropic")gate. Replace theincludes("an unknown error occurred")substring check with an anchored regex/^\s*an unknown error occurred\.?\s*$/iso we only catch the bare wrapper message and not user-text or assistant prose that happens to contain the phrase.Tests previously locked the old behavior in two places:
pi-embedded-helpers.isbillingerrormessage.test.ts:742—classifies provider-scoped generic upstream messages(kept the OpenRouter assertions, dropped the "An unknown error occurred" line because it no longer needs scoping)pi-embedded-helpers.isbillingerrormessage.test.ts:754andfailover-error.test.ts:459—does not classify provider-scoped generic upstream messages without provider context(kept theProvider returned error/Key limit exceededlines, dropped the "An unknown error occurred" lines that were the bug)Added new tests covering bare/case/whitespace variants across providers
anthropic,google,openrouter, plus negative tests for wrapped messages (LLM request failed with an unknown error.and a sentence containing the phrase mid-string) so they keep returningnull.Verification
pnpm test src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts src/agents/failover-error.test.ts— 189/189 passed.pnpm test src/plugins/provider-runtime.test.ts— 30/30 passed (uses the same classifier throughisFailoverErrorMessage).npx oxlint <changed files>— 0 errors, 0 warnings.pnpm format:check <changed files>— clean.pnpm tsgo:core— same set of pre-existing unrelated errors as onc070509b7f(ui/src/ui/views/agents-*.ts,src/mcp/*,src/media/qr-runtime.ts,src/plugin-sdk/*,src/trajectory/metadata.ts); confirmed by stashing and re-running on clean main.Test plan
stopReason: "error"(issue reporter has confirmed the local one-line patch works on 2026.4.15).