| author | |
| committer | |
| log | aa5521dcfb709ddd3f3d079e70efb71916e74fe4 |
| tree | f92e62e1412b75f01adfea803c2215c0b988ee84 |
| parent | 2d03847ccd53ffcd5ba74301ce85aef2afe58ad5 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 63 insertions(+), 8 deletions(-)
apps/server/src/provider/Layers/CodexAdapter.test.ts+36| ... | @@ -508,6 +508,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -508,6 +508,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 508 | }), | 508 | }), |
| 509 | ); | 509 | ); |
| 510 | 510 | ||
| 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 | |||
| 511 | it.effect("preserves request type when mapping serverRequest/resolved", () => | 547 | it.effect("preserves request type when mapping serverRequest/resolved", () => |
| 512 | Effect.gen(function* () { | 548 | Effect.gen(function* () { |
| 513 | const adapter = yield* CodexAdapter; | 549 | const adapter = yield* CodexAdapter; |
apps/server/src/provider/Layers/CodexAdapter.ts+27-8| ... | @@ -112,6 +112,13 @@ function asNumber(value: unknown): number | undefined { | ... | @@ -112,6 +112,13 @@ function asNumber(value: unknown): number | undefined { |
| 112 | return typeof value === "number" && Number.isFinite(value) ? value : undefined; | 112 | return typeof value === "number" && Number.isFinite(value) ? value : undefined; |
| 113 | } | 113 | } |
| 114 | 114 | ||
| 115 | const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"]; | ||
| 116 | |||
| 117 | function isFatalCodexProcessStderrMessage(message: string): boolean { | ||
| 118 | const normalized = message.toLowerCase(); | ||
| 119 | return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet)); | ||
| 120 | } | ||
| 121 | |||
| 115 | function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined { | 122 | function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined { |
| 116 | const usage = asObject(value); | 123 | const usage = asObject(value); |
| 117 | const totalUsage = asObject(usage?.total_token_usage ?? usage?.total); | 124 | const totalUsage = asObject(usage?.total_token_usage ?? usage?.total); |
| ... | @@ -1269,15 +1276,27 @@ function mapToRuntimeEvents( | ... | @@ -1269,15 +1276,27 @@ function mapToRuntimeEvents( |
| 1269 | } | 1276 | } |
| 1270 | 1277 | ||
| 1271 | if (event.method === "process/stderr") { | 1278 | if (event.method === "process/stderr") { |
| 1279 | const message = event.message ?? "Codex process stderr"; | ||
| 1280 | const isFatal = isFatalCodexProcessStderrMessage(message); | ||
| 1272 | return [ | 1281 | return [ |
| 1273 | { | 1282 | isFatal |
| 1274 | type: "runtime.warning", | 1283 | ? { |
| 1275 | ...runtimeEventBase(event, canonicalThreadId), | 1284 | type: "runtime.error", |
| 1276 | payload: { | 1285 | ...runtimeEventBase(event, canonicalThreadId), |
| 1277 | message: event.message ?? "Codex process stderr", | 1286 | payload: { |
| 1278 | ...(event.payload !== undefined ? { detail: event.payload } : {}), | 1287 | message, |
| 1279 | }, | 1288 | class: "provider_error" as const, |
| 1280 | }, | 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 | }, | ||
| 1281 | ]; | 1300 | ]; |
| 1282 | } | 1301 | } |
| 1283 | 1302 |