From 5d0bf6cd1a861e577f644475ab89d687d1a9f337 Mon Sep 17 00:00:00 2001 From: Nicholas Mattteo <130715692+nickmatteo@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:40:52 -0400 Subject: [PATCH] fix(server): avoid full history on Codex resume --- .../Layers/CodexSessionRuntime.test.ts | 97 ++++++++++++++----- .../provider/Layers/CodexSessionRuntime.ts | 26 ++++- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index d7346a0e0db..55e2b622b3e 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -48,16 +48,20 @@ function makeThreadOpenResponse( modelProvider: "openai", approvalPolicy: "never", approvalsReviewer: "user", - sandbox: { type: "danger-full-access" }, + sandbox: { type: "dangerFullAccess" }, thread: { id: threadId, - createdAt: "2026-04-18T00:00:00.000Z", - source: { session: "cli" }, + cliVersion: "0.147.0", + createdAt: 1_776_470_400, + cwd: "/tmp/project", + ephemeral: false, + modelProvider: "openai", + preview: "", + sessionId: threadId, + source: "cli", turns: [], - status: { - state: "idle", - activeFlags: [], - }, + status: { type: "idle" }, + updatedAt: 1_776_470_400, }, } as unknown as CodexRpc.ClientRequestResponsesByMethod["thread/start"]; } @@ -398,19 +402,22 @@ describe("openCodexThread", () => { const calls: Array<{ method: "thread/start" | "thread/resume"; payload: unknown }> = []; const started = makeThreadOpenResponse("fresh-thread"); const client = { - request: ( - method: M, - payload: CodexRpc.ClientRequestParamsByMethod[M], - ) => { - calls.push({ method, payload }); - if (method === "thread/resume") { + raw: { + request: (method: string, payload?: unknown) => { + calls.push({ method: method as "thread/resume", payload }); return Effect.fail( new CodexErrors.CodexAppServerRequestError({ code: -32603, errorMessage: "thread not found", }), ); - } + }, + }, + request: ( + method: M, + payload: CodexRpc.ClientRequestParamsByMethod[M], + ) => { + calls.push({ method, payload }); return Effect.succeed(started as CodexRpc.ClientRequestResponsesByMethod[M]); }, }; @@ -436,18 +443,19 @@ describe("openCodexThread", () => { it.effect("propagates non-recoverable resume failures", () => Effect.gen(function* () { const client = { - request: ( - method: M, - _payload: CodexRpc.ClientRequestParamsByMethod[M], - ) => { - if (method === "thread/resume") { - return Effect.fail( + raw: { + request: () => + Effect.fail( new CodexErrors.CodexAppServerRequestError({ code: -32603, errorMessage: "timed out waiting for server", }), - ); - } + ), + }, + request: ( + _method: M, + _payload: CodexRpc.ClientRequestParamsByMethod[M], + ) => { return Effect.succeed( makeThreadOpenResponse("fresh-thread") as CodexRpc.ClientRequestResponsesByMethod[M], ); @@ -468,4 +476,49 @@ describe("openCodexThread", () => { NodeAssert.equal(error.errorMessage, "timed out waiting for server"); }), ); + + it.effect("resumes without returning historical turns", () => + Effect.gen(function* () { + const calls: Array<{ method: string; payload: unknown }> = []; + const resumed = makeThreadOpenResponse("resumed-thread"); + const client = { + raw: { + request: (method: string, payload?: unknown) => { + calls.push({ method, payload }); + return Effect.succeed(resumed); + }, + }, + request: ( + _method: M, + _payload: CodexRpc.ClientRequestParamsByMethod[M], + ) => Effect.succeed(resumed as CodexRpc.ClientRequestResponsesByMethod[M]), + }; + + const opened = yield* openCodexThread({ + client, + threadId: ThreadId.make("thread-1"), + runtimeMode: "full-access", + cwd: "/tmp/project", + requestedModel: "gpt-5.3-codex", + serviceTier: undefined, + resumeThreadId: "existing-thread", + }); + + NodeAssert.equal(opened.thread.id, "resumed-thread"); + NodeAssert.deepStrictEqual(calls, [ + { + method: "thread/resume", + payload: { + threadId: "existing-thread", + cwd: "/tmp/project", + approvalPolicy: "never", + sandbox: "danger-full-access", + approvalsReviewer: "user", + model: "gpt-5.3-codex", + excludeTurns: true, + }, + }, + ]); + }), + ); }); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 58c012bd63e..2ca3f42be33 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -40,6 +40,9 @@ import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts"; const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); +const decodeV2ThreadResumeResponse = Schema.decodeUnknownEffect( + EffectCodexSchema.V2ThreadResumeResponse, +); const PROVIDER = ProviderDriverKind.make("codex"); @@ -448,6 +451,12 @@ type CodexThreadOpenResponse = type CodexThreadOpenMethod = "thread/start" | "thread/resume"; interface CodexThreadOpenClient { + readonly raw: { + readonly request: ( + method: string, + payload?: unknown, + ) => Effect.Effect; + }; readonly request: ( method: M, payload: CodexRpc.ClientRequestParamsByMethod[M], @@ -475,12 +484,27 @@ export const openCodexThread = (input: { return input.client.request("thread/start", startParams); } - return input.client + return input.client.raw .request("thread/resume", { threadId: resumeThreadId, ...startParams, + // T3 already persists the visible transcript. Asking Codex to rebuild and + // return every historical turn can make large resumes exhaust the local + // backend before the session becomes ready. + excludeTurns: true, }) .pipe( + Effect.flatMap((rawResponse) => + decodeV2ThreadResumeResponse(rawResponse).pipe( + Effect.mapError((error) => + CodexErrors.CodexAppServerProtocolParseError.fromSchemaError( + "decode-response-payload", + error, + { method: "thread/resume" }, + ), + ), + ), + ), Effect.catchIf(isRecoverableThreadResumeError, (error) => Effect.logWarning("codex app-server thread resume fell back to fresh start", { threadId: input.threadId,