authorgravatar for 97126807+Snowy7@users.noreply.github.comSnowy <97126807+Snowy7@users.noreply.github.com> 2026-03-28 03:38:31+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-27 17:38:31-07:00
log83eb396c407f167e70e29b29826d94f9ba1eb3aa
tree3758599bc4a36f6256efd2da62dcb449741e8710
parent59a383e1ea4914ea12d3551dce2f715b59218618
signature Signed by PGP key B5690EEEBB952194

fix(threads): Keep active-turn runtime errors from ending sessions (#1261)


4 files changed, 100 insertions(+), 1 deletions(-)

apps/server/src/codexAppServerManager.test.ts+36
......@@ -207,6 +207,42 @@ describe("classifyCodexStderrLine", () => {
207207 });
208208});
209209
210describe("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
210246describe("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+17-1
......@@ -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+34
......@@ -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+13
......@@ -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 {