Skip to content

Commit 846195c

Browse files
fix(codex): handle app-server stdio stream errors (#101505)
Co-authored-by: Vincent Koc <[email protected]>
1 parent 942b449 commit 846195c

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

extensions/codex/src/app-server/client.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,42 @@ describe("CodexAppServerClient", () => {
422422
await expect(harness.client.request("another/method")).rejects.toThrow("write EPIPE");
423423
});
424424

425+
it("handles stdout stream errors without crashing the process", async () => {
426+
const harness = createClientHarness();
427+
clients.push(harness.client);
428+
429+
const pending = harness.client.request("test/method");
430+
const readError = Object.assign(new Error("stdout pipe broke"), { code: "EIO" });
431+
432+
expect(() => harness.process.stdout.emit("error", readError)).not.toThrow();
433+
434+
await expect(pending).rejects.toThrow("stdout pipe broke");
435+
await expect(harness.client.request("another/method")).rejects.toThrow("stdout pipe broke");
436+
});
437+
438+
it("keeps RPC requests usable after stderr stream errors", async () => {
439+
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
440+
const harness = createClientHarness();
441+
clients.push(harness.client);
442+
443+
const pending = harness.client.request("test/method");
444+
const firstRequest = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
445+
const stderrError = Object.assign(new Error("stderr pipe broke"), { code: "EIO" });
446+
447+
expect(() => harness.process.stderr.emit("error", stderrError)).not.toThrow();
448+
expect(warn).toHaveBeenCalledWith("codex app-server stderr stream failed", {
449+
error: stderrError,
450+
});
451+
452+
harness.send({ id: firstRequest.id, result: { ok: true } });
453+
await expect(pending).resolves.toEqual({ ok: true });
454+
455+
const next = harness.client.request("another/method");
456+
const secondRequest = JSON.parse(harness.writes[1] ?? "{}") as { id?: number };
457+
harness.send({ id: secondRequest.id, result: { ok: "still-connected" } });
458+
await expect(next).resolves.toEqual({ ok: "still-connected" });
459+
});
460+
425461
it("preserves redacted app-server stderr on exit errors", async () => {
426462
const harness = createClientHarness();
427463
clients.push(harness.client);

extensions/codex/src/app-server/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ export class CodexAppServerClient {
139139
this.child = child;
140140
this.lines = createInterface({ input: child.stdout });
141141
this.lines.on("line", (line) => this.handleLine(line));
142+
this.lines.on("error", (error) =>
143+
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
144+
);
145+
child.stdout.on("error", (error) =>
146+
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
147+
);
142148
child.stderr.on("data", (chunk: Buffer | string) => {
143149
const text = chunk.toString("utf8");
144150
this.stderrTail = appendBoundedTail(this.stderrTail, text, CODEX_APP_SERVER_STDERR_TAIL_MAX);
@@ -147,6 +153,11 @@ export class CodexAppServerClient {
147153
embeddedAgentLog.debug(`codex app-server stderr: ${trimmed}`);
148154
}
149155
});
156+
// Codex reserves stderr for diagnostics; losing that stream must not tear
157+
// down an otherwise healthy JSON-RPC connection on stdout.
158+
child.stderr.on("error", (error) => {
159+
embeddedAgentLog.warn("codex app-server stderr stream failed", { error });
160+
});
150161
child.once("error", (error) =>
151162
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
152163
);

0 commit comments

Comments
 (0)