Codex Desktop ignores `plan_mode_reasoning_effort` when entering Plan mode, while CLI applies it correctly

Open 💬 8 comments Opened Apr 20, 2026 by aaudin90
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of the Codex App are you using (From “About Codex” dialog)?

Not captured at the time of filing.

What subscription do you have?

Not captured at the time of filing.

What platform is your computer?

macOS

What issue are you seeing?

Codex Desktop does not apply plan_mode_reasoning_effort when switching a thread into Plan mode.

My config contains:

model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "xhigh"

With this setup:

  • In Codex CLI, entering Plan mode correctly switches the reasoning effort from the global/default value to the Plan-mode override (xhigh).
  • In Codex Desktop, entering Plan mode does not switch the reasoning effort accordingly.
  • I can visually confirm in the Desktop UI that the reasoning level remains medium after entering Plan mode, instead of switching to xhigh.

So the same configuration works in CLI but not in the Desktop app.

What steps can reproduce the bug?

  1. Set the following in ~/.codex/config.toml:
model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "xhigh"
  1. Open Codex CLI and enter Plan mode.
  2. Observe that Plan mode uses the configured override (xhigh).
  3. Open Codex Desktop with the same config.
  4. Enter Plan mode in a thread.
  5. Observe in the Desktop UI that the reasoning level remains medium instead of switching to xhigh.

What is the expected behavior?

When plan_mode_reasoning_effort is explicitly set, Codex Desktop should apply that value on entering Plan mode, the same way Codex CLI does.

In this example, entering Plan mode should switch reasoning effort from medium to xhigh, and the Desktop UI should reflect that change.

Additional information

This looks like a Desktop-specific config/application bug rather than a request for a new feature, because the config key already exists and the CLI respects it.

The issue is specifically that Desktop and CLI behave differently for the same configuration.

If useful, I can also provide:

  • exact Codex Desktop version
  • exact macOS version / architecture
  • screenshot or screen recording showing the reasoning level before and after entering Plan mode
  • session ID from in-app feedback

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 4 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #18624
  • #18412
  • #17328

Powered by Codex Action

Mahkhmood9 · 4 months ago

didnt know it existed

okay thanks, I will use it

Mahkhmood9 · 4 months ago

@aaudin90 are you able to replicate it in the IDE too ?

aaudin90 · 4 months ago
@aaudin90 are you able to replicate it in the IDE too ?

Only in Codex Desktop

nanggo · 21 days ago

Still reproducible on macOS 26.6.1 with Codex Desktop 26.730.61639 (bundled codex-cli 0.147.0-alpha.1.2) and Codex CLI 0.147.0.

Config:

model = "gpt-5.6-sol"
model_reasoning_effort = "high"
plan_mode_reasoning_effort = "ultra"

I confirmed this from the recorded turn context, not only from the visible selector:

  • TUI: entering Plan mode records collaboration_mode = plan, reasoning_effort = ultra, and effective effort = ultra.
  • Desktop: entering Plan mode records collaboration_mode = plan, but both reasoning_effort and effective effort remain high.

So Desktop is not applying the Plan-mode override, while TUI applies the same config correctly.

brzvsk · 7 days ago

Still reproducible on a newer Codex Desktop build.

Environment:

  • ChatGPT/Codex Desktop: 26.814.41407 (bundle 6720)
  • Bundled Codex CLI: 0.148.0-alpha.15
  • macOS: 26.5.2, arm64
  • Model: gpt-5.6-sol

Config:

model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "xhigh"

I reproduced this in a fresh, non-forked thread. After explicitly switching to Plan mode and submitting a prompt, the recorded turn context was:

collaboration_mode = plan
collaboration_mode.settings.reasoning_effort = medium
effort = medium

The global config still contained plan_mode_reasoning_effort = "xhigh" at the time of the turn. This confirms the Desktop app is entering Plan mode but is not applying the Plan-mode reasoning override; the thread-level/default medium effort remains effective.

No project-level model or reasoning override was present.

ariccio · 14 hours ago

Yeah, wait, what? This is surprising. It's hard for me to switch away from the TUI/CLI when this isn't working yet lmao

ariccio · 14 hours ago

I had some extra GitHub copilot usage, so why not, maybe this is good; copilot suggested tagging four OpenAI devs, but that feels spammy, so I'll just tag one:

---------

The following is generated with copilot+ GPT-5.6-sol

Follow-up from Codex, posting on behalf of @ariccio. I traced this against current main (5bf0ba3dd65952f8e37abbb7b535ee88841e2793).

This looks like a small app-server parity gap rather than a Desktop-rendering-only problem.

Root cause

The config value reaches core correctly:

  • Config::plan_mode_reasoning_effort is loaded from config.toml.
  • The TUI explicitly overlays it whenever it constructs or submits a Plan-mode collaboration mask.
  • Desktop sends Plan mode through app-server turn/start.collaborationMode.

The divergent path is in:

  • codex-rs/app-server/src/request_processors/turn_processor.rs
  • TurnRequestProcessor::build_thread_settings_overrides
  • TurnRequestProcessor::normalize_collaboration_mode

At present, app-server normalization fills only the collaboration mode's built-in developer instructions:

let collaboration_mode =
    collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode));

It does not apply thread.config().await.plan_mode_reasoning_effort.

Consequently, if Desktop sends its currently displayed/default effort in the Plan collaboration settings—e.g. medium—that value is accepted and becomes both:

collaboration_mode.settings.reasoning_effort = medium
effort = medium

The TUI does not have this behavior because it overlays the configured Plan override immediately before submission in submit_user_message_with_mode and while switching masks.

Suggested patch

Apply the Plan-mode config override at the shared app-server collaboration-mode normalization boundary. This covers both turn/start and thread/settings/update, keeps the fix independent of Desktop client implementation details, and causes the existing thread/settings/updated notification to report the effective value back to the UI.

diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs
index 84047d2d2..PATCH 100644
--- a/codex-rs/app-server/src/request_processors/turn_processor.rs
+++ b/codex-rs/app-server/src/request_processors/turn_processor.rs
@@ -776,8 +776,20 @@ impl TurnRequestProcessor {
             ));
         }

-        let collaboration_mode =
-            collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode));
+        // Match the TUI's Plan-mode semantics: an explicit
+        // `plan_mode_reasoning_effort` overrides the Plan preset/client mask.
+        // Read this from the thread config rather than the app-server process
+        // config so profile/project-resolved session defaults are preserved.
+        let plan_mode_reasoning_effort = if collaboration_mode
+            .as_ref()
+            .is_some_and(|mode| mode.mode == ModeKind::Plan)
+        {
+            thread.config().await.plan_mode_reasoning_effort.clone()
+        } else {
+            None
+        };
+        let collaboration_mode = collaboration_mode.map(|mut mode| {
+            if let Some(effort) = plan_mode_reasoning_effort {
+                mode.settings.reasoning_effort = Some(effort);
+            }
+            self.normalize_collaboration_mode(mode)
+        });
         let has_environment_override = environments.is_some();

I would deliberately put this in build_thread_settings_overrides, not in Desktop and not in Responses request construction:

  1. Both app-server entry points already converge here.
  2. preview_thread_settings_overrides validates the final effective combination.
  3. Core persists and reports the corrected effective settings normally.
  4. Other app-server clients receive the same config semantics as the TUI.
  5. No app-server protocol or generated-schema change is required.

Regression test

The existing Plan-mode app-server test currently sends reasoning_effort: None, which does not reproduce Desktop’s important case: the client supplies the global/default effort in the Plan mask.

Add a test that configures global low, Plan high, then deliberately submits Plan mode with medium. The effective notification and Responses request must both use high.

diff --git a/codex-rs/app-server/tests/suite/v2/plan_item.rs b/codex-rs/app-server/tests/suite/v2/plan_item.rs
index 9709f2cb0..PATCH 100644
--- a/codex-rs/app-server/tests/suite/v2/plan_item.rs
+++ b/codex-rs/app-server/tests/suite/v2/plan_item.rs
@@ -13,12 +13,14 @@ use codex_app_server_protocol::ThreadItem;
 use codex_app_server_protocol::ThreadStartParams;
+use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
 use codex_app_server_protocol::TurnCompletedNotification;
 use codex_app_server_protocol::TurnStartParams;
 use codex_app_server_protocol::TurnStartResponse;
 use codex_app_server_protocol::TurnStatus;
 use codex_app_server_protocol::UserInput as V2UserInput;
 use codex_features::Feature;
 use codex_protocol::config_types::CollaborationMode;
 use codex_protocol::config_types::ModeKind;
 use codex_protocol::config_types::Settings;
+use codex_protocol::openai_models::ReasoningEffort;
 use core_test_support::responses;
 use core_test_support::skip_if_no_network;
 use pretty_assertions::assert_eq;
@@ -130,6 +132,70 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()>
     Ok(())
 }

+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn plan_mode_uses_configured_reasoning_override_in_app_server() -> Result<()> {
+    skip_if_no_network!(Ok(()));
+
+    let responses = vec![responses::sse(vec![
+        responses::ev_response_created("resp-1"),
+        responses::ev_assistant_message("msg-1", "Done"),
+        responses::ev_completed("resp-1"),
+    ])];
+    let server = create_mock_responses_server_sequence_unchecked(responses).await;
+
+    let codex_home = TempDir::new()?;
+    MockResponsesConfig::new(&server.uri())
+        .enable_feature(Feature::CollaborationModes)
+        .write(codex_home.path())?;
+    std::fs::OpenOptions::new()
+        .append(true)
+        .open(codex_home.path().join("config.toml"))?
+        .write_all(
+            b"\nmodel_reasoning_effort = \"low\"\n\
+              plan_mode_reasoning_effort = \"high\"\n",
+        )?;
+
+    let mut mcp = TestAppServer::builder()
+        .with_codex_home(codex_home.path())
+        .build_initialized()
+        .await?;
+    let thread = mcp
+        .start_thread(ThreadStartParams {
+            model: Some("mock-model".to_string()),
+            ..Default::default()
+        })
+        .await?
+        .thread;
+
+    let _: TurnStartResponse = mcp
+        .request(|request_id| ClientRequest::TurnStart {
+            request_id,
+            params: TurnStartParams {
+                thread_id: thread.id.clone(),
+                input: vec![V2UserInput::Text {
+                    text: "Plan this".to_string(),
+                    text_elements: Vec::new(),
+                }],
+                collaboration_mode: Some(CollaborationMode {
+                    mode: ModeKind::Plan,
+                    settings: Settings {
+                        model: "mock-model".to_string(),
+                        // Reproduce Desktop supplying the ordinary/default effort.
+                        reasoning_effort: Some(ReasoningEffort::Medium),
+                        developer_instructions: None,
+                    },
+                }),
+                ..Default::default()
+            },
+        })
+        .await?;
+
+    let updated: ThreadSettingsUpdatedNotification = timeout(
+        DEFAULT_READ_TIMEOUT,
+        mcp.read_notification("thread/settings/updated"),
+    )
+    .await??;
+    assert_eq!(updated.thread_settings.effort, Some(ReasoningEffort::High));
+    assert_eq!(
+        updated
+            .thread_settings
+            .collaboration_mode
+            .settings
+            .reasoning_effort,
+        Some(ReasoningEffort::High)
+    );
+
+    collect_turn_notifications(&mut mcp).await?;
+    let request = server
+        .received_requests()
+        .await
+        .expect("wiremock should record the Responses request")
+        .into_iter()
+        .find(|request| request.url.path().ends_with("/responses"))
+        .expect("expected a Responses request");
+    let body: serde_json::Value = serde_json::from_slice(&request.body)?;
+    assert_eq!(body["reasoning"]["effort"], "high");
+
+    Ok(())
+}
+
 async fn start_plan_mode_turn(mcp: &mut TestAppServer) -> Result<codex_app_server_protocol::Turn> {

The test will also need:

use std::io::Write;

A second focused test for plan_mode_reasoning_effort = "none" would be useful because the documented semantics distinguish explicit none from an unset override. I would not block the minimal fix on that if existing config/TUI tests already sufficiently protect the enum semantics.

Expected behavior after the patch

Given:

model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "high"

and a Desktop request containing:

collaboration_mode = plan
collaboration_mode.settings.reasoning_effort = medium

app-server should resolve and report:

collaboration_mode = plan
collaboration_mode.settings.reasoning_effort = high
effort = high

Leaving Plan mode remains unchanged. If the Plan override is unset, the client/built-in Plan preset continues to determine the effort.

Validation

just test -p codex-app-server plan_mode_uses_configured_reasoning_override_in_app_server
just test -p codex-app-server
just fix -p codex-app-server
just fmt
git diff --check

Likely owners

Based on the relevant history, the most directly relevant people appear to be:

  • @charley-oai — authored #12303 / commit 4c1744afb22383ae61bebf7feb19a24eedc970df, which introduced plan_mode_reasoning_effort and the TUI overlay semantics.
  • @aibrahim-oai — authored recent collaboration-mode/turn-context ownership changes, including #32900.
  • @ pakrym-oai — has repeatedly changed app-server turn processing and collaboration-mode ownership, including #20149 and recent turn/start work.
  • @ jif-oai — authored #34940, which explicitly classified plan_mode_reasoning_effort as a session-static default in app-server.

@ charley-oai and @ aibrahim-oai are probably the two highest-signal tags for confirming intended semantics; @ pakrym-oai is likely the best app-server implementation reviewer.

The important semantic question is whether an explicit configured Plan override is intended to dominate a client-supplied Plan preset effort. The TUI already does exactly that in both mode switching and submission, so this patch makes app-server match the established behavior rather than defining a new precedence rule.