History

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

Julius Marminge committed 4 months ago

Commit cebbe6f1 parent e4a84a36 files · +113 −4Browse files

Unified · Split

6 files
  1. packages/effect-acp/src/_internal/stdio.test.ts +45 −0
  2. packages/effect-acp/src/_internal/stdio.ts +8 −2
  3. packages/effect-acp/src/errors.ts +2 −0
  4. packages/effect-codex-app-server/src/_internal/stdio.test.ts +48 −0
  5. packages/effect-codex-app-server/src/_internal/stdio.ts +8 −2
  6. packages/effect-codex-app-server/src/errors.ts +2 −0
packages/effect-acp/src/_internal/stdio.test.ts+45−0
@@ -0,0 +1,45 @@
1+import { assert, describe, it } from "@effect/vitest";
2+import * as Effect from "effect/Effect";
3+import * as PlatformError from "effect/PlatformError";
4+import { ChildProcessSpawner } from "effect/unstable/process";
5+
6+import * as AcpError from "../errors.ts";
7+import { makeTerminationError } from "./stdio.ts";
8+
9+describe("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* () { · 43 unmodified lines
4444 };
4545 });
4646
47+type ChildProcessTerminationHandle = Pick<
48+ ChildProcessSpawner.ChildProcessHandle,
49+ "exitCode" | "pid"
50+>;
51+
4752 export 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−0
@@ -91,6 +91,7 @@ export class AcpProcessExitedError extends Schema.TaggedErrorClass<AcpProcessExi · 90 unmodified lines
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 · 63 unmodified lines
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+48−0
@@ -0,0 +1,48 @@
1+import { assert, describe, it } from "@effect/vitest";
2+import * as Effect from "effect/Effect";
3+import * as PlatformError from "effect/PlatformError";
4+import { ChildProcessSpawner } from "effect/unstable/process";
5+
6+import * as CodexError from "../errors.ts";
7+import { makeTerminationError } from "./stdio.ts";
8+
9+describe("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* () { · 43 unmodified lines
4444 };
4545 });
4646
47+type ChildProcessTerminationHandle = Pick<
48+ ChildProcessSpawner.ChildProcessHandle,
49+ "exitCode" | "pid"
50+>;
51+
4752 export 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−0
@@ -146,6 +146,7 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co · 145 unmodified lines
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 · 81 unmodified lines
233234 "CodexAppServerTransportError",
234235 {
235236 operation: CodexAppServerTransportOperation,
237+ pid: Schema.optionalKey(Schema.Int),
236238 cause: Schema.Defect(),
237239 },
238240 ) {