Skip to content

Commit 83eb396

Browse files
authored
fix(threads): Keep active-turn runtime errors from ending sessions (pingdotgg#1261)
1 parent 59a383e commit 83eb396

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

apps/server/src/codexAppServerManager.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,42 @@ describe("classifyCodexStderrLine", () => {
207207
});
208208
});
209209

210+
describe("process stderr events", () => {
211+
it("emits classified stderr lines as notifications", () => {
212+
const manager = new CodexAppServerManager();
213+
const emitEvent = vi
214+
.spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent")
215+
.mockImplementation(() => {});
216+
217+
(
218+
manager as unknown as {
219+
emitNotificationEvent: (
220+
context: { session: { threadId: ThreadId } },
221+
method: string,
222+
message: string,
223+
) => void;
224+
}
225+
).emitNotificationEvent(
226+
{
227+
session: {
228+
threadId: asThreadId("thread-1"),
229+
},
230+
},
231+
"process/stderr",
232+
"fatal: permission denied",
233+
);
234+
235+
expect(emitEvent).toHaveBeenCalledWith(
236+
expect.objectContaining({
237+
kind: "notification",
238+
method: "process/stderr",
239+
threadId: "thread-1",
240+
message: "fatal: permission denied",
241+
}),
242+
);
243+
});
244+
});
245+
210246
describe("normalizeCodexModelSlug", () => {
211247
it("maps 5.3 aliases to gpt-5.3-codex", () => {
212248
expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex");

apps/server/src/codexAppServerManager.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
10461046
continue;
10471047
}
10481048

1049-
this.emitErrorEvent(context, "process/stderr", classified.message);
1049+
this.emitNotificationEvent(context, "process/stderr", classified.message);
10501050
}
10511051
});
10521052

@@ -1354,6 +1354,22 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
13541354
});
13551355
}
13561356

1357+
private emitNotificationEvent(
1358+
context: CodexSessionContext,
1359+
method: string,
1360+
message: string,
1361+
): void {
1362+
this.emitEvent({
1363+
id: EventId.makeUnsafe(randomUUID()),
1364+
kind: "notification",
1365+
provider: "codex",
1366+
threadId: context.session.threadId,
1367+
createdAt: new Date().toISOString(),
1368+
method,
1369+
message,
1370+
});
1371+
}
1372+
13571373
private emitEvent(event: ProviderEvent): void {
13581374
this.emit("event", event);
13591375
}

apps/server/src/provider/Layers/CodexAdapter.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,40 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
474474
}),
475475
);
476476

477+
it.effect("maps process stderr notifications to runtime.warning", () =>
478+
Effect.gen(function* () {
479+
const adapter = yield* CodexAdapter;
480+
const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
481+
482+
lifecycleManager.emit("event", {
483+
id: asEventId("evt-process-stderr"),
484+
kind: "notification",
485+
provider: "codex",
486+
threadId: asThreadId("thread-1"),
487+
createdAt: new Date().toISOString(),
488+
method: "process/stderr",
489+
turnId: asTurnId("turn-1"),
490+
message: "The filename or extension is too long. (os error 206)",
491+
} satisfies ProviderEvent);
492+
493+
const firstEvent = yield* Fiber.join(firstEventFiber);
494+
495+
assert.equal(firstEvent._tag, "Some");
496+
if (firstEvent._tag !== "Some") {
497+
return;
498+
}
499+
assert.equal(firstEvent.value.type, "runtime.warning");
500+
if (firstEvent.value.type !== "runtime.warning") {
501+
return;
502+
}
503+
assert.equal(firstEvent.value.turnId, "turn-1");
504+
assert.equal(
505+
firstEvent.value.payload.message,
506+
"The filename or extension is too long. (os error 206)",
507+
);
508+
}),
509+
);
510+
477511
it.effect("preserves request type when mapping serverRequest/resolved", () =>
478512
Effect.gen(function* () {
479513
const adapter = yield* CodexAdapter;

apps/server/src/provider/Layers/CodexAdapter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,19 @@ function mapToRuntimeEvents(
12671267
];
12681268
}
12691269

1270+
if (event.method === "process/stderr") {
1271+
return [
1272+
{
1273+
type: "runtime.warning",
1274+
...runtimeEventBase(event, canonicalThreadId),
1275+
payload: {
1276+
message: event.message ?? "Codex process stderr",
1277+
...(event.payload !== undefined ? { detail: event.payload } : {}),
1278+
},
1279+
},
1280+
];
1281+
}
1282+
12701283
if (event.method === "windows/worldWritableWarning") {
12711284
return [
12721285
{

0 commit comments

Comments
 (0)