From 7f1d2773db08c8e7531f270bdc2d225c78054b45 Mon Sep 17 00:00:00 2001 From: Emre K <110906681+kocaemre@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:54:10 +0200 Subject: [PATCH] fix: honor plan mode reasoning effort Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com> --- src/CodexAcpClient.ts | 6 +++ src/CodexAcpServer.ts | 34 ++++++++++++++-- .../session-config-options.test.ts | 39 +++++++++++++++++++ src/__tests__/acp-test-utils.ts | 1 + .../app-server-recovery/recovery.test.ts | 3 +- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index f8fc9380..5c234acc 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -1186,6 +1186,12 @@ export class CodexAcpClient { }); } + async getPlanModeReasoningEffort(cwd: string): Promise { + const effectiveConfig = await this.codexClient.configRead({includeLayers: false, cwd}); + const effort = effectiveConfig?.config?.["plan_mode_reasoning_effort"] ?? this.config["plan_mode_reasoning_effort"]; + return typeof effort === "string" ? effort : null; + } + private getCollaborationMode(sessionId: string): ModeKind { return this.codexClient.getThreadSettings(sessionId)?.collaborationMode.mode ?? "default"; } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index d3f90a8b..7aa19153 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -194,6 +194,7 @@ import { export interface SessionState extends SessionIndexTitleState { sessionId: string, currentModelId: string, + defaultModeModelId: string | null, availableModels: Array, supportedReasoningEfforts: Array, supportedInputModalities: Array, @@ -909,6 +910,7 @@ export class CodexAcpServer { const sessionState: SessionState = { sessionId: sessionId, currentModelId: currentModelId, + defaultModeModelId: null, availableModels: models, supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], @@ -1630,8 +1632,13 @@ export class CodexAcpServer { }, onSubscribed); return {collaborationMode: metadata.collaborationMode}; }, - applyCollaborationMode: (session, client) => - client.setCollaborationMode(session.sessionId, session.collaborationMode, session.currentModelId), + applyCollaborationMode: async (session, client) => { + if (session.collaborationMode === PLAN_COLLABORATION_MODE && session.defaultModeModelId === null) { + session.defaultModeModelId = session.currentModelId; + session.currentModelId = await this.createPlanModeModelId(session, client); + } + await client.setCollaborationMode(session.sessionId, session.collaborationMode, session.currentModelId); + }, collaborationMode: (session) => session.collaborationMode, resumed: (session) => session.asyncTasks.refresh(), sessionLifetime: (session) => this.getSessionGeneration(session.sessionId), @@ -1925,14 +1932,34 @@ export class CodexAcpServer { throw RequestError.invalidParams(); } if (this.recovery !== null && !this.recovery.sessionIsLive(sessionState)) { - // The thread is not loaded in a running app-server. Its resume applies the mode, see AppServerRecovery. + // Defer the config read until the app-server has resumed the thread. sessionState.collaborationMode = mode; return; } + if (mode === PLAN_COLLABORATION_MODE && sessionState.collaborationMode !== PLAN_COLLABORATION_MODE) { + sessionState.defaultModeModelId = sessionState.currentModelId; + sessionState.currentModelId = await this.createPlanModeModelId(sessionState); + } else if (mode === DEFAULT_COLLABORATION_MODE && sessionState.defaultModeModelId !== null) { + sessionState.currentModelId = sessionState.defaultModeModelId; + sessionState.defaultModeModelId = null; + } await this.runWithProcessCheck(() => this.codexAcpClient.setCollaborationMode(sessionState.sessionId, mode, sessionState.currentModelId)); sessionState.collaborationMode = mode; } + private async createPlanModeModelId(sessionState: SessionState, client = this.codexAcpClient): Promise { + const planEffort = await client.getPlanModeReasoningEffort(sessionState.cwd); + if (!planEffort) { + return sessionState.currentModelId; + } + const effort = findSupportedEffort(sessionState.supportedReasoningEfforts, planEffort); + if (!effort) { + return sessionState.currentModelId; + } + const {model} = ModelId.fromString(sessionState.currentModelId); + return ModelId.create(model, effort).toString(); + } + private applyModelChange(sessionState: SessionState, value: string): void { const model = sessionState.availableModels.find(m => m.id === value); if (!model) { @@ -2489,6 +2516,7 @@ export class CodexAcpServer { const sessionState: SessionState = { sessionId: sessionId, currentModelId: currentModelId, + defaultModeModelId: null, availableModels: models, supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [], supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"], diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index f66be6f0..eb0e8636 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -327,6 +327,45 @@ describe("Session config options", () => { expect(result.configOptions?.find(o => o.id === COLLABORATION_MODE_CONFIG_ID)).toMatchObject({currentValue: "plan"}); }); + it("uses plan_mode_reasoning_effort while Plan mode is active", async () => { + const {fast} = buildModels(); + const {codexAcpAgent, codexAcpClient} = await createSession("fast-model[low]", [fast]); + vi.spyOn(codexAcpClient, "getPlanModeReasoningEffort").mockResolvedValue("high"); + const update = vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); + + const enabled = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: COLLABORATION_MODE_CONFIG_ID, + value: PLAN_COLLABORATION_MODE, + }); + + expect(update).toHaveBeenCalledWith(expect.objectContaining({ + threadId: "session-id", + collaborationMode: expect.objectContaining({ + mode: "plan", + settings: expect.objectContaining({reasoning_effort: "high"}), + }), + })); + expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("fast-model[high]"); + expect(enabled.configOptions?.find(o => o.id === REASONING_EFFORT_CONFIG_ID)).toMatchObject({currentValue: "high"}); + + const disabled = await codexAcpAgent.setSessionConfigOption({ + sessionId: "session-id", + configId: COLLABORATION_MODE_CONFIG_ID, + value: "default", + }); + + expect(update).toHaveBeenLastCalledWith(expect.objectContaining({ + threadId: "session-id", + collaborationMode: expect.objectContaining({ + mode: "default", + settings: expect.objectContaining({reasoning_effort: "low"}), + }), + })); + expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("fast-model[low]"); + expect(disabled.configOptions?.find(o => o.id === REASONING_EFFORT_CONFIG_ID)).toMatchObject({currentValue: "low"}); + }); + 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]); diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index df6bcb74..3b1c8d45 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -425,6 +425,7 @@ export function createTestSessionState(overrides?: Partial): Sessi additionalDirectories: [], sessionId, currentModelId: "model-id[effort]", + defaultModeModelId: null, availableModels: [], supportedReasoningEfforts: [], supportedInputModalities: ["text", "image"], diff --git a/src/__tests__/app-server-recovery/recovery.test.ts b/src/__tests__/app-server-recovery/recovery.test.ts index 675487a7..98c68ea6 100644 --- a/src/__tests__/app-server-recovery/recovery.test.ts +++ b/src/__tests__/app-server-recovery/recovery.test.ts @@ -398,6 +398,7 @@ describe("app-server recovery", () => { it("keeps a collaboration mode chosen while the app-server was down", async () => { const fixture = createRecoveryFixture(); const sessionId = await openSession(fixture); + fixture.answers.set("config/read", () => ({config: {plan_mode_reasoning_effort: "high"}, origins: {}, layers: null})); await fixture.kill(); await fixture.agent.setSessionConfigOption({sessionId, configId: "collaboration_mode", value: "plan"}); @@ -408,7 +409,7 @@ describe("app-server recovery", () => { expect(requestsOf(fixture.current(), "thread/settings/update")).toEqual([expect.objectContaining({ threadId: sessionId, - collaborationMode: expect.objectContaining({mode: "plan"}), + collaborationMode: expect.objectContaining({mode: "plan", settings: expect.objectContaining({reasoning_effort: "high"})}), })]); });