Unified · Split
| @@ -365,11 +365,12 @@ function completeCodexTurnTokenUsage( · 364 unmodified lines | |||
| 365 | } | 365 | } |
| 366 | 366 | ||
| 367 | function toTurnStatus( | 367 | function toTurnStatus( |
| 368 | value: EffectCodexSchema.V2TurnCompletedNotification["turn"]["status"], | 368 | value: EffectCodexSchema.V2TurnCompletedNotification["turn"]["status"] | "cancelled", |
| 369 | ): "completed" | "failed" | "cancelled" | "interrupted" { | 369 | ): "completed" | "failed" | "cancelled" | "interrupted" { |
| 370 | switch (value) { | 370 | switch (value) { |
| 371 | case "completed": | 371 | case "completed": |
| 372 | case "failed": | 372 | case "failed": |
| 373 | case "cancelled": | ||
| 373 | case "interrupted": | 374 | case "interrupted": |
| 374 | return value; | 375 | return value; |
| 375 | default: | 376 | default: |
| @@ -171,6 +171,27 @@ describe("isRecoverableThreadResumeError", () => { · 170 unmodified lines | |||
| 171 | false, | 171 | false, |
| 172 | ); | 172 | ); |
| 173 | }); | 173 | }); |
| 174 | |||
| 175 | it("ignores unrelated missing-resource errors that do not mention threads", () => { | ||
| 176 | assert.equal( | ||
| 177 | isRecoverableThreadResumeError( | ||
| 178 | new CodexErrors.CodexAppServerRequestError({ | ||
| 179 | code: -32603, | ||
| 180 | errorMessage: "Config file not found", | ||
| 181 | }), | ||
| 182 | ), | ||
| 183 | false, | ||
| 184 | ); | ||
| 185 | assert.equal( | ||
| 186 | isRecoverableThreadResumeError( | ||
| 187 | new CodexErrors.CodexAppServerRequestError({ | ||
| 188 | code: -32603, | ||
| 189 | errorMessage: "Model does not exist", | ||
| 190 | }), | ||
| 191 | ), | ||
| 192 | false, | ||
| 193 | ); | ||
| 194 | }); | ||
| 174 | }); | 195 | }); |
| 175 | 196 | ||
| 176 | describe("openCodexThread", () => { | 197 | describe("openCodexThread", () => { |
| @@ -383,6 +383,9 @@ function classifyCodexStderrLine(rawLine: string): { readonly message: string } · 382 unmodified lines | |||
| 383 | 383 | ||
| 384 | export function isRecoverableThreadResumeError(error: unknown): boolean { | 384 | export function isRecoverableThreadResumeError(error: unknown): boolean { |
| 385 | const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); | 385 | const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); |
| 386 | if (!message.includes("thread")) { | ||
| 387 | return false; | ||
| 388 | } | ||
| 386 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); | 389 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); |
| 387 | } | 390 | } |
| 388 | 391 | ||
| @@ -390,8 +393,17 @@ type CodexThreadOpenResponse = · 1 unmodified line | |||
| 390 | | CodexRpc.ClientRequestResponsesByMethod["thread/start"] | 393 | | CodexRpc.ClientRequestResponsesByMethod["thread/start"] |
| 391 | | CodexRpc.ClientRequestResponsesByMethod["thread/resume"]; | 394 | | CodexRpc.ClientRequestResponsesByMethod["thread/resume"]; |
| 392 | 395 | ||
| 396 | type CodexThreadOpenMethod = "thread/start" | "thread/resume"; | ||
| 397 | |||
| 398 | interface CodexThreadOpenClient { | ||
| 399 | readonly request: <M extends CodexThreadOpenMethod>( | ||
| 400 | method: M, | ||
| 401 | payload: CodexRpc.ClientRequestParamsByMethod[M], | ||
| 402 | ) => Effect.Effect<CodexRpc.ClientRequestResponsesByMethod[M], CodexErrors.CodexAppServerError>; | ||
| 403 | } | ||
| 404 | |||
| 393 | export const openCodexThread = (input: { | 405 | export const openCodexThread = (input: { |
| 394 | readonly client: CodexClient.CodexAppServerClientShape; | 406 | readonly client: CodexThreadOpenClient; |
| 395 | readonly threadId: ThreadId; | 407 | readonly threadId: ThreadId; |
| 396 | readonly runtimeMode: RuntimeMode; | 408 | readonly runtimeMode: RuntimeMode; |
| 397 | readonly cwd: string; | 409 | readonly cwd: string; |
| @@ -1288,6 +1300,7 @@ export const makeCodexSessionRuntime = ( · 890 unmodified lines | |||
| 1288 | requestId, | 1300 | requestId, |
| 1289 | }); | 1301 | }); |
| 1290 | } | 1302 | } |
| 1303 | const codexAnswers = yield* toCodexUserInputAnswers(answers); | ||
| 1291 | yield* Ref.update(pendingUserInputsRef, (current) => { | 1304 | yield* Ref.update(pendingUserInputsRef, (current) => { |
| 1292 | const next = new Map(current); | 1305 | const next = new Map(current); |
| 1293 | next.delete(requestId); | 1306 | next.delete(requestId); |
| @@ -1302,7 +1315,7 @@ export const makeCodexSessionRuntime = ( · 8 unmodified lines | |||
| 1302 | ...(pending.turnId ? { turnId: pending.turnId } : {}), | 1315 | ...(pending.turnId ? { turnId: pending.turnId } : {}), |
| 1303 | ...(pending.itemId ? { itemId: pending.itemId } : {}), | 1316 | ...(pending.itemId ? { itemId: pending.itemId } : {}), |
| 1304 | payload: { | 1317 | payload: { |
| 1305 | answers: yield* toCodexUserInputAnswers(answers), | 1318 | answers: codexAnswers, |
| 1306 | }, | 1319 | }, |
| 1307 | }); | 1320 | }); |
| 1308 | }), | 1321 | }), |
| @@ -121,4 +121,34 @@ it.layer(NodeServices.layer)("effect-codex-app-server client", (it) => { · 120 unmodified lines | |||
| 121 | ]); | 121 | ]); |
| 122 | }), | 122 | }), |
| 123 | ); | 123 | ); |
| 124 | |||
| 125 | it.effect("initializes a command-backed app-server client", () => | ||
| 126 | Effect.gen(function* () { | ||
| 127 | const path = yield* Path.Path; | ||
| 128 | const scope = yield* Scope.make(); | ||
| 129 | const clientLayer = CodexClient.layerCommand({ | ||
| 130 | command: "bun", | ||
| 131 | args: ["run", yield* mockPeerPath], | ||
| 132 | cwd: path.join(import.meta.dirname, ".."), | ||
| 133 | }); | ||
| 134 | const context = yield* Layer.buildWithScope(clientLayer, scope); | ||
| 135 | |||
| 136 | const initialized = yield* Effect.gen(function* () { | ||
| 137 | const client = yield* CodexClient.CodexAppServerClient; | ||
| 138 | return yield* client.request("initialize", { | ||
| 139 | clientInfo: { | ||
| 140 | name: "effect-codex-app-server-test", | ||
| 141 | title: "Effect Codex App Server Test", | ||
| 142 | version: "0.0.0", | ||
| 143 | }, | ||
| 144 | capabilities: { | ||
| 145 | experimentalApi: true, | ||
| 146 | optOutNotificationMethods: null, | ||
| 147 | }, | ||
| 148 | }); | ||
| 149 | }).pipe(Effect.provide(context), Effect.ensuring(Scope.close(scope, Exit.void))); | ||
| 150 | |||
| 151 | assert.equal(initialized.userAgent, "mock-codex-app-server"); | ||
| 152 | }), | ||
| 153 | ); | ||
| 124 | }); | 154 | }); |
| @@ -281,7 +281,7 @@ export const layerCommand = ( · 280 unmodified lines | |||
| 281 | ...(options.env ? { env: { ...process.env, ...options.env } } : {}), | 281 | ...(options.env ? { env: { ...process.env, ...options.env } } : {}), |
| 282 | shell: process.platform === "win32", | 282 | shell: process.platform === "win32", |
| 283 | }); | 283 | }); |
| 284 | const handle = yield* spawner.spawn(command).pipe( | 284 | return yield* spawner.spawn(command).pipe( |
| 285 | Effect.mapError( | 285 | Effect.mapError( |
| 286 | (cause) => | 286 | (cause) => |
| 287 | new CodexError.CodexAppServerSpawnError({ | 287 | new CodexError.CodexAppServerSpawnError({ |
| @@ -290,9 +290,11 @@ export const layerCommand = ( · 2 unmodified lines | |||
| 290 | }), | 290 | }), |
| 291 | ), | 291 | ), |
| 292 | ); | 292 | ); |
| 293 | const client = yield* make(makeChildStdio(handle), options, makeTerminationError(handle)); | ||
| 294 | return { client, handle }; | ||
| 295 | }), | 293 | }), |
| 296 | ({ handle }) => handle.kill().pipe(Effect.orDie), | 294 | (handle) => handle.kill().pipe(Effect.orDie), |
| 297 | ).pipe(Effect.map(({ client }) => client)), | 295 | ).pipe( |
| 296 | Effect.flatMap((handle) => | ||
| 297 | make(makeChildStdio(handle), options, makeTerminationError(handle)), | ||
| 298 | ), | ||
| 299 | ), | ||
| 298 | ); | 300 | ); |