From ccdecf16800025f3456f72e0fa3639bf2bf89264 Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:12:16 +0000 Subject: [PATCH] fix(server): settle error exits without runtime errors --- .../Layers/ProviderRuntimeIngestion.test.ts | 42 +++++++++++++++++++ .../Layers/ProviderRuntimeIngestion.ts | 25 +++++++---- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 1e1374c966b6..c30e9cf15328 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -2714,6 +2714,48 @@ describe("ProviderRuntimeIngestion", () => { expect(thread.session?.lastError).toBe("runtime exploded"); }); + it("settles an active turn when an error session exit arrives without runtime.error", async () => { + const harness = await createHarness(); + const turnId = asTurnId("turn-session-exit-error"); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-session-exit-error-turn-started"), + provider: ProviderDriverKind.make("codex"), + createdAt: "2026-01-01T00:00:00.000Z", + threadId: asThreadId("thread-1"), + turnId, + }); + + await waitForThread( + harness.readModel, + (entry) => entry.session?.status === "running" && entry.session.activeTurnId === turnId, + ); + + harness.emit({ + type: "session.exited", + eventId: asEventId("evt-session-exit-error"), + provider: ProviderDriverKind.make("codex"), + createdAt: "2026-01-01T00:00:01.000Z", + threadId: asThreadId("thread-1"), + payload: { + reason: "Provider process exited unexpectedly (137).", + exitKind: "error", + }, + }); + + const thread = await waitForThread( + harness.readModel, + (entry) => + entry.session?.status === "error" && + entry.session.activeTurnId === null && + entry.session.lastError === "Provider process exited unexpectedly (137).", + ); + expect(thread.session?.status).toBe("error"); + expect(thread.session?.activeTurnId).toBeNull(); + expect(thread.session?.lastError).toBe("Provider process exited unexpectedly (137)."); + }); + it("records runtime.error activities from the typed payload message", async () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 953ba1ec9b0d..f2488c30d80d 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -1572,6 +1572,8 @@ const make = Effect.gen(function* () { event.type === "turn.started" || event.type === "turn.completed" ) { + const unexpectedSessionExit = + event.type === "session.exited" && event.payload.exitKind === "error"; const status = (() => { switch (event.type) { case "session.state.changed": { @@ -1593,6 +1595,7 @@ const make = Effect.gen(function* () { return activeTurnId !== null ? "running" : hasPendingTurnStart ? "starting" : "ready"; } })(); + const lifecycleStatus = unexpectedSessionExit ? "error" : status; const nextActiveTurnId = event.type === "turn.started" ? (eventTurnId ?? null) @@ -1605,14 +1608,18 @@ const make = Effect.gen(function* () { ? null : activeTurnId; const lastError = - event.type === "session.state.changed" && event.payload.state === "error" - ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error") - : event.type === "turn.completed" && - normalizeRuntimeTurnState(event.payload.state) === "failed" - ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed") - : status === "ready" - ? null - : (thread.session?.lastError ?? null); + unexpectedSessionExit + ? (event.payload.reason ?? + thread.session?.lastError ?? + "Provider session exited unexpectedly") + : event.type === "session.state.changed" && event.payload.state === "error" + ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error") + : event.type === "turn.completed" && + normalizeRuntimeTurnState(event.payload.state) === "failed" + ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed") + : lifecycleStatus === "ready" + ? null + : (thread.session?.lastError ?? null); if (shouldApplyThreadLifecycle) { if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) { @@ -1641,7 +1648,7 @@ const make = Effect.gen(function* () { threadId: thread.id, session: { threadId: thread.id, - status, + status: lifecycleStatus, providerName: event.provider, ...(event.providerInstanceId !== undefined ? { providerInstanceId: event.providerInstanceId }