diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index ed7e6120f..b7271800b 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -715,7 +715,7 @@ export class CodexAcpClient { threadStatus: response.thread.status, sessionTitle: normalizeSessionTitle(listedSessionTitle(response.thread)), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, additionalDirectories, skippedMcpServers, ...(instructions ? {instructions} : {}), @@ -798,7 +798,7 @@ export class CodexAcpClient { // Codex sends no thread/settings/updated on a resume. The response holds the mode. collaborationMode: response.collaborationMode ?? this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, thread, history, additionalDirectories, @@ -883,7 +883,7 @@ export class CodexAcpClient { models: codexModels, collaborationMode: this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, additionalDirectories, skippedMcpServers, ...(instructions ? {instructions} : {}), diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 87a438872..e12ce5f6b 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -156,6 +156,7 @@ import { FAST_MODE_ON, modelSupportsFast, resolveFastServiceTier, + serviceTierEnablesFast, } from "./FastModeConfig"; import packageJson from "../package.json"; import {isJetBrains2026_1Client} from "./JBUtils"; @@ -1412,7 +1413,7 @@ export class CodexAcpServer { cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, mcpServers: requestedMcpServers, - fastModeEnabled: sessionMetadata.currentServiceTier === "fast", + fastModeEnabled: serviceTierEnablesFast(sessionMetadata.currentServiceTier), currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, clientCapabilities: this.capabilities, @@ -3645,7 +3646,7 @@ export class CodexAcpServer { cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, mcpServers: requestedMcpServers, - fastModeEnabled: sessionMetadata.currentServiceTier === "fast", + fastModeEnabled: serviceTierEnablesFast(sessionMetadata.currentServiceTier), currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, clientCapabilities: this.capabilities, diff --git a/src/FastModeConfig.ts b/src/FastModeConfig.ts index defab8dc6..f9d99d05a 100644 --- a/src/FastModeConfig.ts +++ b/src/FastModeConfig.ts @@ -14,6 +14,11 @@ export function modelSupportsFast(model: Model | undefined): boolean { return model?.additionalSpeedTiers?.includes("fast") ?? false; } +export function serviceTierEnablesFast(serviceTier: string | null | undefined): boolean { + // Codex returns the canonical "priority" tier even when configured with "fast". + return serviceTier === "fast" || serviceTier === "priority"; +} + export function resolveFastServiceTier(fastModeEnabled: boolean, currentModelSupportsFast: boolean): ServiceTier | null { return fastModeEnabled && currentModelSupportsFast ? "fast" : null; } diff --git a/src/SessionFork.ts b/src/SessionFork.ts index 277f0fc65..f4823e93a 100644 --- a/src/SessionFork.ts +++ b/src/SessionFork.ts @@ -4,7 +4,6 @@ import {RequestError} from "@agentclientprotocol/sdk/unstable"; import type {SessionConfig} from "./CodexAcpClient"; import type {CodexAppServerClient} from "./CodexAppServerClient"; import type {ModeKind} from "./app-server/ModeKind"; -import type {ServiceTier} from "./app-server/ServiceTier"; import type {Model} from "./app-server/v2"; import type {SessionMetadata} from "./SessionMetadata"; import type {AcpMcpServer, WithAcpMcpServers} from "./McpServerConfig"; @@ -68,7 +67,7 @@ export async function forkSession( models, collaborationMode: dependencies.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, additionalDirectories, skippedMcpServers, activePermissionProfileId: response.activePermissionProfile?.id ?? null, diff --git a/src/SessionMetadata.ts b/src/SessionMetadata.ts index f0ccf9af5..b56dfe85f 100644 --- a/src/SessionMetadata.ts +++ b/src/SessionMetadata.ts @@ -1,5 +1,4 @@ import type {ModeKind} from "./app-server/ModeKind"; -import type {ServiceTier} from "./app-server/ServiceTier"; import type { ApprovalsReviewer, AskForApproval, @@ -17,7 +16,7 @@ export type SessionMetadata = { models: Model[], collaborationMode: ModeKind, modelProvider?: string | null, - currentServiceTier?: ServiceTier | null, + currentServiceTier?: string | null, threadStatus?: ThreadStatus, additionalDirectories: string[], skippedMcpServers?: string[], diff --git a/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-off.json b/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-off.json new file mode 100644 index 000000000..e5f38f8d4 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-off.json @@ -0,0 +1,8 @@ +{ + "currentValue": false, + "type": "boolean", + "configId": "fast-mode", + "name": "Fast mode", + "description": "1.5x speed, increased usage", + "category": "model_config" +} diff --git a/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-on.json b/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-on.json new file mode 100644 index 000000000..b464c1951 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fast-mode-on.json @@ -0,0 +1,8 @@ +{ + "currentValue": true, + "type": "boolean", + "configId": "fast-mode", + "name": "Fast mode", + "description": "1.5x speed, increased usage", + "category": "model_config" +} diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index f50944f51..5e38653a7 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -24,7 +24,7 @@ describe("Fast mode session config", () => { }; async function createSession( - currentServiceTier: "fast" | "flex" | null = null, + currentServiceTier: string | null = null, clientInfo: acp.Implementation | null = null, clientCapabilities?: acp.ClientCapabilities, ) { @@ -99,11 +99,23 @@ describe("Fast mode session config", () => { expect(response.configOptions).toContainEqual(createFastModeConfigOption(false)); }); - it("initializes Fast mode as On when the app-server session tier is fast", async () => { - const {response, codexAcpAgent} = await createSession("fast"); + it.each([ + {serviceTier: "fast", enabled: true}, + {serviceTier: "priority", enabled: true}, + {serviceTier: "flex", enabled: false}, + {serviceTier: null, enabled: false}, + {serviceTier: "unknown", enabled: false}, + ])("preserves Fast mode from the app-server tier $serviceTier through the first prompt", async ({serviceTier, enabled}) => { + const {fixture, response, codexAcpAgent} = await createSession(serviceTier); + const turnStartSpy = mockPromptTurn(fixture, "session-id"); - expect(response.configOptions).toContainEqual(createFastModeConfigOption(true)); - expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true); + expect(response.configOptions).toContainEqual(createFastModeConfigOption(enabled)); + + await codexAcpAgent.prompt({sessionId: "session-id", prompt: [{type: "text", text: "test"}]}); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + serviceTier: enabled ? "fast" : null, + })); }); it("omits Fast mode config options for JetBrains 2026.1 IntelliJ clients", async () => { diff --git a/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts b/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts index cfe289d5e..35b333530 100644 --- a/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts +++ b/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts @@ -8,6 +8,7 @@ import {CodexAcpClient} from '../../CodexAcpClient'; import {CodexAppServerClient} from '../../CodexAppServerClient'; import type {PermissionProfileSummary, SkillsListParams, SkillsListResponse, Thread} from '../../app-server/v2'; import {MODE_CONFIG_ID} from '../../AgentMode'; +import {FAST_MODE_CONFIG_ID} from '../../FastModeConfig'; import {createTestModel} from '../acp-test-utils'; import {createMockConnections} from './test-utils'; import {checkV2SessionUpdate, expectConformingV2SessionUpdates} from './v2-session-update-guard'; @@ -115,6 +116,7 @@ async function connectV2Client(options?: { permissionProfiles?: PermissionProfileSummary[]; activePermissionProfileId?: string; defaultPermissionProfileId?: string; + serviceTier?: string | null; }) { const mocks = createMockConnections(); const codexRequests: Array<{method: string, params: unknown}> = []; @@ -136,6 +138,14 @@ async function connectV2Client(options?: { return {config: {default_permissions: options.defaultPermissionProfileId}, origins: {}, layers: []}; } const response = codexResponse(method, options?.replayTurns, options?.thread); + if (options?.serviceTier !== undefined) { + if (["thread/start", "thread/resume", "thread/fork"].includes(method)) { + response["serviceTier"] = options.serviceTier; + } + if (method === "model/list") { + response["data"] = [createTestModel({id: "gpt-5", additionalSpeedTiers: ["fast"]})]; + } + } if (options?.activePermissionProfileId && ["thread/start", "thread/resume", "thread/fork"].includes(method)) { return {...response, activePermissionProfile: {id: options.activePermissionProfileId, extends: null}}; } @@ -243,6 +253,31 @@ describe('Session lifecycle over ACP v2', () => { .toMatchObject({type: "select", currentValue: "permission-profile:team-default"}); }); + describe.each([ + {operation: "new", replayFrom: undefined}, + {operation: "resume", replayFrom: null}, + {operation: "resume", replayFrom: {type: "start"}}, + {operation: "fork", replayFrom: undefined}, + ] as const)('Fast mode over v2 $operation with replayFrom=$replayFrom', ({operation, replayFrom}) => { + it.each([ + {serviceTier: "fast", enabled: true}, + {serviceTier: "priority", enabled: true}, + {serviceTier: "flex", enabled: false}, + {serviceTier: null, enabled: false}, + {serviceTier: "unknown", enabled: false}, + ])('restores Fast mode from the app-server tier $serviceTier', async ({serviceTier, enabled}) => { + const {connection} = await connectV2Client({serviceTier}); + closeClient = () => connection.close(); + + const response = await connection.agent.request(acpV2.methods.agent.session[operation], { + sessionId, cwd, ...(replayFrom === undefined ? {} : {replayFrom}), + }); + + await expect(dump(response.configOptions?.find(option => option.configId === FAST_MODE_CONFIG_ID))) + .toMatchFileSnapshot(`data/session-lifecycle-v2-fast-mode-${enabled ? "on" : "off"}.json`); + }); + }); + it.each([null, {type: "start"}] as const)( 'preserves the default profile on an unprompted v2 resume with replayFrom=%s', async replayFrom => { const {connection} = await connectV2Client({