Refresh MCP config independently across threads (#35216)

## What changed

- Add a best-effort MCP configuration refresh that logs per-thread load errors and continues refreshing the remaining threads.
- Use the best-effort path after account or plugin changes before invalidating MCP runtimes.

## Testing

- Add a test confirming that a thread with a configuration load failure does not prevent healthy threads from receiving updated configuration.

GitOrigin-RevId: 34708d46f63fbe8617567ced16b4c199456ab078
This commit is contained in:
jif
2026-07-24 16:53:11 +00:00
committed by copyberry
parent a177013eb0
commit 58b4277228
2 changed files with 51 additions and 5 deletions
+49
View File
@@ -4,6 +4,7 @@ use codex_core::ThreadManager;
use codex_core::config::Config;
use std::io;
use std::sync::Arc;
use tracing::warn;
pub(crate) async fn reload_mcp_config(
thread_manager: &Arc<ThreadManager>,
@@ -27,6 +28,29 @@ pub(crate) async fn reload_mcp_config(
Ok(())
}
pub(crate) async fn reload_mcp_config_best_effort(
thread_manager: &Arc<ThreadManager>,
config_manager: &ConfigManager,
) {
for thread_id in thread_manager.list_thread_ids().await {
let thread = match thread_manager.get_thread(thread_id).await {
Ok(thread) => thread,
Err(err) => {
warn!(%thread_id, %err, "failed to load thread for MCP configuration refresh");
continue;
}
};
let config = match load_refresh_config(thread.as_ref(), config_manager).await {
Ok(config) => config,
Err(err) => {
warn!(%thread_id, %err, "failed to load thread MCP configuration");
continue;
}
};
thread.refresh_mcp_config(config).await;
}
}
async fn load_refresh_config(
thread: &CodexThread,
config_manager: &ConfigManager,
@@ -97,6 +121,31 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn best_effort_refresh_updates_healthy_threads() -> anyhow::Result<()> {
let (temp_dir, thread_manager, config_manager, loader) = refresh_test_state().await?;
std::fs::write(
temp_dir.path().join(codex_config::CONFIG_TOML_FILE),
"[features]\nsecret_auth_storage = true\n",
)?;
reload_mcp_config_best_effort(&thread_manager, &config_manager).await;
assert_eq!(loader.good_loads.load(Ordering::Relaxed), 1);
assert_eq!(loader.bad_loads.load(Ordering::Relaxed), 1);
for thread_id in thread_manager.list_thread_ids().await {
let thread = thread_manager.get_thread(thread_id).await?;
let config = thread.config().await;
let expected = if config.cwd.ends_with("good") {
AuthKeyringBackendKind::Secrets
} else {
AuthKeyringBackendKind::Direct
};
assert_eq!(config.auth_keyring_backend_kind(), expected);
}
Ok(())
}
#[tokio::test]
async fn invalidation_does_not_reload_thread_config() -> anyhow::Result<()> {
let (_temp_dir, thread_manager, _config_manager, loader) = refresh_test_state().await?;
@@ -259,11 +259,8 @@ impl AccountRequestProcessor {
tokio::spawn(async move {
thread_manager.plugins_manager().clear_cache();
thread_manager.skills_service().clear_cache();
if let Err(err) =
crate::mcp_refresh::reload_mcp_config(&thread_manager, &config_manager).await
{
warn!(%err, "failed to reload MCP configuration after account or plugin change");
}
crate::mcp_refresh::reload_mcp_config_best_effort(&thread_manager, &config_manager)
.await;
thread_manager.invalidate_mcp_runtimes().await;
});
}