From ada080ee67d74437c3712040d2a5be2a4db340a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Sun, 4 Oct 2026 01:01:03 -0300 Subject: [PATCH] fix(codex-acp): resume sessions with their recorded model Restoring a Codex session built its config from the global default model, so codex-core resumed the thread on a different model than the one it was recorded with. That silently switched the chat's model and surfaced the "This session was recorded with model ... but is resuming with ..." warning as an agent message. Before resuming, apply the model and reasoning effort persisted in the thread metadata, mirroring the Codex app-server. Models recorded under a different provider, or threads without a persisted model, keep the configured defaults. --- vendor/codex-acp/src/codex_agent.rs | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/vendor/codex-acp/src/codex_agent.rs b/vendor/codex-acp/src/codex_agent.rs index 191ca303..fbd03751 100644 --- a/vendor/codex-acp/src/codex_agent.rs +++ b/vendor/codex-acp/src/codex_agent.rs @@ -37,6 +37,7 @@ use codex_login::{ use codex_protocol::{ ThreadId, mcp::ClientMcpExtensions, + openai_models::ReasoningEffort, protocol::{SessionConfiguredEvent, SessionSource, ThreadHistoryMode}, }; use codex_rollout::{InitialHistory, ResumedHistory}; @@ -603,6 +604,29 @@ impl CodexAgent { Ok(config) } + /// Resume with the model the thread last ran with instead of the global + /// default, mirroring the Codex app-server. Otherwise codex-core resumes on + /// the default model and emits a "recorded with model" warning. + fn apply_persisted_model_settings( + config: &mut Config, + model_provider: &str, + model: Option<&str>, + reasoning_effort: Option, + ) { + // Switching providers would also require swapping the provider info, + // so only restore models recorded under the configured provider. + let Some(model) = model.filter(|model| !model.is_empty()) else { + return; + }; + if model_provider != config.model_provider_id { + return; + } + config.model = Some(model.to_string()); + if let Some(reasoning_effort) = reasoning_effort { + config.model_reasoning_effort = Some(reasoning_effort); + } + } + fn sync_config_with_session( config: &mut Config, session_configured: &SessionConfiguredEvent, @@ -1054,6 +1078,12 @@ impl CodexAgent { }; let mut config = Self::build_session_config(&self.config, &cwd, mcp_servers)?; + Self::apply_persisted_model_settings( + &mut config, + &stored_thread.model_provider, + stored_thread.model.as_deref(), + stored_thread.reasoning_effort.clone(), + ); let restored = if paginated { let context = self @@ -1565,6 +1595,52 @@ mod tests { Ok(()) } + #[tokio::test] + async fn resume_restores_model_recorded_under_the_configured_provider() -> anyhow::Result<()> { + let (mut config, _codex_home) = test_config().await?; + config.model = Some("default-model".to_string()); + config.model_reasoning_effort = Some(ReasoningEffort::Low); + let provider = config.model_provider_id.clone(); + + CodexAgent::apply_persisted_model_settings( + &mut config, + &provider, + Some("recorded-model"), + Some(ReasoningEffort::High), + ); + assert_eq!(config.model.as_deref(), Some("recorded-model")); + assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!(config.model_provider_id, provider); + + // A missing recorded effort keeps the configured one. + CodexAgent::apply_persisted_model_settings( + &mut config, + &provider, + Some("other-recorded-model"), + None, + ); + assert_eq!(config.model.as_deref(), Some("other-recorded-model")); + assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); + + // Unknown models and models from other providers keep the defaults. + CodexAgent::apply_persisted_model_settings( + &mut config, + &provider, + None, + Some(ReasoningEffort::Low), + ); + CodexAgent::apply_persisted_model_settings( + &mut config, + "other-provider", + Some("foreign-model"), + Some(ReasoningEffort::Low), + ); + assert_eq!(config.model.as_deref(), Some("other-recorded-model")); + assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!(config.model_provider_id, provider); + Ok(()) + } + #[tokio::test] async fn session_config_preserves_mcp_transport_auth_env_and_cwd() -> anyhow::Result<()> { let (mut base_config, _codex_home) = test_config().await?;