Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1641,7 +1641,7 @@ describe("ProviderRuntimeIngestion", () => {
expect(resolvedPayload?.requestType).toBe("command_execution_approval");
});

it("maps runtime.error into errored session state", async () => {
it("maps runtime.error into errored session state when no turn is active", async () => {
const harness = await createHarness();
const now = new Date().toISOString();

Expand All @@ -1668,6 +1668,53 @@ describe("ProviderRuntimeIngestion", () => {
expect(thread.session?.lastError).toBe("runtime exploded");
});

it("keeps the session running when runtime.error arrives for the active turn", async () => {
const harness = await createHarness();
const now = new Date().toISOString();

harness.emit({
type: "turn.started",
eventId: asEventId("evt-runtime-error-turn-started"),
provider: "codex",
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-runtime-error"),
payload: {},
});

await waitForThread(
harness.engine,
(entry) =>
entry.session?.status === "running" && entry.session?.activeTurnId === "turn-runtime-error",
);

harness.emit({
type: "runtime.error",
eventId: asEventId("evt-runtime-error-active-turn"),
provider: "codex",
createdAt: new Date().toISOString(),
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-runtime-error"),
payload: {
message: "The filename or extension is too long. (os error 206)",
},
});

const thread = await waitForThread(
harness.engine,
(entry) =>
entry.session?.status === "running" &&
entry.session?.activeTurnId === "turn-runtime-error" &&
entry.activities.some(
(activity: ProviderRuntimeTestActivity) =>
activity.id === "evt-runtime-error-active-turn" && activity.kind === "runtime.error",
),
);
expect(thread.session?.status).toBe("running");
expect(thread.session?.activeTurnId).toBe("turn-runtime-error");
expect(thread.session?.lastError).toBeNull();
});

it("keeps the session running when a runtime.warning arrives during an active turn", async () => {
const harness = await createHarness();
const now = new Date().toISOString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,13 @@ const make = Effect.gen(function* () {
? true
: activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId);

if (shouldApplyRuntimeError) {
const isActiveTurnRuntimeError =
activeTurnId !== null && (eventTurnId === undefined || sameId(activeTurnId, eventTurnId));

// Some provider/runtime errors are advisory while the active turn continues
// streaming. Keep the projected session runnable until a terminal lifecycle
// event arrives.
if (shouldApplyRuntimeError && !isActiveTurnRuntimeError) {
Comment on lines +1133 to +1139
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new isActiveTurnRuntimeError logic treats runtime.error events with a missing turnId as belonging to the active turn (and therefore not session-terminal). That branch (eventTurnId === undefined while activeTurnId !== null) isn’t covered by the added tests, and it’s an important case because some adapters can emit runtime errors without a turn id. Add a test that starts a turn, emits runtime.error without turnId, and asserts the session remains running while a runtime.error activity is still recorded.

Copilot uses AI. Check for mistakes.
yield* orchestrationEngine.dispatch({
type: "thread.session.set",
commandId: providerCommandId(event, "runtime-error-session-set"),
Expand Down
Loading