From c80f188ecea9f8f1e8cdf52cc529905b69d22572 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 31 Mar 2026 12:01:53 -0700 Subject: [PATCH] Map fatal Codex stderr to runtime errors - Detect websocket connection failures in process stderr - Emit `runtime.error` with `provider_error` for fatal cases - Keep nonfatal stderr mapped to warnings --- .../src/provider/Layers/CodexAdapter.test.ts | 36 +++++++++++++++++++ .../src/provider/Layers/CodexAdapter.ts | 35 +++++++++++++----- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 7ddbdbfc98..b5eb873e85 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -508,6 +508,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { }), ); + it.effect("maps fatal websocket stderr notifications to runtime.error", () => + Effect.gen(function* () { + const adapter = yield* CodexAdapter; + const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); + + lifecycleManager.emit("event", { + id: asEventId("evt-process-stderr-websocket"), + kind: "notification", + provider: "codex", + threadId: asThreadId("thread-1"), + createdAt: new Date().toISOString(), + method: "process/stderr", + turnId: asTurnId("turn-1"), + message: + "2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses", + } satisfies ProviderEvent); + + const firstEvent = yield* Fiber.join(firstEventFiber); + + assert.equal(firstEvent._tag, "Some"); + if (firstEvent._tag !== "Some") { + return; + } + assert.equal(firstEvent.value.type, "runtime.error"); + if (firstEvent.value.type !== "runtime.error") { + return; + } + assert.equal(firstEvent.value.turnId, "turn-1"); + assert.equal(firstEvent.value.payload.class, "provider_error"); + assert.equal( + firstEvent.value.payload.message, + "2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses", + ); + }), + ); + it.effect("preserves request type when mapping serverRequest/resolved", () => Effect.gen(function* () { const adapter = yield* CodexAdapter; diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index b9ac4bfc4a..cee6bca6ed 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -112,6 +112,13 @@ function asNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } +const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"]; + +function isFatalCodexProcessStderrMessage(message: string): boolean { + const normalized = message.toLowerCase(); + return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet)); +} + function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined { const usage = asObject(value); const totalUsage = asObject(usage?.total_token_usage ?? usage?.total); @@ -1269,15 +1276,27 @@ function mapToRuntimeEvents( } if (event.method === "process/stderr") { + const message = event.message ?? "Codex process stderr"; + const isFatal = isFatalCodexProcessStderrMessage(message); return [ - { - type: "runtime.warning", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message: event.message ?? "Codex process stderr", - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, + isFatal + ? { + type: "runtime.error", + ...runtimeEventBase(event, canonicalThreadId), + payload: { + message, + class: "provider_error" as const, + ...(event.payload !== undefined ? { detail: event.payload } : {}), + }, + } + : { + type: "runtime.warning", + ...runtimeEventBase(event, canonicalThreadId), + payload: { + message, + ...(event.payload !== undefined ? { detail: event.payload } : {}), + }, + }, ]; }