fix(failover): add missing network errno patterns to text-based timeout classifier (#42830)

Merged via squash.

Prepared head SHA: 91761487e8825c0fd6582a762d04bba04f726a85
Co-authored-by: jnMetaCode <12096460+jnMetaCode@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
jnMetaCode 2026-03-12 17:34:44 +08:00 committed by GitHub
parent a6711afdc2
commit f640326e31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 29 additions and 0 deletions

View File

@ -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

View File

@ -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", () => {

View File

@ -170,7 +170,9 @@ export function resolveFailoverReasonFromError(err: unknown): FailoverReason | n
"ECONNREFUSED",
"ENETUNREACH",
"EHOSTUNREACH",
"EHOSTDOWN",
"ENETRESET",
"EPIPE",
"EAI_AGAIN",
].includes(code)
) {

View File

@ -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);

View File

@ -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,