diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts index 85ab4636..463b9032 100644 --- a/packages/ai/src/utils/retry.ts +++ b/packages/ai/src/utils/retry.ts @@ -51,6 +51,9 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ "connection.?lost", "other side closed", "fetch failed", + "getaddrinfo", + "ENOTFOUND", + "EAI_AGAIN", "upstream.?connect", "reset before headers", "socket hang up", diff --git a/packages/ai/test/retry.test.ts b/packages/ai/test/retry.test.ts index 12ced28c..fc79f691 100644 --- a/packages/ai/test/retry.test.ts +++ b/packages/ai/test/retry.test.ts @@ -10,6 +10,8 @@ const nvidiaNIMResourceExhaustedMessage = "ResourceExhausted: Worker local total const bunFetchSocketClosedMessage = "The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"; const openAIResponsesEarlyEofMessage = "OpenAI Responses stream ended before a terminal response event"; +const wrappedDnsLookupError = + "The pending stream has been canceled (caused by: getaddrinfo ENOTFOUND bedrock-runtime.us-east-1.amazonaws.com)"; describe("provider retry classification", () => { it("matches explicit provider retry guidance", () => { @@ -38,6 +40,15 @@ describe("provider retry classification", () => { ).toBe(true); }); + it.each([ + wrappedDnsLookupError, + "connect ENOTFOUND api.example.com", + "EAI_AGAIN api.example.com", + "getaddrinfo failed for api.example.com", + ])("matches DNS transport failure wording: %s", (errorMessage) => { + expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true); + }); + it("matches OpenAI Responses streams that end before terminal events", () => { expect( isRetryableAssistantError( diff --git a/packages/coding-agent/test/suite/regressions/6904-dns-transport-retry.test.ts b/packages/coding-agent/test/suite/regressions/6904-dns-transport-retry.test.ts new file mode 100644 index 00000000..9f25cacb --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/6904-dns-transport-retry.test.ts @@ -0,0 +1,28 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { createHarness } from "../harness.ts"; + +const wrappedDnsLookupError = + "The pending stream has been canceled (caused by: getaddrinfo ENOTFOUND bedrock-runtime.us-east-1.amazonaws.com)"; + +describe("issue #6904 DNS transport failure retry", () => { + it("retries a transient DNS lookup failure", async () => { + const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } } }); + try { + harness.setResponses([ + fauxAssistantMessage("", { stopReason: "error", errorMessage: wrappedDnsLookupError }), + fauxAssistantMessage("recovered after DNS retry"), + ]); + + await harness.session.prompt("test"); + + expect(harness.faux.state.callCount).toBe(2); + expect(harness.eventsOfType("auto_retry_start").map((event) => event.errorMessage)).toEqual([ + wrappedDnsLookupError, + ]); + expect(harness.eventsOfType("auto_retry_end").map((event) => event.success)).toEqual([true]); + } finally { + harness.cleanup(); + } + }); +});