Stop prefetching plugin discovery at app-server startup (#59768)

## What changed

- Remove startup prefetching of remote catalogs, featured plugins, and recommendations while retaining installed-plugin refreshes and bundle downloads.
- Fetch featured plugin IDs on demand, concurrently with marketplace fetching when listing the remote global catalog, and reuse the cached results.
- Retire `plugin_discovery_prefetch` as an ignored compatibility flag and remove forced background catalog refreshes.

## Testing

Update app-server tests to verify that startup downloads installed bundles without discovery requests and that two `plugin/list` calls share one featured-plugin fetch.

GitOrigin-RevId: aa10474f3429102d624c83472e81ec85e3b470ae
This commit is contained in:
Alex Zamoshchin
2026-11-06 00:23:38 +00:00
committed by copyberry
parent 9df200a068
commit 08e4f43c9a
10 changed files with 64 additions and 408 deletions
@@ -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,
@@ -322,7 +322,6 @@ impl AccountRequestProcessor {
.maybe_start_remote_plugin_caches_refresh(
&plugins_config,
auth,
/*prefetch_recommendations*/ true,
Some(on_effective_plugins_changed),
);
}
@@ -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!(
+23 -287
View File
@@ -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::<Vec<_>>();
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,
+10 -71
View File
@@ -379,7 +379,6 @@ struct RemoteCatalogCacheRefreshRequest {
service_config: RemotePluginServiceConfig,
auth: Option<CodexAuth>,
scopes: BTreeSet<RemotePluginScope>,
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<RemoteCatalogCacheRefreshRequest>,
@@ -1666,7 +1659,6 @@ impl PluginsManager {
self: &Arc<Self>,
config: &PluginsConfigInput,
auth: Option<CodexAuth>,
prefetch_recommendations: bool,
on_effective_plugins_changed: Option<EffectivePluginsChangedCallback>,
) {
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<CodexAuth>,
scopes: BTreeSet<RemotePluginScope>,
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<Self>,
config: &PluginsConfigInput,
prefetch_discovery: bool,
reload_config: ConfigLayerReload,
on_effective_plugins_changed: Option<EffectivePluginsChangedCallback>,
upgrade_warnings: mpsc::UnboundedSender<String>,
@@ -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;
}
@@ -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,
);
-24
View File
@@ -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<RemotePluginScope> {
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,
+6 -8
View File
@@ -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::<RemotePluginDirectoryItem>(
codex_home.path(),
+1 -1
View File
@@ -268,7 +268,7 @@ fn parse_feature_requirements(
) -> BTreeMap<Feature, bool> {
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() {
+4 -5
View File
@@ -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,