Skip to content

Commit 0b3d876

Browse files
authored
fix(codex): prevent gateway crash when app-server subprocess terminates abruptly (#67947)
Fixes #67886. Handles stdin EPIPE in CodexAppServerClient by attaching an error handler, guarding writeMessage against writes after close, and aligning closeWithError cleanup with close.
1 parent d565c2c commit 0b3d876

4 files changed

Lines changed: 47 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Docs: https://docs.openclaw.ai
131131
- Webchat/security: reject remote-host `file://` URLs in the media embedding path. (#67293) Thanks @pgondhi987.
132132
- Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9.
133133
- Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.
134+
- Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf
134135

135136
## 2026.4.14
136137

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,39 @@ describe("CodexAppServerClient", () => {
199199
expect(process.kill).toHaveBeenCalledWith("SIGKILL");
200200
expect(process.unref).toHaveBeenCalledTimes(1);
201201
});
202+
it("handles stdin write errors without crashing the process", async () => {
203+
const harness = createClientHarness();
204+
clients.push(harness.client);
205+
206+
// Start a pending request so we can verify it gets properly rejected.
207+
const pending = harness.client.request("test/method");
208+
209+
// Simulate the child process closing its pipe — a write to the now-dead
210+
// stdin emits an asynchronous EPIPE error on the stream.
211+
harness.process.stdin.destroy(Object.assign(new Error("write EPIPE"), { code: "EPIPE" }));
212+
213+
// The pending request must be rejected with the pipe error rather than
214+
// an unhandled exception tearing down the gateway.
215+
await expect(pending).rejects.toThrow("write EPIPE");
216+
217+
// Subsequent requests are rejected immediately (client is closed).
218+
await expect(harness.client.request("another/method")).rejects.toThrow(
219+
"codex app-server client is closed",
220+
);
221+
});
222+
223+
it("does not write to stdin after the child process exits", async () => {
224+
const harness = createClientHarness();
225+
clients.push(harness.client);
226+
227+
// Simulate the child process exiting.
228+
harness.process.emit("exit", 1, null);
229+
230+
// A notification after exit must not attempt a write.
231+
harness.client.notify("late/event", { data: "ignored" });
232+
expect(harness.writes).toHaveLength(0);
233+
});
234+
202235
it("reads the Codex version from the app-server user agent", () => {
203236
expect(readCodexVersionFromUserAgent("Codex Desktop/0.118.0")).toBe("0.118.0");
204237
expect(readCodexVersionFromUserAgent("openclaw/0.118.0 (macOS; test)")).toBe("0.118.0");

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ export class CodexAppServerClient {
7474
),
7575
);
7676
});
77+
// Guard against unhandled EPIPE / write-after-close errors on the stdin
78+
// stream. When the child process terminates abruptly the pipe can break
79+
// before the "exit" event fires, so a pending writeMessage() produces an
80+
// asynchronous error on stdin that would otherwise crash the gateway.
81+
child.stdin.on?.("error", (error) =>
82+
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
83+
);
7784
}
7885

7986
static start(options?: Partial<CodexAppServerStartOptions>): CodexAppServerClient {
@@ -212,6 +219,9 @@ export class CodexAppServerClient {
212219
}
213220

214221
private writeMessage(message: RpcRequest | RpcResponse): void {
222+
if (this.closed) {
223+
return;
224+
}
215225
this.child.stdin.write(`${JSON.stringify(message)}\n`);
216226
}
217227

@@ -300,7 +310,9 @@ export class CodexAppServerClient {
300310
return;
301311
}
302312
this.closed = true;
313+
this.lines.close();
303314
this.rejectPendingRequests(error);
315+
closeCodexAppServerTransport(this.child);
304316
}
305317

306318
private rejectPendingRequests(error: Error): void {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type CodexAppServerTransport = {
44
end?: () => unknown;
55
destroy?: () => unknown;
66
unref?: () => unknown;
7+
on?: (event: "error", listener: (error: Error) => void) => unknown;
78
};
89
stdout: NodeJS.ReadableStream & {
910
destroy?: () => unknown;

0 commit comments

Comments
 (0)