authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:12:31-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:12:31-07:00
log6d8d995621065e0513def2fa28bf0f7d8b54dd90
tree1789927186d2595e39444380775fc8110831c4c4
parentb6c590af389d09400fa4f99a56ffc33e9ed4ebf1
signature Signed by PGP key B5690EEEBB952194

[codex] Correlate protocol request failures (#3433)

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

8 files changed, 314 insertions(+), 59 deletions(-)

packages/effect-acp/src/_internal/shared.ts+1-1
......@@ -12,7 +12,7 @@ export const callRpc = <A>(
1212): Effect.Effect<A, AcpError.AcpError> =>
1313 effect.pipe(
1414 Effect.catchIf(isError, (error) =>
15 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error)),
15 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error, { method })),
1616 ),
1717 Effect.catchTags({
1818 RpcClientError: (cause) =>
packages/effect-acp/src/errors.test.ts+2
......@@ -51,6 +51,8 @@ describe("effect-acp errors", () => {
5151 code: -32602,
5252 errorMessage: "Invalid params",
5353 data: { field: "sessionId" },
54 method: "session/load",
55 operation: "receive-response",
5456 });
5557 });
5658 });
packages/effect-acp/src/errors.ts+65-1
......@@ -5,8 +5,11 @@ import * as AcpSchema from "./_generated/schema.gen.ts";
55
66export const AcpRequestOperation = Schema.Literals([
77 "decode-extension-request-payload",
8 "encode-extension-response",
89 "handle-request",
910 "handle-extension-request",
11 "receive-response",
12 "receive-streaming-response",
1013]);
1114export type AcpRequestOperation = typeof AcpRequestOperation.Type;
1215
......@@ -65,6 +68,7 @@ const schemaIssueDiagnostics = (root: SchemaIssue.Issue): AcpSchemaIssueDiagnost
6568
6669export interface AcpRequestDiagnostics {
6770 readonly method?: string;
71 readonly requestId?: string;
6872 readonly operation?: AcpRequestOperation;
6973 readonly cause?: unknown;
7074 readonly issueCount?: number;
......@@ -109,6 +113,7 @@ export class AcpProtocolParseError extends Schema.TaggedErrorClass<AcpProtocolPa
109113 {
110114 operation: AcpProtocolParseOperation,
111115 method: Schema.optionalKey(Schema.String),
116 requestId: Schema.optionalKey(Schema.String),
112117 issueCount: Schema.optionalKey(Schema.Number),
113118 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
114119 maximumPathDepth: Schema.optionalKey(Schema.Number),
......@@ -132,6 +137,19 @@ export class AcpProtocolParseError extends Schema.TaggedErrorClass<AcpProtocolPa
132137 cause,
133138 });
134139 }
140
141 static fromEncodingError(
142 method: string | undefined,
143 requestId: string | undefined,
144 cause: unknown,
145 ) {
146 return new AcpProtocolParseError({
147 operation: "encode-message",
148 ...(method === undefined ? {} : { method }),
149 ...(requestId === undefined ? {} : { requestId }),
150 cause,
151 });
152 }
135153}
136154
137155export class AcpTransportError extends Schema.TaggedErrorClass<AcpTransportError>()(
......@@ -167,6 +185,7 @@ export class AcpRequestError extends Schema.TaggedErrorClass<AcpRequestError>()(
167185 errorMessage: Schema.String,
168186 data: Schema.optional(Schema.Unknown),
169187 method: Schema.optionalKey(Schema.String),
188 requestId: Schema.optionalKey(Schema.String),
170189 operation: Schema.optionalKey(AcpRequestOperation),
171190 issueCount: Schema.optionalKey(Schema.Number),
172191 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
......@@ -177,14 +196,59 @@ export class AcpRequestError extends Schema.TaggedErrorClass<AcpRequestError>()(
177196 return this.errorMessage;
178197 }
179198
180 static fromProtocolError(error: AcpSchema.Error) {
199 static fromProtocolError(
200 error: AcpSchema.Error,
201 context: {
202 readonly method: string;
203 readonly requestId?: string;
204 readonly cause?: unknown;
205 },
206 ) {
181207 return new AcpRequestError({
182208 code: error.code,
183209 errorMessage: error.message,
184210 ...(error.data !== undefined ? { data: error.data } : {}),
211 method: context.method,
212 ...(context.requestId === undefined ? {} : { requestId: context.requestId }),
213 operation: "receive-response",
214 cause: context.cause ?? error,
185215 });
186216 }
187217
218 static fromExtensionResponseFailure(method: string, requestId: string, cause: unknown) {
219 return AcpRequestError.internalError("Extension request failed", undefined, {
220 method,
221 requestId,
222 operation: "receive-response",
223 cause,
224 });
225 }
226
227 static fromExtensionResponseEncodingError(
228 method: string,
229 requestId: string,
230 cause: AcpProtocolParseError,
231 ) {
232 return AcpRequestError.internalError("Internal error", undefined, {
233 method,
234 requestId,
235 operation: "encode-extension-response",
236 cause,
237 });
238 }
239
240 static unsupportedStreamingResponse(method: string, requestId: string) {
241 return AcpRequestError.internalError(
242 "Streaming extension responses are not supported",
243 undefined,
244 {
245 method,
246 requestId,
247 operation: "receive-streaming-response",
248 },
249 );
250 }
251
188252 static fromCoreHandlerError(error: AcpError, method: string) {
189253 if (error._tag === "AcpRequestError") {
190254 return error;
packages/effect-acp/src/protocol.test.ts+73-1
......@@ -260,15 +260,33 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
260260 const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip);
261261 assert.instanceOf(bigintError, AcpError.AcpProtocolParseError);
262262 assert.equal(bigintError.operation, "encode-message");
263 assert.equal(bigintError.method, "x/test");
263264 assert.instanceOf(bigintError.cause, TypeError);
264 assert.equal(bigintError.message, "ACP protocol operation 'encode-message' failed.");
265 assert.equal(
266 bigintError.message,
267 "ACP protocol operation 'encode-message' failed for method 'x/test'.",
268 );
265269
266270 const circular: Record<string, unknown> = {};
267271 circular.self = circular;
268272 const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip);
269273 assert.instanceOf(circularError, AcpError.AcpProtocolParseError);
270274 assert.equal(circularError.operation, "encode-message");
275 assert.equal(circularError.method, "x/test");
271276 assert.instanceOf(circularError.cause, TypeError);
277
278 const requestError = yield* transport.request("x/request", 1n).pipe(
279 Effect.match({
280 onFailure: (error) => error,
281 onSuccess: () => assert.fail("Expected request encoding to fail"),
282 }),
283 );
284 assert.instanceOf(requestError, AcpError.AcpProtocolParseError);
285 assert.deepInclude(requestError, {
286 operation: "encode-message",
287 method: "x/request",
288 requestId: "1",
289 });
272290 }),
273291 );
274292
......@@ -310,6 +328,60 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
310328 }),
311329 );
312330
331 it.effect("correlates extension response errors with the originating request", () =>
332 Effect.gen(function* () {
333 const { stdio, input, output } = yield* makeInMemoryStdio();
334 const transport = yield* AcpProtocol.makeAcpPatchedProtocol({
335 stdio,
336 serverRequestMethods: new Set(),
337 });
338
339 const response = yield* transport
340 .request("x/private", { hello: "world" })
341 .pipe(Effect.forkScoped);
342 yield* Queue.take(output);
343 yield* Queue.offer(
344 input,
345 encoder.encode(
346 `${encodeUnknownJsonString({
347 jsonrpc: "2.0",
348 id: 1,
349 error: {
350 _tag: "Cause",
351 code: -32602,
352 message: "Invalid params",
353 data: [
354 {
355 _tag: "Fail",
356 error: {
357 code: -32602,
358 message: "Invalid params",
359 data: { field: "hello" },
360 },
361 },
362 ],
363 },
364 })}\n`,
365 ),
366 );
367
368 const error = yield* Fiber.join(response).pipe(
369 Effect.match({
370 onFailure: (error) => error,
371 onSuccess: () => assert.fail("Expected extension request to fail"),
372 }),
373 );
374 assert.instanceOf(error, AcpError.AcpRequestError);
375 assert.deepInclude(error, {
376 code: -32602,
377 errorMessage: "Invalid params",
378 method: "x/private",
379 requestId: "1",
380 operation: "receive-response",
381 });
382 }),
383 );
384
313385 it.effect("preserves zero-valued ids for inbound core client requests", () =>
314386 Effect.gen(function* () {
315387 const { stdio, input, output } = yield* makeInMemoryStdio();
packages/effect-acp/src/protocol.ts+67-31
......@@ -66,6 +66,11 @@ export interface AcpPatchedProtocol {
6666 readonly notify: (method: string, payload: unknown) => Effect.Effect<void, AcpError.AcpError>;
6767}
6868
69interface AcpPendingRequest {
70 readonly deferred: Deferred.Deferred<unknown, AcpError.AcpError>;
71 readonly method: string;
72}
73
6974const decodeSessionUpdate = Schema.decodeUnknownEffect(AcpSchema.SessionNotification);
7075const decodeElicitationComplete = Schema.decodeUnknownEffect(
7176 AcpSchema.ElicitationCompleteNotification,
......@@ -83,9 +88,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
8388 const outgoing = yield* Queue.unbounded<string | Uint8Array, Cause.Done<void>>();
8489 const nextRequestId = yield* Ref.make(1n);
8590 const terminationHandled = yield* Ref.make(false);
86 const extPending = yield* Ref.make(
87 new Map<string, Deferred.Deferred<unknown, AcpError.AcpError>>(),
88 );
91 const extPending = yield* Ref.make(new Map<string, AcpPendingRequest>());
8992
9093 const logProtocol = (event: AcpProtocolLogEvent) => {
9194 if (event.direction === "incoming" && !options.logIncoming) {
......@@ -109,13 +112,17 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
109112 payload: message,
110113 });
111114
115 const method = message._tag === "Request" ? message.tag : undefined;
116 const encodedRequestId =
117 message._tag === "Request"
118 ? message.id
119 : "requestId" in message
120 ? message.requestId
121 : undefined;
122 const requestId = encodedRequestId === "" ? undefined : encodedRequestId;
112123 const encoded = yield* Effect.try({
113124 try: () => parser.encode(message),
114 catch: (cause) =>
115 new AcpError.AcpProtocolParseError({
116 operation: "encode-message",
117 cause,
118 }),
125 catch: (cause) => AcpError.AcpProtocolParseError.fromEncodingError(method, requestId, cause),
119126 });
120127
121128 if (encoded) {
......@@ -131,16 +138,16 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
131138
132139 const resolveExtPending = (
133140 requestId: string,
134 onFound: (deferred: Deferred.Deferred<unknown, AcpError.AcpError>) => Effect.Effect<void>,
141 onFound: (pendingRequest: AcpPendingRequest) => Effect.Effect<void>,
135142 ) =>
136143 Ref.modify(extPending, (pending) => {
137 const deferred = pending.get(requestId);
138 if (!deferred) {
144 const pendingRequest = pending.get(requestId);
145 if (!pendingRequest) {
139146 return [Effect.void, pending] as const;
140147 }
141148 const next = new Map(pending);
142149 next.delete(requestId);
143 return [onFound(deferred), next] as const;
150 return [onFound(pendingRequest), next] as const;
144151 }).pipe(Effect.flatten);
145152
146153 const removeExtPending = (requestId: string) =>
......@@ -154,15 +161,15 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
154161 });
155162
156163 const completeExtPendingFailure = (requestId: string, error: AcpError.AcpError) =>
157 resolveExtPending(requestId, (deferred) => Deferred.fail(deferred, error));
164 resolveExtPending(requestId, ({ deferred }) => Deferred.fail(deferred, error));
158165
159166 const completeExtPendingSuccess = (requestId: string, value: unknown) =>
160 resolveExtPending(requestId, (deferred) => Deferred.succeed(deferred, value));
167 resolveExtPending(requestId, ({ deferred }) => Deferred.succeed(deferred, value));
161168
162169 const failAllExtPending = (error: AcpError.AcpError) =>
163170 Ref.getAndSet(extPending, new Map()).pipe(
164171 Effect.flatMap((pending) =>
165 Effect.forEach([...pending.values()], (deferred) => Deferred.fail(deferred, error), {
172 Effect.forEach([...pending.values()], ({ deferred }) => Deferred.fail(deferred, error), {
166173 discard: true,
167174 }),
168175 ),
......@@ -303,7 +310,26 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
303310
304311 if (!options.serverRequestMethods.has(message.tag)) {
305312 return handleExtRequest(message).pipe(
306 Effect.catch(() => respondWithError(message.id, AcpError.AcpRequestError.internalError())),
313 Effect.catchTags({
314 AcpProtocolParseError: (error) =>
315 Effect.logWarning(error).pipe(
316 Effect.annotateLogs({
317 method: message.tag,
318 requestId: message.id,
319 operation: error.operation,
320 }),
321 Effect.andThen(
322 respondWithError(
323 message.id,
324 AcpError.AcpRequestError.fromExtensionResponseEncodingError(
325 message.tag,
326 message.id,
327 error,
328 ),
329 ),
330 ),
331 ),
332 }),
307333 Effect.asVoid,
308334 );
309335 }
......@@ -314,7 +340,8 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
314340 const handleExitEncoded = (message: RpcMessage.ResponseExitEncoded) =>
315341 Ref.get(extPending).pipe(
316342 Effect.flatMap((pending) => {
317 if (!pending.has(message.requestId)) {
343 const pendingRequest = pending.get(message.requestId);
344 if (!pendingRequest) {
318345 return Queue.offer(clientQueue, message).pipe(Effect.asVoid);
319346 }
320347 if (message.exit._tag === "Success") {
......@@ -324,12 +351,20 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
324351 if (failure && isProtocolError(failure.error)) {
325352 return completeExtPendingFailure(
326353 message.requestId,
327 AcpError.AcpRequestError.fromProtocolError(failure.error),
354 AcpError.AcpRequestError.fromProtocolError(failure.error, {
355 method: pendingRequest.method,
356 requestId: message.requestId,
357 cause: message.exit.cause,
358 }),
328359 );
329360 }
330361 return completeExtPendingFailure(
331362 message.requestId,
332 AcpError.AcpRequestError.internalError("Extension request failed"),
363 AcpError.AcpRequestError.fromExtensionResponseFailure(
364 pendingRequest.method,
365 message.requestId,
366 message.exit.cause,
367 ),
333368 );
334369 }),
335370 );
......@@ -344,16 +379,18 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
344379 return handleExitEncoded(message);
345380 case "Chunk":
346381 return Ref.get(extPending).pipe(
347 Effect.flatMap((pending) =>
348 pending.has(message.requestId)
382 Effect.flatMap((pending) => {
383 const pendingRequest = pending.get(message.requestId);
384 return pendingRequest
349385 ? completeExtPendingFailure(
350386 message.requestId,
351 AcpError.AcpRequestError.internalError(
352 "Streaming extension responses are not supported",
387 AcpError.AcpRequestError.unsupportedStreamingResponse(
388 pendingRequest.method,
389 message.requestId,
353390 ),
354391 )
355 : Queue.offer(clientQueue, message).pipe(Effect.asVoid),
356 ),
392 : Queue.offer(clientQueue, message).pipe(Effect.asVoid);
393 }),
357394 );
358395 case "Defect":
359396 case "ClientProtocolError":
......@@ -401,6 +438,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
401438 payload: {
402439 operation: error.operation,
403440 ...(error.method === undefined ? {} : { method: error.method }),
441 ...(error.requestId === undefined ? {} : { requestId: error.requestId }),
404442 ...(error.issueCount === undefined ? {} : { issueCount: error.issueCount }),
405443 ...(error.issueKinds === undefined ? {} : { issueKinds: error.issueKinds }),
406444 ...(error.maximumPathDepth === undefined
......@@ -494,18 +532,16 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
494532 (current) => [current, current + 1n] as const,
495533 );
496534 const deferred = yield* Deferred.make<unknown, AcpError.AcpError>();
497 yield* Ref.update(extPending, (pending) => new Map(pending).set(String(requestId), deferred));
535 yield* Ref.update(extPending, (pending) =>
536 new Map(pending).set(String(requestId), { deferred, method }),
537 );
498538 yield* offerOutgoing({
499539 _tag: "Request",
500540 id: String(requestId),
501541 tag: method,
502542 payload,
503543 headers: [],
504 }).pipe(
505 Effect.catch((error) =>
506 removeExtPending(String(requestId)).pipe(Effect.andThen(Effect.fail(error))),
507 ),
508 );
544 }).pipe(Effect.tapError(() => removeExtPending(String(requestId))));
509545 return yield* Deferred.await(deferred).pipe(
510546 Effect.onInterrupt(() => removeExtPending(String(requestId))),
511547 );
packages/effect-codex-app-server/src/errors.ts+14-2
......@@ -5,6 +5,7 @@ export const CodexAppServerRequestOperation = Schema.Literals([
55 "decode-payload",
66 "encode-payload",
77 "handle-request",
8 "receive-response",
89]);
910export type CodexAppServerRequestOperation = typeof CodexAppServerRequestOperation.Type;
1011
......@@ -83,6 +84,7 @@ const payloadKind = (payload: unknown): CodexAppServerPayloadKind => {
8384
8485export interface CodexAppServerRequestDiagnostics {
8586 readonly method?: string;
87 readonly requestId?: string;
8688 readonly operation?: CodexAppServerRequestOperation;
8789 readonly cause?: unknown;
8890 readonly issueCount?: number;
......@@ -154,6 +156,7 @@ export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<Co
154156 {
155157 operation: CodexAppServerProtocolParseOperation,
156158 method: Schema.optionalKey(Schema.String),
159 requestId: Schema.optionalKey(Schema.String),
157160 detail: Schema.optionalKey(Schema.String),
158161 issueCount: Schema.optionalKey(Schema.Number),
159162 issueKinds: Schema.optionalKey(Schema.Array(CodexAppServerSchemaIssueKind)),
......@@ -169,7 +172,7 @@ export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<Co
169172 static fromSchemaError(
170173 operation: CodexAppServerProtocolParseOperation,
171174 cause: Schema.SchemaError,
172 context: { readonly method?: string } = {},
175 context: { readonly method?: string; readonly requestId?: string } = {},
173176 ) {
174177 return new CodexAppServerProtocolParseError({
175178 operation,
......@@ -235,6 +238,7 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp
235238 errorMessage: Schema.String,
236239 data: Schema.optional(Schema.Unknown),
237240 method: Schema.optionalKey(Schema.String),
241 requestId: Schema.optionalKey(Schema.String),
238242 operation: Schema.optionalKey(CodexAppServerRequestOperation),
239243 issueCount: Schema.optionalKey(Schema.Number),
240244 issueKinds: Schema.optionalKey(Schema.Array(CodexAppServerSchemaIssueKind)),
......@@ -247,11 +251,19 @@ export class CodexAppServerRequestError extends Schema.TaggedErrorClass<CodexApp
247251 return this.errorMessage;
248252 }
249253
250 static fromProtocolError(error: CodexAppServerProtocolErrorShape) {
254 static fromProtocolError(
255 error: CodexAppServerProtocolErrorShape,
256 method: string,
257 requestId: string,
258 ) {
251259 return new CodexAppServerRequestError({
252260 code: error.code,
253261 errorMessage: error.message,
254262 ...(error.data !== undefined ? { data: error.data } : {}),
263 method,
264 requestId,
265 operation: "receive-response",
266 cause: error,
255267 });
256268 }
257269
packages/effect-codex-app-server/src/protocol.test.ts+52-1
......@@ -212,10 +212,11 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => {
212212 const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip);
213213 assert.instanceOf(bigintError, CodexError.CodexAppServerProtocolParseError);
214214 assert.equal(bigintError.operation, "encode-wire-message");
215 assert.equal(bigintError.method, "x/test");
215216 assert.exists(bigintError.cause);
216217 assert.equal(
217218 bigintError.message,
218 "Codex App Server protocol operation 'encode-wire-message' failed.",
219 "Codex App Server protocol operation 'encode-wire-message' failed for method 'x/test'.",
219220 );
220221
221222 const circular: Record<string, unknown> = {};
......@@ -223,7 +224,57 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => {
223224 const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip);
224225 assert.instanceOf(circularError, CodexError.CodexAppServerProtocolParseError);
225226 assert.equal(circularError.operation, "encode-wire-message");
227 assert.equal(circularError.method, "x/test");
226228 assert.exists(circularError.cause);
229
230 const requestError = yield* transport.request("x/request", 1n).pipe(
231 Effect.match({
232 onFailure: (error) => error,
233 onSuccess: () => assert.fail("Expected request encoding to fail"),
234 }),
235 );
236 assert.instanceOf(requestError, CodexError.CodexAppServerProtocolParseError);
237 assert.deepInclude(requestError, {
238 operation: "encode-wire-message",
239 method: "x/request",
240 requestId: "1",
241 });
242 }),
243 );
244
245 it.effect("correlates response errors with the originating request", () =>
246 Effect.gen(function* () {
247 const { stdio, input, output } = yield* makeInMemoryStdio();
248 const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ stdio });
249
250 const response = yield* transport.request("thread/start", {}).pipe(Effect.forkScoped);
251 yield* Queue.take(output);
252 yield* Queue.offer(
253 input,
254 encodeJsonl({
255 id: 1,
256 error: {
257 code: -32602,
258 message: "Invalid params",
259 data: { field: "cwd" },
260 },
261 }),
262 );
263
264 const error = yield* Fiber.join(response).pipe(
265 Effect.match({
266 onFailure: (error) => error,
267 onSuccess: () => assert.fail("Expected Codex App Server request to fail"),
268 }),
269 );
270 assert.instanceOf(error, CodexError.CodexAppServerRequestError);
271 assert.deepInclude(error, {
272 code: -32602,
273 errorMessage: "Invalid params",
274 method: "thread/start",
275 requestId: "1",
276 operation: "receive-response",
277 });
227278 }),
228279 );
229280
packages/effect-codex-app-server/src/protocol.ts+40-22
......@@ -67,6 +67,11 @@ export interface CodexAppServerPatchedProtocol {
6767 ) => Effect.Effect<void, CodexError.CodexAppServerError>;
6868}
6969
70interface CodexAppServerPendingRequest {
71 readonly deferred: Deferred.Deferred<unknown, CodexError.CodexAppServerError>;
72 readonly method: string;
73}
74
7075function isObject(value: unknown): value is Record<string, unknown> {
7176 return typeof value === "object" && value !== null;
7277}
......@@ -94,9 +99,21 @@ const encodeWireMessage = (
9499): Effect.Effect<string, CodexError.CodexAppServerProtocolParseError> =>
95100 encodeJsonString(message).pipe(
96101 Effect.map((encoded) => `${encoded}\n`),
97 Effect.mapError((cause) =>
98 CodexError.CodexAppServerProtocolParseError.fromSchemaError("encode-wire-message", cause),
99 ),
102 Effect.mapError((cause) => {
103 const method = typeof message.method === "string" ? message.method : undefined;
104 const requestId =
105 typeof message.id === "string" || typeof message.id === "number"
106 ? String(message.id)
107 : undefined;
108 return CodexError.CodexAppServerProtocolParseError.fromSchemaError(
109 "encode-wire-message",
110 cause,
111 {
112 ...(method === undefined ? {} : { method }),
113 ...(requestId === undefined ? {} : { requestId }),
114 },
115 );
116 }),
100117 );
101118
102119const decodeWireMessage = (
......@@ -138,9 +155,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
138155 const outgoing = yield* Queue.unbounded<string, Cause.Done<void>>();
139156 const incomingNotifications = yield* Queue.unbounded<CodexAppServerIncomingNotification>();
140157 const incomingRequests = yield* Queue.unbounded<CodexAppServerIncomingRequest>();
141 const pending = yield* Ref.make(
142 new Map<string, Deferred.Deferred<unknown, CodexError.CodexAppServerError>>(),
143 );
158 const pending = yield* Ref.make(new Map<string, CodexAppServerPendingRequest>());
144159 const nextRequestId = yield* Ref.make(1);
145160 const remainder = yield* Ref.make("");
146161 const terminationHandled = yield* Ref.make(false);
......@@ -161,7 +176,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
161176 const failAllPending = (error: CodexError.CodexAppServerError) =>
162177 Ref.get(pending).pipe(
163178 Effect.flatMap((current) =>
164 Effect.forEach([...current.values()], (deferred) => Deferred.fail(deferred, error), {
179 Effect.forEach([...current.values()], ({ deferred }) => Deferred.fail(deferred, error), {
165180 discard: true,
166181 }),
167182 ),
......@@ -214,18 +229,16 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
214229
215230 const resolvePending = (
216231 requestId: string,
217 handler: (
218 deferred: Deferred.Deferred<unknown, CodexError.CodexAppServerError>,
219 ) => Effect.Effect<void>,
232 handler: (pendingRequest: CodexAppServerPendingRequest) => Effect.Effect<void>,
220233 ) =>
221234 Ref.modify(pending, (current) => {
222 const deferred = current.get(requestId);
223 if (!deferred) {
235 const pendingRequest = current.get(requestId);
236 if (!pendingRequest) {
224237 return [Effect.void, current] as const;
225238 }
226239 const next = new Map(current);
227240 next.delete(requestId);
228 return [handler(deferred), next] as const;
241 return [handler(pendingRequest), next] as const;
229242 }).pipe(Effect.flatten);
230243
231244 const respond = (requestId: string | number, result: unknown) =>
......@@ -240,14 +253,20 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
240253 const requestId = String(response.id);
241254 const protocolError = response.error;
242255 if (protocolError !== undefined) {
243 return resolvePending(requestId, (deferred) =>
256 return resolvePending(requestId, ({ deferred, method }) =>
244257 Deferred.fail(
245258 deferred,
246 CodexError.CodexAppServerRequestError.fromProtocolError(protocolError),
259 CodexError.CodexAppServerRequestError.fromProtocolError(
260 protocolError,
261 method,
262 requestId,
263 ),
247264 ),
248265 );
249266 }
250 return resolvePending(requestId, (deferred) => Deferred.succeed(deferred, response.result));
267 return resolvePending(requestId, ({ deferred }) =>
268 Deferred.succeed(deferred, response.result),
269 );
251270 };
252271
253272 const handleRequest = (request: CodexAppServerIncomingRequest) =>
......@@ -322,6 +341,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
322341 payload: {
323342 operation: error.operation,
324343 ...(error.method === undefined ? {} : { method: error.method }),
344 ...(error.requestId === undefined ? {} : { requestId: error.requestId }),
325345 ...(error.issueCount === undefined ? {} : { issueCount: error.issueCount }),
326346 ...(error.issueKinds === undefined ? {} : { issueKinds: error.issueKinds }),
327347 ...(error.maximumPathDepth === undefined
......@@ -375,16 +395,14 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa
375395 (current) => [current, current + 1] as const,
376396 );
377397 const deferred = yield* Deferred.make<unknown, CodexError.CodexAppServerError>();
378 yield* Ref.update(pending, (current) => new Map(current).set(String(requestId), deferred));
398 yield* Ref.update(pending, (current) =>
399 new Map(current).set(String(requestId), { deferred, method }),
400 );
379401 yield* offerOutgoing({
380402 id: requestId,
381403 method,
382404 ...(payload !== undefined ? { params: payload } : {}),
383 }).pipe(
384 Effect.catch((error) =>
385 removePending(String(requestId)).pipe(Effect.andThen(Effect.fail(error))),
386 ),
387 );
405 }).pipe(Effect.tapError(() => removePending(String(requestId))));
388406 return yield* Deferred.await(deferred).pipe(
389407 Effect.onInterrupt(() => removePending(String(requestId))),
390408 );