From 532b9c83ae83a3cdcf1e174c5239451314cd1508 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 19 May 2026 20:05:52 -0700 Subject: [PATCH] Move plugin and skill warmup into session startup (#23535) ## Why Plugin and skill loading is useful as warmup and early validation, but session startup does not need to wait for that work before it can continue building the session. Keeping it on the serial startup path adds avoidable latency to every fresh thread start. We still want invalid skill configurations to show up quickly, and we want the warmup to exercise the same plugin and skill manager caches that the normal turn path uses. ## What changed - moved plugin and skill warmup into the session startup async path instead of eagerly awaiting it on the serial setup path - kept the warmup using the session's resolved filesystem/environment context so skill loading still sees the right roots - preserved early skill-load error logging so broken skill configurations still surface during startup - left the per-turn plugin and skill loading path unchanged, so turns still use the normal cached managers ## Testing - Not run locally; relying on CI for validation. --- codex-rs/core/src/session/mod.rs | 14 ------- codex-rs/core/src/session/session.rs | 57 +++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8f0d2f1c01..646528cf97 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -480,20 +480,6 @@ impl Codex { } = args; let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - let fs = environment_selections.primary_filesystem(); - let plugins_input = config.plugins_config_input(); - let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; - let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); - let skills_input = skills_load_input_from_config(&config, effective_skill_roots); - let loaded_skills = skills_manager.skills_for_config(&skills_input, fs).await; - - for err in &loaded_skills.errors { - error!( - "failed to load skill {}: {}", - err.path.display(), - err.message - ); - } if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source && depth >= config.agent_max_depth diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 884c440f28..8945880c33 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,6 +1,7 @@ use super::input_queue::InputQueue; use super::*; use crate::goals::GoalRuntimeState; +use crate::skills::SkillError; use crate::state::ActiveTurn; use codex_protocol::SessionId; use codex_protocol::config_types::ServiceTier; @@ -405,6 +406,29 @@ pub(crate) struct AppServerClientMetadata { pub(crate) client_version: Option, } +async fn warm_plugins_and_skills_for_session_init( + config: Arc, + environment_manager: Arc, + plugins_manager: Arc, + skills_manager: Arc, + environments: Vec, +) -> Vec { + let fs = crate::environment_selection::resolve_environment_selections( + environment_manager.as_ref(), + &environments, + ) + .ok() + .and_then(|resolved| resolved.primary_filesystem()); + let plugins_input = config.plugins_config_input(); + let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; + let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); + let skills_input = skills_load_input_from_config(config.as_ref(), effective_skill_roots); + skills_manager + .skills_for_config(&skills_input, fs) + .await + .errors +} + impl Session { /// Returns the concrete identity for this thread. pub(crate) fn thread_id(&self) -> ThreadId { @@ -578,9 +602,38 @@ impl Session { otel.name = "session_init.auth_mcp", )); + let plugin_and_skill_warmup_fut = warm_plugins_and_skills_for_session_init( + Arc::clone(&config), + Arc::clone(&environment_manager), + Arc::clone(&plugins_manager), + Arc::clone(&skills_manager), + session_configuration.environments.clone(), + ) + .instrument(info_span!( + "session_init.plugin_skill_warmup", + otel.name = "session_init.plugin_skill_warmup", + )); + // Join all independent futures. - let (thread_persistence_result, state_db_ctx, (auth, mcp_servers, auth_statuses)) = - tokio::join!(thread_persistence_fut, state_db_fut, auth_and_mcp_fut); + let ( + thread_persistence_result, + state_db_ctx, + (auth, mcp_servers, auth_statuses), + plugin_skill_errors, + ) = tokio::join!( + thread_persistence_fut, + state_db_fut, + auth_and_mcp_fut, + plugin_and_skill_warmup_fut + ); + + for err in &plugin_skill_errors { + error!( + "failed to load skill {}: {}", + err.path.display(), + err.message + ); + } let mut live_thread_init = LiveThreadInitGuard::new(thread_persistence_result.map_err(|e| {