diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index d42199cb..6c3cff12 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -393,6 +393,7 @@ export class CodexAcpClient { const response = await this.codexClient.threadResume({ config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []), cwd: request.cwd, + excludeTurns: true, modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, }); @@ -417,6 +418,7 @@ export class CodexAcpClient { const response = await this.codexClient.threadResume({ config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []), cwd: request.cwd, + excludeTurns: true, modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, }); diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 50a95e42..99cfa7f0 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -526,7 +526,7 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "thread/start", params: params }); } - async threadResume(params: ThreadResumeParams): Promise { + async threadResume(params: ExperimentalThreadResumeParams): Promise { return await this.sendRequest({ method: "thread/resume", params: params }); } @@ -974,7 +974,11 @@ export type CompactionCompletedNotification = | { method: "thread/compacted", params: Extract["params"] } | { method: "item/completed", params: ItemCompletedNotification & { item: Extract } }; -type CodexRequest = DistributiveOmit +type StableCodexRequest = DistributiveOmit + +type CodexRequest = + | Exclude + | { method: "thread/resume", params: ExperimentalThreadResumeParams } type DistributiveOmit = T extends any ? Omit @@ -992,6 +996,12 @@ export interface ExperimentalThreadSettingsUpdateParams { }; } +// The adapter opts into app-server's experimental API, while the checked-in +// generated bindings currently contain only stable fields. +type ExperimentalThreadResumeParams = ThreadResumeParams & { + excludeTurns?: boolean; +}; + type McpServerStartupSnapshot = { status: McpServerStartupState; error: string | null; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index d1df6999..12e88295 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -4,12 +4,14 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR, type CodexAuthRequest} from "../../CodexAuthMethod"; import type * as acp from "@agentclientprotocol/sdk"; import { + createBaseTestFixture, createCodexMockTestFixture, createTestFixture, createTestModel, createTestSessionState, type TestFixture } from "../acp-test-utils"; +import type {MessageConnection} from "vscode-jsonrpc/node"; import type {ServerNotification} from "../../app-server"; import type {SessionState} from "../../CodexAcpServer"; import {AgentMode} from "../../AgentMode"; @@ -533,6 +535,92 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); + it('excludes unused app-server turns when resuming and loading sessions', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + + vi.spyOn(codexAppServerClient, "skillsExtraRootsSet").mockResolvedValue(undefined); + vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []}); + const threadResumeSpy = vi.spyOn(codexAppServerClient, "threadResume").mockResolvedValue({ + thread: {id: "thread-id"} as any, + model: "gpt-5", + modelProvider: "openai", + reasoningEffort: "medium", + serviceTier: null, + } as any); + const threadReadSpy = vi.spyOn(codexAppServerClient, "threadRead").mockResolvedValue({ + thread: {id: "thread-id"} as any, + }); + vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({ + data: [createTestModel({id: "gpt-5"})], + nextCursor: null, + }); + + await codexAcpClient.resumeSession({ + sessionId: "resume-id", + cwd: "/workspace", + }); + await codexAcpClient.loadSession({ + sessionId: "load-id", + cwd: "/workspace", + mcpServers: [], + }); + + expect(threadResumeSpy).toHaveBeenNthCalledWith(1, expect.objectContaining({ + threadId: "resume-id", + excludeTurns: true, + })); + expect(threadResumeSpy).toHaveBeenNthCalledWith(2, expect.objectContaining({ + threadId: "load-id", + excludeTurns: true, + })); + expect(threadReadSpy).toHaveBeenCalledOnce(); + expect(threadReadSpy).toHaveBeenCalledWith({ + threadId: "thread-id", + includeTurns: true, + }); + }); + + it('sends excludeTurns through the app-server transport', async () => { + const sendRequest = vi.fn().mockResolvedValue({ + thread: {id: "thread-id"}, + model: "gpt-5", + modelProvider: "openai", + reasoningEffort: "medium", + serviceTier: null, + }); + const connection = { + sendRequest, + onUnhandledNotification: () => {}, + onNotification: () => {}, + onRequest: () => {}, + end: () => {}, + } as unknown as MessageConnection; + const transportFixture = createBaseTestFixture({ + connection, + getExitCode: () => null, + }); + + await transportFixture.getCodexAppServerClient().threadResume({ + threadId: "thread-id", + excludeTurns: true, + }); + + expect(sendRequest).toHaveBeenCalledWith("thread/resume", { + threadId: "thread-id", + excludeTurns: true, + }); + expect(transportFixture.getCodexConnectionEvents([])[0]).toEqual({ + eventType: "request", + method: "thread/resume", + params: { + threadId: "thread-id", + excludeTurns: true, + }, + }); + }); + it('restores collaboration mode for resumed and loaded sessions', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent();