From 1dd731305ad709b792fb3bf345733d932ee426ed Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 2 Jun 2026 15:42:47 +0200 Subject: [PATCH] Reduce stack pressure in session startup and config rebuilds (#25844) ## Why `/clear` starts a fresh thread with `InitialHistory::Cleared`, which re-enters the thread/session startup path. That path now builds large async futures through `ThreadManagerState::spawn_thread_with_source`, `Codex::spawn`, and `Session::new`. Separately, TUI config rebuilds for cwd and permission-profile changes build a similarly heavy `ConfigBuilder::build()` future inside the app task. In debug and Bazel runs, those call chains can put enough state on the caller stack to abort before startup or config refresh completes. This change keeps the behavior the same while moving the heaviest future frames off the caller stack. ## What changed - Box `Codex::spawn(...)` in `codex-rs/core/src/thread_manager.rs` before awaiting it from `spawn_thread_with_source`. - Box `Session::new(...)` in `codex-rs/core/src/session/mod.rs` before awaiting it from `Codex::spawn_internal`. - Route `ConfigBuilder::build()` through a small `tokio::spawn` helper in `codex-rs/tui/src/app/config_persistence.rs` so cwd and permission-profile config rebuilds run on a runtime worker stack while preserving error context. ## Verification CI is running on the PR. No new targeted tests were added. This is a mechanical stack-pressure reduction that keeps the existing behavior and error propagation intact. --- codex-rs/core/src/session/mod.rs | 4 +-- codex-rs/core/src/thread_manager.rs | 4 +-- codex-rs/tui/src/app/config_persistence.rs | 37 +++++++++++++++------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index a63b0a91f..b3dcf504e 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -625,7 +625,7 @@ impl Codex { let session_source_clone = session_configuration.session_source.clone(); let (agent_status_tx, agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let session = Session::new( + let session = Box::pin(Session::new( session_configuration, config.clone(), installation_id, @@ -647,7 +647,7 @@ impl Codex { parent_rollout_thread_trace, attestation_provider, multi_agent_version, - ) + )) .await .map_err(|e| { error!("Failed to create session: {e:#}"); diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 0b8ebaa14..d130a3581 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1308,7 +1308,7 @@ impl ThreadManagerState { .await; let CodexSpawnOk { codex, thread_id, .. - } = Codex::spawn(CodexSpawnArgs { + } = Box::pin(Codex::spawn(CodexSpawnArgs { config, installation_id: self.installation_id.clone(), auth_manager, @@ -1336,7 +1336,7 @@ impl ThreadManagerState { thread_store: Arc::clone(&self.thread_store), attestation_provider: self.attestation_provider.clone(), inherited_multi_agent_version: multi_agent_version, - }) + })) .await?; let new_thread = self .finalize_thread_spawn(codex, thread_id, tracked_session_source) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 92ed5d470..ec72c5974 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -14,19 +14,32 @@ pub(super) struct WindowsSetupPermissions { pub(super) workspace_roots: Vec, } +async fn build_config_on_runtime_worker( + builder: ConfigBuilder, + error_context: String, +) -> Result { + match tokio::spawn(async move { builder.build().await }).await { + Ok(build_result) => build_result.wrap_err(error_context), + Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()), + Err(err) => Err(err).wrap_err_with(|| format!("{error_context} task failed")), + } +} + impl App { pub(super) async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result { let mut overrides = self.harness_overrides.clone(); overrides.cwd = Some(cwd.clone()); let cwd_display = cwd.display().to_string(); - ConfigBuilder::default() + let builder = ConfigBuilder::default() .codex_home(self.config.codex_home.to_path_buf()) .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) - .loader_overrides(self.loader_overrides.clone()) - .build() - .await - .wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}")) + .loader_overrides(self.loader_overrides.clone()); + build_config_on_runtime_worker( + builder, + format!("Failed to rebuild config for cwd {cwd_display}"), + ) + .await } pub(super) async fn rebuild_config_for_permission_profile( @@ -38,16 +51,16 @@ impl App { overrides.sandbox_mode = None; overrides.permission_profile = None; overrides.default_permissions = Some(profile_id.to_string()); - ConfigBuilder::default() + let builder = ConfigBuilder::default() .codex_home(self.config.codex_home.to_path_buf()) .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) - .loader_overrides(self.loader_overrides.clone()) - .build() - .await - .wrap_err_with(|| { - format!("Failed to rebuild config for permission profile {profile_id}") - }) + .loader_overrides(self.loader_overrides.clone()); + build_config_on_runtime_worker( + builder, + format!("Failed to rebuild config for permission profile {profile_id}"), + ) + .await } #[cfg(target_os = "windows")]