From bea2ba028462e1832b6d8cf63b7522da6a58eccc Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 6 May 2026 23:01:18 -0700 Subject: [PATCH 1/9] Avoid fetching connector directory for tool suggestions --- codex-rs/core/src/connectors.rs | 65 ++++----------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 0bd53a50eed0..bd6dfbcb32e7 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -6,14 +6,11 @@ use std::sync::Mutex as StdMutex; use std::time::Duration; use std::time::Instant; -use anyhow::Context; use async_channel::unbounded; -use codex_api::SharedAuthProvider; pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; use codex_connectors::AllConnectorsCacheKey; -use codex_connectors::DirectoryListResponse; use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; @@ -21,7 +18,6 @@ use codex_protocol::models::PermissionProfile; use codex_tools::DiscoverableTool; use rmcp::model::ToolAnnotations; use serde::Deserialize; -use serde::de::DeserializeOwned; use tracing::warn; use crate::config::Config; @@ -36,7 +32,6 @@ use codex_core_plugins::PluginsManager; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_login::default_client::create_client; use codex_login::default_client::originator; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpConnectionManager; @@ -49,7 +44,6 @@ use codex_mcp::host_owned_codex_apps_enabled; use codex_mcp::with_codex_apps_mcp; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); -const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct AppToolPolicy { @@ -121,7 +115,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( accessible_connectors: &[AppInfo], ) -> anyhow::Result> { let directory_connectors = - list_directory_connectors_for_tool_suggest_with_auth(config, auth).await?; + cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await; let connector_ids = tool_suggest_connector_ids(config).await; let discoverable_connectors = codex_connectors::filter::filter_tool_suggest_discoverable_connectors( @@ -435,12 +429,12 @@ async fn tool_suggest_connector_ids(config: &Config) -> HashSet { connector_ids } -async fn list_directory_connectors_for_tool_suggest_with_auth( +async fn cached_directory_connectors_for_tool_suggest_with_auth( config: &Config, auth: Option<&CodexAuth>, -) -> anyhow::Result> { +) -> Vec { if !config.features.enabled(Feature::Apps) { - return Ok(Vec::new()); + return Vec::new(); } let loaded_auth; @@ -453,14 +447,13 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( loaded_auth.as_ref() }; let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else { - return Ok(Vec::new()); + return Vec::new(); }; let account_id = match auth.get_account_id() { Some(account_id) if !account_id.is_empty() => account_id, - _ => return Ok(Vec::new()), + _ => return Vec::new(), }; - let auth_provider = codex_model_provider::auth_provider_from_auth(auth); let is_workspace_account = auth.is_workspace_account(); let cache_key = AllConnectorsCacheKey::new( config.chatgpt_base_url.clone(), @@ -469,51 +462,7 @@ async fn list_directory_connectors_for_tool_suggest_with_auth( is_workspace_account, ); - codex_connectors::list_all_connectors_with_options( - cache_key, - is_workspace_account, - /*force_refetch*/ false, - |path| { - let auth_provider = auth_provider.clone(); - async move { - chatgpt_get_request_with_auth_provider::( - config, - path, - auth_provider, - ) - .await - } - }, - ) - .await -} - -async fn chatgpt_get_request_with_auth_provider( - config: &Config, - path: String, - auth_provider: SharedAuthProvider, -) -> anyhow::Result { - let client = create_client(); - let url = format!("{}{}", config.chatgpt_base_url, path); - let response = client - .get(&url) - .headers(auth_provider.to_auth_headers()) - .header("Content-Type", "application/json") - .timeout(DIRECTORY_CONNECTORS_TIMEOUT) - .send() - .await - .context("failed to send request")?; - - if response.status().is_success() { - response - .json() - .await - .context("failed to parse JSON response") - } else { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!("request failed with status {status}: {body}"); - } + codex_connectors::cached_all_connectors(&cache_key).unwrap_or_default() } pub(crate) fn accessible_connectors_from_mcp_tools( From 972038892748ad11735df33f8cb10b634f7dbd7a Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 6 May 2026 23:35:04 -0700 Subject: [PATCH 2/9] update --- codex-rs/core/src/connectors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index bd6dfbcb32e7..d0d02ffc7931 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -457,7 +457,7 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth( let is_workspace_account = auth.is_workspace_account(); let cache_key = AllConnectorsCacheKey::new( config.chatgpt_base_url.clone(), - Some(account_id.clone()), + Some(account_id), auth.get_chatgpt_user_id(), is_workspace_account, ); From 86745f4b15d60bddd2033bedbe7b4e83dea2bebd Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 6 May 2026 23:47:39 -0700 Subject: [PATCH 3/9] update --- codex-rs/core/src/connectors.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index d0d02ffc7931..d71cf92e5b51 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -114,8 +114,37 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( auth: Option<&CodexAuth>, accessible_connectors: &[AppInfo], ) -> anyhow::Result> { - let directory_connectors = + let mut directory_connectors = cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await; + let cached_connector_ids = directory_connectors + .iter() + .map(|connector| connector.id.clone()) + .collect::>(); + // Keep explicitly configured connector suggestions available on cold start + // without fetching the full connector directory. + directory_connectors.extend( + config + .tool_suggest + .discoverables + .iter() + .filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Connector) + .filter(|discoverable| !cached_connector_ids.contains(discoverable.id.as_str())) + .map(|discoverable| AppInfo { + id: discoverable.id.clone(), + name: discoverable.id.clone(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + install_url: None, + branding: None, + app_metadata: None, + labels: None, + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + }), + ); let connector_ids = tool_suggest_connector_ids(config).await; let discoverable_connectors = codex_connectors::filter::filter_tool_suggest_discoverable_connectors( From f56d10c8c55fe77e5704f04e73867d54649504b4 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 7 May 2026 21:28:52 -0700 Subject: [PATCH 4/9] update --- codex-rs/Cargo.lock | 3 + codex-rs/chatgpt/src/connectors.rs | 29 ++-- codex-rs/connectors/Cargo.toml | 3 + codex-rs/connectors/src/directory_cache.rs | 133 +++++++++++++++ codex-rs/connectors/src/lib.rs | 186 ++++++++++++++++++--- codex-rs/core/src/connectors.rs | 18 +- codex-rs/core/src/connectors_tests.rs | 49 ++++++ 7 files changed, 377 insertions(+), 44 deletions(-) create mode 100644 codex-rs/connectors/src/directory_cache.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f78f27ffa347..52236035fc62 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2415,6 +2415,9 @@ dependencies = [ "codex-app-server-protocol", "pretty_assertions", "serde", + "serde_json", + "sha1", + "tempfile", "tokio", "urlencoding", ] diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index cbeb4fd1b797..2e54192e8558 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -4,7 +4,8 @@ use std::time::Duration; use crate::chatgpt_client::chatgpt_get_request_with_timeout; use codex_app_server_protocol::AppInfo; -use codex_connectors::AllConnectorsCacheKey; +use codex_connectors::ConnectorDirectoryCacheContext; +use codex_connectors::ConnectorDirectoryCacheKey; use codex_connectors::DirectoryListResponse; use codex_connectors::filter::filter_disallowed_connectors; use codex_connectors::merge::merge_connectors; @@ -75,8 +76,8 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option> } let auth = connector_auth(config).await.ok()?; - let cache_key = all_connectors_cache_key(config, &auth); - let connectors = codex_connectors::cached_all_connectors(&cache_key)?; + let cache_context = connector_directory_cache_context(config, &auth); + let connectors = codex_connectors::cached_directory_connectors(&cache_context)?; let connectors = merge_plugin_connectors( connectors, plugin_apps_for_config(config) @@ -98,9 +99,9 @@ pub async fn list_all_connectors_with_options( return Ok(Vec::new()); } let auth = connector_auth(config).await?; - let cache_key = all_connectors_cache_key(config, &auth); + let cache_context = connector_directory_cache_context(config, &auth); let connectors = codex_connectors::list_all_connectors_with_options( - cache_key, + cache_context, auth.is_workspace_account(), force_refetch, |path| async move { @@ -126,12 +127,18 @@ pub async fn list_all_connectors_with_options( )) } -fn all_connectors_cache_key(config: &Config, auth: &CodexAuth) -> AllConnectorsCacheKey { - AllConnectorsCacheKey::new( - config.chatgpt_base_url.clone(), - auth.get_account_id(), - auth.get_chatgpt_user_id(), - auth.is_workspace_account(), +fn connector_directory_cache_context( + config: &Config, + auth: &CodexAuth, +) -> ConnectorDirectoryCacheContext { + ConnectorDirectoryCacheContext::new( + config.codex_home.to_path_buf(), + ConnectorDirectoryCacheKey::new( + config.chatgpt_base_url.clone(), + auth.get_account_id(), + auth.get_chatgpt_user_id(), + auth.is_workspace_account(), + ), ) } diff --git a/codex-rs/connectors/Cargo.toml b/codex-rs/connectors/Cargo.toml index 9cd2428a711a..c4f1e42e547d 100644 --- a/codex-rs/connectors/Cargo.toml +++ b/codex-rs/connectors/Cargo.toml @@ -11,8 +11,11 @@ workspace = true anyhow = { workspace = true } codex-app-server-protocol = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha1 = { workspace = true } urlencoding = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/codex-rs/connectors/src/directory_cache.rs b/codex-rs/connectors/src/directory_cache.rs new file mode 100644 index 000000000000..2e8e4eda8900 --- /dev/null +++ b/codex-rs/connectors/src/directory_cache.rs @@ -0,0 +1,133 @@ +use std::path::PathBuf; +use std::time::Duration; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use codex_app_server_protocol::AppInfo; +use serde::Deserialize; +use serde::Serialize; +use sha1::Digest; +use sha1::Sha1; + +use crate::ConnectorDirectoryCacheKey; + +pub(crate) const CONNECTOR_DIRECTORY_DISK_CACHE_SCHEMA_VERSION: u8 = 1; +const CONNECTOR_DIRECTORY_DISK_CACHE_DIR: &str = "cache/codex-app-directory"; + +#[derive(Clone)] +pub struct ConnectorDirectoryCacheContext { + pub(crate) codex_home: PathBuf, + pub(crate) cache_key: ConnectorDirectoryCacheKey, +} + +impl ConnectorDirectoryCacheContext { + pub fn new(codex_home: PathBuf, cache_key: ConnectorDirectoryCacheKey) -> Self { + Self { + codex_home, + cache_key, + } + } + + pub(crate) fn cache_path(&self) -> PathBuf { + let cache_key_json = serde_json::to_string(&self.cache_key).unwrap_or_default(); + let cache_key_hash = sha1_hex(&cache_key_json); + self.codex_home + .join(CONNECTOR_DIRECTORY_DISK_CACHE_DIR) + .join(format!("{cache_key_hash}.json")) + } +} + +pub(crate) enum CachedConnectorDirectoryDiskLoad { + Hit { + connectors: Vec, + ttl_remaining: Duration, + }, + Missing, + Invalid, +} + +pub(crate) fn load_cached_directory_connectors_from_disk( + cache_context: &ConnectorDirectoryCacheContext, +) -> CachedConnectorDirectoryDiskLoad { + let cache_path = cache_context.cache_path(); + let bytes = match std::fs::read(&cache_path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return CachedConnectorDirectoryDiskLoad::Missing; + } + Err(_) => return CachedConnectorDirectoryDiskLoad::Invalid, + }; + let cache: ConnectorDirectoryDiskCache = match serde_json::from_slice(&bytes) { + Ok(cache) => cache, + Err(_) => { + let _ = std::fs::remove_file(cache_path); + return CachedConnectorDirectoryDiskLoad::Invalid; + } + }; + if cache.schema_version != CONNECTOR_DIRECTORY_DISK_CACHE_SCHEMA_VERSION { + let _ = std::fs::remove_file(cache_path); + return CachedConnectorDirectoryDiskLoad::Invalid; + } + + let now_unix_ms = unix_timestamp_millis(); + let Some(ttl_remaining_ms) = cache.expires_at_unix_ms.checked_sub(now_unix_ms) else { + let _ = std::fs::remove_file(cache_path); + return CachedConnectorDirectoryDiskLoad::Invalid; + }; + if ttl_remaining_ms == 0 { + let _ = std::fs::remove_file(cache_path); + return CachedConnectorDirectoryDiskLoad::Invalid; + } + + CachedConnectorDirectoryDiskLoad::Hit { + connectors: cache.connectors, + ttl_remaining: Duration::from_millis(ttl_remaining_ms), + } +} + +pub(crate) fn write_cached_directory_connectors_to_disk( + cache_context: &ConnectorDirectoryCacheContext, + connectors: &[AppInfo], + ttl: Duration, +) { + let cache_path = cache_context.cache_path(); + if let Some(parent) = cache_path.parent() + && std::fs::create_dir_all(parent).is_err() + { + return; + } + let Some(expires_at_unix_ms) = + unix_timestamp_millis().checked_add(ttl.as_millis().try_into().unwrap_or(u64::MAX)) + else { + return; + }; + let Ok(bytes) = serde_json::to_vec_pretty(&ConnectorDirectoryDiskCache { + schema_version: CONNECTOR_DIRECTORY_DISK_CACHE_SCHEMA_VERSION, + expires_at_unix_ms, + connectors: connectors.to_vec(), + }) else { + return; + }; + let _ = std::fs::write(cache_path, bytes); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ConnectorDirectoryDiskCache { + schema_version: u8, + expires_at_unix_ms: u64, + connectors: Vec, +} + +fn unix_timestamp_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis().try_into().unwrap_or(u64::MAX)) + .unwrap_or_default() +} + +fn sha1_hex(value: &str) -> String { + let mut hasher = Sha1::new(); + hasher.update(value.as_bytes()); + let sha1 = hasher.finalize(); + format!("{sha1:x}") +} diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index e6260d0e1d93..edf50ea1daea 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -9,23 +9,27 @@ use codex_app_server_protocol::AppBranding; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppMetadata; use serde::Deserialize; +use serde::Serialize; pub mod accessible; +mod directory_cache; pub mod filter; pub mod merge; pub mod metadata; +pub use directory_cache::ConnectorDirectoryCacheContext; + pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600); -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AllConnectorsCacheKey { +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConnectorDirectoryCacheKey { chatgpt_base_url: String, account_id: Option, chatgpt_user_id: Option, is_workspace_account: bool, } -impl AllConnectorsCacheKey { +impl ConnectorDirectoryCacheKey { pub fn new( chatgpt_base_url: String, account_id: Option, @@ -42,13 +46,13 @@ impl AllConnectorsCacheKey { } #[derive(Clone)] -struct CachedAllConnectors { - key: AllConnectorsCacheKey, +struct CachedConnectorDirectory { + key: ConnectorDirectoryCacheKey, expires_at: Instant, connectors: Vec, } -static ALL_CONNECTORS_CACHE: LazyLock>> = +static CONNECTOR_DIRECTORY_CACHE: LazyLock>> = LazyLock::new(|| StdMutex::new(None)); #[derive(Debug, Deserialize)] @@ -76,8 +80,33 @@ pub struct DirectoryApp { visibility: Option, } -pub fn cached_all_connectors(cache_key: &AllConnectorsCacheKey) -> Option> { - let mut cache_guard = ALL_CONNECTORS_CACHE +pub fn cached_directory_connectors( + cache_context: &ConnectorDirectoryCacheContext, +) -> Option> { + if let Some(cached_connectors) = cached_directory_connectors_in_memory(&cache_context.cache_key) + { + return Some(cached_connectors); + } + + let directory_cache::CachedConnectorDirectoryDiskLoad::Hit { + connectors, + ttl_remaining, + } = directory_cache::load_cached_directory_connectors_from_disk(cache_context) + else { + return None; + }; + write_cached_directory_connectors_in_memory( + cache_context.cache_key.clone(), + &connectors, + ttl_remaining, + ); + Some(connectors) +} + +fn cached_directory_connectors_in_memory( + cache_key: &ConnectorDirectoryCacheKey, +) -> Option> { + let mut cache_guard = CONNECTOR_DIRECTORY_CACHE .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let now = Instant::now(); @@ -95,7 +124,7 @@ pub fn cached_all_connectors(cache_key: &AllConnectorsCacheKey) -> Option( - cache_key: AllConnectorsCacheKey, + cache_context: ConnectorDirectoryCacheContext, is_workspace_account: bool, force_refetch: bool, mut fetch_page: F, @@ -104,7 +133,7 @@ where F: FnMut(String) -> Fut, Fut: Future>, { - if !force_refetch && let Some(cached_connectors) = cached_all_connectors(&cache_key) { + if !force_refetch && let Some(cached_connectors) = cached_directory_connectors(&cache_context) { return Ok(cached_connectors); } @@ -132,17 +161,37 @@ where .cmp(&right.name) .then_with(|| left.id.cmp(&right.id)) }); - write_cached_all_connectors(cache_key, &connectors); + write_cached_directory_connectors(&cache_context, &connectors); Ok(connectors) } -fn write_cached_all_connectors(cache_key: AllConnectorsCacheKey, connectors: &[AppInfo]) { - let mut cache_guard = ALL_CONNECTORS_CACHE +fn write_cached_directory_connectors( + cache_context: &ConnectorDirectoryCacheContext, + connectors: &[AppInfo], +) { + write_cached_directory_connectors_in_memory( + cache_context.cache_key.clone(), + connectors, + CONNECTORS_CACHE_TTL, + ); + directory_cache::write_cached_directory_connectors_to_disk( + cache_context, + connectors, + CONNECTORS_CACHE_TTL, + ); +} + +fn write_cached_directory_connectors_in_memory( + cache_key: ConnectorDirectoryCacheKey, + connectors: &[AppInfo], + ttl: Duration, +) { + let mut cache_guard = CONNECTOR_DIRECTORY_CACHE .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - *cache_guard = Some(CachedAllConnectors { + *cache_guard = Some(CachedConnectorDirectory { key: cache_key, - expires_at: Instant::now() + CONNECTORS_CACHE_TTL, + expires_at: Instant::now() + ttl, connectors: connectors.to_vec(), }); } @@ -417,12 +466,13 @@ mod tests { use std::sync::Mutex; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; + use tempfile::TempDir; - static ALL_CONNECTORS_CACHE_TEST_LOCK: LazyLock> = + static CONNECTOR_DIRECTORY_CACHE_TEST_LOCK: LazyLock> = LazyLock::new(|| tokio::sync::Mutex::new(())); - fn cache_key(id: &str) -> AllConnectorsCacheKey { - AllConnectorsCacheKey::new( + fn cache_key(id: &str) -> ConnectorDirectoryCacheKey { + ConnectorDirectoryCacheKey::new( "https://chatgpt.example".to_string(), Some(format!("account-{id}")), Some(format!("user-{id}")), @@ -430,6 +480,17 @@ mod tests { ) } + fn cache_context(codex_home: &TempDir, id: &str) -> ConnectorDirectoryCacheContext { + ConnectorDirectoryCacheContext::new(codex_home.path().to_path_buf(), cache_key(id)) + } + + fn clear_directory_memory_cache() { + let mut cache_guard = CONNECTOR_DIRECTORY_CACHE + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *cache_guard = None; + } + fn app(id: &str, name: &str) -> DirectoryApp { DirectoryApp { id: id.to_string(), @@ -450,15 +511,16 @@ mod tests { clippy::await_holding_invalid_type, reason = "test serializes access to the shared connector cache for its full duration" )] - async fn list_all_connectors_uses_shared_cache() -> anyhow::Result<()> { - let _cache_guard = ALL_CONNECTORS_CACHE_TEST_LOCK.lock().await; + async fn list_all_connectors_uses_shared_directory_cache() -> anyhow::Result<()> { + let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; let calls = Arc::new(AtomicUsize::new(0)); let call_counter = Arc::clone(&calls); - let key = cache_key("shared"); + let codex_home = TempDir::new()?; + let cache_context = cache_context(&codex_home, "shared"); let first = list_all_connectors_with_options( - key.clone(), + cache_context.clone(), /*is_workspace_account*/ false, /*force_refetch*/ false, move |_path| { @@ -475,7 +537,7 @@ mod tests { .await?; let second = list_all_connectors_with_options( - key, + cache_context, /*is_workspace_account*/ false, /*force_refetch*/ false, move |_path| async move { @@ -495,14 +557,15 @@ mod tests { reason = "test serializes access to the shared connector cache for its full duration" )] async fn list_all_connectors_merges_and_normalizes_directory_apps() -> anyhow::Result<()> { - let _cache_guard = ALL_CONNECTORS_CACHE_TEST_LOCK.lock().await; + let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; - let key = cache_key("merged"); + let codex_home = TempDir::new()?; + let cache_context = cache_context(&codex_home, "merged"); let calls = Arc::new(AtomicUsize::new(0)); let call_counter = Arc::clone(&calls); let connectors = list_all_connectors_with_options( - key, + cache_context, /*is_workspace_account*/ true, /*force_refetch*/ true, move |path| { @@ -566,6 +629,77 @@ mod tests { Ok(()) } + #[tokio::test] + #[expect( + clippy::await_holding_invalid_type, + reason = "test serializes access to the shared connector cache for its full duration" + )] + async fn list_all_connectors_rehydrates_memory_from_directory_disk_cache() -> anyhow::Result<()> + { + let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; + + let codex_home = TempDir::new()?; + let cache_context = cache_context(&codex_home, "disk"); + let calls = Arc::new(AtomicUsize::new(0)); + let call_counter = Arc::clone(&calls); + + let first = list_all_connectors_with_options( + cache_context.clone(), + /*is_workspace_account*/ false, + /*force_refetch*/ false, + move |_path| { + let call_counter = Arc::clone(&call_counter); + async move { + call_counter.fetch_add(1, Ordering::SeqCst); + Ok(DirectoryListResponse { + apps: vec![app("alpha", "Alpha")], + next_token: None, + }) + } + }, + ) + .await?; + + clear_directory_memory_cache(); + + let second = list_all_connectors_with_options( + cache_context, + /*is_workspace_account*/ false, + /*force_refetch*/ false, + move |_path| async move { + anyhow::bail!("disk cache should have been used"); + }, + ) + .await?; + + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(first, second); + Ok(()) + } + + #[tokio::test] + async fn cached_directory_connectors_drops_stale_disk_schema() -> anyhow::Result<()> { + let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; + + clear_directory_memory_cache(); + let codex_home = TempDir::new()?; + let cache_context = cache_context(&codex_home, "stale-schema"); + let cache_path = cache_context.cache_path(); + std::fs::create_dir_all(cache_path.parent().expect("cache parent"))?; + std::fs::write( + &cache_path, + serde_json::to_vec_pretty(&serde_json::json!({ + "schema_version": 0, + "expires_at_unix_ms": u64::MAX, + "connectors": [], + }))?, + )?; + + assert_eq!(cached_directory_connectors(&cache_context), None); + assert!(!cache_path.exists()); + Ok(()) + } + #[tokio::test] async fn list_directory_connectors_omits_tier_for_all_pages() -> anyhow::Result<()> { let requested_paths: Arc>> = Arc::new(Mutex::new(Vec::new())); diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index d71cf92e5b51..af9a2c263810 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -10,7 +10,8 @@ use async_channel::unbounded; pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; -use codex_connectors::AllConnectorsCacheKey; +use codex_connectors::ConnectorDirectoryCacheContext; +use codex_connectors::ConnectorDirectoryCacheKey; use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; @@ -484,14 +485,17 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth( _ => return Vec::new(), }; let is_workspace_account = auth.is_workspace_account(); - let cache_key = AllConnectorsCacheKey::new( - config.chatgpt_base_url.clone(), - Some(account_id), - auth.get_chatgpt_user_id(), - is_workspace_account, + let cache_context = ConnectorDirectoryCacheContext::new( + config.codex_home.to_path_buf(), + ConnectorDirectoryCacheKey::new( + config.chatgpt_base_url.clone(), + Some(account_id), + auth.get_chatgpt_user_id(), + is_workspace_account, + ), ); - codex_connectors::cached_all_connectors(&cache_key).unwrap_or_default() + codex_connectors::cached_directory_connectors(&cache_context).unwrap_or_default() } pub(crate) fn accessible_connectors_from_mcp_tools( diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 513f677e9b92..f0033ded70c7 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -19,6 +19,7 @@ use codex_connectors::metadata::connector_install_url; use codex_connectors::metadata::connector_mention_slug; use codex_connectors::metadata::sanitize_name; use codex_features::Feature; +use codex_login::CodexAuth; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::ToolInfo; use codex_utils_absolute_path::AbsolutePathBuf; @@ -1138,6 +1139,54 @@ disabled_tools = [ ); } +#[tokio::test] +async fn tool_suggest_uses_configured_connector_fallback_when_directory_cache_is_empty() { + let codex_home = tempdir().expect("tempdir should succeed"); + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[features] +apps = true + +[tool_suggest] +discoverables = [ + { type = "connector", id = "connector_gmail" } +] +"#, + ) + .expect("write config"); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config should load"); + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + + let discoverable_tools = + list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[]) + .await + .expect("discoverable tools should load"); + + assert_eq!( + discoverable_tools, + vec![DiscoverableTool::from(AppInfo { + id: "connector_gmail".to_string(), + name: "connector_gmail".to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + })] + ); +} + #[test] fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() { let filtered = filter_tool_suggest_discoverable_connectors( From 31573e1782abfb1755708fe6b1a1e78bed27fc3e Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 7 May 2026 21:46:05 -0700 Subject: [PATCH 5/9] update --- codex-rs/connectors/src/directory_cache.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/connectors/src/directory_cache.rs b/codex-rs/connectors/src/directory_cache.rs index 2e8e4eda8900..3bbcb893ecca 100644 --- a/codex-rs/connectors/src/directory_cache.rs +++ b/codex-rs/connectors/src/directory_cache.rs @@ -12,7 +12,7 @@ use sha1::Sha1; use crate::ConnectorDirectoryCacheKey; pub(crate) const CONNECTOR_DIRECTORY_DISK_CACHE_SCHEMA_VERSION: u8 = 1; -const CONNECTOR_DIRECTORY_DISK_CACHE_DIR: &str = "cache/codex-app-directory"; +const CONNECTOR_DIRECTORY_DISK_CACHE_DIR: &str = "cache/codex_app_directory"; #[derive(Clone)] pub struct ConnectorDirectoryCacheContext { From 57592810b82d78754c7fdf4d6e0ffe2bd7fe8a4d Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 7 May 2026 22:29:50 -0700 Subject: [PATCH 6/9] update --- codex-rs/core/src/connectors.rs | 35 +++------------------------ codex-rs/core/src/connectors_tests.rs | 20 +++------------ 2 files changed, 8 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index af9a2c263810..21309c0ef319 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -115,38 +115,11 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( auth: Option<&CodexAuth>, accessible_connectors: &[AppInfo], ) -> anyhow::Result> { - let mut directory_connectors = - cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await; - let cached_connector_ids = directory_connectors - .iter() - .map(|connector| connector.id.clone()) - .collect::>(); - // Keep explicitly configured connector suggestions available on cold start - // without fetching the full connector directory. - directory_connectors.extend( - config - .tool_suggest - .discoverables - .iter() - .filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Connector) - .filter(|discoverable| !cached_connector_ids.contains(discoverable.id.as_str())) - .map(|discoverable| AppInfo { - id: discoverable.id.clone(), - name: discoverable.id.clone(), - description: None, - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - install_url: None, - branding: None, - app_metadata: None, - labels: None, - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - }), - ); let connector_ids = tool_suggest_connector_ids(config).await; + let directory_connectors = codex_connectors::merge::merge_plugin_connectors( + cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await, + connector_ids.iter().cloned(), + ); let discoverable_connectors = codex_connectors::filter::filter_tool_suggest_discoverable_connectors( directory_connectors, diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index f0033ded70c7..6525cd9a01d9 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -1140,7 +1140,7 @@ disabled_tools = [ } #[tokio::test] -async fn tool_suggest_uses_configured_connector_fallback_when_directory_cache_is_empty() { +async fn tool_suggest_uses_connector_id_fallback_when_directory_cache_is_empty() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -1169,21 +1169,9 @@ discoverables = [ assert_eq!( discoverable_tools, - vec![DiscoverableTool::from(AppInfo { - id: "connector_gmail".to_string(), - name: "connector_gmail".to_string(), - description: None, - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - branding: None, - app_metadata: None, - labels: None, - install_url: None, - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - })] + vec![DiscoverableTool::from(plugin_connector_to_app_info( + "connector_gmail".to_string(), + ))] ); } From 3748aed344832c0023c3eb31d9e1e10bdc933f6f Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 7 May 2026 23:07:30 -0700 Subject: [PATCH 7/9] update --- codex-rs/connectors/src/directory_cache.rs | 34 +----- codex-rs/connectors/src/lib.rs | 117 ++++++++++++++++----- 2 files changed, 89 insertions(+), 62 deletions(-) diff --git a/codex-rs/connectors/src/directory_cache.rs b/codex-rs/connectors/src/directory_cache.rs index 3bbcb893ecca..a68013ecea13 100644 --- a/codex-rs/connectors/src/directory_cache.rs +++ b/codex-rs/connectors/src/directory_cache.rs @@ -1,7 +1,4 @@ use std::path::PathBuf; -use std::time::Duration; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; use codex_app_server_protocol::AppInfo; use serde::Deserialize; @@ -38,10 +35,7 @@ impl ConnectorDirectoryCacheContext { } pub(crate) enum CachedConnectorDirectoryDiskLoad { - Hit { - connectors: Vec, - ttl_remaining: Duration, - }, + Hit { connectors: Vec }, Missing, Invalid, } @@ -69,26 +63,14 @@ pub(crate) fn load_cached_directory_connectors_from_disk( return CachedConnectorDirectoryDiskLoad::Invalid; } - let now_unix_ms = unix_timestamp_millis(); - let Some(ttl_remaining_ms) = cache.expires_at_unix_ms.checked_sub(now_unix_ms) else { - let _ = std::fs::remove_file(cache_path); - return CachedConnectorDirectoryDiskLoad::Invalid; - }; - if ttl_remaining_ms == 0 { - let _ = std::fs::remove_file(cache_path); - return CachedConnectorDirectoryDiskLoad::Invalid; - } - CachedConnectorDirectoryDiskLoad::Hit { connectors: cache.connectors, - ttl_remaining: Duration::from_millis(ttl_remaining_ms), } } pub(crate) fn write_cached_directory_connectors_to_disk( cache_context: &ConnectorDirectoryCacheContext, connectors: &[AppInfo], - ttl: Duration, ) { let cache_path = cache_context.cache_path(); if let Some(parent) = cache_path.parent() @@ -96,14 +78,8 @@ pub(crate) fn write_cached_directory_connectors_to_disk( { return; } - let Some(expires_at_unix_ms) = - unix_timestamp_millis().checked_add(ttl.as_millis().try_into().unwrap_or(u64::MAX)) - else { - return; - }; let Ok(bytes) = serde_json::to_vec_pretty(&ConnectorDirectoryDiskCache { schema_version: CONNECTOR_DIRECTORY_DISK_CACHE_SCHEMA_VERSION, - expires_at_unix_ms, connectors: connectors.to_vec(), }) else { return; @@ -114,17 +90,9 @@ pub(crate) fn write_cached_directory_connectors_to_disk( #[derive(Debug, Clone, Serialize, Deserialize)] struct ConnectorDirectoryDiskCache { schema_version: u8, - expires_at_unix_ms: u64, connectors: Vec, } -fn unix_timestamp_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis().try_into().unwrap_or(u64::MAX)) - .unwrap_or_default() -} - fn sha1_hex(value: &str) -> String { let mut hasher = Sha1::new(); hasher.update(value.as_bytes()); diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index edf50ea1daea..2397d2a68c55 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -88,17 +88,15 @@ pub fn cached_directory_connectors( return Some(cached_connectors); } - let directory_cache::CachedConnectorDirectoryDiskLoad::Hit { - connectors, - ttl_remaining, - } = directory_cache::load_cached_directory_connectors_from_disk(cache_context) + let directory_cache::CachedConnectorDirectoryDiskLoad::Hit { connectors } = + directory_cache::load_cached_directory_connectors_from_disk(cache_context) else { return None; }; write_cached_directory_connectors_in_memory( cache_context.cache_key.clone(), &connectors, - ttl_remaining, + Duration::ZERO, ); Some(connectors) } @@ -106,20 +104,25 @@ pub fn cached_directory_connectors( fn cached_directory_connectors_in_memory( cache_key: &ConnectorDirectoryCacheKey, ) -> Option> { - let mut cache_guard = CONNECTOR_DIRECTORY_CACHE + let cache_guard = CONNECTOR_DIRECTORY_CACHE .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - let now = Instant::now(); + cache_guard + .as_ref() + .filter(|cached| cached.key == *cache_key) + .map(|cached| cached.connectors.clone()) +} - if let Some(cached) = cache_guard.as_ref() { - if now < cached.expires_at && cached.key == *cache_key { - return Some(cached.connectors.clone()); - } - if now >= cached.expires_at { - *cache_guard = None; - } +fn fresh_directory_connectors_in_memory( + cache_key: &ConnectorDirectoryCacheKey, +) -> Option> { + let cache_guard = CONNECTOR_DIRECTORY_CACHE + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let cached = cache_guard.as_ref()?; + if cached.key == *cache_key && Instant::now() < cached.expires_at { + return Some(cached.connectors.clone()); } - None } @@ -133,7 +136,10 @@ where F: FnMut(String) -> Fut, Fut: Future>, { - if !force_refetch && let Some(cached_connectors) = cached_directory_connectors(&cache_context) { + if !force_refetch + && let Some(cached_connectors) = + fresh_directory_connectors_in_memory(&cache_context.cache_key) + { return Ok(cached_connectors); } @@ -174,11 +180,7 @@ fn write_cached_directory_connectors( connectors, CONNECTORS_CACHE_TTL, ); - directory_cache::write_cached_directory_connectors_to_disk( - cache_context, - connectors, - CONNECTORS_CACHE_TTL, - ); + directory_cache::write_cached_directory_connectors_to_disk(cache_context, connectors); } fn write_cached_directory_connectors_in_memory( @@ -634,8 +636,7 @@ mod tests { clippy::await_holding_invalid_type, reason = "test serializes access to the shared connector cache for its full duration" )] - async fn list_all_connectors_rehydrates_memory_from_directory_disk_cache() -> anyhow::Result<()> - { + async fn cached_directory_connectors_reads_directory_disk_cache() -> anyhow::Result<()> { let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; let codex_home = TempDir::new()?; @@ -662,18 +663,77 @@ mod tests { clear_directory_memory_cache(); - let second = list_all_connectors_with_options( + let second = cached_directory_connectors(&cache_context).expect("disk cache should load"); + + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(first, second); + Ok(()) + } + + #[tokio::test] + #[expect( + clippy::await_holding_invalid_type, + reason = "test serializes access to the shared connector cache for its full duration" + )] + async fn list_all_connectors_refreshes_when_only_directory_disk_cache_exists() + -> anyhow::Result<()> { + let _cache_guard = CONNECTOR_DIRECTORY_CACHE_TEST_LOCK.lock().await; + + let codex_home = TempDir::new()?; + let cache_context = cache_context(&codex_home, "disk-refresh"); + let calls = Arc::new(AtomicUsize::new(0)); + let call_counter = Arc::clone(&calls); + + list_all_connectors_with_options( + cache_context.clone(), + /*is_workspace_account*/ false, + /*force_refetch*/ false, + move |_path| { + let call_counter = Arc::clone(&call_counter); + async move { + call_counter.fetch_add(1, Ordering::SeqCst); + Ok(DirectoryListResponse { + apps: vec![app("alpha", "Alpha")], + next_token: None, + }) + } + }, + ) + .await?; + + clear_directory_memory_cache(); + let mut cached_expected = directory_app_to_app_info(app("alpha", "Alpha")); + cached_expected.install_url = Some(connector_install_url( + &cached_expected.name, + &cached_expected.id, + )); + assert_eq!( + cached_directory_connectors(&cache_context), + Some(vec![cached_expected]) + ); + let refreshed_calls = Arc::clone(&calls); + + let refreshed = list_all_connectors_with_options( cache_context, /*is_workspace_account*/ false, /*force_refetch*/ false, - move |_path| async move { - anyhow::bail!("disk cache should have been used"); + move |_path| { + let call_counter = Arc::clone(&refreshed_calls); + async move { + call_counter.fetch_add(1, Ordering::SeqCst); + Ok(DirectoryListResponse { + apps: vec![app("beta", "Beta")], + next_token: None, + }) + } }, ) .await?; - assert_eq!(calls.load(Ordering::SeqCst), 1); - assert_eq!(first, second); + let mut expected = directory_app_to_app_info(app("beta", "Beta")); + expected.install_url = Some(connector_install_url(&expected.name, &expected.id)); + assert_eq!(calls.load(Ordering::SeqCst), 2); + assert_eq!(refreshed, vec![expected]); Ok(()) } @@ -690,7 +750,6 @@ mod tests { &cache_path, serde_json::to_vec_pretty(&serde_json::json!({ "schema_version": 0, - "expires_at_unix_ms": u64::MAX, "connectors": [], }))?, )?; From 1bdbc91a4e487c6ab36a2d713dacb2d7f493b8bf Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Thu, 7 May 2026 23:27:13 -0700 Subject: [PATCH 8/9] update --- codex-rs/connectors/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index 2397d2a68c55..c2bf8911153a 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -113,7 +113,7 @@ fn cached_directory_connectors_in_memory( .map(|cached| cached.connectors.clone()) } -fn fresh_directory_connectors_in_memory( +fn unexpired_directory_connectors_in_memory( cache_key: &ConnectorDirectoryCacheKey, ) -> Option> { let cache_guard = CONNECTOR_DIRECTORY_CACHE @@ -138,7 +138,7 @@ where { if !force_refetch && let Some(cached_connectors) = - fresh_directory_connectors_in_memory(&cache_context.cache_key) + unexpired_directory_connectors_in_memory(&cache_context.cache_key) { return Ok(cached_connectors); } From ec44562fd0556a5de5ea176e86483500315c6bd2 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Fri, 8 May 2026 12:14:30 -0700 Subject: [PATCH 9/9] update --- codex-rs/Cargo.lock | 1 + codex-rs/connectors/Cargo.toml | 1 + codex-rs/connectors/src/directory_cache.rs | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 52236035fc62..bb012fdf1730 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2419,6 +2419,7 @@ dependencies = [ "sha1", "tempfile", "tokio", + "tracing", "urlencoding", ] diff --git a/codex-rs/connectors/Cargo.toml b/codex-rs/connectors/Cargo.toml index c4f1e42e547d..62283ccfca74 100644 --- a/codex-rs/connectors/Cargo.toml +++ b/codex-rs/connectors/Cargo.toml @@ -13,6 +13,7 @@ codex-app-server-protocol = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha1 = { workspace = true } +tracing = { workspace = true } urlencoding = { workspace = true } [dev-dependencies] diff --git a/codex-rs/connectors/src/directory_cache.rs b/codex-rs/connectors/src/directory_cache.rs index a68013ecea13..581193b87c1c 100644 --- a/codex-rs/connectors/src/directory_cache.rs +++ b/codex-rs/connectors/src/directory_cache.rs @@ -5,6 +5,7 @@ use serde::Deserialize; use serde::Serialize; use sha1::Digest; use sha1::Sha1; +use tracing::warn; use crate::ConnectorDirectoryCacheKey; @@ -49,11 +50,21 @@ pub(crate) fn load_cached_directory_connectors_from_disk( Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return CachedConnectorDirectoryDiskLoad::Missing; } - Err(_) => return CachedConnectorDirectoryDiskLoad::Invalid, + Err(err) => { + warn!( + cache_path = %cache_path.display(), + "failed to read connector directory disk cache: {err}" + ); + return CachedConnectorDirectoryDiskLoad::Invalid; + } }; let cache: ConnectorDirectoryDiskCache = match serde_json::from_slice(&bytes) { Ok(cache) => cache, - Err(_) => { + Err(err) => { + warn!( + cache_path = %cache_path.display(), + "failed to parse connector directory disk cache: {err}" + ); let _ = std::fs::remove_file(cache_path); return CachedConnectorDirectoryDiskLoad::Invalid; }