authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-31 15:10:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-31 15:10:16-07:00
logaa5521dcfb709ddd3f3d079e70efb71916e74fe4
treef92e62e1412b75f01adfea803c2215c0b988ee84
parent2d03847ccd53ffcd5ba74301ce85aef2afe58ad5
signature Signed by PGP key B5690EEEBB952194

Map fatal Codex stderr to runtime errors (#1615)


2 files changed, 63 insertions(+), 8 deletions(-)

apps/server/src/provider/Layers/CodexAdapter.test.ts+36
......@@ -508,6 +508,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
508508 }),
509509 );
510510
511 it.effect("maps fatal websocket stderr notifications to runtime.error", () =>
512 Effect.gen(function* () {
513 const adapter = yield* CodexAdapter;
514 const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
515
516 lifecycleManager.emit("event", {
517 id: asEventId("evt-process-stderr-websocket"),
518 kind: "notification",
519 provider: "codex",
520 threadId: asThreadId("thread-1"),
521 createdAt: new Date().toISOString(),
522 method: "process/stderr",
523 turnId: asTurnId("turn-1"),
524 message:
525 "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",
526 } satisfies ProviderEvent);
527
528 const firstEvent = yield* Fiber.join(firstEventFiber);
529
530 assert.equal(firstEvent._tag, "Some");
531 if (firstEvent._tag !== "Some") {
532 return;
533 }
534 assert.equal(firstEvent.value.type, "runtime.error");
535 if (firstEvent.value.type !== "runtime.error") {
536 return;
537 }
538 assert.equal(firstEvent.value.turnId, "turn-1");
539 assert.equal(firstEvent.value.payload.class, "provider_error");
540 assert.equal(
541 firstEvent.value.payload.message,
542 "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",
543 );
544 }),
545 );
546
511547 it.effect("preserves request type when mapping serverRequest/resolved", () =>
512548 Effect.gen(function* () {
513549 const adapter = yield* CodexAdapter;
apps/server/src/provider/Layers/CodexAdapter.ts+27-8
......@@ -112,6 +112,13 @@ function asNumber(value: unknown): number | undefined {
112112 return typeof value === "number" && Number.isFinite(value) ? value : undefined;
113113}
114114
115const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"];
116
117function isFatalCodexProcessStderrMessage(message: string): boolean {
118 const normalized = message.toLowerCase();
119 return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet));
120}
121
115122function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined {
116123 const usage = asObject(value);
117124 const totalUsage = asObject(usage?.total_token_usage ?? usage?.total);
......@@ -1269,15 +1276,27 @@ function mapToRuntimeEvents(
12691276 }
12701277
12711278 if (event.method === "process/stderr") {
1279 const message = event.message ?? "Codex process stderr";
1280 const isFatal = isFatalCodexProcessStderrMessage(message);
12721281 return [
1273 {
1274 type: "runtime.warning",
1275 ...runtimeEventBase(event, canonicalThreadId),
1276 payload: {
1277 message: event.message ?? "Codex process stderr",
1278 ...(event.payload !== undefined ? { detail: event.payload } : {}),
1279 },
1280 },
1282 isFatal
1283 ? {
1284 type: "runtime.error",
1285 ...runtimeEventBase(event, canonicalThreadId),
1286 payload: {
1287 message,
1288 class: "provider_error" as const,
1289 ...(event.payload !== undefined ? { detail: event.payload } : {}),
1290 },
1291 }
1292 : {
1293 type: "runtime.warning",
1294 ...runtimeEventBase(event, canonicalThreadId),
1295 payload: {
1296 message,
1297 ...(event.payload !== undefined ? { detail: event.payload } : {}),
1298 },
1299 },
12811300 ];
12821301 }
12831302