From a943bcef232d68b2e05705411bd7575a4d2026c6 Mon Sep 17 00:00:00 2001 From: Gilbert Leung Date: Sun, 26 Jul 2026 14:47:36 +0800 Subject: [PATCH 1/4] fix: persist session config in Codex threads --- src/AgentMode.ts | 12 +++++++ src/CodexAcpClient.ts | 24 ++++++++++++++ src/CodexAcpServer.ts | 31 ++++++++++++------ src/CodexAppServerClient.ts | 13 ++++++-- .../CodexACPAgent/CodexAcpClient.test.ts | 8 ++++- .../CodexACPAgent/fast-mode-config.test.ts | 1 + .../session-config-options.test.ts | 32 ++++++++++++++----- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/AgentMode.ts b/src/AgentMode.ts index aed9710c..d7f19427 100644 --- a/src/AgentMode.ts +++ b/src/AgentMode.ts @@ -96,6 +96,18 @@ export class AgentMode { return match ?? null; } + static fromSettings( + approvalPolicy: AskForApproval | undefined, + sandboxPolicy: SandboxPolicy | undefined, + ): AgentMode | null { + if (!sandboxPolicy) return null; + const match = AgentMode.all().find(mode => + mode.approvalPolicy === approvalPolicy + && mode.sandboxPolicy.type === sandboxPolicy.type + ); + return match ?? null; + } + static getInitialAgentMode(): AgentMode { const predefinedAgentMode = process.env["INITIAL_AGENT_MODE"]; if (predefinedAgentMode) { diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 0c7af207..d38cf49e 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -343,6 +343,8 @@ export class CodexAcpClient { sessionId: request.sessionId, currentModelId: currentModelId, models: codexModels, + agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox) + ?? AgentMode.getInitialAgentMode(), collaborationMode: this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, @@ -371,6 +373,8 @@ export class CodexAcpClient { sessionId: request.sessionId, currentModelId: currentModelId, models: codexModels, + agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox) + ?? AgentMode.getInitialAgentMode(), collaborationMode: this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, @@ -398,6 +402,8 @@ export class CodexAcpClient { sessionId: response.thread.id, currentModelId: currentModelId, models: codexModels, + agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox) + ?? AgentMode.getInitialAgentMode(), collaborationMode: this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, currentServiceTier: response.serviceTier as ServiceTier ?? null, @@ -719,6 +725,23 @@ export class CodexAcpClient { }); } + async setAgentMode(sessionId: string, mode: AgentMode): Promise { + await this.codexClient.threadSettingsUpdate({ + threadId: sessionId, + approvalPolicy: mode.approvalPolicy, + sandboxPolicy: mode.sandboxPolicy, + }); + } + + async setModelAndEffort(sessionId: string, currentModelId: string): Promise { + const modelId = ModelId.fromString(currentModelId); + await this.codexClient.threadSettingsUpdate({ + threadId: sessionId, + model: modelId.model, + effort: modelId.effort as ReasoningEffort, + }); + } + private getCollaborationMode(sessionId: string): ModeKind { return this.codexClient.getThreadSettings(sessionId)?.collaborationMode.mode ?? "default"; } @@ -897,6 +920,7 @@ export type SessionMetadata = { sessionId: string, currentModelId: string, models: Model[], + agentMode?: AgentMode, collaborationMode: ModeKind, modelProvider?: string | null, currentServiceTier?: ServiceTier | null, diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 928f2b16..8b1716a4 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -448,7 +448,7 @@ export class CodexAcpServer { availableModels: models, supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], - agentMode: AgentMode.getInitialAgentMode(), + agentMode: sessionMetadata.agentMode ?? AgentMode.getInitialAgentMode(), collaborationMode: sessionMetadata.collaborationMode, currentTurnId: null, lastTokenUsage: null, @@ -728,7 +728,7 @@ export class CodexAcpServer { const sessionState = this.sessions.get(_params.sessionId); if (!sessionState) throw new Error(`Session ${_params.sessionId} not found`); - this.applyModeChange(sessionState, _params.modeId); + await this.applyModeChange(sessionState, _params.modeId); return {}; } @@ -753,16 +753,16 @@ export class CodexAcpServer { this.applyFastModeChange(sessionState, params); break; case MODE_CONFIG_ID: - this.applyModeChange(sessionState, this.stringConfigValue(params)); + await this.applyModeChange(sessionState, this.stringConfigValue(params)); break; case COLLABORATION_MODE_CONFIG_ID: await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params)); break; case MODEL_CONFIG_ID: - this.applyModelChange(sessionState, this.stringConfigValue(params)); + await this.applyModelChange(sessionState, this.stringConfigValue(params)); break; case REASONING_EFFORT_CONFIG_ID: - this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params)); + await this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params)); break; default: throw RequestError.invalidParams(); @@ -788,11 +788,12 @@ export class CodexAcpServer { return params.value; } - private applyModeChange(sessionState: SessionState, value: string): void { + private async applyModeChange(sessionState: SessionState, value: string): Promise { const newMode = AgentMode.find(value); if (!newMode) { throw RequestError.invalidParams(); } + await this.codexAcpClient.setAgentMode(sessionState.sessionId, newMode); sessionState.agentMode = newMode; } @@ -805,7 +806,7 @@ export class CodexAcpServer { sessionState.collaborationMode = mode; } - private applyModelChange(sessionState: SessionState, value: string): void { + private async applyModelChange(sessionState: SessionState, value: string): Promise { const model = sessionState.availableModels.find(m => m.id === value); if (!model) { const currentModel = ModelId.fromString(sessionState.currentModelId).model; @@ -817,16 +818,22 @@ export class CodexAcpServer { const currentEffort = ModelId.fromString(sessionState.currentModelId).effort; const effort = findSupportedEffort(model.supportedReasoningEfforts, currentEffort) ?? model.defaultReasoningEffort; + await this.codexAcpClient.setModelAndEffort( + sessionState.sessionId, + ModelId.fromComponents(model, effort).toString(), + ); this.applyModelAndEffort(sessionState, model, effort); } - private applyReasoningEffortChange(sessionState: SessionState, value: string): void { + private async applyReasoningEffortChange(sessionState: SessionState, value: string): Promise { const effort = findSupportedEffort(sessionState.supportedReasoningEfforts, value); if (!effort) { throw RequestError.invalidParams(); } const {model} = ModelId.fromString(sessionState.currentModelId); - sessionState.currentModelId = ModelId.create(model, effort).toString(); + const currentModelId = ModelId.create(model, effort).toString(); + await this.codexAcpClient.setModelAndEffort(sessionState.sessionId, currentModelId); + sessionState.currentModelId = currentModelId; } private applyModelAndEffort(sessionState: SessionState, model: Model, effort: ReasoningEffort): void { @@ -862,6 +869,10 @@ export class CodexAcpServer { } sessionState.availableModels = models; + await this.codexAcpClient.setModelAndEffort( + sessionState.sessionId, + ModelId.fromComponents(model, reasoningEffort).toString(), + ); this.applyModelAndEffort(sessionState, model, reasoningEffort); return {}; @@ -1290,7 +1301,7 @@ export class CodexAcpServer { availableModels: models, supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], - agentMode: AgentMode.getInitialAgentMode(), + agentMode: sessionMetadata.agentMode ?? AgentMode.getInitialAgentMode(), collaborationMode: sessionMetadata.collaborationMode, currentTurnId: null, lastTokenUsage: null, diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index eb26c83f..43bf770a 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -3,9 +3,11 @@ import type { ClientRequest, InitializeParams, InitializeResponse, + ReasoningEffort, ServerNotification } from "./app-server"; import type { + AskForApproval, ConfigReadParams, ConfigReadResponse, GetAccountParams, @@ -63,6 +65,7 @@ import type { TurnStartResponse, TurnSteerParams, TurnSteerResponse, + SandboxPolicy, CommandExecutionRequestApprovalParams, CommandExecutionRequestApprovalResponse, FileChangeRequestApprovalParams, @@ -532,7 +535,7 @@ export class CodexAppServerClient { return this.threadSettings.get(threadId); } - async threadSettingsUpdate(params: ExperimentalThreadSettingsUpdateParams): Promise { + async threadSettingsUpdate(params: ThreadSettingsUpdateParams): Promise { await this.connection.sendRequest("thread/settings/update", params); } @@ -974,9 +977,13 @@ type DistributiveOmit = T extends any ? Omit : never; -export interface ExperimentalThreadSettingsUpdateParams { +export interface ThreadSettingsUpdateParams { threadId: string; - collaborationMode: { + approvalPolicy?: AskForApproval; + sandboxPolicy?: SandboxPolicy; + model?: string; + effort?: ReasoningEffort; + collaborationMode?: { mode: "default" | "plan"; settings: { model: string; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index d58bd594..1a5dcb2b 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -454,7 +454,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); - it('restores collaboration mode for resumed and loaded sessions', async () => { + it('restores collaboration and agent modes for resumed and loaded sessions', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent(); const codexAcpClient = mockFixture.getCodexAcpClient(); @@ -483,6 +483,8 @@ describe('ACP server test', { timeout: 40_000 }, () => { modelProvider: "openai", reasoningEffort: "medium", serviceTier: null, + approvalPolicy: "never", + sandbox: {type: "dangerFullAccess"}, } as any; }); vi.spyOn(codexAppServerClient, "threadRead").mockImplementation(async ({threadId}) => ({ @@ -505,8 +507,12 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(codexAcpAgent.getSessionState("resume-id").collaborationMode).toBe("plan"); expect(codexAcpAgent.getSessionState("load-id").collaborationMode).toBe("plan"); + expect(codexAcpAgent.getSessionState("resume-id").agentMode).toBe(AgentMode.AgentFullAccess); + expect(codexAcpAgent.getSessionState("load-id").agentMode).toBe(AgentMode.AgentFullAccess); expect(resumed.configOptions?.find(option => option.id === "collaboration_mode")).toMatchObject({currentValue: "plan"}); expect(loaded.configOptions?.find(option => option.id === "collaboration_mode")).toMatchObject({currentValue: "plan"}); + expect(resumed.configOptions?.find(option => option.id === "mode")).toMatchObject({currentValue: "agent-full-access"}); + expect(loaded.configOptions?.find(option => option.id === "mode")).toMatchObject({currentValue: "agent-full-access"}); }); it('uses configured model provider when resuming sessions without an explicit provider', async () => { diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index 6007d529..11fd8ced 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -47,6 +47,7 @@ describe("Fast mode session config", () => { currentServiceTier, additionalDirectories: [], }); + vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); await codexAcpAgent.initialize({ protocolVersion: acp.PROTOCOL_VERSION, diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index ab669916..4c5a8735 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -49,9 +49,11 @@ async function createSession(currentModelId: string, availableModels: Array { @@ -141,7 +143,7 @@ describe("Session config options", () => { it("changes the agent mode via setSessionConfigOption", async () => { const {fast} = buildModels(); - const {codexAcpAgent} = await createSession("fast-model[medium]", [fast]); + const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); const result = await codexAcpAgent.setSessionConfigOption({ sessionId: "session-id", @@ -150,14 +152,18 @@ describe("Session config options", () => { }); expect(codexAcpAgent.getSessionState("session-id").agentMode).toBe(AgentMode.ReadOnly); + expect(update).toHaveBeenCalledWith({ + threadId: "session-id", + approvalPolicy: AgentMode.ReadOnly.approvalPolicy, + sandboxPolicy: AgentMode.ReadOnly.sandboxPolicy, + }); const modeOption = result.configOptions?.find(o => o.id === MODE_CONFIG_ID); expect((modeOption as any).currentValue).toBe(AgentMode.ReadOnly.id); }); it("changes collaboration mode without starting a model turn", async () => { const {fast} = buildModels(); - const {codexAcpAgent, codexAcpClient} = await createSession("fast-model[medium]", [fast]); - const update = vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); + const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); const result = await codexAcpAgent.setSessionConfigOption({ sessionId: "session-id", @@ -175,8 +181,7 @@ describe("Session config options", () => { it("toggles collaboration mode with /plan without starting a model turn", async () => { const {fast} = buildModels(); - const {fixture, codexAcpAgent, codexAcpClient} = await createSession("fast-model[medium]", [fast]); - const update = vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); + const {fixture, codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); const turnStart = vi.spyOn(fixture.getCodexAppServerClient(), "turnStart"); const enabledResponse = await codexAcpAgent.prompt({ @@ -230,7 +235,7 @@ describe("Session config options", () => { it("changes the model and keeps the current reasoning effort when supported", async () => { const {fast, slow} = buildModels(); - const {codexAcpAgent} = await createSession("fast-model[medium]", [fast, slow]); + const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast, slow]); await codexAcpAgent.setSessionConfigOption({ sessionId: "session-id", @@ -239,6 +244,11 @@ describe("Session config options", () => { }); expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("slow-model[medium]"); + expect(update).toHaveBeenCalledWith({ + threadId: "session-id", + model: "slow-model", + effort: "medium", + }); }); it("falls back to the new model's default effort when the current effort is unsupported", async () => { @@ -256,7 +266,7 @@ describe("Session config options", () => { it("changes only the reasoning effort", async () => { const {fast} = buildModels(); - const {codexAcpAgent} = await createSession("fast-model[medium]", [fast]); + const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); await codexAcpAgent.setSessionConfigOption({ sessionId: "session-id", @@ -265,6 +275,11 @@ describe("Session config options", () => { }); expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("fast-model[high]"); + expect(update).toHaveBeenCalledWith({ + threadId: "session-id", + model: "fast-model", + effort: "high", + }); }); it("refreshes the cached model list when unstable_setSessionModel picks a freshly fetched model", async () => { @@ -292,6 +307,7 @@ describe("Session config options", () => { defaultReasoningEffort: "medium", }); vi.spyOn(codexAcpClient, "fetchAvailableModels").mockResolvedValue([fast, extraModel]); + vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); await codexAcpAgent.unstable_setSessionModel({ sessionId: "session-id", From edd908bbd561498a1e82f01f9ef90934eecc20d3 Mon Sep 17 00:00:00 2001 From: Gilbert Leung Date: Mon, 27 Jul 2026 10:38:32 +0800 Subject: [PATCH 2/4] fix: apply agent mode before persisting --- src/CodexAcpServer.ts | 8 ++- .../session-config-options.test.ts | 62 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 8b1716a4..c2a451fa 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -793,8 +793,14 @@ export class CodexAcpServer { if (!newMode) { throw RequestError.invalidParams(); } - await this.codexAcpClient.setAgentMode(sessionState.sessionId, newMode); + const previousMode = sessionState.agentMode; sessionState.agentMode = newMode; + try { + await this.codexAcpClient.setAgentMode(sessionState.sessionId, newMode); + } catch (error) { + sessionState.agentMode = previousMode; + throw error; + } } private async applyCollaborationModeChange(sessionState: SessionState, value: string): Promise { diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index 4c5a8735..8bd843ee 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -1,5 +1,9 @@ import {describe, expect, it, vi} from "vitest"; -import {createCodexMockTestFixture, createTestModel} from "../acp-test-utils"; +import { + createCodexMockTestFixture, + createTestModel, + mockPromptTurn, +} from "../acp-test-utils"; import {AgentMode, MODE_CONFIG_ID} from "../../AgentMode"; import { MODEL_CONFIG_ID, @@ -16,6 +20,17 @@ const lowEffort: ReasoningEffortOption = {reasoningEffort: "low", description: " const mediumEffort: ReasoningEffortOption = {reasoningEffort: "medium", description: "Balanced"}; const highEffort: ReasoningEffortOption = {reasoningEffort: "high", description: "Thorough"}; +function deferred(): { + promise: Promise; + resolve: (value: T | PromiseLike) => void; +} { + let resolve!: (value: T | PromiseLike) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return {promise, resolve}; +} + function buildModels(): {fast: Model; slow: Model} { const fast = createTestModel({ id: "fast-model", @@ -161,6 +176,51 @@ describe("Session config options", () => { expect((modeOption as any).currentValue).toBe(AgentMode.ReadOnly.id); }); + it("uses a new agent mode for prompts while thread persistence is pending", async () => { + const {fast} = buildModels(); + const {fixture, codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); + const sessionState = codexAcpAgent.getSessionState("session-id"); + const turnStartSpy = mockPromptTurn(fixture, sessionState.sessionId); + const persistence = deferred(); + update.mockReturnValue(persistence.promise); + + const modeChange = codexAcpAgent.setSessionConfigOption({ + sessionId: sessionState.sessionId, + configId: MODE_CONFIG_ID, + value: AgentMode.AgentFullAccess.id, + }); + + expect(sessionState.agentMode).toBe(AgentMode.AgentFullAccess); + + await codexAcpAgent.prompt({ + sessionId: sessionState.sessionId, + prompt: [{type: "text", text: "test"}], + }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + approvalPolicy: AgentMode.AgentFullAccess.approvalPolicy, + sandboxPolicy: AgentMode.AgentFullAccess.sandboxPolicy, + })); + + persistence.resolve(); + await modeChange; + }); + + it("rolls back the agent mode when thread persistence fails", async () => { + const {fast} = buildModels(); + const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); + const sessionState = codexAcpAgent.getSessionState("session-id"); + update.mockRejectedValue(new Error("settings update failed")); + + await expect(codexAcpAgent.setSessionConfigOption({ + sessionId: sessionState.sessionId, + configId: MODE_CONFIG_ID, + value: AgentMode.AgentFullAccess.id, + })).rejects.toThrow("settings update failed"); + + expect(sessionState.agentMode).toBe(AgentMode.Agent); + }); + it("changes collaboration mode without starting a model turn", async () => { const {fast} = buildModels(); const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]); From 3487c8bcff942ef9ed83f2802fd7522cee64e243 Mon Sep 17 00:00:00 2001 From: Gilbert Leung Date: Mon, 27 Jul 2026 10:52:37 +0800 Subject: [PATCH 3/4] fix: return session id when loading sessions --- src/AcpExtensions.ts | 1 + src/CodexAcpServer.ts | 1 + src/__tests__/CodexACPAgent/load-session.test.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AcpExtensions.ts b/src/AcpExtensions.ts index 5d8e4602..1264fd59 100644 --- a/src/AcpExtensions.ts +++ b/src/AcpExtensions.ts @@ -34,6 +34,7 @@ export type LegacyNewSessionResponse = NewSessionResponse & { } export type LegacyLoadSessionResponse = LoadSessionResponse & { + sessionId: SessionId; models?: LegacySessionModelState | null; } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index c2a451fa..ccda71ee 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -538,6 +538,7 @@ export class CodexAcpServer { availableModelCount: modelState.availableModels.length }); return { + sessionId, models: modelState, modes: modeState, ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId)), diff --git a/src/__tests__/CodexACPAgent/load-session.test.ts b/src/__tests__/CodexACPAgent/load-session.test.ts index a64089e1..76b33f53 100644 --- a/src/__tests__/CodexACPAgent/load-session.test.ts +++ b/src/__tests__/CodexACPAgent/load-session.test.ts @@ -216,8 +216,9 @@ describe("CodexACPAgent - loadSession", () => { cwd: "/test/project", mcpServers: [], }; - await codexAcpAgent.loadSession(loadParams); + const response = await codexAcpAgent.loadSession(loadParams); + expect(response.sessionId).toBe(thread.id); expect(codexAppServerClient.threadRead).toHaveBeenCalledWith({ threadId: thread.id, includeTurns: true, From 10f6541a68dfcb16e65ec90c47d3f8df6258a621 Mon Sep 17 00:00:00 2001 From: Gilbert Leung Date: Mon, 27 Jul 2026 15:15:53 +0800 Subject: [PATCH 4/4] fix: use latest agent mode when starting turns --- src/CodexAcpClient.ts | 3 +- src/CodexAcpServer.ts | 3 +- .../session-config-options.test.ts | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index d38cf49e..97dda776 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -691,7 +691,7 @@ export class CodexAcpClient { async sendPrompt( request: acp.PromptRequest, - agentMode: AgentMode, + getAgentMode: () => AgentMode, modelId: ModelId, serviceTier: ServiceTier | null, disableSummary: boolean, @@ -706,6 +706,7 @@ export class CodexAcpClient { if (shouldCancel?.()) { return null; } + const agentMode = getAgentMode(); return await this.codexClient.runTurn({ threadId: request.sessionId, input: input, diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index ccda71ee..94a71d03 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1993,7 +1993,6 @@ export class CodexAcpServer { if (!sessionState.supportedInputModalities.includes("image") && params.prompt.some(b => b.type === "image")) { throw RequestError.invalidRequest("The current model does not support image input"); } - const agentMode = sessionState.agentMode; const serviceTier = resolveFastServiceTier( sessionState.fastModeEnabled, sessionState.currentModelSupportsFast, @@ -2002,7 +2001,7 @@ export class CodexAcpServer { const sendPromptPromise = this.runWithProcessCheck( () => this.codexAcpClient.sendPrompt( params, - agentMode, + () => sessionState.agentMode, modelId, serviceTier, disableSummary, diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index 8bd843ee..43c142d5 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -206,6 +206,35 @@ describe("Session config options", () => { await modeChange; }); + it("uses a new agent mode when it changes during prompt preparation", async () => { + const {fast} = buildModels(); + const {fixture, codexAcpAgent} = await createSession("fast-model[medium]", [fast]); + const sessionState = codexAcpAgent.getSessionState("session-id"); + const turnStartSpy = mockPromptTurn(fixture, sessionState.sessionId); + const skillRefresh = deferred<{data: []}>(); + const listSkillsSpy = vi.spyOn(fixture.getCodexAppServerClient(), "listSkills") + .mockReturnValue(skillRefresh.promise); + + const prompt = codexAcpAgent.prompt({ + sessionId: sessionState.sessionId, + prompt: [{type: "text", text: "test"}], + }); + await vi.waitFor(() => expect(listSkillsSpy).toHaveBeenCalled()); + + await codexAcpAgent.setSessionConfigOption({ + sessionId: sessionState.sessionId, + configId: MODE_CONFIG_ID, + value: AgentMode.AgentFullAccess.id, + }); + skillRefresh.resolve({data: []}); + await prompt; + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + approvalPolicy: AgentMode.AgentFullAccess.approvalPolicy, + sandboxPolicy: AgentMode.AgentFullAccess.sandboxPolicy, + })); + }); + it("rolls back the agent mode when thread persistence fails", async () => { const {fast} = buildModels(); const {codexAcpAgent, update} = await createSession("fast-model[medium]", [fast]);