From ee945d27bfe037dd120289c72b91d9bff2597b34 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 07:52:36 +0000 Subject: [PATCH 1/4] test(server): reproduce #28032 Codex turn stuck running on unlisted error info A Codex turn that completes failed with codexErrorInfo "modelNotFound" (sent by ChatGPT-account Codex when it rejects a model) never reaches turn.terminal: the generated schema's closed CodexErrorInfo union rejects the value, so the error and turn/completed notifications are dropped at decode. This test reproduces #28032 and fails on main. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/CodexAdapterV2.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index e50b6689ef19..e793eeca4991 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5394,6 +5394,90 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + // regression: #28032 an unlisted codexErrorInfo dropped the failed turn/completed + it.effect("fails the turn when Codex reports an error info the schema does not list", () => + Effect.scoped( + Effect.gen(function* () { + const nativeThreadId = "native-unlisted-error-info"; + const nativeTurnId = "turn-unlisted-error-info"; + const message = + "The 'gpt-5-codex' model is not supported when using Codex with a ChatGPT account."; + const error = { message, codexErrorInfo: "modelNotFound", additionalDetails: null }; + const usage = { + totalTokens: 1, + inputTokens: 1, + cachedInputTokens: 0, + outputTokens: 0, + reasoningOutputTokens: 0, + }; + const transcript = makeCodexReplayTranscript({ + scenario: "codex-unlisted-error-info", + entries: [ + ...codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "say hi" }), + { + type: "emit_inbound", + label: "error", + frame: { + method: "error", + params: { threadId: nativeThreadId, turnId: nativeTurnId, willRetry: false, error }, + }, + }, + { + type: "emit_inbound", + label: "turn/completed", + frame: { + method: "turn/completed", + params: { + threadId: nativeThreadId, + turn: { ...makeCodexReplayTurn({ id: nativeTurnId, status: "failed" }), error }, + }, + }, + }, + // Notifications dispatch in order, so this marks turn/completed as handled. + { + type: "emit_inbound", + label: "usage", + frame: { + method: "thread/tokenUsage/updated", + params: { + threadId: nativeThreadId, + turnId: nativeTurnId, + tokenUsage: { total: usage, last: usage, modelContextWindow: 200_000 }, + }, + }, + }, + ], + }); + const harness = yield* makeCodexReplayHarness(transcript); + yield* harness.runtime.startTurn( + makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + text: "say hi", + attemptId: RunAttemptId.make("attempt-unlisted-error-info"), + }), + ); + yield* harness.until( + () => + harness.terminalEvents().length > 0 || + harness.events.some( + (event) => + event.type === "provider_turn.updated" && event.providerTurn.tokenUsage != null, + ), + ); + const terminal = harness.terminalEvents()[0]; + assert.equal(terminal?.status, "failed"); + if (terminal?.status !== "failed") return; + assert.equal(terminal.failure.message, message); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + const FAILED_SCENARIO = "codex-failed-mid-command"; const FAILED_NATIVE_THREAD = "native-codex-failed-thread"; const FAILED_NATIVE_TURN = "native-codex-failed-turn"; From 1d8e6d60956a5172ec0842d5461ad0639810879a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 07:58:12 +0000 Subject: [PATCH 2/4] fix(server): fail Codex turns that report an unlisted error code Codex sends codexErrorInfo codes the pinned protocol schema does not list, such as "modelNotFound" when a ChatGPT account rejects a model. The closed literal union failed decode, so the client dropped both the error and the failed turn/completed notifications and the turn stayed running forever. Override CodexErrorInfo in the generator so its string branch accepts any code, keeping the typed struct variants, and regenerate the schema. Fixes pingdotgg/t3code#28032 Co-Authored-By: Claude Opus 5.5 --- .../scripts/generate.ts | 56 ++ .../src/_generated/schema.gen.ts | 544 ++---------------- 2 files changed, 88 insertions(+), 512 deletions(-) diff --git a/packages/effect-codex-app-server/scripts/generate.ts b/packages/effect-codex-app-server/scripts/generate.ts index 8f9a45bb9293..68412c8b281c 100644 --- a/packages/effect-codex-app-server/scripts/generate.ts +++ b/packages/effect-codex-app-server/scripts/generate.ts @@ -168,6 +168,62 @@ const DefinitionOverrides: Record = { }, ], }, + // Codex sends codes this ref does not list (e.g. `modelNotFound`), and a decode failure + // drops the failed `turn/completed`, leaving the turn running forever. + CodexErrorInfo: { + description: + "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + oneOf: [ + { type: "string" }, + ...[ + { key: "httpConnectionFailed", title: "HttpConnectionFailedCodexErrorInfo" }, + { + key: "responseStreamConnectionFailed", + title: "ResponseStreamConnectionFailedCodexErrorInfo", + description: "Failed to connect to the response SSE stream.", + }, + { + key: "responseStreamDisconnected", + title: "ResponseStreamDisconnectedCodexErrorInfo", + description: + "The response SSE stream disconnected in the middle of a turn before completion.", + }, + { + key: "responseTooManyFailedAttempts", + title: "ResponseTooManyFailedAttemptsCodexErrorInfo", + description: "Reached the retry limit for responses.", + }, + ].map(({ key, ...annotations }) => ({ + additionalProperties: false, + ...annotations, + properties: { + [key]: { + properties: { + httpStatusCode: { format: "uint16", minimum: 0, type: ["integer", "null"] }, + }, + type: "object", + }, + }, + required: [key], + type: "object", + })), + { + additionalProperties: false, + description: + "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + properties: { + activeTurnNotSteerable: { + properties: { turnKind: { $ref: "#/definitions/NonSteerableTurnKind" } }, + required: ["turnKind"], + type: "object", + }, + }, + required: ["activeTurnNotSteerable"], + title: "ActiveTurnNotSteerableCodexErrorInfo", + type: "object", + }, + ], + }, }; const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { diff --git a/packages/effect-codex-app-server/src/_generated/schema.gen.ts b/packages/effect-codex-app-server/src/_generated/schema.gen.ts index 90869b56aa89..50fa95918b43 100644 --- a/packages/effect-codex-app-server/src/_generated/schema.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/schema.gen.ts @@ -17949,21 +17949,7 @@ export const PermissionsRequestApprovalResponse__FileSystemSpecialPath = Schema. ).annotate({ identifier: "PermissionsRequestApprovalResponse__FileSystemSpecialPath" }); export type ServerNotification__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -17975,23 +17961,7 @@ export type ServerNotification__CodexErrorInfo = }; export const ServerNotification__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -20035,21 +20005,7 @@ export const V2ConfigWriteResponse__ConfigLayerSource = Schema.Union( ).annotate({ identifier: "V2ConfigWriteResponse__ConfigLayerSource" }); export type V2ErrorNotification__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -20061,23 +20017,7 @@ export type V2ErrorNotification__CodexErrorInfo = }; export const V2ErrorNotification__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -22491,21 +22431,7 @@ export const V2RawResponseItemCompletedNotification__ConfigurationReasoning = Sc }); export type V2ReviewStartResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -22517,23 +22443,7 @@ export type V2ReviewStartResponse__CodexErrorInfo = }; export const V2ReviewStartResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -23029,21 +22939,7 @@ export const V2ThreadForkResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadForkResponse__ThreadStatus" }); export type V2ThreadForkResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -23055,23 +22951,7 @@ export type V2ThreadForkResponse__CodexErrorInfo = }; export const V2ThreadForkResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -23679,21 +23559,7 @@ export const V2ThreadListResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadListResponse__ThreadStatus" }); export type V2ThreadListResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -23705,23 +23571,7 @@ export type V2ThreadListResponse__CodexErrorInfo = }; export const V2ThreadListResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -24088,21 +23938,7 @@ export const V2ThreadMetadataUpdateResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadMetadataUpdateResponse__ThreadStatus" }); export type V2ThreadMetadataUpdateResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -24114,23 +23950,7 @@ export type V2ThreadMetadataUpdateResponse__CodexErrorInfo = }; export const V2ThreadMetadataUpdateResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -24502,21 +24322,7 @@ export const V2ThreadReadResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadReadResponse__ThreadStatus" }); export type V2ThreadReadResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -24528,23 +24334,7 @@ export type V2ThreadReadResponse__CodexErrorInfo = }; export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -25270,21 +25060,7 @@ export const V2ThreadResumeResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadResumeResponse__ThreadStatus" }); export type V2ThreadResumeResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -25296,23 +25072,7 @@ export type V2ThreadResumeResponse__CodexErrorInfo = }; export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -25641,21 +25401,7 @@ export const V2ThreadRevertResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadRevertResponse__ThreadStatus" }); export type V2ThreadRevertResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -25667,23 +25413,7 @@ export type V2ThreadRevertResponse__CodexErrorInfo = }; export const V2ThreadRevertResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -26181,21 +25911,7 @@ export const V2ThreadStartedNotification__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadStartedNotification__ThreadStatus" }); export type V2ThreadStartedNotification__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -26207,23 +25923,7 @@ export type V2ThreadStartedNotification__CodexErrorInfo = }; export const V2ThreadStartedNotification__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -26686,21 +26386,7 @@ export const V2ThreadStartResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadStartResponse__ThreadStatus" }); export type V2ThreadStartResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -26712,23 +26398,7 @@ export type V2ThreadStartResponse__CodexErrorInfo = }; export const V2ThreadStartResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -27016,21 +26686,7 @@ export const V2ThreadTokenUsageUpdatedNotification__ThreadTokenUsage = Schema.St }).annotate({ identifier: "V2ThreadTokenUsageUpdatedNotification__ThreadTokenUsage" }); export type V2ThreadTurnsListResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -27042,23 +26698,7 @@ export type V2ThreadTurnsListResponse__CodexErrorInfo = }; export const V2ThreadTurnsListResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -27429,21 +27069,7 @@ export const V2ThreadUnarchiveResponse__ThreadStatus = Schema.Union( ).annotate({ identifier: "V2ThreadUnarchiveResponse__ThreadStatus" }); export type V2ThreadUnarchiveResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -27455,23 +27081,7 @@ export type V2ThreadUnarchiveResponse__CodexErrorInfo = }; export const V2ThreadUnarchiveResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -27760,21 +27370,7 @@ export const V2ThreadUnarchiveResponse__CollabAgentState = Schema.Struct({ }).annotate({ identifier: "V2ThreadUnarchiveResponse__CollabAgentState" }); export type V2TurnCompletedNotification__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -27786,23 +27382,7 @@ export type V2TurnCompletedNotification__CodexErrorInfo = }; export const V2TurnCompletedNotification__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -28101,21 +27681,7 @@ export const V2TurnPlanUpdatedNotification__TurnPlanStep = Schema.Struct({ }).annotate({ identifier: "V2TurnPlanUpdatedNotification__TurnPlanStep" }); export type V2TurnStartedNotification__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -28127,23 +27693,7 @@ export type V2TurnStartedNotification__CodexErrorInfo = }; export const V2TurnStartedNotification__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( @@ -28569,21 +28119,7 @@ export const V2TurnStartParams__SandboxPolicy = Schema.Union( ).annotate({ identifier: "V2TurnStartParams__SandboxPolicy" }); export type V2TurnStartResponse__CodexErrorInfo = - | "contextWindowExceeded" - | "sessionBudgetExceeded" - | "usageLimitExceeded" - | "rateLimitExceeded" - | "flexUnavailable" - | "serverOverloaded" - | "cyberPolicy" - | "misalignmentPolicyViolation" - | "tooManyDenials" - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | "other" + | string | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } } | { readonly responseStreamDisconnected: { readonly httpStatusCode?: number | null } } @@ -28595,23 +28131,7 @@ export type V2TurnStartResponse__CodexErrorInfo = }; export const V2TurnStartResponse__CodexErrorInfo = Schema.Union( [ - Schema.Literals([ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "flexUnavailable", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "tooManyDenials", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other", - ]), + Schema.String, Schema.Struct({ httpConnectionFailed: Schema.Struct({ httpStatusCode: Schema.optionalKey( From 275bca04bb16aab6d38d8debc1491006a8b45874 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 08:01:37 +0000 Subject: [PATCH 3/4] refactor(codex): widen CodexErrorInfo's string codes instead of freezing a copy Co-Authored-By: Claude Opus 5.5 --- .../scripts/generate.ts | 77 +++++-------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/packages/effect-codex-app-server/scripts/generate.ts b/packages/effect-codex-app-server/scripts/generate.ts index 68412c8b281c..86c0dc7e662d 100644 --- a/packages/effect-codex-app-server/scripts/generate.ts +++ b/packages/effect-codex-app-server/scripts/generate.ts @@ -168,64 +168,24 @@ const DefinitionOverrides: Record = { }, ], }, - // Codex sends codes this ref does not list (e.g. `modelNotFound`), and a decode failure - // drops the failed `turn/completed`, leaving the turn running forever. - CodexErrorInfo: { - description: - "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", - oneOf: [ - { type: "string" }, - ...[ - { key: "httpConnectionFailed", title: "HttpConnectionFailedCodexErrorInfo" }, - { - key: "responseStreamConnectionFailed", - title: "ResponseStreamConnectionFailedCodexErrorInfo", - description: "Failed to connect to the response SSE stream.", - }, - { - key: "responseStreamDisconnected", - title: "ResponseStreamDisconnectedCodexErrorInfo", - description: - "The response SSE stream disconnected in the middle of a turn before completion.", - }, - { - key: "responseTooManyFailedAttempts", - title: "ResponseTooManyFailedAttemptsCodexErrorInfo", - description: "Reached the retry limit for responses.", - }, - ].map(({ key, ...annotations }) => ({ - additionalProperties: false, - ...annotations, - properties: { - [key]: { - properties: { - httpStatusCode: { format: "uint16", minimum: 0, type: ["integer", "null"] }, - }, - type: "object", - }, - }, - required: [key], - type: "object", - })), - { - additionalProperties: false, - description: - "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", - properties: { - activeTurnNotSteerable: { - properties: { turnKind: { $ref: "#/definitions/NonSteerableTurnKind" } }, - required: ["turnKind"], - type: "object", - }, - }, - required: ["activeTurnNotSteerable"], - title: "ActiveTurnNotSteerableCodexErrorInfo", - type: "object", - }, - ], - }, }; +// Codex sends error codes this ref does not list yet (e.g. `modelNotFound`). A closed enum +// fails the whole notification decode, which drops a failed `turn/completed` and leaves the turn running. +const OpenStringEnumDefinitions = new Set(["CodexErrorInfo"]); + +function openStringEnum(schema: Schema.Json): Schema.Json { + if (!isJsonSchemaNode(schema) || !Array.isArray(schema.oneOf)) return schema; + return { + ...schema, + oneOf: schema.oneOf.map((variant: Schema.Json) => + isJsonSchemaNode(variant) && variant.type === "string" && Array.isArray(variant.enum) + ? { type: "string" } + : variant, + ), + }; +} + const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { const path = yield* Path.Path; const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated"); @@ -718,7 +678,10 @@ const generateFiles = Effect.fn("generateFiles")(function* () { for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) { aggregateSchemas[localDefinitionNames.get(definitionName)!] = pipe( rewriteExternalRefs( - DefinitionOverrides[definitionName] ?? definitionSchema, + DefinitionOverrides[definitionName] ?? + (OpenStringEnumDefinitions.has(definitionName) + ? openStringEnum(definitionSchema) + : definitionSchema), localDefinitionNames, file.namespace, exportNameByQualifiedName, From d44ce22cb6894e0610bbb39771f66a08b4f03062 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:06:28 -0800 Subject: [PATCH 4/4] test(server): provide NodeCrypto to the unlisted error info Codex test Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Adapters/CodexAdapterV2.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index d46188346ace..222f04389e82 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -5822,7 +5822,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { assert.equal(terminal.failure.message, message); }).pipe( Effect.provide( - Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), ), ), ),