From 9992e78755e74e801e9a9244e9b1bf4a4ff51f53 Mon Sep 17 00:00:00 2001 From: minpeter Date: Sat, 25 Jul 2026 00:10:28 +0900 Subject: [PATCH] fix(ai): retry unavailable Codex upstreams --- packages/ai/src/changes.md | 13 +++++++++++++ packages/ai/src/utils/retry.ts | 1 + packages/ai/test/retry.test.ts | 10 ++++++++++ .../test/suite/retry-fallback-engine.test.ts | 6 ++++-- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/changes.md b/packages/ai/src/changes.md index a68630497..432fa1bd3 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 - Preserve persisted freeform identity when replaying OpenAI Responses calls (#256) ### What changed and why diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts index 1f990af2f..3cbec0609 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 56a5163f6..d693f6b86 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"; describe("provider retry classification", () => { it("matches explicit provider retry guidance", () => { @@ -40,6 +42,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" },