From 6d2b0df06f131f2769acb0a1262d6789c77239d1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Mar 2026 17:16:44 -0700 Subject: [PATCH] Treat retryable Codex errors as runtime warnings - Map Codex `error` notifications with `willRetry: true` to `runtime.warning` - Keep active turns/sessions running during reconnect warnings - Add adapter and ingestion tests for retry-warning behavior --- .../Layers/ProviderRuntimeIngestion.test.ts | 44 +++++++++++++++++++ .../src/provider/Layers/CodexAdapter.test.ts | 36 +++++++++++++++ .../src/provider/Layers/CodexAdapter.ts | 5 ++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 24409655eb..7a1d0399f8 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -1091,6 +1091,50 @@ describe("ProviderRuntimeIngestion", () => { expect(thread.session?.lastError).toBe("runtime exploded"); }); + it("keeps the session running when a runtime.warning arrives during an active turn", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-warning-turn-started"), + provider: "codex", + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-warning"), + payload: {}, + }); + + harness.emit({ + type: "runtime.warning", + eventId: asEventId("evt-warning-runtime"), + provider: "codex", + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-warning"), + payload: { + message: "Reconnecting... 2/5", + detail: { + willRetry: true, + }, + }, + }); + + const thread = await waitForThread( + harness.engine, + (entry) => + entry.session?.status === "running" && + entry.session?.activeTurnId === "turn-warning" && + entry.activities.some( + (activity: ProviderRuntimeTestActivity) => + activity.id === "evt-warning-runtime" && activity.kind === "runtime.warning", + ), + ); + expect(thread.session?.status).toBe("running"); + expect(thread.session?.activeTurnId).toBe("turn-warning"); + expect(thread.session?.lastError).toBeNull(); + }); + it("maps session/thread lifecycle and item.started into session/activity projections", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index c977f5dadc..3ad206d0be 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -407,6 +407,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { }), ); + it.effect("maps retryable Codex error notifications to runtime.warning", () => + Effect.gen(function* () { + const adapter = yield* CodexAdapter; + const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); + + lifecycleManager.emit("event", { + id: asEventId("evt-retryable-error"), + kind: "notification", + provider: "codex", + threadId: asThreadId("thread-1"), + createdAt: new Date().toISOString(), + method: "error", + turnId: asTurnId("turn-1"), + payload: { + error: { + message: "Reconnecting... 2/5", + }, + willRetry: true, + }, + } satisfies ProviderEvent); + + const firstEvent = yield* Fiber.join(firstEventFiber); + + assert.equal(firstEvent._tag, "Some"); + if (firstEvent._tag !== "Some") { + return; + } + assert.equal(firstEvent.value.type, "runtime.warning"); + if (firstEvent.value.type !== "runtime.warning") { + return; + } + assert.equal(firstEvent.value.turnId, "turn-1"); + assert.equal(firstEvent.value.payload.message, "Reconnecting... 2/5"); + }), + ); + 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 a31ba2907e..cb11219fcb 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -1192,13 +1192,14 @@ function mapToRuntimeEvents( if (event.method === "error") { const message = asString(asObject(payload?.error)?.message) ?? event.message ?? "Provider runtime error"; + const willRetry = payload?.willRetry === true; return [ { - type: "runtime.error", + type: willRetry ? "runtime.warning" : "runtime.error", ...runtimeEventBase(event, canonicalThreadId), payload: { message, - class: "provider_error", + ...(!willRetry ? { class: "provider_error" as const } : {}), ...(event.payload !== undefined ? { detail: event.payload } : {}), }, },