mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Trace MCP refresh causes and plugin metadata changes (#60505)
## What changed - Track and log `McpRefreshReason` values for runtime refreshes, including invalidations received during thread startup and forced refreshes. Restore claimed reasons when a refresh is cancelled before publication, while preserving concurrent invalidations. - Correlate installed-plugin metadata invalidations with downstream MCP refreshes using a shared change ID. Log changed field names by plugin ID, additions, removals, and reordering without logging field values. - Record whether background installed-plugin bundle updates were requested by startup, plugin listing, or installation. ## Testing Add coverage for metadata change attribution across membership changes and reordering, and for retaining refresh causes across cancellation and concurrent invalidation. GitOrigin-RevId: 3d4485ccfcc1d8788eb9909a060ac200c467af13
This commit is contained in:
@@ -188,6 +188,7 @@ pub use thread_manager::AgentTreeShutdownFailureReason;
|
||||
pub use thread_manager::AgentTreeShutdownReport;
|
||||
pub use thread_manager::ForkSnapshot;
|
||||
pub use thread_manager::InternalSessionParent;
|
||||
pub use thread_manager::McpRefreshReason;
|
||||
pub use thread_manager::NewThread;
|
||||
pub use thread_manager::PromptCacheKey;
|
||||
pub use thread_manager::StartThreadOptions;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Publishes resolved MCP configuration and host timing rollouts for future turns.
|
||||
|
||||
use crate::McpRefreshReason;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ConfigRefreshOutcome;
|
||||
@@ -25,7 +26,7 @@ impl Session {
|
||||
config.disable_mcp_enterprise_auth();
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
self.services.mcp_runtime.invalidate_resource_caches();
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::EnterpriseAuthDisabled);
|
||||
}
|
||||
self.services.mcp_runtime.quiesce_ema().await;
|
||||
self.schedule_mcp_prewarm();
|
||||
@@ -192,7 +193,11 @@ impl Session {
|
||||
if matches!(scope, RuntimeConfigRefresh::Mcp) {
|
||||
self.services.mcp_runtime.invalidate_resource_caches();
|
||||
}
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(match scope {
|
||||
RuntimeConfigRefresh::User => McpRefreshReason::UserConfigReloaded,
|
||||
RuntimeConfigRefresh::Mcp => McpRefreshReason::McpConfigReloaded,
|
||||
RuntimeConfigRefresh::UserFiles => McpRefreshReason::UserConfigFilesReloaded,
|
||||
});
|
||||
let new_config = notify_contributors
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
(previous_config, new_config)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::McpRefreshReason;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -369,7 +370,7 @@ impl Session {
|
||||
state.session_configuration.environments = future;
|
||||
if update_current {
|
||||
// Invalidate MCP before installed configuration can wake a waiting turn.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::CurrentEnvironmentsChanged);
|
||||
self.services.turn_environments.update_selections(¤t);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::Submission;
|
||||
use crate::McpRefreshReason;
|
||||
use crate::WithTurnExtensionData;
|
||||
use crate::realtime_conversation::handle_audio as handle_realtime_conversation_audio;
|
||||
use crate::realtime_conversation::handle_close as handle_realtime_conversation_close;
|
||||
@@ -260,7 +261,7 @@ pub async fn dynamic_tool_response(sess: &Arc<Session>, id: String, response: Dy
|
||||
|
||||
pub fn refresh_mcp_servers(sess: &Session) {
|
||||
sess.services.mcp_runtime.reconnect_on_next_refresh();
|
||||
sess.request_mcp_runtime_refresh();
|
||||
sess.request_mcp_runtime_refresh(McpRefreshReason::ExplicitServerReconnect);
|
||||
}
|
||||
|
||||
pub async fn reload_user_config(sess: &Arc<Session>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::mcp_refresh::McpRefreshInvalidationGuard;
|
||||
use super::*;
|
||||
use crate::McpRefreshReason;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::environment_selection::combine_selected_capability_roots;
|
||||
use crate::mcp_tool_call::McpToolApprovalContext;
|
||||
@@ -119,8 +119,11 @@ impl Session {
|
||||
/*skill_turn*/ None,
|
||||
)
|
||||
.await;
|
||||
if inventory_changed || discovery_recovered {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
if inventory_changed {
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::ExecutorCapabilityInventoryChanged);
|
||||
}
|
||||
if discovery_recovered {
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::ExecutorCapabilityDiscoveryRecovered);
|
||||
}
|
||||
let mcp_projection =
|
||||
self.services
|
||||
@@ -199,7 +202,7 @@ impl Session {
|
||||
&environment_selections,
|
||||
&environments.ready_environment_handles(),
|
||||
) {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::EnvironmentBindingsChanged);
|
||||
}
|
||||
let auth = self.services.auth_manager.auth_cached();
|
||||
if !self
|
||||
@@ -207,15 +210,11 @@ impl Session {
|
||||
.mcp_runtime
|
||||
.current_auth_matches(auth.as_ref())
|
||||
{
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::RuntimeAuthChanged);
|
||||
}
|
||||
|
||||
if !self.mcp_refresh.claim() {
|
||||
let Some(mut refresh_invalidation) = self.mcp_refresh.claim(self.thread_id) else {
|
||||
return;
|
||||
}
|
||||
let mut refresh_invalidation = McpRefreshInvalidationGuard {
|
||||
refresh: &self.mcp_refresh,
|
||||
published: false,
|
||||
};
|
||||
let auth = self.services.auth_manager.auth().await;
|
||||
// Capture after claiming the refresh so a new invalidation cannot be consumed
|
||||
@@ -282,7 +281,7 @@ impl Session {
|
||||
self: &Arc<Self>,
|
||||
) -> anyhow::Result<codex_mcp::CodexAppsToolSnapshot> {
|
||||
// Reconcile unchanged config so failed or closed clients can be replaced.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::CodexAppsToolRefresh);
|
||||
self.refresh_mcp_if_dirty().await;
|
||||
let _refresh = self
|
||||
.mcp_refresh
|
||||
@@ -302,11 +301,9 @@ impl Session {
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("MCP runtime refresh semaphore closed"))?;
|
||||
// This is forced work even when there was no pending invalidation.
|
||||
self.mcp_refresh.claim();
|
||||
let mut refresh_invalidation = McpRefreshInvalidationGuard {
|
||||
refresh: &self.mcp_refresh,
|
||||
published: false,
|
||||
};
|
||||
let mut refresh_invalidation = self
|
||||
.mcp_refresh
|
||||
.claim_forced(self.thread_id, McpRefreshReason::HardRefreshCodexAppsTools);
|
||||
let auth = self.services.auth_manager.auth().await;
|
||||
let environments = self.services.turn_environments.snapshot().await;
|
||||
let environment_selections = environments.all_selections();
|
||||
@@ -380,8 +377,8 @@ impl Session {
|
||||
refreshed
|
||||
}
|
||||
|
||||
pub(super) fn mark_mcp_runtime_dirty(&self) {
|
||||
self.mcp_refresh.invalidate();
|
||||
pub(super) fn mark_mcp_runtime_dirty(&self, reason: McpRefreshReason) {
|
||||
self.mcp_refresh.invalidate(reason);
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
@@ -404,7 +401,7 @@ impl Session {
|
||||
.current_ready_selected_capability_roots()
|
||||
!= ready_selected_capability_roots
|
||||
{
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::SelectedCapabilityRootsChanged);
|
||||
}
|
||||
|
||||
let recovered_oauth_servers =
|
||||
@@ -423,7 +420,7 @@ impl Session {
|
||||
.has_authentication_failed_servers(&recovered_oauth_servers)
|
||||
.await
|
||||
{
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::OAuthCredentialsRecovered);
|
||||
}
|
||||
self.refresh_mcp_if_dirty().await;
|
||||
// Prewarm and other context-only captures must not start cloud discovery.
|
||||
@@ -450,7 +447,7 @@ impl Session {
|
||||
.await
|
||||
{
|
||||
// Preserve MCP updates if a later refresh is cancelled.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::ExtensionCatalogChanged);
|
||||
}
|
||||
}
|
||||
self.refresh_mcp_if_dirty().await;
|
||||
@@ -861,11 +858,9 @@ impl Session {
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
// Claim after config mutation, before capturing the state this forced refresh handles.
|
||||
self.mcp_refresh.claim();
|
||||
let mut refresh_invalidation = McpRefreshInvalidationGuard {
|
||||
refresh: &self.mcp_refresh,
|
||||
published: false,
|
||||
};
|
||||
let mut refresh_invalidation = self
|
||||
.mcp_refresh
|
||||
.claim_forced(self.thread_id, McpRefreshReason::SkillMcpDependenciesAdded);
|
||||
let auth = self.services.auth_manager.auth().await;
|
||||
let ready_selected_capability_roots = self
|
||||
.services
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
//! newest thread state; exact model steps remain the correctness path.
|
||||
|
||||
use super::*;
|
||||
use crate::McpRefreshReason;
|
||||
|
||||
impl Session {
|
||||
pub(crate) fn request_mcp_runtime_refresh(&self) {
|
||||
pub(crate) fn request_mcp_runtime_refresh(&self, reason: McpRefreshReason) {
|
||||
// Plugin changes can reuse connections but still change their skill resources.
|
||||
self.services.mcp_runtime.invalidate_resource_caches();
|
||||
self.request_mcp_runtime_reprojection();
|
||||
self.request_mcp_runtime_reprojection(reason);
|
||||
}
|
||||
|
||||
/// Reproject contributor state without invalidating cached MCP resources.
|
||||
pub(crate) fn request_mcp_runtime_reprojection(&self) {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
pub(crate) fn request_mcp_runtime_reprojection(&self, reason: McpRefreshReason) {
|
||||
self.mark_mcp_runtime_dirty(reason);
|
||||
self.schedule_mcp_prewarm();
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ impl Session {
|
||||
break;
|
||||
};
|
||||
if auth_changed {
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::AuthChangeNotification);
|
||||
}
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
@@ -1,34 +1,78 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use crate::McpRefreshReason;
|
||||
use codex_protocol::ThreadId;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::AcquireError;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::SemaphorePermit;
|
||||
|
||||
/// Owns MCP invalidation and the single gate used to publish runtime updates.
|
||||
pub(super) struct McpRefresh {
|
||||
pending: AtomicBool,
|
||||
pending: Mutex<BTreeSet<McpRefreshReason>>,
|
||||
gate: Semaphore,
|
||||
}
|
||||
|
||||
impl McpRefresh {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
pending: AtomicBool::new(false),
|
||||
pending: Mutex::default(),
|
||||
gate: Semaphore::new(/*permits*/ 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn invalidate(&self) {
|
||||
self.pending.store(true, Ordering::Release);
|
||||
pub(super) fn invalidate(&self, reason: McpRefreshReason) {
|
||||
self.pending
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(reason);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Acquire)
|
||||
!self
|
||||
.pending
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn claim(&self) -> bool {
|
||||
self.pending.swap(false, Ordering::AcqRel)
|
||||
/// Take only the causes known at the start of this refresh. Later invalidations
|
||||
/// belong to the next publication, even if they have the same cause.
|
||||
pub(super) fn claim(&self, thread_id: ThreadId) -> Option<McpRefreshInvalidationGuard<'_>> {
|
||||
let reasons = self.take_pending();
|
||||
(!reasons.is_empty()).then(|| self.start(thread_id, reasons))
|
||||
}
|
||||
|
||||
pub(super) fn claim_forced(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
reason: McpRefreshReason,
|
||||
) -> McpRefreshInvalidationGuard<'_> {
|
||||
let mut reasons = self.take_pending();
|
||||
reasons.insert(reason);
|
||||
self.start(thread_id, reasons)
|
||||
}
|
||||
|
||||
pub(super) fn take_pending(&self) -> BTreeSet<McpRefreshReason> {
|
||||
std::mem::take(
|
||||
&mut *self
|
||||
.pending
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
)
|
||||
}
|
||||
|
||||
fn start(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
reasons: BTreeSet<McpRefreshReason>,
|
||||
) -> McpRefreshInvalidationGuard<'_> {
|
||||
tracing::info!(%thread_id, ?reasons, "refreshing MCP runtime");
|
||||
McpRefreshInvalidationGuard {
|
||||
refresh: self,
|
||||
reasons,
|
||||
published: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "mcp.runtime.refresh_wait", skip_all)]
|
||||
@@ -43,14 +87,21 @@ impl McpRefresh {
|
||||
|
||||
/// Restores a claimed refresh when its task is cancelled before publication.
|
||||
pub(super) struct McpRefreshInvalidationGuard<'a> {
|
||||
pub(super) refresh: &'a McpRefresh,
|
||||
refresh: &'a McpRefresh,
|
||||
reasons: BTreeSet<McpRefreshReason>,
|
||||
pub(super) published: bool,
|
||||
}
|
||||
|
||||
impl Drop for McpRefreshInvalidationGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if !self.published {
|
||||
self.refresh.invalidate();
|
||||
let mut pending = self
|
||||
.refresh
|
||||
.pending
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
pending.append(&mut self.reasons);
|
||||
pending.insert(McpRefreshReason::PreviousRefreshUnpublished);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
//! Checks that successful refreshes preserve newly pending work.
|
||||
//! Verifies that refreshes consume their own causes and preserve concurrent work.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn successful_manual_claim_consumes_only_preexisting_work() {
|
||||
for initially_pending in [false, true] {
|
||||
for invalidated_during_refresh in [false, true] {
|
||||
let refresh = McpRefresh::new();
|
||||
if initially_pending {
|
||||
refresh.invalidate();
|
||||
}
|
||||
assert_eq!(refresh.claim(), initially_pending);
|
||||
let mut guard = McpRefreshInvalidationGuard {
|
||||
refresh: &refresh,
|
||||
published: false,
|
||||
};
|
||||
if invalidated_during_refresh {
|
||||
refresh.invalidate();
|
||||
}
|
||||
guard.published = true;
|
||||
drop(guard);
|
||||
assert_eq!(refresh.is_pending(), invalidated_during_refresh);
|
||||
}
|
||||
}
|
||||
fn refresh_keeps_concurrent_invalidations_and_restores_unpublished_causes() {
|
||||
let thread_id = ThreadId::new();
|
||||
let refresh = McpRefresh::new();
|
||||
refresh.invalidate(McpRefreshReason::McpConfigReloaded);
|
||||
let claimed = refresh.claim(thread_id).expect("pending");
|
||||
refresh.invalidate(McpRefreshReason::RuntimeAuthChanged);
|
||||
drop(claimed);
|
||||
|
||||
let mut retry = refresh.claim(thread_id).expect("retry");
|
||||
assert_eq!(
|
||||
retry.reasons,
|
||||
BTreeSet::from([
|
||||
McpRefreshReason::RuntimeAuthChanged,
|
||||
McpRefreshReason::McpConfigReloaded,
|
||||
McpRefreshReason::PreviousRefreshUnpublished
|
||||
])
|
||||
);
|
||||
refresh.invalidate(McpRefreshReason::RuntimeAuthChanged);
|
||||
retry.published = true;
|
||||
drop(retry);
|
||||
|
||||
let mut next = refresh.claim(thread_id).expect("new invalidation");
|
||||
assert_eq!(
|
||||
next.reasons,
|
||||
BTreeSet::from([McpRefreshReason::RuntimeAuthChanged])
|
||||
);
|
||||
next.published = true;
|
||||
drop(next);
|
||||
assert!(refresh.claim(thread_id).is_none());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) mod startup;
|
||||
|
||||
use crate::McpRefreshReason;
|
||||
use crate::PromptCacheKey;
|
||||
use crate::WithTurnExtensionData;
|
||||
use crate::runtime_auth::RuntimeAuthSlot;
|
||||
@@ -2207,7 +2208,7 @@ impl Session {
|
||||
!= updated.step_settings.service_tier;
|
||||
let mcp_inputs_changed = self.mcp_inputs_differ(&state.session_configuration, &updated);
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::SessionMcpInputsChanged);
|
||||
}
|
||||
// Save new environment defaults for future turns. The running turn keeps its own.
|
||||
windows_sandbox_changed =
|
||||
@@ -4378,8 +4379,11 @@ impl Session {
|
||||
)
|
||||
.await
|
||||
};
|
||||
if inventory_changed || discovery_recovered {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
if inventory_changed {
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::ExecutorCapabilityInventoryChanged);
|
||||
}
|
||||
if discovery_recovered {
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::ExecutorCapabilityDiscoveryRecovered);
|
||||
}
|
||||
let extension_data =
|
||||
codex_extension_api::ExtensionData::new(turn_context.sub_id.clone());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Activates an admitted task's plugin selection without changing pending settings.
|
||||
|
||||
use crate::McpRefreshReason;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::Session;
|
||||
@@ -11,7 +12,7 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
if state.active_disabled_plugin_ids != turn_context.disabled_plugin_ids {
|
||||
state.active_disabled_plugin_ids = turn_context.disabled_plugin_ids.clone();
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.mark_mcp_runtime_dirty(McpRefreshReason::DisabledPluginSelectionChanged);
|
||||
}
|
||||
Arc::clone(&state.session_configuration.original_config_do_not_use)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::McpRefreshReason;
|
||||
use crate::agents_md_manager::AgentsMdManager;
|
||||
use crate::agents_md_manager::SessionInstructions;
|
||||
use crate::session::Submission;
|
||||
@@ -612,7 +613,7 @@ async fn submitted_sparse_updates_preserve_captured_steps_and_ordering() {
|
||||
.acquire()
|
||||
.await
|
||||
.expect("MCP refresh lock");
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
let capture_cancel = CancellationToken::new();
|
||||
let mut during = Box::pin(tokio::task::unconstrained(
|
||||
session.capture_step_context(Arc::clone(&turn), &capture_cancel),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::McpRefreshReason;
|
||||
use crate::agent::LocalAgentControl;
|
||||
use codex_protocol::ContextWindowId;
|
||||
#[path = "compaction_replay_tests.rs"]
|
||||
@@ -2329,7 +2330,7 @@ async fn refresh_config_rejects_stale_owner_without_effects(scope: RuntimeConfig
|
||||
crate::ConfigRefreshOutcome::Published
|
||||
);
|
||||
let current_owner = session.get_config().await;
|
||||
assert!(session.mcp_refresh.claim());
|
||||
assert!(!session.mcp_refresh.take_pending().is_empty());
|
||||
|
||||
assert_eq!(
|
||||
session
|
||||
@@ -7073,7 +7074,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
"turn_id".to_string(),
|
||||
skills_snapshot,
|
||||
);
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
crate::guardian::test_host::install(&session, &turn_context.config);
|
||||
(session, turn_context)
|
||||
}
|
||||
@@ -9417,7 +9418,7 @@ where
|
||||
"turn_id".to_string(),
|
||||
skills_snapshot,
|
||||
));
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
crate::guardian::test_host::install(&session, &turn_context.config);
|
||||
(session, turn_context, rx_event)
|
||||
}
|
||||
@@ -9604,7 +9605,7 @@ async fn refresh_mcp_servers_uses_latest_state_for_existing_turns() {
|
||||
.expect("enable secret auth storage");
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
|
||||
let next_turn = session.new_default_turn().await;
|
||||
let new_step = session
|
||||
@@ -9774,7 +9775,7 @@ async fn mcp_elicitation_reviewer_uses_active_reviewer_and_latest_runtime_policy
|
||||
.spawn_task(Arc::clone(&old_turn), Vec::new(), task)
|
||||
.await;
|
||||
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
session.refresh_mcp_if_dirty().await;
|
||||
if live_update {
|
||||
assert_eq!(
|
||||
@@ -9990,7 +9991,7 @@ async fn mcp_elicitation_reviewer_is_reused_across_runtime_refreshes() {
|
||||
let session = Arc::new(session);
|
||||
let previous = session.mcp_elicitation_reviewer();
|
||||
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
session.refresh_mcp_if_dirty().await;
|
||||
|
||||
assert!(Arc::ptr_eq(&previous, &session.mcp_elicitation_reviewer()));
|
||||
@@ -10336,7 +10337,7 @@ async fn step_context_keeps_its_mcp_runtime_for_tools() -> anyhow::Result<()> {
|
||||
);
|
||||
assert_ne!(step_context.mcp.config().plugins, current_config.plugins);
|
||||
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
session.refresh_mcp_if_dirty().await;
|
||||
let current = session
|
||||
.services
|
||||
|
||||
@@ -616,7 +616,7 @@ async fn automatic_admission_rechecks_plan_mode_without_committing_sparse_settin
|
||||
.acquire()
|
||||
.await
|
||||
.expect("acquire MCP refresh gate");
|
||||
session.mcp_refresh.claim();
|
||||
session.mcp_refresh.take_pending();
|
||||
assert!(!session.mcp_refresh.is_pending());
|
||||
|
||||
for (submission_id, prepared) in [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Turn-start MCP reconciliation, discovery cancellation, and prewarm refresh boundaries.
|
||||
|
||||
use super::*;
|
||||
use crate::McpRefreshReason;
|
||||
use codex_mcp::McpResourceClient;
|
||||
use codex_mcp::McpResourceClientCacheKey;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -73,10 +74,10 @@ async fn turn_start_refreshes_dirty_mcp_reuses_clean_runtime_and_cancels_discove
|
||||
.requires_mcp
|
||||
.store(turn > 0, std::sync::atomic::Ordering::Relaxed);
|
||||
if turn == 0 {
|
||||
session.mark_mcp_runtime_dirty();
|
||||
session.mark_mcp_runtime_dirty(McpRefreshReason::ExplicitInvalidation);
|
||||
} else if turn == 3 {
|
||||
session.services.mcp_runtime.reconnect_on_next_refresh();
|
||||
session.request_mcp_runtime_refresh();
|
||||
session.request_mcp_runtime_refresh(McpRefreshReason::ExplicitInvalidation);
|
||||
}
|
||||
session
|
||||
.spawn_task(
|
||||
|
||||
@@ -5,6 +5,7 @@ mod regular;
|
||||
mod review;
|
||||
mod user_shell;
|
||||
|
||||
use crate::McpRefreshReason;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
@@ -304,7 +305,7 @@ impl Session {
|
||||
.enabled(Feature::ExecutorCapabilityDiscoveryV2)
|
||||
{
|
||||
// MCP contributors must project the new inventory even if their other inputs match.
|
||||
self.request_mcp_runtime_reprojection();
|
||||
self.request_mcp_runtime_reprojection(McpRefreshReason::TaskStartedExecutorDiscovery);
|
||||
}
|
||||
self.activate_plugin_selection(&turn_context).await;
|
||||
// Inherited or recovered roots are applied before task start. Otherwise this
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::McpRefreshReason;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_async_utils::OrCancelExt;
|
||||
@@ -92,7 +93,9 @@ impl SessionTask for RegularTask {
|
||||
.await;
|
||||
// Even cancelled discovery may have cleared the previous account's catalog.
|
||||
if prepares_mcp {
|
||||
sess.request_mcp_runtime_reprojection();
|
||||
sess.request_mcp_runtime_reprojection(
|
||||
McpRefreshReason::TurnStartContributorPreparedMcp,
|
||||
);
|
||||
}
|
||||
if preparation.is_err() {
|
||||
return SessionStartupPrewarmResolution::Cancelled;
|
||||
|
||||
@@ -263,6 +263,51 @@ enum ShutdownOutcome {
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
type StartupMcpInvalidations = std::sync::Mutex<std::collections::BTreeSet<McpRefreshReason>>;
|
||||
|
||||
/// Identifies the cause of an MCP runtime publication in CLI diagnostics.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum McpRefreshReason {
|
||||
AccountAuthReloaded,
|
||||
AccountLoggedOut,
|
||||
AccountLoginCompleted,
|
||||
AccountPluginCacheChanged,
|
||||
AccountWorkspaceSwitched,
|
||||
AuthChangeNotification,
|
||||
CodexAppsToolRefresh,
|
||||
CurrentEnvironmentsChanged,
|
||||
DisabledPluginSelectionChanged,
|
||||
EffectivePluginsChanged,
|
||||
EnterpriseAuthDisabled,
|
||||
EnvironmentBindingsChanged,
|
||||
ExecutorCapabilityDiscoveryRecovered,
|
||||
ExecutorCapabilityInventoryChanged,
|
||||
ExplicitInvalidation,
|
||||
ExplicitServerReconnect,
|
||||
ExtensionCatalogChanged,
|
||||
HardRefreshCodexAppsTools,
|
||||
MarketplaceUpgraded,
|
||||
McpConfigReloaded,
|
||||
McpOAuthLoginCompleted,
|
||||
NonCuratedPluginCacheRefreshed,
|
||||
OAuthCredentialsRecovered,
|
||||
PluginInstalled,
|
||||
PluginOAuthLoginCompleted,
|
||||
PluginUninstalled,
|
||||
PreviousRefreshUnpublished,
|
||||
RemotePluginUninstalled,
|
||||
RuntimeAuthChanged,
|
||||
SelectedCapabilityRootsChanged,
|
||||
SessionMcpInputsChanged,
|
||||
SkillMcpDependenciesAdded,
|
||||
TaskStartedExecutorDiscovery,
|
||||
TurnStartContributorPreparedMcp,
|
||||
UserConfigFilesReloaded,
|
||||
UserConfigReloaded,
|
||||
/// Links to the field-level diff logged when the installed plugin cache changed.
|
||||
InstalledPluginMetadata(uuid::Uuid),
|
||||
}
|
||||
|
||||
/// [`ThreadManager`] is responsible for creating threads and maintaining
|
||||
/// them in memory.
|
||||
pub struct ThreadManager {
|
||||
@@ -545,7 +590,7 @@ pub(crate) struct ThreadManagerState {
|
||||
model_catalog: model_catalog::ThreadModelCatalog,
|
||||
git_root_discovery: Arc<GitRootDiscovery>,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
starting_mcp_runtimes: std::sync::Mutex<Vec<std::sync::Weak<AtomicBool>>>,
|
||||
starting_mcp_runtimes: std::sync::Mutex<Vec<std::sync::Weak<StartupMcpInvalidations>>>,
|
||||
skills_service: Arc<HostSkillsService>,
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
@@ -990,7 +1035,12 @@ impl ThreadManager {
|
||||
|
||||
/// Refreshes every loaded thread and marks threads that are still being created.
|
||||
pub async fn invalidate_mcp_runtimes(&self) {
|
||||
self.invalidate_starting_mcp_runtimes();
|
||||
self.invalidate_mcp_runtimes_for(McpRefreshReason::ExplicitInvalidation)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn invalidate_mcp_runtimes_for(&self, reason: McpRefreshReason) {
|
||||
self.invalidate_starting_mcp_runtimes(reason);
|
||||
let threads = self
|
||||
.state
|
||||
.threads
|
||||
@@ -1000,7 +1050,7 @@ impl ThreadManager {
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for thread in threads {
|
||||
thread.session.request_mcp_runtime_refresh();
|
||||
thread.session.request_mcp_runtime_refresh(reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,7 +1070,7 @@ impl ThreadManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate_starting_mcp_runtimes(&self) {
|
||||
fn invalidate_starting_mcp_runtimes(&self, reason: McpRefreshReason) {
|
||||
let mut starting = self
|
||||
.state
|
||||
.starting_mcp_runtimes
|
||||
@@ -1030,7 +1080,10 @@ impl ThreadManager {
|
||||
let Some(runtime) = runtime.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
runtime.store(true, Ordering::Release);
|
||||
runtime
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(reason);
|
||||
true
|
||||
});
|
||||
}
|
||||
@@ -2702,14 +2755,14 @@ impl ThreadManagerState {
|
||||
(Arc::clone(&self.extensions), Arc::clone(&self.mcp_manager))
|
||||
};
|
||||
let tracked_session_source = session_source.clone();
|
||||
let source_changed_during_startup = Arc::new(AtomicBool::new(false));
|
||||
let startup_invalidations = Arc::new(StartupMcpInvalidations::default());
|
||||
{
|
||||
let mut starting = self
|
||||
.starting_mcp_runtimes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
starting.retain(|runtime| runtime.strong_count() != 0);
|
||||
starting.push(Arc::downgrade(&source_changed_during_startup));
|
||||
starting.push(Arc::downgrade(&startup_invalidations));
|
||||
}
|
||||
let windows_sandbox_proxy_settings_mode = if matches!(
|
||||
&session_source,
|
||||
@@ -2907,8 +2960,15 @@ impl ThreadManagerState {
|
||||
if matches!(ready_notification, ThreadReadyNotification::Emit) {
|
||||
new_thread.thread.emit_thread_ready_lifecycle().await;
|
||||
}
|
||||
if source_changed_during_startup.load(Ordering::Acquire) {
|
||||
new_thread.thread.session.request_mcp_runtime_refresh();
|
||||
for reason in std::mem::take(
|
||||
&mut *startup_invalidations
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
) {
|
||||
new_thread
|
||||
.thread
|
||||
.session
|
||||
.request_mcp_runtime_refresh(reason);
|
||||
}
|
||||
if is_resumed_thread {
|
||||
new_thread.thread.emit_thread_resume_lifecycle().await;
|
||||
|
||||
Reference in New Issue
Block a user