Apply stable environment identity across MCP projection

This commit is contained in:
jif-oai
2026-06-25 17:44:16 +01:00
parent 338cbd0bf9
commit 1cef19d72c
4 changed files with 22 additions and 17 deletions
@@ -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<Option<Self>> {
let Some(plugin) = ExecutorPluginProvider::resolve_pinned(root).await? else {
return Ok(None);
+1 -1
View File
@@ -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);
+11 -8
View File
@@ -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<ExecutorPluginRuntime>,
}
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,
+7 -3
View File
@@ -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::<Vec<SelectedCapabilityRoot>>()
{
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(),