authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 19:20:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 19:20:04-07:00
logcebbe6ff193c9598bf77c4f673a933db34ed7bcb
treea892f6b3c787bd17d3a47c3f32ed177043bc5a37
parente4a84a348e59d59e03bca15baa627aed99aec790
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve child process termination context (#3469)

Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 113 insertions(+), 4 deletions(-)

packages/effect-acp/src/_internal/stdio.test.ts created+45
......@@ -0,0 +1,45 @@
1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as PlatformError from "effect/PlatformError";
4import { ChildProcessSpawner } from "effect/unstable/process";
5
6import * as AcpError from "../errors.ts";
7import { makeTerminationError } from "./stdio.ts";
8
9describe("ACP child process termination", () => {
10 it.effect("retains the process identifier with the exit code", () =>
11 Effect.gen(function* () {
12 const error = yield* makeTerminationError({
13 pid: ChildProcessSpawner.ProcessId(41),
14 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(7)),
15 });
16
17 assert.instanceOf(error, AcpError.AcpProcessExitedError);
18 assert.equal(error.pid, 41);
19 assert.equal(error.code, 7);
20 assert.equal(error.message, "ACP process exited with code 7");
21 }),
22 );
23
24 it.effect("retains the process identifier and exact exit-status cause", () =>
25 Effect.gen(function* () {
26 const rootCause = new Error("private process diagnostics");
27 const cause = PlatformError.systemError({
28 _tag: "Unknown",
29 module: "ChildProcess",
30 method: "exitCode",
31 cause: rootCause,
32 });
33 const error = yield* makeTerminationError({
34 pid: ChildProcessSpawner.ProcessId(42),
35 exitCode: Effect.fail(cause),
36 });
37
38 assert.instanceOf(error, AcpError.AcpTransportError);
39 assert.equal(error.pid, 42);
40 assert.strictEqual(error.cause, cause);
41 assert.equal(error.message, "ACP transport operation read-process-exit-status failed.");
42 assert.notInclude(error.message, rootCause.message);
43 }),
44 );
45});
packages/effect-acp/src/_internal/stdio.ts+8-2
......@@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () {
4444 };
4545});
4646
47type ChildProcessTerminationHandle = Pick<
48 ChildProcessSpawner.ChildProcessHandle,
49 "exitCode" | "pid"
50>;
51
4752export const makeTerminationError = (
48 handle: ChildProcessSpawner.ChildProcessHandle,
53 handle: ChildProcessTerminationHandle,
4954): Effect.Effect<AcpError.AcpError> =>
5055 Effect.match(handle.exitCode, {
5156 onFailure: (cause) =>
5257 new AcpError.AcpTransportError({
5358 operation: "read-process-exit-status",
59 pid: handle.pid,
5460 cause,
5561 }),
56 onSuccess: (code) => new AcpError.AcpProcessExitedError({ code }),
62 onSuccess: (code) => new AcpError.AcpProcessExitedError({ code, pid: handle.pid }),
5763 });
packages/effect-acp/src/errors.ts+2
......@@ -91,6 +91,7 @@ export class AcpProcessExitedError extends Schema.TaggedErrorClass<AcpProcessExi
9191 "AcpProcessExitedError",
9292 {
9393 code: Schema.optional(Schema.Number),
94 pid: Schema.optionalKey(Schema.Int),
9495 cause: Schema.optional(Schema.Defect()),
9596 },
9697) {
......@@ -160,6 +161,7 @@ export class AcpTransportError extends Schema.TaggedErrorClass<AcpTransportError
160161 ),
161162 method: Schema.optional(Schema.String),
162163 detail: Schema.optional(Schema.String),
164 pid: Schema.optionalKey(Schema.Int),
163165 cause: Schema.Defect(),
164166 },
165167) {
packages/effect-codex-app-server/src/_internal/stdio.test.ts created+48
......@@ -0,0 +1,48 @@
1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as PlatformError from "effect/PlatformError";
4import { ChildProcessSpawner } from "effect/unstable/process";
5
6import * as CodexError from "../errors.ts";
7import { makeTerminationError } from "./stdio.ts";
8
9describe("Codex App Server child process termination", () => {
10 it.effect("retains the process identifier with the exit code", () =>
11 Effect.gen(function* () {
12 const error = yield* makeTerminationError({
13 pid: ChildProcessSpawner.ProcessId(51),
14 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(9)),
15 });
16
17 assert.instanceOf(error, CodexError.CodexAppServerProcessExitedError);
18 assert.equal(error.pid, 51);
19 assert.equal(error.code, 9);
20 assert.equal(error.message, "Codex App Server process exited with code 9");
21 }),
22 );
23
24 it.effect("retains the process identifier and exact exit-status cause", () =>
25 Effect.gen(function* () {
26 const rootCause = new Error("private process diagnostics");
27 const cause = PlatformError.systemError({
28 _tag: "Unknown",
29 module: "ChildProcess",
30 method: "exitCode",
31 cause: rootCause,
32 });
33 const error = yield* makeTerminationError({
34 pid: ChildProcessSpawner.ProcessId(52),
35 exitCode: Effect.fail(cause),
36 });
37
38 assert.instanceOf(error, CodexError.CodexAppServerTransportError);
39 assert.equal(error.pid, 52);
40 assert.strictEqual(error.cause, cause);
41 assert.equal(
42 error.message,
43 "Codex App Server transport operation 'read-process-exit-status' failed.",
44 );
45 assert.notInclude(error.message, rootCause.message);
46 }),
47 );
48});
packages/effect-codex-app-server/src/_internal/stdio.ts+8-2
......@@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () {
4444 };
4545});
4646
47type ChildProcessTerminationHandle = Pick<
48 ChildProcessSpawner.ChildProcessHandle,
49 "exitCode" | "pid"
50>;
51
4752export const makeTerminationError = (
48 handle: ChildProcessSpawner.ChildProcessHandle,
53 handle: ChildProcessTerminationHandle,
4954): Effect.Effect<CodexError.CodexAppServerError> =>
5055 Effect.match(handle.exitCode, {
5156 onFailure: (cause) =>
5257 new CodexError.CodexAppServerTransportError({
5358 operation: "read-process-exit-status",
59 pid: handle.pid,
5460 cause,
5561 }),
56 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code }),
62 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code, pid: handle.pid }),
5763 });
packages/effect-codex-app-server/src/errors.ts+2
......@@ -146,6 +146,7 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co
146146 "CodexAppServerProcessExitedError",
147147 {
148148 code: Schema.optional(Schema.Number),
149 pid: Schema.optionalKey(Schema.Int),
149150 cause: Schema.optional(Schema.Defect()),
150151 },
151152) {
......@@ -233,6 +234,7 @@ export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexA
233234 "CodexAppServerTransportError",
234235 {
235236 operation: CodexAppServerTransportOperation,
237 pid: Schema.optionalKey(Schema.Int),
236238 cause: Schema.Defect(),
237239 },
238240) {