diff --git a/packages/ai/src/changes.md b/packages/ai/src/changes.md index 73dab1aee..e51375e0a 100644 --- a/packages/ai/src/changes.md +++ b/packages/ai/src/changes.md @@ -1,5 +1,18 @@ # AI Source Changes +## 2026-07-26 - Retry transient Codex upstream websocket failures + +### What changed and why + +- `utils/retry.ts` classifies `upstream_unavailable` provider errors as transient so the existing bounded retry policy + retries Codex websocket proxy disconnects such as `ConnectionClosedOK`. +- The retry classifier and coding-agent event-contract tests pin the exact reported error through the existing retry + lifecycle rather than introducing provider-specific retry behavior. + +### Expected merge conflict zones + +- LOW: `utils/retry.ts` transient transport error patterns. + ## 2026-07-26 - Repair unpaired Anthropic server-tool blocks and let the pairing 400 retry ### What changed and why diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts index 9840a9f41..3e9fd7c90 100644 --- a/packages/ai/src/utils/retry.ts +++ b/packages/ai/src/utils/retry.ts @@ -55,6 +55,7 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ "ENOTFOUND", "EAI_AGAIN", "upstream.?connect", + "upstream.?unavailable", "reset before headers", "socket hang up", "socket connection was closed", diff --git a/packages/ai/test/retry.test.ts b/packages/ai/test/retry.test.ts index 9297b425c..10cd466d7 100644 --- a/packages/ai/test/retry.test.ts +++ b/packages/ai/test/retry.test.ts @@ -12,6 +12,8 @@ const bunFetchSocketClosedMessage = 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)"; +const codexUpstreamUnavailableMessage = + "Error: upstream_unavailable: Codex upstream websocket send failed via proxy endpoint unknown: ConnectionClosedOK"; const anthropicOrphanServerToolMessage = '400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.1: `web_search` tool use with id `srvtoolu_01Gchdhqw1UaCNUuVq2LhMH9` was found without a corresponding `web_search_tool_result` block"},"request_id":"req_011CdQL9JsEk5NWJxWQX4NiG"}'; const anthropicInvalidMaxTokensMessage = @@ -44,6 +46,14 @@ describe("provider retry classification", () => { ).toBe(true); }); + it("matches Codex upstream websocket unavailability", () => { + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: codexUpstreamUnavailableMessage }), + ), + ).toBe(true); + }); + it("classifies agent-loop stream idle timeouts as retryable", () => { // Emitted by @earendil-works/pi-agent-core when a provider stream stops // delivering events (e.g. a connection that died after a network change). diff --git a/packages/coding-agent/test/suite/retry-fallback-engine.test.ts b/packages/coding-agent/test/suite/retry-fallback-engine.test.ts index 076563330..646240c0f 100644 --- a/packages/coding-agent/test/suite/retry-fallback-engine.test.ts +++ b/packages/coding-agent/test/suite/retry-fallback-engine.test.ts @@ -6,6 +6,8 @@ import { createHarness, type Harness } from "./harness.ts"; const primary = "faux/faux-1"; const fallback = "faux/faux-2"; +const codexUpstreamUnavailableMessage = + "Error: upstream_unavailable: Codex upstream websocket send failed via proxy endpoint unknown: ConnectionClosedOK"; type EventTranscriptEntry = | { type: "message_start" | "message_end"; role: string } @@ -637,7 +639,7 @@ describe("retry fallback engine", () => { harness.setResponses([ fauxAssistantMessage("", { stopReason: "error", - errorMessage: "overloaded_error", + errorMessage: codexUpstreamUnavailableMessage, }), fauxAssistantMessage("recovered"), ]); @@ -662,7 +664,7 @@ describe("retry fallback engine", () => { attempt: 1, maxAttempts: 3, delayMs: 1, - errorMessage: "overloaded_error", + errorMessage: codexUpstreamUnavailableMessage, }, { type: "agent_start" }, { type: "turn_start" },