From d9a7ffe003195ea8bd8e3b6f00dae557a895d9ad Mon Sep 17 00:00:00 2001 From: Logan Ye Date: Thu, 2 Apr 2026 05:02:31 +0800 Subject: [PATCH] failover: classify AbortError / stream-abort messages as timeout (#58315) (#58324) Merged via squash. Prepared head SHA: d8412f27e6d91bb53a244b964adbb3d99ec77956 Co-authored-by: yelog <14227866+yelog@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + ...pi-embedded-helpers.isbillingerrormessage.test.ts | 12 ++++++++++++ src/agents/pi-embedded-helpers/failover-matches.ts | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50442bc031a..c9df539bf48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life. - Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente. - BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris. +- Agents/failover: classify AbortError and stream-abort messages as timeout so Ollama NDJSON stream aborts stop showing `reason=unknown` in model fallback logs. (#58324) Thanks @yelog ## 2026.4.2 diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index eb16f20d49a..dabe78c2d26 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -656,6 +656,18 @@ describe("isFailoverErrorMessage", () => { ]); }); + it("matches AbortError / stream-abort messages as timeout (#58315)", () => { + expectTimeoutFailoverSamples([ + "The operation was aborted", + "This operation was aborted", + "the operation was aborted", + "stream closed", + "stream was closed", + "stream aborted", + "stream was aborted", + ]); + }); + it("matches Gemini MALFORMED_RESPONSE stop reason as timeout (#42149)", () => { expectTimeoutFailoverSamples([ "Unhandled stop reason: MALFORMED_RESPONSE", diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index 405a31a81e8..1b3cb262e15 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -63,6 +63,11 @@ const ERROR_PATTERNS = { /\bstop reason:\s*(?:abort|error|malformed_response|network_error)\b/i, /\breason:\s*(?:abort|error|malformed_response|network_error)\b/i, /\bunhandled stop reason:\s*(?:abort|error|malformed_response|network_error)\b/i, + // AbortError messages from fetch/stream aborts (Ollama NDJSON stream + // timeouts, signal aborts, etc.) — without these the flattened message + // falls through to reason=unknown (#58315). + /\boperation was aborted\b/i, + /\bstream (?:was )?(?:closed|aborted)\b/i, ], billing: [ /["']?(?: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/i,