authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:54:38-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:54:38-07:00
log430ece4c0aa49ede37bb3bd29f451fde97b1b64a
tree38f991aaf6086bdffb603ba7cb71f40c91808caf
parent4238a0e769c9f149d4083968823298ace1e2bd0b
signature Signed by PGP key B5690EEEBB952194

[codex] Structure Codex app-server request errors (#3258)

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

8 files changed, 631 insertions(+), 158 deletions(-)

apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+108-66
...@@ -1,8 +1,9 @@...@@ -1,8 +1,9 @@
1import * as NodeAssert from "node:assert/strict";1import * as NodeAssert from "node:assert/strict";
22
3import { it } from "@effect/vitest";
3import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
5import { describe, it } from "vite-plus/test";6import { describe } from "vite-plus/test";
6import { ThreadId } from "@t3tools/contracts";7import { ThreadId } from "@t3tools/contracts";
7import * as CodexErrors from "effect-codex-app-server/errors";8import * as CodexErrors from "effect-codex-app-server/errors";
8import * as CodexRpc from "effect-codex-app-server/rpc";9import * as CodexRpc from "effect-codex-app-server/rpc";
...@@ -19,6 +20,23 @@ import {...@@ -19,6 +20,23 @@ import {
19} from "./CodexSessionRuntime.ts";20} from "./CodexSessionRuntime.ts";
20const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);21const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);
2122
23describe("CodexSessionRuntimeIdentifierGenerationError", () => {
24 it("retains identifier purpose and the random source failure", () => {
25 const cause = new Error("random source unavailable");
26 const error = new CodexErrors.CodexAppServerIdentifierGenerationError({
27 purpose: "provider-event",
28 cause,
29 });
30
31 NodeAssert.equal(error.purpose, "provider-event");
32 NodeAssert.strictEqual(error.cause, cause);
33 NodeAssert.equal(
34 error.message,
35 "Failed to generate Codex App Server identifier for provider-event.",
36 );
37 });
38});
39
22function makeThreadOpenResponse(40function makeThreadOpenResponse(
23 threadId: string,41 threadId: string,
24): CodexRpc.ClientRequestResponsesByMethod["thread/start"] {42): CodexRpc.ClientRequestResponsesByMethod["thread/start"] {
...@@ -43,6 +61,32 @@ function makeThreadOpenResponse(...@@ -43,6 +61,32 @@ function makeThreadOpenResponse(
43}61}
4462
45describe("buildTurnStartParams", () => {63describe("buildTurnStartParams", () => {
64 it("keeps invalid turn values only in the schema cause", () => {
65 const secret = "codex-turn-input-secret-sentinel";
66 const error = Effect.runSync(
67 buildTurnStartParams({
68 threadId: "provider-thread-1",
69 runtimeMode: "full-access",
70 attachments: [
71 {
72 type: "image",
73 url: { secret } as unknown as string,
74 },
75 ],
76 }).pipe(Effect.flip),
77 );
78 const { cause, ...directDiagnostics } = error;
79
80 NodeAssert.equal(error.operation, "decode-request-payload");
81 NodeAssert.equal(error.method, "turn/start");
82 NodeAssert.ok((error.issueCount ?? 0) > 0);
83 NodeAssert.ok(error.issueKinds?.includes("Pointer"));
84 NodeAssert.ok((error.maximumPathDepth ?? 0) > 0);
85 NodeAssert.ok(Schema.isSchemaError(cause));
86 NodeAssert.doesNotMatch(error.message, new RegExp(secret));
87 NodeAssert.doesNotMatch(JSON.stringify(directDiagnostics), new RegExp(secret));
88 });
89
46 it("includes plan collaboration mode when requested", () => {90 it("includes plan collaboration mode when requested", () => {
47 const params = Effect.runSync(91 const params = Effect.runSync(
48 buildTurnStartParams({92 buildTurnStartParams({
...@@ -223,29 +267,29 @@ describe("isRecoverableThreadResumeError", () => {...@@ -223,29 +267,29 @@ describe("isRecoverableThreadResumeError", () => {
223});267});
224268
225describe("openCodexThread", () => {269describe("openCodexThread", () => {
226 it("falls back to thread/start when resume fails recoverably", async () => {270 it.effect("falls back to thread/start when resume fails recoverably", () =>
227 const calls: Array<{ method: "thread/start" | "thread/resume"; payload: unknown }> = [];271 Effect.gen(function* () {
228 const started = makeThreadOpenResponse("fresh-thread");272 const calls: Array<{ method: "thread/start" | "thread/resume"; payload: unknown }> = [];
229 const client = {273 const started = makeThreadOpenResponse("fresh-thread");
230 request: <M extends "thread/start" | "thread/resume">(274 const client = {
231 method: M,275 request: <M extends "thread/start" | "thread/resume">(
232 payload: CodexRpc.ClientRequestParamsByMethod[M],276 method: M,
233 ) => {277 payload: CodexRpc.ClientRequestParamsByMethod[M],
234 calls.push({ method, payload });278 ) => {
235 if (method === "thread/resume") {279 calls.push({ method, payload });
236 return Effect.fail(280 if (method === "thread/resume") {
237 new CodexErrors.CodexAppServerRequestError({281 return Effect.fail(
238 code: -32603,282 new CodexErrors.CodexAppServerRequestError({
239 errorMessage: "thread not found",283 code: -32603,
240 }),284 errorMessage: "thread not found",
241 );285 }),
242 }286 );
243 return Effect.succeed(started as CodexRpc.ClientRequestResponsesByMethod[M]);287 }
244 },288 return Effect.succeed(started as CodexRpc.ClientRequestResponsesByMethod[M]);
245 };289 },
290 };
246291
247 const opened = await Effect.runPromise(292 const opened = yield* openCodexThread({
248 openCodexThread({
249 client,293 client,
250 threadId: ThreadId.make("thread-1"),294 threadId: ThreadId.make("thread-1"),
251 runtimeMode: "full-access",295 runtimeMode: "full-access",
...@@ -253,51 +297,49 @@ describe("openCodexThread", () => {...@@ -253,51 +297,49 @@ describe("openCodexThread", () => {
253 requestedModel: "gpt-5.3-codex",297 requestedModel: "gpt-5.3-codex",
254 serviceTier: undefined,298 serviceTier: undefined,
255 resumeThreadId: "stale-thread",299 resumeThreadId: "stale-thread",
256 }),300 });
257 );
258301
259 NodeAssert.equal(opened.thread.id, "fresh-thread");302 NodeAssert.equal(opened.thread.id, "fresh-thread");
260 NodeAssert.deepStrictEqual(303 NodeAssert.deepStrictEqual(
261 calls.map((call) => call.method),304 calls.map((call) => call.method),
262 ["thread/resume", "thread/start"],305 ["thread/resume", "thread/start"],
263 );306 );
264 });307 }),
308 );
265309
266 it("propagates non-recoverable resume failures", async () => {310 it.effect("propagates non-recoverable resume failures", () =>
267 const client = {311 Effect.gen(function* () {
268 request: <M extends "thread/start" | "thread/resume">(312 const client = {
269 method: M,313 request: <M extends "thread/start" | "thread/resume">(
270 _payload: CodexRpc.ClientRequestParamsByMethod[M],314 method: M,
271 ) => {315 _payload: CodexRpc.ClientRequestParamsByMethod[M],
272 if (method === "thread/resume") {316 ) => {
273 return Effect.fail(317 if (method === "thread/resume") {
274 new CodexErrors.CodexAppServerRequestError({318 return Effect.fail(
275 code: -32603,319 new CodexErrors.CodexAppServerRequestError({
276 errorMessage: "timed out waiting for server",320 code: -32603,
277 }),321 errorMessage: "timed out waiting for server",
322 }),
323 );
324 }
325 return Effect.succeed(
326 makeThreadOpenResponse("fresh-thread") as CodexRpc.ClientRequestResponsesByMethod[M],
278 );327 );
279 }328 },
280 return Effect.succeed(329 };
281 makeThreadOpenResponse("fresh-thread") as CodexRpc.ClientRequestResponsesByMethod[M],
282 );
283 },
284 };
285330
286 await NodeAssert.rejects(331 const error = yield* openCodexThread({
287 Effect.runPromise(332 client,
288 openCodexThread({333 threadId: ThreadId.make("thread-1"),
289 client,334 runtimeMode: "full-access",
290 threadId: ThreadId.make("thread-1"),335 cwd: "/tmp/project",
291 runtimeMode: "full-access",336 requestedModel: "gpt-5.3-codex",
292 cwd: "/tmp/project",337 serviceTier: undefined,
293 requestedModel: "gpt-5.3-codex",338 resumeThreadId: "stale-thread",
294 serviceTier: undefined,339 }).pipe(Effect.flip);
295 resumeThreadId: "stale-thread",340
296 }),341 NodeAssert.ok(isCodexAppServerRequestError(error));
297 ),342 NodeAssert.equal(error.errorMessage, "timed out waiting for server");
298 (error: unknown) =>343 }),
299 isCodexAppServerRequestError(error) &&344 );
300 error.errorMessage === "timed out waiting for server",
301 );
302 });
303});345});
apps/server/src/provider/Layers/CodexSessionRuntime.ts+30-29
...@@ -26,10 +26,9 @@ import * as Exit from "effect/Exit";...@@ -26,10 +26,9 @@ import * as Exit from "effect/Exit";
26import * as Layer from "effect/Layer";26import * as Layer from "effect/Layer";
27import * as Queue from "effect/Queue";27import * as Queue from "effect/Queue";
28import * as Ref from "effect/Ref";28import * as Ref from "effect/Ref";
29import * as Scope from "effect/Scope";
30import * as Schema from "effect/Schema";29import * as Schema from "effect/Schema";
30import * as Scope from "effect/Scope";
31import * as Stream from "effect/Stream";31import * as Stream from "effect/Stream";
32import * as SchemaIssue from "effect/SchemaIssue";
33import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";32import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
34import * as CodexClient from "effect-codex-app-server/client";33import * as CodexClient from "effect-codex-app-server/client";
35import * as CodexErrors from "effect-codex-app-server/errors";34import * as CodexErrors from "effect-codex-app-server/errors";
...@@ -89,7 +88,6 @@ const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffe...@@ -89,7 +88,6 @@ const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffe
8988
90export type CodexTurnStartParamsWithCollaborationMode =89export type CodexTurnStartParamsWithCollaborationMode =
91 typeof CodexTurnStartParamsWithCollaborationMode.Type;90 typeof CodexTurnStartParamsWithCollaborationMode.Type;
92const formatSchemaIssue = SchemaIssue.makeFormatterDefault();
9391
94export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type;92export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type;
95type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>;93type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>;
...@@ -390,7 +388,13 @@ export function buildTurnStartParams(input: {...@@ -390,7 +388,13 @@ export function buildTurnStartParams(input: {
390 ...(input.effort ? { effort: input.effort } : {}),388 ...(input.effort ? { effort: input.effort } : {}),
391 ...(collaborationMode ? { collaborationMode } : {}),389 ...(collaborationMode ? { collaborationMode } : {}),
392 }).pipe(390 }).pipe(
393 Effect.mapError((error) => toProtocolParseError("Invalid turn/start request payload", error)),391 Effect.mapError((cause) =>
392 CodexErrors.CodexAppServerProtocolParseError.fromSchemaError(
393 "decode-request-payload",
394 cause,
395 { method: "turn/start" },
396 ),
397 ),
394 );398 );
395}399}
396400
...@@ -468,7 +472,7 @@ export const openCodexThread = (input: {...@@ -468,7 +472,7 @@ export const openCodexThread = (input: {
468 requestedRuntimeMode: input.runtimeMode,472 requestedRuntimeMode: input.runtimeMode,
469 resumeThreadId,473 resumeThreadId,
470 recoverable: true,474 recoverable: true,
471 cause: error.message,475 cause: error,
472 }).pipe(Effect.andThen(input.client.request("thread/start", startParams))),476 }).pipe(Effect.andThen(input.client.request("thread/start", startParams))),
473 ),477 ),
474 );478 );
...@@ -658,16 +662,6 @@ function toCodexUserInputAnswers(...@@ -658,16 +662,6 @@ function toCodexUserInputAnswers(
658 ).pipe(Effect.map((entries) => Object.fromEntries(entries)));662 ).pipe(Effect.map((entries) => Object.fromEntries(entries)));
659}663}
660664
661function toProtocolParseError(
662 detail: string,
663 cause: Schema.SchemaError,
664): CodexErrors.CodexAppServerProtocolParseError {
665 return new CodexErrors.CodexAppServerProtocolParseError({
666 detail: `${detail}: ${formatSchemaIssue(cause.issue)}`,
667 cause,
668 });
669}
670
671function currentProviderThreadId(session: ProviderSession): string | undefined {665function currentProviderThreadId(session: ProviderSession): string | undefined {
672 return readResumeCursorThreadId(session.resumeCursor);666 return readResumeCursorThreadId(session.resumeCursor);
673}667}
...@@ -760,15 +754,16 @@ export const makeCodexSessionRuntime = (...@@ -760,15 +754,16 @@ export const makeCodexSessionRuntime = (
760 );754 );
761 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();755 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();
762 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);756 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
763 const randomUUIDv4 = crypto.randomUUIDv4.pipe(757 const randomUUIDv4 = (purpose: CodexErrors.CodexAppServerIdentifierPurpose) =>
764 Effect.mapError(758 crypto.randomUUIDv4.pipe(
765 (cause) =>759 Effect.mapError(
766 new CodexErrors.CodexAppServerTransportError({760 (cause) =>
767 detail: "Failed to generate Codex runtime identifier.",761 new CodexErrors.CodexAppServerIdentifierGenerationError({
768 cause,762 purpose,
769 }),763 cause,
770 ),764 }),
771 );765 ),
766 );
772767
773 const sessionCreatedAt = yield* nowIso;768 const sessionCreatedAt = yield* nowIso;
774 const initialSession = {769 const initialSession = {
...@@ -788,7 +783,7 @@ export const makeCodexSessionRuntime = (...@@ -788,7 +783,7 @@ export const makeCodexSessionRuntime = (
788783
789 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>784 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>
790 Effect.gen(function* () {785 Effect.gen(function* () {
791 const id = yield* randomUUIDv4;786 const id = yield* randomUUIDv4("provider-event");
792 return yield* offerEvent({787 return yield* offerEvent({
793 id: EventId.make(id),788 id: EventId.make(id),
794 provider: PROVIDER,789 provider: PROVIDER,
...@@ -956,7 +951,7 @@ export const makeCodexSessionRuntime = (...@@ -956,7 +951,7 @@ export const makeCodexSessionRuntime = (
956951
957 yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) =>952 yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) =>
958 Effect.gen(function* () {953 Effect.gen(function* () {
959 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);954 const requestId = ApprovalRequestId.make(yield* randomUUIDv4("command-approval-request"));
960 const turnId = TurnId.make(payload.turnId);955 const turnId = TurnId.make(payload.turnId);
961 const itemId = ProviderItemId.make(payload.itemId);956 const itemId = ProviderItemId.make(payload.itemId);
962 const decision = yield* Deferred.make<ProviderApprovalDecision>();957 const decision = yield* Deferred.make<ProviderApprovalDecision>();
...@@ -1012,7 +1007,9 @@ export const makeCodexSessionRuntime = (...@@ -1012,7 +1007,9 @@ export const makeCodexSessionRuntime = (
10121007
1013 yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) =>1008 yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) =>
1014 Effect.gen(function* () {1009 Effect.gen(function* () {
1015 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);1010 const requestId = ApprovalRequestId.make(
1011 yield* randomUUIDv4("file-change-approval-request"),
1012 );
1016 const turnId = TurnId.make(payload.turnId);1013 const turnId = TurnId.make(payload.turnId);
1017 const itemId = ProviderItemId.make(payload.itemId);1014 const itemId = ProviderItemId.make(payload.itemId);
1018 const decision = yield* Deferred.make<ProviderApprovalDecision>();1015 const decision = yield* Deferred.make<ProviderApprovalDecision>();
...@@ -1068,7 +1065,7 @@ export const makeCodexSessionRuntime = (...@@ -1068,7 +1065,7 @@ export const makeCodexSessionRuntime = (
10681065
1069 yield* client.handleServerRequest("item/tool/requestUserInput", (payload) =>1066 yield* client.handleServerRequest("item/tool/requestUserInput", (payload) =>
1070 Effect.gen(function* () {1067 Effect.gen(function* () {
1071 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);1068 const requestId = ApprovalRequestId.make(yield* randomUUIDv4("user-input-request"));
1072 const turnId = TurnId.make(payload.turnId);1069 const turnId = TurnId.make(payload.turnId);
1073 const itemId = ProviderItemId.make(payload.itemId);1070 const itemId = ProviderItemId.make(payload.itemId);
1074 const answers = yield* Deferred.make<ProviderUserInputAnswers>();1071 const answers = yield* Deferred.make<ProviderUserInputAnswers>();
...@@ -1293,7 +1290,11 @@ export const makeCodexSessionRuntime = (...@@ -1293,7 +1290,11 @@ export const makeCodexSessionRuntime = (
1293 const rawResponse = yield* client.raw.request("turn/start", params);1290 const rawResponse = yield* client.raw.request("turn/start", params);
1294 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(1291 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
1295 Effect.mapError((error) =>1292 Effect.mapError((error) =>
1296 toProtocolParseError("Invalid turn/start response payload", error),1293 CodexErrors.CodexAppServerProtocolParseError.fromSchemaError(
1294 "decode-response-payload",
1295 error,
1296 { method: "turn/start" },
1297 ),
1297 ),1298 ),
1298 );1299 );
1299 const turnId = TurnId.make(response.turn.id);1300 const turnId = TurnId.make(response.turn.id);
packages/effect-codex-app-server/src/_internal/shared.test.ts created+151
...@@ -0,0 +1,151 @@
1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4
5import * as CodexError from "../errors.ts";
6import * as Shared from "./shared.ts";
7
8const decodeNestedNumberPayload = Schema.decodeUnknownEffect(
9 Schema.Struct({ profile: Schema.Struct({ token: Schema.Number }) }),
10);
11const encodeUnknownJson = Schema.encodeSync(Schema.UnknownFromJsonString);
12
13it.effect("preserves schema decode diagnostics without deriving the message from the cause", () =>
14 Effect.gen(function* () {
15 const error = yield* Shared.decodeOptionalPayload("thread/start", Schema.String, 42).pipe(
16 Effect.flip,
17 );
18
19 assert.instanceOf(error, CodexError.CodexAppServerRequestError);
20 assert.equal(error.code, -32602);
21 assert.equal(error.method, "thread/start");
22 assert.equal(error.operation, "decode-payload");
23 assert.equal(
24 error.message,
25 "Invalid payload for method 'thread/start' during 'decode-payload'",
26 );
27 assert.isTrue(Schema.isSchemaError(error.cause));
28
29 const protocolError = error.toProtocolError();
30 assert.equal(protocolError.code, -32602);
31 assert.equal(protocolError.message, error.message);
32 assert.property(protocolError, "data");
33 assert.notProperty(protocolError, "method");
34 assert.notProperty(protocolError, "operation");
35 assert.notProperty(protocolError, "cause");
36 }),
37);
38
39it.effect("preserves schema encode diagnostics", () =>
40 Effect.gen(function* () {
41 const error = yield* Shared.encodeOptionalPayload(
42 "thread/start",
43 Schema.Number,
44 "not-a-number" as never,
45 ).pipe(Effect.flip);
46
47 assert.equal(error.method, "thread/start");
48 assert.equal(error.operation, "encode-payload");
49 assert.equal(
50 error.message,
51 "Invalid payload for method 'thread/start' during 'encode-payload'",
52 );
53 assert.isTrue(Schema.isSchemaError(error.cause));
54 }),
55);
56
57it.effect("does not invent a cause when a method has no payload schema", () =>
58 Effect.gen(function* () {
59 const secret = "unexpected-payload-secret";
60 const error = yield* Shared.decodeOptionalPayload<never, never>("initialized", undefined, {
61 token: secret,
62 }).pipe(Effect.flip);
63
64 assert.equal(error.method, "initialized");
65 assert.equal(error.operation, "decode-payload");
66 assert.equal(error.payloadKind, "object");
67 assert.deepEqual(error.data, { payloadKind: "object" });
68 assert.isUndefined(error.cause);
69 assert.notInclude(error.message, secret);
70 assert.notInclude(encodeUnknownJson(error.toProtocolError()), secret);
71 }),
72);
73
74it.effect("keeps invalid payload values only in the exact schema cause", () =>
75 Effect.gen(function* () {
76 const secret = "codex-schema-payload-secret";
77 const cause = yield* decodeNestedNumberPayload({ profile: { token: secret } }).pipe(
78 Effect.flip,
79 );
80 const error = CodexError.CodexAppServerRequestError.invalidPayload(
81 "thread/start",
82 "decode-payload",
83 cause,
84 );
85 const { cause: directCause, ...directDiagnostics } = error;
86
87 assert.strictEqual(directCause, cause);
88 assert.equal(error.method, "thread/start");
89 assert.equal(error.operation, "decode-payload");
90 assert.equal(error.maximumPathDepth, 2);
91 assert.isAbove(error.issueCount ?? 0, 0);
92 assert.include(error.issueKinds ?? [], "Pointer");
93 assert.notInclude(error.message, secret);
94 assert.notInclude(encodeUnknownJson(directDiagnostics), secret);
95 assert.notInclude(encodeUnknownJson(error.toProtocolError()), secret);
96 }),
97);
98
99it.effect("retains the request-handler error as the internal error cause", () =>
100 Effect.gen(function* () {
101 const rootCause = new Error("socket closed");
102 const source = new CodexError.CodexAppServerTransportError({
103 operation: "read-input-stream",
104 cause: rootCause,
105 });
106 const error = yield* Shared.runHandler(
107 (_payload: void) => Effect.fail(source),
108 undefined,
109 "thread/start",
110 ).pipe(Effect.flip);
111
112 assert.equal(error.code, -32603);
113 assert.equal(error.method, "thread/start");
114 assert.equal(error.operation, "handle-request");
115 assert.equal(
116 error.message,
117 "Codex App Server request handler failed for method 'thread/start'",
118 );
119 assert.strictEqual(error.cause, source);
120 assert.strictEqual(source.cause, rootCause);
121 assert.notInclude(error.message, source.message);
122 }),
123);
124
125it.effect("passes request errors through without adding a wrapper", () =>
126 Effect.gen(function* () {
127 const source = CodexError.CodexAppServerRequestError.invalidParams("Invalid thread id");
128 const error = yield* Shared.runHandler(
129 (_payload: void) => Effect.fail(source),
130 undefined,
131 "thread/start",
132 ).pipe(Effect.flip);
133
134 assert.strictEqual(error, source);
135 }),
136);
137
138it.effect("retains the full notification payload decode cause chain", () =>
139 Effect.gen(function* () {
140 const error = yield* Shared.decodeNotificationPayload(
141 "item/agentMessage/delta",
142 Schema.String,
143 42,
144 ).pipe(Effect.flip);
145
146 assert.equal(error.method, "item/agentMessage/delta");
147 assert.equal(error.operation, "decode-notification-payload");
148 assert.instanceOf(error.cause, CodexError.CodexAppServerRequestError);
149 assert.isTrue(Schema.isSchemaError(error.cause.cause));
150 }),
151);
packages/effect-codex-app-server/src/_internal/shared.ts+13-23
...@@ -1,11 +1,8 @@...@@ -1,11 +1,8 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
3import * as SchemaIssue from "effect/SchemaIssue";
43
5import * as CodexError from "../errors.ts";4import * as CodexError from "../errors.ts";
65
7const formatSchemaIssue = SchemaIssue.makeFormatterDefault();
8
9export const JsonRpcId = Schema.Union([Schema.Number, Schema.String]);6export const JsonRpcId = Schema.Union([Schema.Number, Schema.String]);
107
11export const JsonRpcError = Schema.Struct({8export const JsonRpcError = Schema.Struct({
...@@ -30,16 +27,13 @@ export const decodeOptionalPayload = <A, I>(...@@ -30,16 +27,13 @@ export const decodeOptionalPayload = <A, I>(
30 return Effect.sync(() => undefined as A);27 return Effect.sync(() => undefined as A);
31 }28 }
32 return Effect.fail(29 return Effect.fail(
33 CodexError.CodexAppServerRequestError.invalidParams(`${method} does not accept params`, raw),30 CodexError.CodexAppServerRequestError.unexpectedPayload(method, "decode-payload", raw),
34 );31 );
35 }32 }
3633
37 return Schema.decodeUnknownEffect(schema)(raw).pipe(34 return Schema.decodeUnknownEffect(schema)(raw).pipe(
38 Effect.mapError((error) =>35 Effect.mapError((error) =>
39 CodexError.CodexAppServerRequestError.invalidParams(36 CodexError.CodexAppServerRequestError.invalidPayload(method, "decode-payload", error),
40 `Invalid ${method} payload: ${formatSchemaIssue(error.issue)}`,
41 { issue: error.issue },
42 ),
43 ),37 ),
44 );38 );
45};39};
...@@ -54,19 +48,13 @@ export const encodeOptionalPayload = <A, I>(...@@ -54,19 +48,13 @@ export const encodeOptionalPayload = <A, I>(
54 return Effect.sync(() => undefined);48 return Effect.sync(() => undefined);
55 }49 }
56 return Effect.fail(50 return Effect.fail(
57 CodexError.CodexAppServerRequestError.invalidParams(51 CodexError.CodexAppServerRequestError.unexpectedPayload(method, "encode-payload", payload),
58 `${method} does not accept params`,
59 payload,
60 ),
61 );52 );
62 }53 }
6354
64 return Schema.encodeEffect(schema)(payload).pipe(55 return Schema.encodeEffect(schema)(payload).pipe(
65 Effect.mapError((error) =>56 Effect.mapError((error) =>
66 CodexError.CodexAppServerRequestError.invalidParams(57 CodexError.CodexAppServerRequestError.invalidPayload(method, "encode-payload", error),
67 `Invalid ${method} payload: ${formatSchemaIssue(error.issue)}`,
68 { issue: error.issue },
69 ),
70 ),58 ),
71 );59 );
72};60};
...@@ -77,12 +65,12 @@ export const decodeNotificationPayload = <A, I>(...@@ -77,12 +65,12 @@ export const decodeNotificationPayload = <A, I>(
77 raw: unknown,65 raw: unknown,
78): Effect.Effect<A, CodexError.CodexAppServerProtocolParseError> =>66): Effect.Effect<A, CodexError.CodexAppServerProtocolParseError> =>
79 decodeOptionalPayload(method, schema, raw).pipe(67 decodeOptionalPayload(method, schema, raw).pipe(
80 Effect.mapError(68 Effect.mapError((error) =>
81 (error) =>69 CodexError.CodexAppServerProtocolParseError.fromRequestError(
82 new CodexError.CodexAppServerProtocolParseError({70 "decode-notification-payload",
83 detail: error.message,71 method,
84 cause: error,72 error,
85 }),73 ),
86 ),74 ),
87 );75 );
8876
...@@ -96,6 +84,8 @@ export const runHandler = Effect.fnUntraced(function* <A, B>(...@@ -96,6 +84,8 @@ export const runHandler = Effect.fnUntraced(function* <A, B>(
96 }84 }
9785
98 return yield* handler(payload).pipe(86 return yield* handler(payload).pipe(
99 Effect.mapError((error) => CodexError.normalizeToRequestError(error)),87 Effect.mapError((error) =>
88 CodexError.CodexAppServerRequestError.fromAppServerError(error, method),
89 ),
100 );90 );
101});91});
packages/effect-codex-app-server/src/_internal/stdio.ts+1-1
...@@ -50,7 +50,7 @@ export const makeTerminationError = (...@@ -50,7 +50,7 @@ export const makeTerminationError = (
50 Effect.match(handle.exitCode, {50 Effect.match(handle.exitCode, {
51 onFailure: (cause) =>51 onFailure: (cause) =>
52 new CodexError.CodexAppServerTransportError({52 new CodexError.CodexAppServerTransportError({
53 detail: "Failed to determine Codex App Server process exit status",53 operation: "read-process-exit-status",
54 cause,54 cause,
55 }),55 }),
56 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code }),56 onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code }),
packages/effect-codex-app-server/src/errors.ts+243-13
...@@ -1,4 +1,119 @@...@@ -1,4 +1,119 @@
1import * as Schema from "effect/Schema";1import * as Schema from "effect/Schema";
2import type * as SchemaIssue from "effect/SchemaIssue";
3
4export const CodexAppServerRequestOperation = Schema.Literals([
5 "decode-payload",
6 "encode-payload",
7 "handle-request",
8]);
9export type CodexAppServerRequestOperation = typeof CodexAppServerRequestOperation.Type;
10
11export const CodexAppServerSchemaIssueKind = Schema.Literals([
12 "Filter",
13 "Encoding",
14 "Pointer",
15 "Composite",
16 "AnyOf",
17 "InvalidType",
18 "InvalidValue",
19 "MissingKey",
20 "UnexpectedKey",
21 "Forbidden",
22 "OneOf",
23]);
24export type CodexAppServerSchemaIssueKind = typeof CodexAppServerSchemaIssueKind.Type;
25
26export interface CodexAppServerSchemaIssueDiagnostics {
27 readonly issueCount: number;
28 readonly issueKinds: ReadonlyArray<CodexAppServerSchemaIssueKind>;
29 readonly maximumPathDepth: number;
30}
31
32const schemaIssueDiagnostics = (root: SchemaIssue.Issue): CodexAppServerSchemaIssueDiagnostics => {
33 let issueCount = 0;
34 let maximumPathDepth = 0;
35 const issueKinds = new Set<CodexAppServerSchemaIssueKind>();
36
37 const visit = (issue: SchemaIssue.Issue, pathDepth: number): void => {
38 issueCount += 1;
39 issueKinds.add(issue._tag);
40 maximumPathDepth = Math.max(maximumPathDepth, pathDepth);
41 switch (issue._tag) {
42 case "Filter":
43 case "Encoding":
44 visit(issue.issue, pathDepth);
45 break;
46 case "Pointer":
47 visit(issue.issue, pathDepth + issue.path.length);
48 break;
49 case "Composite":
50 case "AnyOf":
51 for (const child of issue.issues) visit(child, pathDepth);
52 break;
53 }
54 };
55
56 visit(root, 0);
57 return {
58 issueCount,
59 issueKinds: [...issueKinds],
60 maximumPathDepth,
61 };
62};
63
64export const CodexAppServerPayloadKind = Schema.Literals([
65 "null",
66 "array",
67 "string",
68 "number",
69 "boolean",
70 "bigint",
71 "object",
72 "symbol",
73 "function",
74 "undefined",
75]);
76export type CodexAppServerPayloadKind = typeof CodexAppServerPayloadKind.Type;
77
78const payloadKind = (payload: unknown): CodexAppServerPayloadKind => {
79 if (payload === null) return "null";
80 if (Array.isArray(payload)) return "array";
81 return typeof payload;
82};
83
84export interface CodexAppServerRequestDiagnostics {
85 readonly method?: string;
86 readonly operation?: CodexAppServerRequestOperation;
87 readonly cause?: unknown;
88 readonly issueCount?: number;
89 readonly issueKinds?: ReadonlyArray<CodexAppServerSchemaIssueKind>;
90 readonly maximumPathDepth?: number;
91 readonly payloadKind?: CodexAppServerPayloadKind;
92}
93
94export const CodexAppServerProtocolParseOperation = Schema.Literals([
95 "encode-wire-message",
96 "decode-wire-message",
97 "route-wire-message",
98 "decode-notification-payload",
99 "decode-request-payload",
100 "decode-response-payload",
101]);
102export type CodexAppServerProtocolParseOperation = typeof CodexAppServerProtocolParseOperation.Type;
103
104export const CodexAppServerTransportOperation = Schema.Literals([
105 "read-input-stream",
106 "read-process-exit-status",
107]);
108export type CodexAppServerTransportOperation = typeof CodexAppServerTransportOperation.Type;
109
110export const CodexAppServerIdentifierPurpose = Schema.Literals([
111 "provider-event",
112 "command-approval-request",
113 "file-change-approval-request",
114 "user-input-request",
115]);
116export type CodexAppServerIdentifierPurpose = typeof CodexAppServerIdentifierPurpose.Type;
2117
3export interface CodexAppServerProtocolErrorShape {118export interface CodexAppServerProtocolErrorShape {
4 readonly code: number;119 readonly code: number;
...@@ -37,24 +152,79 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co...@@ -37,24 +152,79 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass<Co
37export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<CodexAppServerProtocolParseError>()(152export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<CodexAppServerProtocolParseError>()(
38 "CodexAppServerProtocolParseError",153 "CodexAppServerProtocolParseError",
39 {154 {
40 detail: Schema.String,155 operation: CodexAppServerProtocolParseOperation,
156 method: Schema.optionalKey(Schema.String),
157 detail: Schema.optionalKey(Schema.String),
158 issueCount: Schema.optionalKey(Schema.Number),
159 issueKinds: Schema.optionalKey(Schema.Array(CodexAppServerSchemaIssueKind)),
160 maximumPathDepth: Schema.optionalKey(Schema.Number),
41 cause: Schema.optional(Schema.Defect()),161 cause: Schema.optional(Schema.Defect()),
42 },162 },
43) {163) {
44 override get message() {164 override get message() {
45 return `Failed to parse Codex App Server protocol message: ${this.detail}`;165 const method = this.method === undefined ? "" : ` for method '${this.method}'`;
166 return `Codex App Server protocol operation '${this.operation}' failed${method}.`;
167 }
168
169 static fromSchemaError(
170 operation: CodexAppServerProtocolParseOperation,
171 cause: Schema.SchemaError,
172 context: { readonly method?: string } = {},
173 ) {
174 return new CodexAppServerProtocolParseError({
175 operation,
176 ...context,
177 ...schemaIssueDiagnostics(cause.issue),
178 cause,
179 });
180 }
181
182 static fromRequestError(
183 operation: CodexAppServerProtocolParseOperation,
184 method: string,
185 cause: CodexAppServerRequestError,
186 ) {
187 return new CodexAppServerProtocolParseError({
188 operation,
189 method,
190 ...(cause.issueCount === undefined ? {} : { issueCount: cause.issueCount }),
191 ...(cause.issueKinds === undefined ? {} : { issueKinds: cause.issueKinds }),
192 ...(cause.maximumPathDepth === undefined ? {} : { maximumPathDepth: cause.maximumPathDepth }),
193 cause,
194 });
46 }195 }
47}196}
48197
49export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexAppServerTransportError>()(198export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexAppServerTransportError>()(
50 "CodexAppServerTransportError",199 "CodexAppServerTransportError",
51 {200 {
52 detail: Schema.String,201 operation: CodexAppServerTransportOperation,
53 cause: Schema.Defect(),202 cause: Schema.Defect(),
54 },203 },
55) {204) {
56 override get message() {205 override get message() {
57 return this.detail;206 return `Codex App Server transport operation '${this.operation}' failed.`;
207 }
208}
209
210export class CodexAppServerIdentifierGenerationError extends Schema.TaggedErrorClass<CodexAppServerIdentifierGenerationError>()(
211 "CodexAppServerIdentifierGenerationError",
212 {
213 purpose: CodexAppServerIdentifierPurpose,
214 cause: Schema.Defect(),
215 },
216) {
217 override get message() {
218 return `Failed to generate Codex App Server identifier for ${this.purpose}.`;
219 }
220}
221
222export class CodexAppServerInputStreamEndedError extends Schema.TaggedErrorClass<CodexAppServerInputStreamEndedError>()(
223 "CodexAppServerInputStreamEndedError",
224 {},
225) {
226 override get message() {
227 return "Codex App Server input stream ended.";
58 }228 }
59}229}
60230
...@@ -64,6 +234,13 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp...@@ -64,6 +234,13 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp
64 code: Schema.Number,234 code: Schema.Number,
65 errorMessage: Schema.String,235 errorMessage: Schema.String,
66 data: Schema.optional(Schema.Unknown),236 data: Schema.optional(Schema.Unknown),
237 method: Schema.optionalKey(Schema.String),
238 operation: Schema.optionalKey(CodexAppServerRequestOperation),
239 issueCount: Schema.optionalKey(Schema.Number),
240 issueKinds: Schema.optionalKey(Schema.Array(CodexAppServerSchemaIssueKind)),
241 maximumPathDepth: Schema.optionalKey(Schema.Number),
242 payloadKind: Schema.optionalKey(CodexAppServerPayloadKind),
243 cause: Schema.optionalKey(Schema.Defect()),
67 },244 },
68) {245) {
69 override get message() {246 override get message() {
...@@ -78,6 +255,21 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp...@@ -78,6 +255,21 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp
78 });255 });
79 }256 }
80257
258 static fromAppServerError(error: CodexAppServerError, method: string) {
259 if (error._tag === "CodexAppServerRequestError") {
260 return error;
261 }
262 return CodexAppServerRequestError.internalError(
263 `Codex App Server request handler failed for method '${method}'`,
264 undefined,
265 {
266 method,
267 operation: "handle-request",
268 cause: error,
269 },
270 );
271 }
272
81 static parseError(message = "Parse error", data?: unknown) {273 static parseError(message = "Parse error", data?: unknown) {
82 return new CodexAppServerRequestError({274 return new CodexAppServerRequestError({
83 code: -32700,275 code: -32700,
...@@ -101,19 +293,62 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp...@@ -101,19 +293,62 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp
101 });293 });
102 }294 }
103295
104 static invalidParams(message = "Invalid params", data?: unknown) {296 static invalidParams(
297 message = "Invalid params",
298 data?: unknown,
299 diagnostics: CodexAppServerRequestDiagnostics = {},
300 ) {
105 return new CodexAppServerRequestError({301 return new CodexAppServerRequestError({
106 code: -32602,302 code: -32602,
107 errorMessage: message,303 errorMessage: message,
108 ...(data !== undefined ? { data } : {}),304 ...(data !== undefined ? { data } : {}),
305 ...diagnostics,
109 });306 });
110 }307 }
111308
112 static internalError(message = "Internal error", data?: unknown) {309 static invalidPayload(
310 method: string,
311 operation: "decode-payload" | "encode-payload",
312 cause: Schema.SchemaError,
313 ) {
314 const diagnostics = schemaIssueDiagnostics(cause.issue);
315 return new CodexAppServerRequestError({
316 code: -32602,
317 errorMessage: `Invalid payload for method '${method}' during '${operation}'`,
318 data: diagnostics,
319 method,
320 operation,
321 ...diagnostics,
322 cause,
323 });
324 }
325
326 static unexpectedPayload(
327 method: string,
328 operation: "decode-payload" | "encode-payload",
329 payload: unknown,
330 ) {
331 const diagnostics = { payloadKind: payloadKind(payload) };
332 return new CodexAppServerRequestError({
333 code: -32602,
334 errorMessage: `Method '${method}' does not accept a payload during '${operation}'`,
335 data: diagnostics,
336 method,
337 operation,
338 ...diagnostics,
339 });
340 }
341
342 static internalError(
343 message = "Internal error",
344 data?: unknown,
345 diagnostics: CodexAppServerRequestDiagnostics = {},
346 ) {
113 return new CodexAppServerRequestError({347 return new CodexAppServerRequestError({
114 code: -32603,348 code: -32603,
115 errorMessage: message,349 errorMessage: message,
116 ...(data !== undefined ? { data } : {}),350 ...(data !== undefined ? { data } : {}),
351 ...diagnostics,
117 });352 });
118 }353 }
119354
...@@ -140,13 +375,8 @@ export const CodexAppServerError = Schema.Union([...@@ -140,13 +375,8 @@ export const CodexAppServerError = Schema.Union([
140 CodexAppServerProcessExitedError,375 CodexAppServerProcessExitedError,
141 CodexAppServerProtocolParseError,376 CodexAppServerProtocolParseError,
142 CodexAppServerTransportError,377 CodexAppServerTransportError,
378 CodexAppServerIdentifierGenerationError,
379 CodexAppServerInputStreamEndedError,
143]);380]);
144381
145export type CodexAppServerError = typeof CodexAppServerError.Type;382export type CodexAppServerError = typeof CodexAppServerError.Type;
146const isCodexAppServerRequestError = Schema.is(CodexAppServerRequestError);
147
148export function normalizeToRequestError(error: CodexAppServerError): CodexAppServerRequestError {
149 return isCodexAppServerRequestError(error)
150 ? error
151 : CodexAppServerRequestError.internalError(error.message);
152}
packages/effect-codex-app-server/src/protocol.test.ts+59-2
...@@ -211,13 +211,70 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => {...@@ -211,13 +211,70 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => {
211211
212 const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip);212 const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip);
213 assert.instanceOf(bigintError, CodexError.CodexAppServerProtocolParseError);213 assert.instanceOf(bigintError, CodexError.CodexAppServerProtocolParseError);
214 assert.equal(bigintError.detail, "Failed to encode Codex App Server message");214 assert.equal(bigintError.operation, "encode-wire-message");
215 assert.exists(bigintError.cause);
216 assert.equal(
217 bigintError.message,
218 "Codex App Server protocol operation 'encode-wire-message' failed.",
219 );
215220
216 const circular: Record<string, unknown> = {};221 const circular: Record<string, unknown> = {};
217 circular.self = circular;222 circular.self = circular;
218 const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip);223 const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip);
219 assert.instanceOf(circularError, CodexError.CodexAppServerProtocolParseError);224 assert.instanceOf(circularError, CodexError.CodexAppServerProtocolParseError);
220 assert.equal(circularError.detail, "Failed to encode Codex App Server message");225 assert.equal(circularError.operation, "encode-wire-message");
226 assert.exists(circularError.cause);
227 }),
228 );
229
230 it.effect("logs decode failures without copying the cause or wire payload", () =>
231 Effect.gen(function* () {
232 const secret = "codex-wire-secret-sentinel";
233 const { stdio, input } = yield* makeInMemoryStdio();
234 const events: Array<CodexProtocol.CodexAppServerProtocolLogEvent> = [];
235 const termination = yield* Deferred.make<CodexError.CodexAppServerError>();
236 yield* CodexProtocol.makeCodexAppServerPatchedProtocol({
237 stdio,
238 logIncoming: true,
239 logger: (event) =>
240 Effect.sync(() => {
241 events.push(event);
242 }),
243 onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid),
244 });
245
246 yield* Queue.offer(input, encoder.encode(`{"secret":"${secret}"\n`));
247 yield* Deferred.await(termination);
248
249 const event = events.find(({ stage }) => stage === "decode_failed");
250 assert.exists(event);
251 assert.equal(event.direction, "incoming");
252 const payload = event.payload as Record<string, unknown>;
253 assert.equal(payload.operation, "decode-wire-message");
254 assert.isNumber(payload.issueCount);
255 assert.isArray(payload.issueKinds);
256 assert.isNumber(payload.maximumPathDepth);
257 assert.equal("cause" in payload, false);
258 assert.equal("detail" in payload, false);
259 assert.notInclude(encodeUnknownJsonString(event), secret);
260 }),
261 );
262
263 it.effect("classifies an input stream ending without inventing a cause", () =>
264 Effect.gen(function* () {
265 const { stdio, input } = yield* makeInMemoryStdio();
266 const termination = yield* Deferred.make<CodexError.CodexAppServerError>();
267 yield* CodexProtocol.makeCodexAppServerPatchedProtocol({
268 stdio,
269 onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid),
270 });
271
272 yield* Queue.end(input);
273
274 const error = yield* Deferred.await(termination);
275 assert.instanceOf(error, CodexError.CodexAppServerInputStreamEndedError);
276 assert.equal(error.message, "Codex App Server input stream ended.");
277 assert.equal("cause" in error, false);
221 }),278 }),
222 );279 );
223});280});
packages/effect-codex-app-server/src/protocol.ts+26-24
...@@ -94,12 +94,8 @@ const encodeWireMessage = (...@@ -94,12 +94,8 @@ const encodeWireMessage = (
94): Effect.Effect<string, CodexError.CodexAppServerProtocolParseError> =>94): Effect.Effect<string, CodexError.CodexAppServerProtocolParseError> =>
95 encodeJsonString(message).pipe(95 encodeJsonString(message).pipe(
96 Effect.map((encoded) => `${encoded}\n`),96 Effect.map((encoded) => `${encoded}\n`),
97 Effect.mapError(97 Effect.mapError((cause) =>
98 (cause) =>98 CodexError.CodexAppServerProtocolParseError.fromSchemaError("encode-wire-message", cause),
99 new CodexError.CodexAppServerProtocolParseError({
100 detail: "Failed to encode Codex App Server message",
101 cause,
102 }),
103 ),99 ),
104 );100 );
105101
...@@ -107,20 +103,19 @@ const decodeWireMessage = (...@@ -107,20 +103,19 @@ const decodeWireMessage = (
107 line: string,103 line: string,
108): Effect.Effect<unknown, CodexError.CodexAppServerProtocolParseError> =>104): Effect.Effect<unknown, CodexError.CodexAppServerProtocolParseError> =>
109 decodeJsonString(line).pipe(105 decodeJsonString(line).pipe(
110 Effect.mapError(106 Effect.mapError((cause) =>
111 (cause) =>107 CodexError.CodexAppServerProtocolParseError.fromSchemaError("decode-wire-message", cause),
112 new CodexError.CodexAppServerProtocolParseError({
113 detail: "Failed to decode Codex App Server wire message",
114 cause,
115 }),
116 ),108 ),
117 );109 );
118110
119const normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError =>111const normalizeIncomingError = (
112 error: unknown,
113 operation: CodexError.CodexAppServerTransportOperation,
114): CodexError.CodexAppServerError =>
120 isCodexAppServerError(error)115 isCodexAppServerError(error)
121 ? error116 ? error
122 : new CodexError.CodexAppServerTransportError({117 : new CodexError.CodexAppServerTransportError({
123 detail,118 operation,
124 cause: error,119 cause: error,
125 });120 });
126121
...@@ -262,7 +257,13 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa...@@ -262,7 +257,13 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
262 ? options.onRequest(request).pipe(257 ? options.onRequest(request).pipe(
263 Effect.matchEffect({258 Effect.matchEffect({
264 onFailure: (error) =>259 onFailure: (error) =>
265 respondError(request.id, CodexError.normalizeToRequestError(error)),260 respondError(
261 request.id,
262 CodexError.CodexAppServerRequestError.fromAppServerError(
263 error,
264 request.method,
265 ),
266 ),
266 onSuccess: (result) => respond(request.id, result),267 onSuccess: (result) => respond(request.id, result),
267 }),268 }),
268 )269 )
...@@ -292,6 +293,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa...@@ -292,6 +293,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
292 return Effect.fail(293 return Effect.fail(
293 new CodexError.CodexAppServerProtocolParseError({294 new CodexError.CodexAppServerProtocolParseError({
294 detail: "Received protocol message in an unknown shape",295 detail: "Received protocol message in an unknown shape",
296 operation: "route-wire-message",
295 }),297 }),
296 );298 );
297 };299 };
...@@ -318,8 +320,13 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa...@@ -318,8 +320,13 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
318 direction: "incoming",320 direction: "incoming",
319 stage: "decode_failed",321 stage: "decode_failed",
320 payload: {322 payload: {
321 detail: error.detail,323 operation: error.operation,
322 cause: error.cause,324 ...(error.method === undefined ? {} : { method: error.method }),
325 ...(error.issueCount === undefined ? {} : { issueCount: error.issueCount }),
326 ...(error.issueKinds === undefined ? {} : { issueKinds: error.issueKinds }),
327 ...(error.maximumPathDepth === undefined
328 ? {}
329 : { maximumPathDepth: error.maximumPathDepth }),
323 },330 },
324 }),331 }),
325 ),332 ),
...@@ -340,7 +347,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa...@@ -340,7 +347,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
340 Effect.matchEffect({347 Effect.matchEffect({
341 onFailure: (error) =>348 onFailure: (error) =>
342 handleTermination(() =>349 handleTermination(() =>
343 Effect.succeed(normalizeIncomingError(error, "Codex App Server input stream failed")),350 Effect.succeed(normalizeIncomingError(error, "read-input-stream")),
344 ),351 ),
345 onSuccess: () =>352 onSuccess: () =>
346 Ref.get(remainder).pipe(353 Ref.get(remainder).pipe(
...@@ -351,12 +358,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa...@@ -351,12 +358,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
351 handleTermination(358 handleTermination(
352 () =>359 () =>
353 options.terminationError ??360 options.terminationError ??
354 Effect.succeed(361 Effect.succeed(new CodexError.CodexAppServerInputStreamEndedError({})),
355 new CodexError.CodexAppServerTransportError({
356 detail: "Codex App Server input stream ended",
357 cause: new Error("Codex App Server input stream ended"),
358 }),
359 ),
360 ),362 ),
361 }),363 }),
362 ),364 ),