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* () {...@@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () {
44 };44 };
45});45});
4646
47type ChildProcessTerminationHandle = Pick<
48 ChildProcessSpawner.ChildProcessHandle,
49 "exitCode" | "pid"
50>;
51
47export const makeTerminationError = (52export const makeTerminationError = (
48 handle: ChildProcessSpawner.ChildProcessHandle,53 handle: ChildProcessTerminationHandle,
49): Effect.Effect<AcpError.AcpError> =>54): Effect.Effect<AcpError.AcpError> =>
50 Effect.match(handle.exitCode, {55 Effect.match(handle.exitCode, {
51 onFailure: (cause) =>56 onFailure: (cause) =>
52 new AcpError.AcpTransportError({57 new AcpError.AcpTransportError({
53 operation: "read-process-exit-status",58 operation: "read-process-exit-status",
59 pid: handle.pid,
54 cause,60 cause,
55 }),61 }),
56 onSuccess: (code) => new AcpError.AcpProcessExitedError({ code }),62 onSuccess: (code) => new AcpError.AcpProcessExitedError({ code, pid: handle.pid }),
57 });63 });
packages/effect-acp/src/errors.ts+2
...@@ -91,6 +91,7 @@ export class AcpProcessExitedError extends Schema.TaggedErrorClass<AcpProcessExi...@@ -91,6 +91,7 @@ export class AcpProcessExitedError extends Schema.TaggedErrorClass<AcpProcessExi
91 "AcpProcessExitedError",91 "AcpProcessExitedError",
92 {92 {
93 code: Schema.optional(Schema.Number),93 code: Schema.optional(Schema.Number),
94 pid: Schema.optionalKey(Schema.Int),
94 cause: Schema.optional(Schema.Defect()),95 cause: Schema.optional(Schema.Defect()),
95 },96 },
96) {97) {
...@@ -160,6 +161,7 @@ export class AcpTransportError extends Schema.TaggedErrorClass<AcpTransportError...@@ -160,6 +161,7 @@ export class AcpTransportError extends Schema.TaggedErrorClass<AcpTransportError
160 ),161 ),
161 method: Schema.optional(Schema.String),162 method: Schema.optional(Schema.String),
162 detail: Schema.optional(Schema.String),163 detail: Schema.optional(Schema.String),
164 pid: Schema.optionalKey(Schema.Int),
163 cause: Schema.Defect(),165 cause: Schema.Defect(),
164 },166 },
165) {167) {
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* () {...@@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () {
44 };44 };
45});45});
4646
47type ChildProcessTerminationHandle = Pick<
48 ChildProcessSpawner.ChildProcessHandle,
49 "exitCode" | "pid"
50>;
51
47export const makeTerminationError = (52export const makeTerminationError = (
48 handle: ChildProcessSpawner.ChildProcessHandle,53 handle: ChildProcessTerminationHandle,
49): Effect.Effect<CodexError.CodexAppServerError> =>54): Effect.Effect<CodexError.CodexAppServerError> =>
50 Effect.match(handle.exitCode, {55 Effect.match(handle.exitCode, {
51 onFailure: (cause) =>56 onFailure: (cause) =>
52 new CodexError.CodexAppServerTransportError({57 new CodexError.CodexAppServerTransportError({
53 operation: "read-process-exit-status",58 operation: "read-process-exit-status",
59 pid: handle.pid,
54 cause,60 cause,
55 }),61 }),
56 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code }),62 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code, pid: handle.pid }),
57 });63 });
packages/effect-codex-app-server/src/errors.ts+2
...@@ -146,6 +146,7 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co...@@ -146,6 +146,7 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co
146 "CodexAppServerProcessExitedError",146 "CodexAppServerProcessExitedError",
147 {147 {
148 code: Schema.optional(Schema.Number),148 code: Schema.optional(Schema.Number),
149 pid: Schema.optionalKey(Schema.Int),
149 cause: Schema.optional(Schema.Defect()),150 cause: Schema.optional(Schema.Defect()),
150 },151 },
151) {152) {
...@@ -233,6 +234,7 @@ export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexA...@@ -233,6 +234,7 @@ export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexA
233 "CodexAppServerTransportError",234 "CodexAppServerTransportError",
234 {235 {
235 operation: CodexAppServerTransportOperation,236 operation: CodexAppServerTransportOperation,
237 pid: Schema.optionalKey(Schema.Int),
236 cause: Schema.Defect(),238 cause: Schema.Defect(),
237 },239 },
238) {240) {