diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 6b626c6741..aada73793f 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -592,7 +592,6 @@ impl MessageProcessor { turn_cost_worker.as_ref().map(TurnCostWorker::handle), ); if let Some(startup_config) = plugin_startup_tasks { - // Keep plugin startup warmups aligned at app-server startup. let reload_config = match startup_config { PluginStartupConfig::Current => { plugin_config_reload::for_cwd(config_manager.clone(), config.cwd.clone()) @@ -607,7 +606,6 @@ impl MessageProcessor { .plugins_manager() .maybe_start_plugin_startup_tasks_for_config( &config.plugins_config_input(), - config.features.enabled(Feature::PluginDiscoveryPrefetch), reload_config, Some(on_effective_plugins_changed), startup_warnings_tx, diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index 46841fe415..4c46c69e25 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -322,7 +322,6 @@ impl AccountRequestProcessor { .maybe_start_remote_plugin_caches_refresh( &plugins_config, auth, - /*prefetch_recommendations*/ true, Some(on_effective_plugins_changed), ); } diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index f87e8e5762..141b8cc5fb 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -736,16 +736,27 @@ impl PluginRequestProcessor { if include_shared_with_me && config.features.enabled(Feature::PluginSharing) { remote_sources.push(RemoteMarketplaceSource::SharedWithMe); } + let featured = + plugins_manager.featured_plugin_ids_for_config(&context.global_config, auth.as_ref()); + tokio::pin!(featured); + let mut featured_result = None; if !remote_sources.is_empty() { - match codex_core_plugins::remote::fetch_remote_marketplaces( + let marketplaces = codex_core_plugins::remote::fetch_remote_marketplaces( &remote_plugin_service_config, auth.as_ref(), &remote_sources, /*catalog_cache_root*/ Some(config.codex_home.as_path()), remote_catalog_cache_mode, - ) - .await - { + ); + tokio::pin!(marketplaces); + let outcome = tokio::select! { + outcome = &mut marketplaces => outcome, + ids = &mut featured, if use_remote_global_catalog => { + featured_result = Some(ids); + marketplaces.await + } + }; + match outcome { Ok(outcome) => { remote_catalog_cache_refresh_scopes = outcome.catalog_cache_refresh_scopes; for remote_marketplace in outcome @@ -804,10 +815,11 @@ impl PluginRequestProcessor { marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME || marketplace.name == REMOTE_GLOBAL_MARKETPLACE_NAME }) { - match plugins_manager - .featured_plugin_ids_for_config(&context.global_config, auth.as_ref()) - .await - { + let result = match featured_result { + Some(result) => result, + None => featured.await, + }; + match result { Ok(featured_plugin_ids) => featured_plugin_ids, Err(err) => { warn!( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 956cb5d652..e93746deee 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -1961,8 +1961,7 @@ enabled = true } #[tokio::test] -async fn app_server_startup_downloads_installed_plugin_bundles_without_discovery_prefetch() --> Result<()> { +async fn app_server_startup_downloads_installed_plugin_bundles_without_discovery() -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; write_remote_plugin_catalog_config( @@ -2017,9 +2016,8 @@ async fn app_server_startup_downloads_installed_plugin_bundles_without_discovery let installed_path = codex_home .path() .join("plugins/cache/openai-curated-remote/linear/1.2.3"); - let _mcp = TestAppServer::builder() + let mut mcp = TestAppServer::builder() .with_codex_home(codex_home.path()) - .with_args(&["-c", "features.plugin_discovery_prefetch=false"]) .with_plugin_startup_tasks() .with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))]) .build_initialized_with_timeout(DEFAULT_TIMEOUT) @@ -2039,6 +2037,8 @@ async fn app_server_startup_downloads_installed_plugin_bundles_without_discovery assert!(installed_path.join("skills/plan-work/SKILL.md").is_file()); let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; assert!(!config.contains("linear@openai-curated-remote")); + timeout(DEFAULT_TIMEOUT, mcp.shutdown_gracefully()).await??; + server.verify().await; Ok(()) } @@ -2151,7 +2151,7 @@ enum ProjectPluginConfiguration { Invalid, } -#[test_case(ProjectPluginConfiguration::None, None, "plugin_discovery_prefetch = false", true, true; "without discovery prefetch")] +#[test_case(ProjectPluginConfiguration::None, None, "", true, true; "default configuration")] #[test_case(ProjectPluginConfiguration::Disabled, None, "", true, true; "project disables local plugins")] #[test_case(ProjectPluginConfiguration::Invalid, None, "", true, true; "invalid project preserves remote plugins")] #[test_case(ProjectPluginConfiguration::None, Some("tpp"), "", true, true; "configured product sku")] @@ -2652,265 +2652,6 @@ async fn plugin_list_honors_global_remote_catalog_cache_ttl() -> Result<()> { Ok(()) } -#[tokio::test] -async fn app_server_startup_refreshes_cached_remote_catalog_without_blocking_plugin_list() --> Result<()> { - let codex_home = TempDir::new()?; - let server = MockServer::start().await; - write_remote_plugin_catalog_config( - codex_home.path(), - &format!("{}/backend-api/", server.uri()), - )?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - - let cached_remote_plugin_id = "plugins~Plugin_00000000000000000000000000000000"; - let refreshed_remote_plugin_id = "plugins~Plugin_11111111111111111111111111111111"; - mount_remote_plugin_list( - &server, - "GLOBAL", - &remote_plugin_list_body(cached_remote_plugin_id, "linear", "Linear", "Plan work"), - ) - .await; - mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await; - mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) - .await; - mount_empty_user_installed_plugins(&server).await; - - let mut app_server = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build() - .await?; - timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??; - let request_id = app_server - .send_plugin_list_request(PluginListParams { - cwds: None, - marketplace_kinds: None, - force_refetch: false, - }) - .await?; - let _: PluginListResponse = to_response( - timeout( - DEFAULT_TIMEOUT, - app_server.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??, - )?; - wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[cached_remote_plugin_id]) - .await?; - timeout(DEFAULT_TIMEOUT, app_server.shutdown_gracefully()).await??; - - server.reset().await; - let refreshed_body = remote_plugin_list_body( - refreshed_remote_plugin_id, - "notion", - "Notion", - "Capture notes", - ); - Mock::given(method("GET")) - .and(path("/backend-api/ps/plugins/list")) - .and(query_param("scope", "GLOBAL")) - .and(query_param("limit", "200")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(refreshed_body) - .set_delay(Duration::from_secs(/*secs*/ 2)), - ) - .mount(&server) - .await; - mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await; - mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) - .await; - mount_empty_user_installed_plugins(&server).await; - - let mut app_server = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .with_plugin_startup_tasks() - .build() - .await?; - timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??; - wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?; - - let request_id = app_server - .send_plugin_list_request(PluginListParams { - cwds: None, - marketplace_kinds: None, - force_refetch: false, - }) - .await?; - let response: PluginListResponse = to_response( - timeout( - DEFAULT_TIMEOUT, - app_server.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??, - )?; - let remote_marketplace = response - .marketplaces - .iter() - .find(|marketplace| marketplace.name == "openai-curated-remote") - .expect("expected cached remote marketplace"); - assert_eq!( - remote_marketplace.plugins[0].id, - "linear@openai-curated-remote" - ); - - wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[refreshed_remote_plugin_id]) - .await?; - let request_id = app_server - .send_plugin_list_request(PluginListParams { - cwds: None, - marketplace_kinds: None, - force_refetch: false, - }) - .await?; - let response: PluginListResponse = to_response( - timeout( - DEFAULT_TIMEOUT, - app_server.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??, - )?; - let remote_marketplace = response - .marketplaces - .iter() - .find(|marketplace| marketplace.name == "openai-curated-remote") - .expect("expected refreshed remote marketplace"); - assert_eq!( - remote_marketplace.plugins[0].id, - "notion@openai-curated-remote" - ); - sleep(Duration::from_millis(100)).await; - wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?; - - Ok(()) -} - -#[tokio::test] -async fn app_server_startup_skips_disabled_remote_plugin_catalog_scopes() -> Result<()> { - let codex_home = TempDir::new()?; - let server = MockServer::start().await; - let base_url = format!("{}/backend-api/", server.uri()); - write_remote_plugin_catalog_config(codex_home.path(), &base_url)?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - - let global_plugin_id = "plugins~Plugin_00000000000000000000000000000000"; - let user_plugin_id = "plugins~Plugin_11111111111111111111111111111111"; - let workspace_plugin_id = "plugins~Plugin_22222222222222222222222222222222"; - let global_body = remote_plugin_list_body(global_plugin_id, "global-linear", "Linear", "Plan"); - let user_body = user_remote_plugin_page_body( - user_plugin_id, - "private-linear", - "Private Linear", - "PRIVATE", - /*enabled*/ None, - ); - let workspace_body = workspace_remote_plugin_page_body( - workspace_plugin_id, - "workspace-linear", - "Workspace Linear", - "LISTED", - /*enabled*/ None, - ); - mount_remote_plugin_list(&server, "GLOBAL", &global_body).await; - mount_remote_plugin_list(&server, "USER", &user_body).await; - mount_remote_plugin_list(&server, "WORKSPACE", &workspace_body).await; - for scope in ["GLOBAL", "USER", "WORKSPACE"] { - mount_remote_installed_plugins(&server, scope, empty_remote_installed_plugins_body()).await; - } - - let mut app_server = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build() - .await?; - timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??; - for marketplace_kinds in [ - None, - Some(vec![ - PluginListMarketplaceKind::CreatedByMeRemote, - PluginListMarketplaceKind::WorkspaceDirectory, - ]), - ] { - let request_id = app_server - .send_plugin_list_request(PluginListParams { - cwds: None, - marketplace_kinds, - force_refetch: false, - }) - .await?; - let _: PluginListResponse = to_response( - timeout( - DEFAULT_TIMEOUT, - app_server.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??, - )?; - } - wait_for_cached_remote_catalog_plugin_ids( - codex_home.path(), - &[global_plugin_id, user_plugin_id, workspace_plugin_id], - ) - .await?; - timeout(DEFAULT_TIMEOUT, app_server.shutdown_gracefully()).await??; - - write_remote_plugins_disabled_config_with_base_url(codex_home.path(), &base_url)?; - server.reset().await; - mount_remote_plugin_list(&server, "GLOBAL", &global_body).await; - mount_remote_plugin_list(&server, "USER", &user_body).await; - mount_remote_plugin_list(&server, "WORKSPACE", &workspace_body).await; - for scope in ["GLOBAL", "USER", "WORKSPACE"] { - mount_remote_installed_plugins(&server, scope, empty_remote_installed_plugins_body()).await; - } - - let mut app_server = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .with_plugin_startup_tasks() - .build() - .await?; - timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??; - wait_for_remote_plugin_list_scope_request_count( - &server, - "WORKSPACE", - /*expected_count*/ 1, - ) - .await?; - - let requested_scopes = server - .received_requests() - .await - .expect("wiremock should record requests") - .into_iter() - .filter(|request| { - request.method == "GET" && request.url.path().ends_with("/ps/plugins/list") - }) - .filter_map(|request| { - request - .url - .query_pairs() - .find(|(name, _)| name == "scope") - .map(|(_, scope)| scope.into_owned()) - }) - .collect::>(); - assert_eq!(requested_scopes, vec!["WORKSPACE".to_string()]); - - Ok(()) -} - #[tokio::test] async fn plugin_list_force_refetch_bypasses_fresh_global_remote_catalog_cache() -> Result<()> { let codex_home = TempDir::new()?; @@ -4992,7 +4733,7 @@ async fn plugin_list_omits_featured_plugin_ids_without_chatgpt_auth() -> Result< } #[tokio::test] -async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Result<()> { +async fn plugin_list_fetches_and_caches_featured_plugin_ids_on_demand() -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?; @@ -5012,33 +4753,28 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> .with_plugin_startup_tasks() .build_initialized_with_timeout(DEFAULT_TIMEOUT) .await?; - wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?; + for _ in 0..2 { + let request_id = mcp + .send_plugin_list_request(PluginListParams { + cwds: None, + marketplace_kinds: None, + force_refetch: false, + }) + .await?; - let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - marketplace_kinds: None, - force_refetch: false, - }) - .await?; + let response: PluginListResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; - let response: PluginListResponse = - timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; - - assert_eq!( - response.featured_plugin_ids, - vec!["linear@openai-curated".to_string()] - ); + assert_eq!( + response.featured_plugin_ids, + vec!["linear@openai-curated".to_string()] + ); + } + timeout(DEFAULT_TIMEOUT, mcp.shutdown_gracefully()).await??; + server.verify().await; Ok(()) } -async fn wait_for_featured_plugin_request_count( - server: &MockServer, - expected_count: usize, -) -> Result<()> { - wait_for_remote_plugin_request_count(server, "/plugins/featured", expected_count).await -} - async fn wait_for_remote_plugin_request_count( server: &MockServer, path_suffix: &str, diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 782be99858..a0845699fc 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -379,7 +379,6 @@ struct RemoteCatalogCacheRefreshRequest { service_config: RemotePluginServiceConfig, auth: Option, scopes: BTreeSet, - mode: RemoteCatalogCacheRefreshMode, } impl RemoteCatalogCacheRefreshRequest { @@ -394,12 +393,6 @@ impl RemoteCatalogCacheRefreshRequest { } } -#[derive(Clone, Copy, PartialEq, Eq)] -enum RemoteCatalogCacheRefreshMode { - OnlyIfStale, - Force, -} - #[derive(Default)] struct RemoteCatalogCacheRefreshState { requests: VecDeque, @@ -1666,7 +1659,6 @@ impl PluginsManager { self: &Arc, config: &PluginsConfigInput, auth: Option, - prefetch_recommendations: bool, on_effective_plugins_changed: Option, ) { self.maybe_start_remote_installed_plugins_cache_refresh_with_notify( @@ -1677,9 +1669,6 @@ impl PluginsManager { EffectivePluginsChange::default(), ); - if !prefetch_recommendations { - return; - } let manager = Arc::clone(self); let config = config.clone(); tokio::spawn(async move { @@ -1924,7 +1913,6 @@ impl PluginsManager { config: &PluginsConfigInput, auth: Option, scopes: BTreeSet, - mode: RemoteCatalogCacheRefreshMode, ) { if !config.plugins_enabled || scopes.is_empty() { return; @@ -1934,7 +1922,6 @@ impl PluginsManager { service_config: self.remote_plugin_service_config(config), auth, scopes, - mode, }); } @@ -1950,12 +1937,10 @@ impl PluginsManager { &context.global_config, auth.clone(), options.remote_catalog_cache_refresh_scopes, - RemoteCatalogCacheRefreshMode::OnlyIfStale, ); self.maybe_start_remote_plugin_caches_refresh( &context.global_config, auth.clone(), - /*prefetch_recommendations*/ true, on_effective_plugins_changed.clone(), ); self.maybe_start_remote_installed_plugin_bundle_sync( @@ -2957,7 +2942,6 @@ impl PluginsManager { pub fn maybe_start_plugin_startup_tasks_for_config( self: &Arc, config: &PluginsConfigInput, - prefetch_discovery: bool, reload_config: ConfigLayerReload, on_effective_plugins_changed: Option, upgrade_warnings: mpsc::UnboundedSender, @@ -3044,53 +3028,20 @@ impl PluginsManager { } let config_for_remote_sync = config.clone(); let manager = Arc::clone(self); - let on_effective_plugins_changed = on_effective_plugins_changed.clone(); tokio::spawn(async move { let auth = manager.auth_manager.auth().await; - manager.maybe_start_remote_plugin_caches_refresh( + manager.maybe_start_remote_installed_plugins_cache_refresh_with_notify( &config_for_remote_sync, auth.clone(), - prefetch_discovery, + RemoteInstalledPluginsCacheRefreshNotify::IfCacheChanged, on_effective_plugins_changed.clone(), + EffectivePluginsChange::default(), ); manager.maybe_start_remote_installed_plugin_bundle_sync( &config_for_remote_sync, - auth.clone(), + auth, on_effective_plugins_changed, ); - if !prefetch_discovery { - return; - } - let scopes_to_probe: &[RemotePluginScope] = - if config_for_remote_sync.remote_plugin_enabled { - &[RemotePluginScope::User, RemotePluginScope::Workspace] - } else { - &[RemotePluginScope::Workspace] - }; - let mut scopes = crate::remote::cached_remote_plugin_catalog_scopes( - manager.codex_home.as_path(), - &manager.remote_plugin_service_config(&config_for_remote_sync), - auth.as_ref(), - scopes_to_probe, - ); - if config_for_remote_sync.remote_plugin_enabled { - scopes.insert(RemotePluginScope::Global); - } - manager.maybe_start_remote_catalog_cache_refresh( - &config_for_remote_sync, - auth.clone(), - scopes, - RemoteCatalogCacheRefreshMode::Force, - ); - if let Err(err) = manager - .featured_plugin_ids_for_config(&config_for_remote_sync, auth.as_ref()) - .await - { - warn!( - error = %err, - "failed to warm featured plugin ids cache" - ); - } }); } } @@ -3326,16 +3277,6 @@ impl PluginsManager { { pending.scopes.extend(request.scopes); pending.auth = request.auth; - pending.mode = match (pending.mode, request.mode) { - (RemoteCatalogCacheRefreshMode::Force, _) - | (_, RemoteCatalogCacheRefreshMode::Force) => { - RemoteCatalogCacheRefreshMode::Force - } - ( - RemoteCatalogCacheRefreshMode::OnlyIfStale, - RemoteCatalogCacheRefreshMode::OnlyIfStale, - ) => RemoteCatalogCacheRefreshMode::OnlyIfStale, - }; } else { state.requests.push_back(request); } @@ -3596,14 +3537,12 @@ impl PluginsManager { }; for scope in request.scopes { - if request.mode == RemoteCatalogCacheRefreshMode::OnlyIfStale - && crate::remote::has_fresh_cached_remote_plugin_catalog( - self.codex_home.as_path(), - &request.service_config, - request.auth.as_ref(), - scope, - ) - { + if crate::remote::has_fresh_cached_remote_plugin_catalog( + self.codex_home.as_path(), + &request.service_config, + request.auth.as_ref(), + scope, + ) { continue; } diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index e75b552ca1..c4189fdebc 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -6152,7 +6152,6 @@ plugins = true manager.maybe_start_remote_plugin_caches_refresh( &config, Some(auth.clone()), - /*prefetch_recommendations*/ true, /*on_effective_plugins_changed*/ None, ); diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 6d06b3b503..957d0d6eff 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -1272,30 +1272,6 @@ pub(crate) fn has_fresh_cached_remote_plugin_catalog( ) } -pub(crate) fn cached_remote_plugin_catalog_scopes( - codex_home: &Path, - config: &RemotePluginServiceConfig, - auth: Option<&CodexAuth>, - scopes: &[RemotePluginScope], -) -> BTreeSet { - let Ok(auth) = ensure_chatgpt_auth(auth) else { - return BTreeSet::new(); - }; - scopes - .iter() - .copied() - .filter(|scope| { - catalog_cache::has_cached_directory_plugins( - codex_home, - config, - auth, - *scope, - RemotePluginCatalogCacheMode::PreferCache, - ) - }) - .collect() -} - pub fn cached_global_remote_discoverable_plugins( codex_home: &Path, config: &RemotePluginServiceConfig, diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index ebf2769002..f53e5eb3f5 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -480,10 +480,10 @@ fn catalog_cache_invalidation_clears_global_collections_and_preserves_other_scop ); } - assert_eq!( - cached_remote_plugin_catalog_scopes(codex_home.path(), &config, Some(&auth), &scopes), - BTreeSet::from(scopes) - ); + let has_cached_scope = |scope| { + has_fresh_cached_remote_plugin_catalog(codex_home.path(), &config, Some(&auth), scope) + }; + assert!(scopes.into_iter().all(has_cached_scope)); catalog_cache::write_cached_directory_plugins( codex_home.path(), &config, @@ -500,10 +500,8 @@ fn catalog_cache_invalidation_clears_global_collections_and_preserves_other_scop &[RemotePluginScope::Global], ); - assert_eq!( - cached_remote_plugin_catalog_scopes(codex_home.path(), &config, Some(&auth), &scopes), - BTreeSet::from([RemotePluginScope::Workspace]), - ); + assert!(!has_cached_scope(RemotePluginScope::Global)); + assert!(has_cached_scope(RemotePluginScope::Workspace)); assert!( catalog_cache::load_cached_directory_plugins::( codex_home.path(), diff --git a/codex-rs/core/src/config/managed_features.rs b/codex-rs/core/src/config/managed_features.rs index 1e3fe109db..fba265828e 100644 --- a/codex-rs/core/src/config/managed_features.rs +++ b/codex-rs/core/src/config/managed_features.rs @@ -268,7 +268,7 @@ fn parse_feature_requirements( ) -> BTreeMap { let mut pinned_features = BTreeMap::new(); for (key, enabled) in feature_requirements.entries { - if key == Feature::Personality.key() { + if key == Feature::Personality.key() || key == Feature::PluginDiscoveryPrefetch.key() { continue; } if key == Feature::Worktrees.key() || key == Feature::RealtimeConversation.key() { diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 1b1c0cbf3d..56d7a531dd 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -378,8 +378,7 @@ pub enum Feature { ComputerUse, /// Enable the PS-backed remote plugin catalog. RemotePlugin, - /// Prefetch the plugin directory, featured plugins, and recommendations at app-server startup. - /// Inventory-only hosts can disable this. Legacy suggestions read the cached directory. + /// Removed compatibility flag for plugin discovery prefetch. PluginDiscoveryPrefetch, /// Enable remote plugin sharing flows. PluginSharing, @@ -770,7 +769,7 @@ impl Features { "image_detail_original" | "resize_all_images" | "item_ids" => { continue; } - "personality" | "plugin_hooks" => { + "personality" | "plugin_discovery_prefetch" | "plugin_hooks" => { continue; } "skill_env_var_dependency_prompt" => { @@ -1952,8 +1951,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::PluginDiscoveryPrefetch, key: "plugin_discovery_prefetch", - stage: Stage::Stable, - default_enabled: true, + stage: Stage::Removed, + default_enabled: false, }, FeatureSpec { id: Feature::PluginSharing,