diff --git a/CHANGELOG.md b/CHANGELOG.md index 8551a0ccd8b..b871b976065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,6 +225,7 @@ Docs: https://docs.openclaw.ai - macOS/browser proxy: serialize non-GET browser proxy request bodies through `AnyCodable.foundationValue` so nested JSON bodies no longer crash the macOS app with `Invalid type in JSON write (__SwiftValue)`. (#43069) Thanks @Effet. - CLI/skills tables: keep terminal table borders aligned for wide graphemes, use full reported terminal width, and switch a few ambiguous skill icons to Terminal-safe emoji so `openclaw skills` renders more consistently in Terminal.app and iTerm. Thanks @vincentkoc. - Memory/Gemini: normalize returned Gemini embeddings across direct query, direct batch, and async batch paths so memory search uses consistent vector handling for Gemini too. (#43409) Thanks @gumadeiras. +- Agents/failover: recognize additional serialized network errno strings plus `EHOSTDOWN` and `EPIPE` structured codes so transient transport failures trigger timeout failover more reliably. (#42830) Thanks @jnMetaCode. ## 2026.3.7 diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index db01c03d8c4..1548ce5496a 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -274,6 +274,8 @@ describe("failover-error", () => { it("infers timeout from common node error codes", () => { expect(resolveFailoverReasonFromError({ code: "ETIMEDOUT" })).toBe("timeout"); expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe("timeout"); + expect(resolveFailoverReasonFromError({ code: "EHOSTDOWN" })).toBe("timeout"); + expect(resolveFailoverReasonFromError({ code: "EPIPE" })).toBe("timeout"); }); it("infers timeout from abort/error stop-reason messages", () => { diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index a39685e1b16..8c49df40acb 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -170,7 +170,9 @@ export function resolveFailoverReasonFromError(err: unknown): FailoverReason | n "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH", + "EHOSTDOWN", "ENETRESET", + "EPIPE", "EAI_AGAIN", ].includes(code) ) { diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 27c89afe425..9ed183a6910 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -535,6 +535,23 @@ describe("isFailoverErrorMessage", () => { } }); + it("matches network errno codes in serialized error messages", () => { + const samples = [ + "Error: connect ETIMEDOUT 10.0.0.1:443", + "Error: connect ESOCKETTIMEDOUT 10.0.0.1:443", + "Error: connect EHOSTUNREACH 10.0.0.1:443", + "Error: connect ENETUNREACH 10.0.0.1:443", + "Error: write EPIPE", + "Error: read ENETRESET", + "Error: connect EHOSTDOWN 192.168.1.1:443", + ]; + for (const sample of samples) { + expect(isTimeoutErrorMessage(sample)).toBe(true); + expect(classifyFailoverReason(sample)).toBe("timeout"); + expect(isFailoverErrorMessage(sample)).toBe(true); + } + }); + it("does not classify MALFORMED_FUNCTION_CALL as timeout", () => { const sample = "Unhandled stop reason: MALFORMED_FUNCTION_CALL"; expect(isTimeoutErrorMessage(sample)).toBe(false); diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index a9f16fa6202..ffe0c428f55 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -37,6 +37,13 @@ const ERROR_PATTERNS = { "fetch failed", "socket hang up", /\beconn(?:refused|reset|aborted)\b/i, + /\benetunreach\b/i, + /\behostunreach\b/i, + /\behostdown\b/i, + /\benetreset\b/i, + /\betimedout\b/i, + /\besockettimedout\b/i, + /\bepipe\b/i, /\benotfound\b/i, /\beai_again\b/i, /without sending (?:any )?chunks?/i,