fix: allow fallback on timeout aborts

Co-authored-by: Larus Ivarsson <larusivar@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-20 19:23:06 +00:00
parent 4999f15688
commit 21370fc09b
3 changed files with 24 additions and 4 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.clawd.bot
### Fixes
- Discovery: shorten Bonjour DNS-SD service type to `_clawdbot-gw._tcp` and update discovery clients/docs.
- Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241) — thanks @gnarco.
- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs.
- Doctor: clarify plugin auto-enable hint text in the startup banner.
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).

View File

@@ -326,6 +326,26 @@ describe("runWithModelFallback", () => {
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
});
it("falls back when message says aborted but error is a timeout", async () => {
const cfg = makeCfg();
const run = vi
.fn()
.mockRejectedValueOnce(Object.assign(new Error("request aborted"), { code: "ETIMEDOUT" }))
.mockResolvedValueOnce("ok");
const result = await runWithModelFallback({
cfg,
provider: "openai",
model: "gpt-4.1-mini",
run,
});
expect(result.result).toBe("ok");
expect(run).toHaveBeenCalledTimes(2);
expect(run.mock.calls[1]?.[0]).toBe("anthropic");
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
});
it("does not fall back on user aborts", async () => {
const cfg = makeCfg();
const run = vi

View File

@@ -33,10 +33,9 @@ function isAbortError(err: unknown): boolean {
if (!err || typeof err !== "object") return false;
if (isFailoverError(err)) return false;
const name = "name" in err ? String(err.name) : "";
if (name === "AbortError") return true;
const message =
"message" in err && typeof err.message === "string" ? err.message.toLowerCase() : "";
return message.includes("aborted");
// Only treat explicit AbortError names as user aborts.
// Message-based checks (e.g., "aborted") can mask timeouts and skip fallback.
return name === "AbortError";
}
function shouldRethrowAbort(err: unknown): boolean {