diff --git a/codex-rs/core-plugins/src/executor_runtime.rs b/codex-rs/core-plugins/src/executor_runtime.rs index 5c6becf997..0f7e7a5e8e 100644 --- a/codex-rs/core-plugins/src/executor_runtime.rs +++ b/codex-rs/core-plugins/src/executor_runtime.rs @@ -17,7 +17,7 @@ use crate::ResolvedExecutorPlugin; const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json"; -/// MCP and connector declarations read from one exact executor binding. +/// MCP and connector declarations read from one stable executor capability root. #[derive(Clone, Debug)] pub struct ExecutorPluginRuntime { plugin: ResolvedPlugin, @@ -26,10 +26,8 @@ pub struct ExecutorPluginRuntime { } impl ExecutorPluginRuntime { - /// Reads both runtime declaration files through the root's pinned filesystem. - /// - /// `Ok(None)` is intentionally not cacheable: the plugin manifest may appear - /// once a deferred executor finishes starting. + /// Reads both runtime declaration files through the root's current ready filesystem. + /// `Ok(None)` means the stable root is not a plugin and may be cached by the caller. pub async fn project(root: &ResolvedSelectedCapabilityRoot) -> anyhow::Result> { let Some(plugin) = ExecutorPluginProvider::resolve_pinned(root).await? else { return Ok(None); diff --git a/codex-rs/core/src/session/mcp_projection.rs b/codex-rs/core/src/session/mcp_projection.rs index 143a779816..d1dcf6956d 100644 --- a/codex-rs/core/src/session/mcp_projection.rs +++ b/codex-rs/core/src/session/mcp_projection.rs @@ -116,7 +116,7 @@ impl Session { .environment_manager() .resolve_selected_capability_roots( selected_roots, - &environments.captured_environments(), + &environments.captured_environment_availability(), ) .await; let bindings = selected_bindings(selected_roots, &resolved_roots); diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs index 263bf96dd9..2bec6cb1ae 100644 --- a/codex-rs/core/src/session/mcp_runtime.rs +++ b/codex-rs/core/src/session/mcp_runtime.rs @@ -9,7 +9,8 @@ use codex_protocol::capabilities::SelectedCapabilityRoot; /// /// Selected environment identity and contents are stable. Plugin manifests, MCP declarations, and /// app declarations are therefore cached by the complete selected root for the session lifetime. -/// Missing or failed projections are not cached, so a deferred environment can recover later. +/// Successful projections and stable non-plugin roots are cached. Failed reads are not cached, so +/// a transient executor error can recover later. /// /// A live runtime is a separate cache: it is reusable only for the same ordered selected roots and /// the same process-local environment handles. The caller additionally compares the effective MCP @@ -37,7 +38,7 @@ struct CachedSelectedRuntime { struct CachedExecutorPluginProjection { selected_root: SelectedCapabilityRoot, - plugin: ExecutorPluginRuntime, + plugin: Option, } impl SelectedMcpRuntimeCache { @@ -73,26 +74,28 @@ impl SelectedMcpRuntimeCache { let mut plugins = Vec::new(); for (selection_order, root) in bindings { let selected_root = root.selected_root(); - if let Some(plugin) = self + if let Some(cached) = self .plugin_projections .iter() .find(|cached| &cached.selected_root == selected_root) - .map(|cached| cached.plugin.clone()) { - plugins.push((*selection_order, plugin)); + if let Some(plugin) = &cached.plugin { + plugins.push((*selection_order, plugin.clone())); + } continue; } match ExecutorPluginRuntime::project(root).await { - Ok(Some(plugin)) => { + Ok(plugin) => { self.plugin_projections .push(CachedExecutorPluginProjection { selected_root: selected_root.clone(), plugin: plugin.clone(), }); - plugins.push((*selection_order, plugin)); + if let Some(plugin) = plugin { + plugins.push((*selection_order, plugin)); + } } - Ok(None) => {} Err(err) => { tracing::warn!( selected_root = selected_root.id, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 8ab001294a..761f74a07c 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -557,10 +557,14 @@ impl Session { config.current_time_reminder.as_ref(), external_time_provider, )?; - let selected_capability_roots = if selected_capability_roots.is_empty() { - initial_history.get_selected_capability_roots() - } else { + let selected_capability_roots = if !selected_capability_roots.is_empty() { selected_capability_roots + } else if let Some(selected_capability_roots) = + thread_extension_init.get::>() + { + selected_capability_roots.as_ref().clone() + } else { + initial_history.get_selected_capability_roots() }; let thread_extension_data = codex_extension_api::ExtensionData::new_with_init( thread_id.to_string(),