From 0b3d876e745739607c4acf079a14df02dab58c62 Mon Sep 17 00:00:00 2001 From: Chunyue Wang <80630709+openperf@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:28:37 +0800 Subject: [PATCH] fix(codex): prevent gateway crash when app-server subprocess terminates abruptly (#67947) Fixes openclaw#67886. Handles stdin EPIPE in CodexAppServerClient by attaching an error handler, guarding writeMessage against writes after close, and aligning closeWithError cleanup with close. --- CHANGELOG.md | 1 + .../codex/src/app-server/client.test.ts | 33 +++++++++++++++++++ extensions/codex/src/app-server/client.ts | 12 +++++++ extensions/codex/src/app-server/transport.ts | 1 + 4 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7137e076c..3163361e11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ Docs: https://docs.openclaw.ai - Webchat/security: reject remote-host `file://` URLs in the media embedding path. (#67293) Thanks @pgondhi987. - 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. - 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. +- 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 ## 2026.4.14 diff --git a/extensions/codex/src/app-server/client.test.ts b/extensions/codex/src/app-server/client.test.ts index 534d1084bc6..8f30b5da360 100644 --- a/extensions/codex/src/app-server/client.test.ts +++ b/extensions/codex/src/app-server/client.test.ts @@ -199,6 +199,39 @@ describe("CodexAppServerClient", () => { expect(process.kill).toHaveBeenCalledWith("SIGKILL"); expect(process.unref).toHaveBeenCalledTimes(1); }); + it("handles stdin write errors without crashing the process", async () => { + const harness = createClientHarness(); + clients.push(harness.client); + + // Start a pending request so we can verify it gets properly rejected. + const pending = harness.client.request("test/method"); + + // Simulate the child process closing its pipe — a write to the now-dead + // stdin emits an asynchronous EPIPE error on the stream. + harness.process.stdin.destroy(Object.assign(new Error("write EPIPE"), { code: "EPIPE" })); + + // The pending request must be rejected with the pipe error rather than + // an unhandled exception tearing down the gateway. + await expect(pending).rejects.toThrow("write EPIPE"); + + // Subsequent requests are rejected immediately (client is closed). + await expect(harness.client.request("another/method")).rejects.toThrow( + "codex app-server client is closed", + ); + }); + + it("does not write to stdin after the child process exits", async () => { + const harness = createClientHarness(); + clients.push(harness.client); + + // Simulate the child process exiting. + harness.process.emit("exit", 1, null); + + // A notification after exit must not attempt a write. + harness.client.notify("late/event", { data: "ignored" }); + expect(harness.writes).toHaveLength(0); + }); + it("reads the Codex version from the app-server user agent", () => { expect(readCodexVersionFromUserAgent("Codex Desktop/0.118.0")).toBe("0.118.0"); expect(readCodexVersionFromUserAgent("openclaw/0.118.0 (macOS; test)")).toBe("0.118.0"); diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index e38d6fda9d9..25e71d7548d 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -74,6 +74,13 @@ export class CodexAppServerClient { ), ); }); + // Guard against unhandled EPIPE / write-after-close errors on the stdin + // stream. When the child process terminates abruptly the pipe can break + // before the "exit" event fires, so a pending writeMessage() produces an + // asynchronous error on stdin that would otherwise crash the gateway. + child.stdin.on?.("error", (error) => + this.closeWithError(error instanceof Error ? error : new Error(String(error))), + ); } static start(options?: Partial): CodexAppServerClient { @@ -212,6 +219,9 @@ export class CodexAppServerClient { } private writeMessage(message: RpcRequest | RpcResponse): void { + if (this.closed) { + return; + } this.child.stdin.write(`${JSON.stringify(message)}\n`); } @@ -300,7 +310,9 @@ export class CodexAppServerClient { return; } this.closed = true; + this.lines.close(); this.rejectPendingRequests(error); + closeCodexAppServerTransport(this.child); } private rejectPendingRequests(error: Error): void { diff --git a/extensions/codex/src/app-server/transport.ts b/extensions/codex/src/app-server/transport.ts index 423d7f0c474..b11fd764a55 100644 --- a/extensions/codex/src/app-server/transport.ts +++ b/extensions/codex/src/app-server/transport.ts @@ -4,6 +4,7 @@ export type CodexAppServerTransport = { end?: () => unknown; destroy?: () => unknown; unref?: () => unknown; + on?: (event: "error", listener: (error: Error) => void) => unknown; }; stdout: NodeJS.ReadableStream & { destroy?: () => unknown;