From 580400de9ecc393894a4302cd9ca5ca92a9e7e4c Mon Sep 17 00:00:00 2001 From: Trevor Edwards <9443483+trevoedwards@users.noreply.github.com> Date: Wed, 4 Nov 2026 04:59:27 +0000 Subject: [PATCH 1/6] fix(codex): preserve terminal connection failure categories --- .../Adapters/CodexAdapterV2.test.ts | 31 +++++++++++++++++ .../Adapters/CodexAdapterV2.ts | 33 +++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 9482bb5a3fe25..fb0fd26dd0dc5 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5607,6 +5607,37 @@ describe("CodexAdapterV2 post-settle continuation", () => { notification: false, expectedClass: "provider_error", }, + ...([false, true] as const).map( + (notification) => + ({ + name: `disconnected-${notification ? "notification" : "completion"}`, + code: "responseStreamDisconnected", + errorInfo: { responseStreamDisconnected: { httpStatusCode: null } }, + notification, + expectedClass: "transport_error", + }) as const, + ), + { + name: "connection-failed", + code: "responseStreamConnectionFailed", + errorInfo: { responseStreamConnectionFailed: { httpStatusCode: null } }, + notification: false, + expectedClass: "transport_error", + }, + { + name: "http-connection-failed", + code: "httpConnectionFailed", + errorInfo: { httpConnectionFailed: { httpStatusCode: null } }, + notification: false, + expectedClass: "transport_error", + }, + { + name: "http-provider-failure", + code: "httpConnectionFailed", + errorInfo: { httpConnectionFailed: { httpStatusCode: 503 } }, + notification: false, + expectedClass: "provider_error", + }, { name: "chatgpt-workspace-limit", code: "other", diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 0f8e0ef1f3366..040b4ebb1b388 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1146,6 +1146,25 @@ function codexUsageLimitError( ); } +function codexTerminalFailureClass( + errorInfo: CodexErrorInfo | null | undefined, + code: string | null, +) { + if (codexUsageLimitError(code, errorInfo)) return "usage_limit"; + if (typeof errorInfo === "object" && errorInfo !== null) { + const transport = + "responseStreamDisconnected" in errorInfo + ? errorInfo.responseStreamDisconnected + : "responseStreamConnectionFailed" in errorInfo + ? errorInfo.responseStreamConnectionFailed + : "httpConnectionFailed" in errorInfo + ? errorInfo.httpConnectionFailed + : undefined; + if (transport !== undefined && transport.httpStatusCode == null) return "transport_error"; + } + return "provider_error"; +} + /** * ChatGPT sharing reports its own reason inside Codex's generic turn error, so * the code it names replaces Codex's, and clients pick a recovery from it. @@ -4696,9 +4715,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( makeProviderFailure({ message: payload.error.additionalDetails?.trim() || payload.error.message, code: notificationCode, - class: codexUsageLimitError(notificationCode, payload.error.codexErrorInfo) - ? "usage_limit" - : "provider_error", + class: codexTerminalFailureClass( + payload.error.codexErrorInfo, + notificationCode, + ), }), payload.error, ), @@ -5790,9 +5810,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( input.failureErrorInfo === undefined ? undefined : codexErrorInfoCode(input.failureErrorInfo); - const failureClass = codexUsageLimitError(failureCode ?? null, input.failureErrorInfo) - ? "usage_limit" - : "provider_error"; + const failureClass = codexTerminalFailureClass( + input.failureErrorInfo, + failureCode ?? null, + ); // The notification carries the richer message, so it wins when the // completion repeats it. An explicit exhaustion status on the // completion still decides the class; a missing one is not evidence. From 53edc75cc53688bd77164fdcfe838de303b74e87 Mon Sep 17 00:00:00 2001 From: Trevor Edwards <9443483+trevoedwards@users.noreply.github.com> Date: Wed, 4 Nov 2026 05:41:32 +0000 Subject: [PATCH 2/6] fix(codex): honor explicit completion failure evidence --- .../Adapters/CodexAdapterV2.test.ts | 64 +++++++++++++++++-- .../Adapters/CodexAdapterV2.ts | 11 ++-- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index fb0fd26dd0dc5..db92be5df839e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5848,35 +5848,78 @@ describe("CodexAdapterV2 post-settle continuation", () => { // regression: #17501 Codex 429 retry exhaustion classifies as a usage limit it.effect.each([ { + code: "responseTooManyFailedAttempts", notificationStatus: 500, completionStatus: 429, willRetry: false, expectedClass: "usage_limit", }, { + code: "responseTooManyFailedAttempts", notificationStatus: 429, completionStatus: 500, willRetry: true, expectedClass: "provider_error", }, { + code: "responseTooManyFailedAttempts", notificationStatus: 429, completionStatus: null, willRetry: false, expectedClass: "usage_limit", }, { + code: "responseTooManyFailedAttempts", notificationStatus: 429, completionStatus: null, willRetry: true, expectedClass: "usage_limit", }, + ...( + [ + "responseStreamDisconnected", + "responseStreamConnectionFailed", + "httpConnectionFailed", + ] as const + ).flatMap( + (code) => + [ + { + code, + notificationStatus: null, + completionStatus: 503, + willRetry: false, + expectedClass: "provider_error", + }, + { + code, + notificationStatus: 503, + completionStatus: null, + willRetry: false, + expectedClass: "transport_error", + }, + ] as const, + ), + { + code: "responseStreamDisconnected", + notificationStatus: null, + completionStatus: 503, + willRetry: true, + expectedClass: "provider_error", + }, + { + code: "responseStreamDisconnected", + notificationStatus: null, + completionStatus: null, + willRetry: false, + expectedClass: "transport_error", + }, ] as const)( - "honors completion HTTP status from $notificationStatus to $completionStatus (retry: $willRetry)", - ({ notificationStatus, completionStatus, expectedClass, willRetry }) => + "honors $code completion HTTP status from $notificationStatus to $completionStatus (retry: $willRetry)", + ({ code, notificationStatus, completionStatus, expectedClass, willRetry }) => Effect.scoped( Effect.gen(function* () { - const name = `${notificationStatus}-${completionStatus}-${willRetry}`; + const name = `${code}-${notificationStatus}-${completionStatus}-${willRetry}`; const nativeThreadId = `native-exhaustion-status-${name}`; const nativeTurnId = `turn-exhaustion-status-${name}`; const message = "Provider stopped this request."; @@ -5897,7 +5940,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { error: { message, codexErrorInfo: { - responseTooManyFailedAttempts: { httpStatusCode: notificationStatus }, + [code]: { httpStatusCode: notificationStatus }, }, additionalDetails, }, @@ -5916,7 +5959,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { error: { message, codexErrorInfo: { - responseTooManyFailedAttempts: { httpStatusCode: completionStatus }, + [code]: { httpStatusCode: completionStatus }, }, }, }, @@ -5940,9 +5983,16 @@ describe("CodexAdapterV2 post-settle continuation", () => { assert.equal(terminal?.status, "failed"); if (terminal?.status !== "failed") return; assert.equal(terminal.failure.class, expectedClass); - assert.equal(terminal.failure.code, "responseTooManyFailedAttempts"); + assert.equal(terminal.failure.code, code); assert.equal(terminal.threadDisposition, "reusable"); - const notificationClass = notificationStatus === 429 ? "usage_limit" : "provider_error"; + const notificationClass = + code === "responseTooManyFailedAttempts" + ? notificationStatus === 429 + ? "usage_limit" + : "provider_error" + : notificationStatus === null + ? "transport_error" + : "provider_error"; assert.equal( terminal.failure.message, expectedClass === notificationClass ? additionalDetails : message, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 040b4ebb1b388..9e1da81284920 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -5814,16 +5814,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( input.failureErrorInfo, failureCode ?? null, ); - // The notification carries the richer message, so it wins when the - // completion repeats it. An explicit exhaustion status on the - // completion still decides the class; a missing one is not evidence. - const exhaustionStatus = codexExhaustionHttpStatus(input.failureErrorInfo); + // Retry exhaustion without a status cannot replace the notification's evidence. + const completionHasClass = + failureCode != null && + (failureCode !== "responseTooManyFailedAttempts" || + codexExhaustionHttpStatus(input.failureErrorInfo) !== null); const failure = previousFailure !== undefined && (input.failureMessage === undefined || input.failureMessage === previousFailure.nativeMessage) && (failureCode === undefined || failureCode === previousFailure.nativeCode) && - (exhaustionStatus === null || previousFailure.failure.class === failureClass) + (!completionHasClass || previousFailure.failure.class === failureClass) ? previousFailure.failure : withCodexManagedFailure( makeProviderFailure({ From 5b6ab8abdc7338ac2b8de2324f3e8bf1cab5df0f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 4 Nov 2026 08:29:04 +0000 Subject: [PATCH 3/6] fix(codex): retain transport evidence after generic retry exhaustion --- .../Adapters/CodexAdapterV2.test.ts | 66 +++++++++++++++++-- .../Adapters/CodexAdapterV2.ts | 17 ++++- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index db92be5df839e..0f42c01f826ec 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5645,6 +5645,34 @@ describe("CodexAdapterV2 post-settle continuation", () => { notification: true, expectedClass: "provider_error", }, + ...( + [ + { + name: "generic-disconnection", + status: null, + terminalMessage: "Connection lost.", + expectedClass: "transport_error", + }, + { + name: "generic-replacement", + status: null, + terminalMessage: "A different failure.", + expectedClass: "provider_error", + }, + { + name: "generic-http-status", + status: 503, + terminalMessage: "Connection lost.", + expectedClass: "provider_error", + }, + ] as const + ).map((scenario) => ({ + ...scenario, + code: "responseStreamDisconnected" as const, + errorInfo: { responseStreamDisconnected: { httpStatusCode: scenario.status } }, + notification: true, + details: "Connection lost.", + })), ] as const)("classifies Codex terminal failures from $name evidence", (scenario) => Effect.scoped( Effect.gen(function* () { @@ -5717,7 +5745,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { params: { threadId: nativeThreadId, turnId: nativeTurnId, - willRetry: scenario.name === "retry", + willRetry: scenario.name === "retry" || "terminalMessage" in scenario, error: { message, codexErrorInfo: errorInfo, @@ -5733,6 +5761,23 @@ describe("CodexAdapterV2 post-settle continuation", () => { }, ] : []), + ...("terminalMessage" in scenario + ? [ + { + type: "emit_inbound" as const, + label: "error/terminal", + frame: { + method: "error", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + willRetry: false, + error: { message: scenario.terminalMessage, codexErrorInfo: "other" }, + }, + }, + }, + ] + : []), { type: "emit_inbound", label: "turn/completed", @@ -5743,12 +5788,19 @@ describe("CodexAdapterV2 post-settle continuation", () => { turn: { ...makeCodexReplayTurn({ id: nativeTurnId, status: "failed" }), error: { - message: scenario.name === "replacement" ? "A different failure." : message, - ...(scenario.notification && - scenario.name !== "matching-details" && - !("details" in scenario) - ? {} - : { codexErrorInfo: errorInfo }), + message: + "terminalMessage" in scenario + ? scenario.terminalMessage + : scenario.name === "replacement" + ? "A different failure." + : message, + ...("terminalMessage" in scenario + ? { codexErrorInfo: "other" } + : scenario.notification && + scenario.name !== "matching-details" && + !("details" in scenario) + ? {} + : { codexErrorInfo: errorInfo }), }, }, }, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 9e1da81284920..f9c09d6c65bc4 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1217,6 +1217,7 @@ interface ActiveCodexProviderRetry { readonly context?: ActiveCodexTurnContext; readonly nativeMessage: string; readonly nativeCode: string | null; + readonly errorInfo?: CodexErrorInfo | null; readonly retry: OrchestrationV2ProviderRetry; readonly failure: OrchestrationV2ProviderFailure; readonly startedAt: DateTime.Utc; @@ -4758,6 +4759,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ...(previous?.context === undefined ? {} : { context: previous.context }), nativeMessage: payload.error.message, nativeCode: code, + ...(payload.error.codexErrorInfo === undefined + ? {} + : { errorInfo: payload.error.codexErrorInfo }), retry, failure, startedAt: previous?.startedAt ?? updatedAt, @@ -5819,7 +5823,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( failureCode != null && (failureCode !== "responseTooManyFailedAttempts" || codexExhaustionHttpStatus(input.failureErrorInfo) !== null); - const failure = + let failure = previousFailure !== undefined && (input.failureMessage === undefined || input.failureMessage === previousFailure.nativeMessage) && @@ -5834,6 +5838,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( }), input.failureMessage, ); + const retry = input.providerRetry; + // Codex 0.164 reports an exhausted stream error as `other` with the same details. + if ( + failure.code === "other" && + failure.class === "provider_error" && + retry !== undefined && + failure.message === retry.failure.message && + codexTerminalFailureClass(retry.errorInfo, retry.nativeCode) === "transport_error" + ) { + failure = { ...failure, class: "transport_error" }; + } return { type: "turn.terminal", driver: CODEX_PROVIDER, From 928e82a273fbcdd9c0c2203f10b6d29349db0414 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 4 Nov 2026 08:37:42 +0000 Subject: [PATCH 4/6] fix(codex): compare native retry details before formatting --- .../src/orchestration-v2/Adapters/CodexAdapterV2.test.ts | 9 ++++++++- .../src/orchestration-v2/Adapters/CodexAdapterV2.ts | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 0f42c01f826ec..e172eb32d5f12 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5665,13 +5665,20 @@ describe("CodexAdapterV2 post-settle continuation", () => { terminalMessage: "Connection lost.", expectedClass: "provider_error", }, + { + name: "generic-truncated-replacement", + status: null, + details: `${"Connection lost. ".repeat(300)}Network cause.`, + terminalMessage: `${"Connection lost. ".repeat(300)}Different cause.`, + expectedClass: "provider_error", + }, ] as const ).map((scenario) => ({ ...scenario, code: "responseStreamDisconnected" as const, errorInfo: { responseStreamDisconnected: { httpStatusCode: scenario.status } }, notification: true, - details: "Connection lost.", + details: "details" in scenario ? scenario.details : "Connection lost.", })), ] as const)("classifies Codex terminal failures from $name evidence", (scenario) => Effect.scoped( diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index f9c09d6c65bc4..62835057ec195 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1217,6 +1217,7 @@ interface ActiveCodexProviderRetry { readonly context?: ActiveCodexTurnContext; readonly nativeMessage: string; readonly nativeCode: string | null; + readonly nativeDetails?: string; readonly errorInfo?: CodexErrorInfo | null; readonly retry: OrchestrationV2ProviderRetry; readonly failure: OrchestrationV2ProviderFailure; @@ -4759,6 +4760,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ...(previous?.context === undefined ? {} : { context: previous.context }), nativeMessage: payload.error.message, nativeCode: code, + nativeDetails: payload.error.additionalDetails || payload.error.message, ...(payload.error.codexErrorInfo === undefined ? {} : { errorInfo: payload.error.codexErrorInfo }), @@ -5844,7 +5846,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( failure.code === "other" && failure.class === "provider_error" && retry !== undefined && - failure.message === retry.failure.message && + input.failureMessage !== undefined && + input.failureMessage === retry.nativeDetails && codexTerminalFailureClass(retry.errorInfo, retry.nativeCode) === "transport_error" ) { failure = { ...failure, class: "transport_error" }; From 7c444b9734e16ab8ac440aade13cc8811a8a2454 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 4 Nov 2026 08:51:02 +0000 Subject: [PATCH 5/6] fix(codex): honor terminal status before retry evidence --- .../Adapters/CodexAdapterV2.test.ts | 36 +++++++++++++++++-- .../Adapters/CodexAdapterV2.ts | 23 +++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index e172eb32d5f12..a699396913330 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5672,6 +5672,29 @@ describe("CodexAdapterV2 post-settle continuation", () => { terminalMessage: `${"Connection lost. ".repeat(300)}Different cause.`, expectedClass: "provider_error", }, + { + name: "generic-terminal-status", + status: null, + terminalMessage: "Connection lost.", + terminalErrorInfo: { responseStreamDisconnected: { httpStatusCode: 503 } }, + expectedClass: "provider_error", + }, + { + name: "generic-exhaustion", + status: null, + terminalMessage: "Connection lost.", + terminalErrorInfo: { responseTooManyFailedAttempts: { httpStatusCode: null } }, + completionErrorInfo: { responseTooManyFailedAttempts: { httpStatusCode: null } }, + expectedClass: "transport_error", + }, + { + name: "generic-exhaustion-status", + status: null, + terminalMessage: "Connection lost.", + terminalErrorInfo: { responseTooManyFailedAttempts: { httpStatusCode: 503 } }, + completionErrorInfo: { responseTooManyFailedAttempts: { httpStatusCode: null } }, + expectedClass: "provider_error", + }, ] as const ).map((scenario) => ({ ...scenario, @@ -5779,7 +5802,11 @@ describe("CodexAdapterV2 post-settle continuation", () => { threadId: nativeThreadId, turnId: nativeTurnId, willRetry: false, - error: { message: scenario.terminalMessage, codexErrorInfo: "other" }, + error: { + message: scenario.terminalMessage, + codexErrorInfo: + "terminalErrorInfo" in scenario ? scenario.terminalErrorInfo : "other", + }, }, }, }, @@ -5802,7 +5829,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { ? "A different failure." : message, ...("terminalMessage" in scenario - ? { codexErrorInfo: "other" } + ? { + codexErrorInfo: + "completionErrorInfo" in scenario + ? scenario.completionErrorInfo + : "other", + } : scenario.notification && scenario.name !== "matching-details" && !("details" in scenario) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 62835057ec195..680b514c3bff8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1165,6 +1165,16 @@ function codexTerminalFailureClass( return "provider_error"; } +function codexTerminalErrorNeedsEvidence( + errorInfo: CodexErrorInfo | null | undefined, + code: string | null, +) { + return ( + code === "other" || + (code === "responseTooManyFailedAttempts" && codexExhaustionHttpStatus(errorInfo) === null) + ); +} + /** * ChatGPT sharing reports its own reason inside Codex's generic turn error, so * the code it names replaces Codex's, and clients pick a recovery from it. @@ -1187,6 +1197,7 @@ interface ActiveCodexTurnContext { latestProviderFailure?: { readonly nativeMessage: string; readonly nativeCode: string | null; + readonly errorInfo?: CodexErrorInfo | null; readonly failure: OrchestrationV2ProviderFailure; }; readonly nativeStartReady?: Deferred.Deferred; @@ -4713,6 +4724,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( context.latestProviderFailure = { nativeMessage: payload.error.message, nativeCode: notificationCode, + ...(payload.error.codexErrorInfo === undefined + ? {} + : { errorInfo: payload.error.codexErrorInfo }), failure: withCodexManagedFailure( makeProviderFailure({ message: payload.error.additionalDetails?.trim() || payload.error.message, @@ -5841,10 +5855,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( input.failureMessage, ); const retry = input.providerRetry; + const notification = input.context.latestProviderFailure; // Codex 0.164 reports an exhausted stream error as `other` with the same details. if ( - failure.code === "other" && + codexTerminalErrorNeedsEvidence(input.failureErrorInfo, failure.code) && failure.class === "provider_error" && + (notification === undefined || + notification.failure.class === "transport_error" || + codexTerminalErrorNeedsEvidence( + notification.errorInfo, + notification.nativeCode, + )) && retry !== undefined && input.failureMessage !== undefined && input.failureMessage === retry.nativeDetails && From 234ec867821314837c91b920b29d307063afbc71 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 5 Nov 2026 04:53:19 +0000 Subject: [PATCH 6/6] fix(codex): reuse matching transport notifications and blank-detail retries --- .../Adapters/CodexAdapterV2.test.ts | 17 ++++++++++++++++- .../Adapters/CodexAdapterV2.ts | 18 +++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 7569ab931d6a5..b6ff070c4bbf7 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5682,7 +5682,15 @@ describe("CodexAdapterV2 post-settle continuation", () => { errorInfo: { responseStreamDisconnected: { httpStatusCode: null } }, message: "Transport unavailable; this is not subscription_sharing_usage_limit_exceeded.", notification: false, - expectedClass: "provider_error", + expectedClass: "transport_error", + }, + { + name: "terminal-disconnection", + code: "responseStreamDisconnected", + notification: false, + terminalMessage: "Connection lost.", + terminalErrorInfo: { responseStreamDisconnected: { httpStatusCode: null } }, + expectedClass: "transport_error", }, { name: "sharing-code-mention-retry", @@ -5735,6 +5743,13 @@ describe("CodexAdapterV2 post-settle continuation", () => { completionErrorInfo: { responseTooManyFailedAttempts: { httpStatusCode: null } }, expectedClass: "transport_error", }, + { + name: "generic-blank-details", + status: null, + details: " ", + terminalMessage: "Provider stopped this request.", + expectedClass: "transport_error", + }, { name: "generic-exhaustion-status", status: null, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index a4101480fffe2..26ab4a31ee4ce 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -4790,7 +4790,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ...(previous?.context === undefined ? {} : { context: previous.context }), nativeMessage: payload.error.message, nativeCode: code, - nativeDetails: payload.error.additionalDetails || payload.error.message, + nativeDetails: additionalDetails + ? (payload.error.additionalDetails ?? additionalDetails) + : payload.error.message, ...(payload.error.codexErrorInfo === undefined ? {} : { errorInfo: payload.error.codexErrorInfo }), @@ -5853,8 +5855,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( // Retry exhaustion without a status cannot replace the notification's evidence. const completionHasClass = failureCode != null && - (failureCode !== "responseTooManyFailedAttempts" || - codexExhaustionHttpStatus(input.failureErrorInfo) !== null); + !codexTerminalErrorNeedsEvidence(input.failureErrorInfo, failureCode); let failure = previousFailure !== undefined && (input.failureMessage === undefined || @@ -5882,10 +5883,13 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( notification.errorInfo, notification.nativeCode, )) && - retry !== undefined && - input.failureMessage !== undefined && - input.failureMessage === retry.nativeDetails && - codexTerminalFailureClass(retry.errorInfo, retry.nativeCode) === "transport_error" + ((retry !== undefined && + input.failureMessage !== undefined && + input.failureMessage === retry.nativeDetails && + codexTerminalFailureClass(retry.errorInfo, retry.nativeCode) === + "transport_error") || + (notification?.failure.class === "transport_error" && + input.failureMessage === notification.nativeMessage)) ) { failure = { ...failure, class: "transport_error" }; }