From c14b7fab754bee45cd2475b003b342aa55d65df3 Mon Sep 17 00:00:00 2001 From: John Gregg Date: Thu, 28 May 2026 16:35:10 -0700 Subject: [PATCH] Track used Codex app connectors --- codex-rs/core/src/mcp_tool_call.rs | 25 +++++- codex-rs/core/src/mcp_tool_call_tests.rs | 42 +++++++++- codex-rs/core/src/session/mod.rs | 32 ++++++++ codex-rs/core/src/session/session.rs | 15 +++- codex-rs/core/src/state/session.rs | 32 ++++++++ codex-rs/core/src/state/session_tests.rs | 20 +++++ .../0036_threads_used_connector_ids.sql | 1 + codex-rs/state/src/extract.rs | 1 + codex-rs/state/src/model/thread_metadata.rs | 14 ++++ codex-rs/state/src/runtime/memories.rs | 6 +- codex-rs/state/src/runtime/test_support.rs | 1 + codex-rs/state/src/runtime/threads.rs | 80 +++++++++++++++++-- .../src/local/update_thread_metadata.rs | 9 +++ codex-rs/thread-store/src/types.rs | 6 ++ 14 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 codex-rs/state/migrations/0036_threads_used_connector_ids.sql diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index e514fa0d03..fffe3d0c9b 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -339,8 +339,14 @@ async fn handle_approved_mcp_tool_call( }; let result = async { let rewritten_arguments = rewrite?; - let request_meta = - build_mcp_tool_call_request_meta(turn_context, &server, call_id, metadata); + let used_connector_ids = sess.get_used_connector_ids().await; + let request_meta = build_mcp_tool_call_request_meta( + turn_context, + &server, + call_id, + metadata, + &used_connector_ids, + ); let result = execute_mcp_tool_call( sess, turn_context, @@ -931,6 +937,9 @@ async fn maybe_track_codex_app_used( let (connector_id, app_name) = metadata .map(|metadata| (metadata.connector_id, metadata.app_name)) .unwrap_or((None, None)); + if let Some(connector_id) = connector_id.as_deref() { + sess.record_used_connector_id(connector_id).await; + } let invocation_type = if let Some(connector_id) = connector_id.as_deref() { let mentioned_connector_ids = sess.get_connector_selection().await; if mentioned_connector_ids.contains(connector_id) { @@ -983,6 +992,7 @@ const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate"; const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri"; const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id"; const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId"; +const MCP_TOOL_USED_CONNECTOR_IDS_META_KEY: &str = "used_connector_ids"; async fn custom_mcp_tool_approval_mode( sess: &Session, @@ -1038,6 +1048,7 @@ fn build_mcp_tool_call_request_meta( server: &str, call_id: &str, metadata: Option<&McpToolApprovalMetadata>, + used_connector_ids: &[String], ) -> Option { let mut request_meta = serde_json::Map::new(); @@ -1062,6 +1073,16 @@ fn build_mcp_tool_call_request_meta( "call_id".to_string(), serde_json::Value::String(call_id.to_string()), ); + codex_apps_meta.insert( + MCP_TOOL_USED_CONNECTOR_IDS_META_KEY.to_string(), + serde_json::Value::Array( + used_connector_ids + .iter() + .cloned() + .map(serde_json::Value::String) + .collect(), + ), + ); request_meta.insert( MCP_TOOL_CODEX_APPS_META_KEY.to_string(), serde_json::Value::Object(codex_apps_meta), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 534c2f0118..a43147e5ce 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1034,6 +1034,7 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() { "custom_server", "call-custom", /*metadata*/ None, + &[], ) .expect("custom servers should receive turn metadata"); let turn_metadata = meta @@ -1076,6 +1077,7 @@ async fn mcp_tool_call_request_meta_includes_turn_started_at_unix_ms() { "custom_server", "call-custom", /*metadata*/ None, + &[], ) .expect("custom servers should receive turn metadata"); let turn_metadata = meta @@ -1105,7 +1107,13 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() { metadata.plugin_id = Some("sample@test".to_string()); assert_eq!( - build_mcp_tool_call_request_meta(&turn_context, "sample", "call-plugin", Some(&metadata),), + build_mcp_tool_call_request_meta( + &turn_context, + "sample", + "call-plugin", + Some(&metadata), + &[], + ), Some(serde_json::json!({ crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, MCP_TOOL_PLUGIN_ID_META_KEY: "sample@test", @@ -1182,6 +1190,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps CODEX_APPS_MCP_SERVER_NAME, "call_abc123xyz789", Some(&metadata), + &[], ), Some(serde_json::json!({ crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, @@ -1190,6 +1199,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps "resource_uri": "connector://calendar/tools/calendar_create_event", "contains_mcp_source": true, "connector_id": "calendar", + "used_connector_ids": [], }, })) ); @@ -1209,16 +1219,46 @@ async fn codex_apps_tool_call_request_meta_includes_call_id_without_existing_cod CODEX_APPS_MCP_SERVER_NAME, "call_abc123xyz789", /*metadata*/ None, + &[], ), Some(serde_json::json!({ crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, MCP_TOOL_CODEX_APPS_META_KEY: { "call_id": "call_abc123xyz789", + "used_connector_ids": [], }, })) ); } +#[tokio::test] +async fn codex_apps_tool_call_request_meta_includes_used_connector_ids() { + let (_, turn_context) = make_session_and_context().await; + let used_connector_ids = vec![ + "connector_calendar".to_string(), + "connector_drive".to_string(), + ]; + + let meta = build_mcp_tool_call_request_meta( + &turn_context, + CODEX_APPS_MCP_SERVER_NAME, + "call_abc123xyz789", + /*metadata*/ None, + &used_connector_ids, + ) + .expect("codex apps tool call metadata"); + + assert_eq!( + meta.get(MCP_TOOL_CODEX_APPS_META_KEY) + .and_then(serde_json::Value::as_object) + .and_then(|meta| meta.get(MCP_TOOL_USED_CONNECTOR_IDS_META_KEY)), + Some(&serde_json::json!([ + "connector_calendar", + "connector_drive" + ])) + ); +} + fn codex_apps_auth_failure_result() -> CallToolResult { CallToolResult { content: vec![serde_json::json!({ diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8c535cd728..4c37937b90 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -138,6 +138,7 @@ use codex_thread_store::LocalThreadStore; use codex_thread_store::ReadThreadParams; use codex_thread_store::ResumeThreadParams; use codex_thread_store::ThreadEventPersistenceMode; +use codex_thread_store::ThreadMetadataPatch; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; use codex_utils_output_truncation::TruncationPolicy; @@ -1167,6 +1168,37 @@ impl Session { state.clear_connector_selection(); } + pub(crate) async fn record_used_connector_id(&self, connector_id: &str) { + let used_connector_ids = { + let mut state = self.state.lock().await; + state.record_used_connector_id(connector_id) + }; + let Some(used_connector_ids) = used_connector_ids else { + return; + }; + + let Some(live_thread) = self.live_thread() else { + return; + }; + if let Err(err) = live_thread + .update_metadata( + ThreadMetadataPatch { + used_connector_ids: Some(used_connector_ids), + ..Default::default() + }, + /*include_archived*/ true, + ) + .await + { + warn!("failed to persist used connector metadata: {err}"); + } + } + + pub(crate) async fn get_used_connector_ids(&self) -> Vec { + let state = self.state.lock().await; + state.get_used_connector_ids() + } + async fn record_initial_history(&self, conversation_history: InitialHistory) { let turn_context = self.new_default_turn().await; let is_subagent = { diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index c9e1807fe6..45c5d8b63e 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -875,7 +875,20 @@ impl Session { session_configuration.thread_name = thread_name.clone(); validate_config_lock_if_configured(&session_configuration).await?; export_config_lock_if_configured(&session_configuration, thread_id).await?; - let state = SessionState::new(session_configuration.clone()); + let initial_used_connector_ids = if let Some(state_db) = state_db_ctx.as_ref() { + match state_db.get_thread(thread_id).await { + Ok(Some(metadata)) => metadata.used_connector_ids, + Ok(None) => Vec::new(), + Err(err) => { + warn!("failed to load used connector metadata for {thread_id}: {err}"); + Vec::new() + } + } + } else { + Vec::new() + }; + let mut state = SessionState::new(session_configuration.clone()); + state.set_used_connector_ids(initial_used_connector_ids); let managed_network_requirements_configured = config .config_layer_stack .requirements_toml() diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index ac59cb32d5..789c393329 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -36,6 +36,7 @@ pub(crate) struct SessionState { /// Startup prewarmed session prepared during session initialization. pub(crate) startup_prewarm: Option, pub(crate) active_connector_selection: HashSet, + used_connector_ids: Vec, pub(crate) pending_session_start_sources: VecDeque, granted_permissions: Option, next_turn_is_first: bool, @@ -56,6 +57,7 @@ impl SessionState { auto_compact_window: AutoCompactWindow::new(), startup_prewarm: None, active_connector_selection: HashSet::new(), + used_connector_ids: Vec::new(), pending_session_start_sources: VecDeque::new(), granted_permissions: None, next_turn_is_first: true, @@ -222,6 +224,36 @@ impl SessionState { self.active_connector_selection.clear(); } + pub(crate) fn set_used_connector_ids(&mut self, used_connector_ids: Vec) { + self.used_connector_ids = Vec::new(); + for connector_id in used_connector_ids { + if !self + .used_connector_ids + .iter() + .any(|used_connector_id| used_connector_id == &connector_id) + { + self.used_connector_ids.push(connector_id); + } + } + } + + pub(crate) fn record_used_connector_id(&mut self, connector_id: &str) -> Option> { + if self + .used_connector_ids + .iter() + .any(|used_connector_id| used_connector_id == connector_id) + { + return None; + } + + self.used_connector_ids.push(connector_id.to_string()); + Some(self.used_connector_ids.clone()) + } + + pub(crate) fn get_used_connector_ids(&self) -> Vec { + self.used_connector_ids.clone() + } + pub(crate) fn queue_pending_session_start_source( &mut self, value: codex_hooks::SessionStartSource, diff --git a/codex-rs/core/src/state/session_tests.rs b/codex-rs/core/src/state/session_tests.rs index b6b5a057a0..15ea14b8d3 100644 --- a/codex-rs/core/src/state/session_tests.rs +++ b/codex-rs/core/src/state/session_tests.rs @@ -34,6 +34,26 @@ async fn clear_connector_selection_removes_entries() { assert_eq!(state.get_connector_selection(), HashSet::new()); } +#[tokio::test] +async fn record_used_connector_id_preserves_first_use_order() { + let session_configuration = make_session_configuration_for_tests().await; + let mut state = SessionState::new(session_configuration); + + assert_eq!( + state.record_used_connector_id("calendar"), + Some(vec!["calendar".to_string()]) + ); + assert_eq!(state.record_used_connector_id("calendar"), None); + assert_eq!( + state.record_used_connector_id("drive"), + Some(vec!["calendar".to_string(), "drive".to_string()]) + ); + assert_eq!( + state.get_used_connector_ids(), + vec!["calendar".to_string(), "drive".to_string()] + ); +} + #[tokio::test] async fn set_rate_limits_defaults_limit_id_to_codex_when_missing() { let session_configuration = make_session_configuration_for_tests().await; diff --git a/codex-rs/state/migrations/0036_threads_used_connector_ids.sql b/codex-rs/state/migrations/0036_threads_used_connector_ids.sql new file mode 100644 index 0000000000..11d940ccab --- /dev/null +++ b/codex-rs/state/migrations/0036_threads_used_connector_ids.sql @@ -0,0 +1 @@ +ALTER TABLE threads ADD COLUMN used_connector_ids TEXT NOT NULL DEFAULT '[]'; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 5f0734e330..31c8c53f98 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -493,6 +493,7 @@ mod tests { git_sha: None, git_branch: None, git_origin_url: None, + used_connector_ids: Vec::new(), } } diff --git a/codex-rs/state/src/model/thread_metadata.rs b/codex-rs/state/src/model/thread_metadata.rs index f0b8d315e5..ed8497ff8b 100644 --- a/codex-rs/state/src/model/thread_metadata.rs +++ b/codex-rs/state/src/model/thread_metadata.rs @@ -107,6 +107,8 @@ pub struct ThreadMetadata { pub git_branch: Option, /// The git origin URL, if known. pub git_origin_url: Option, + /// Connector IDs used by this thread, in first-use order. + pub used_connector_ids: Vec, } /// Builder data required to construct [`ThreadMetadata`] without parsing filenames. @@ -221,6 +223,7 @@ impl ThreadMetadataBuilder { git_sha: self.git_sha.clone(), git_branch: self.git_branch.clone(), git_origin_url: self.git_origin_url.clone(), + used_connector_ids: Vec::new(), } } } @@ -311,6 +314,9 @@ impl ThreadMetadata { if self.git_origin_url != other.git_origin_url { diffs.push("git_origin_url"); } + if self.used_connector_ids != other.used_connector_ids { + diffs.push("used_connector_ids"); + } diffs } } @@ -345,6 +351,7 @@ pub(crate) struct ThreadRow { git_sha: Option, git_branch: Option, git_origin_url: Option, + used_connector_ids: String, } impl ThreadRow { @@ -374,6 +381,7 @@ impl ThreadRow { git_sha: row.try_get("git_sha")?, git_branch: row.try_get("git_branch")?, git_origin_url: row.try_get("git_origin_url")?, + used_connector_ids: row.try_get("used_connector_ids")?, }) } } @@ -407,11 +415,14 @@ impl TryFrom for ThreadMetadata { git_sha, git_branch, git_origin_url, + used_connector_ids, } = row; let thread_source = thread_source .map(|thread_source| thread_source.parse()) .transpose() .map_err(anyhow::Error::msg)?; + let used_connector_ids = serde_json::from_str::>(&used_connector_ids) + .map_err(|err| anyhow::anyhow!("invalid used_connector_ids JSON: {err}"))?; Ok(Self { id: ThreadId::try_from(id)?, rollout_path: PathBuf::from(rollout_path), @@ -438,6 +449,7 @@ impl TryFrom for ThreadMetadata { git_sha, git_branch, git_origin_url, + used_connector_ids, }) } } @@ -524,6 +536,7 @@ mod tests { git_sha: None, git_branch: None, git_origin_url: None, + used_connector_ids: "[]".to_string(), } } @@ -554,6 +567,7 @@ mod tests { git_sha: None, git_branch: None, git_origin_url: None, + used_connector_ids: Vec::new(), } } diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index 3101500796..24951719eb 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -190,7 +190,8 @@ SELECT threads.archived_at, threads.git_sha, threads.git_branch, - threads.git_origin_url + threads.git_origin_url, + threads.used_connector_ids FROM threads "#, ); @@ -560,7 +561,8 @@ SELECT threads.archived_at, threads.git_sha, threads.git_branch, - threads.git_origin_url + threads.git_origin_url, + threads.used_connector_ids FROM threads WHERE threads.id = ? AND threads.memory_mode = 'enabled' "#, diff --git a/codex-rs/state/src/runtime/test_support.rs b/codex-rs/state/src/runtime/test_support.rs index 848a193335..e023395b98 100644 --- a/codex-rs/state/src/runtime/test_support.rs +++ b/codex-rs/state/src/runtime/test_support.rs @@ -67,5 +67,6 @@ pub(super) fn test_thread_metadata( git_sha: None, git_branch: None, git_origin_url: None, + used_connector_ids: Vec::new(), } } diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index c67056ab54..2166744a17 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -31,7 +31,8 @@ SELECT threads.archived_at, threads.git_sha, threads.git_branch, - threads.git_origin_url + threads.git_origin_url, + threads.used_connector_ids FROM threads WHERE threads.id = ? "#, @@ -506,8 +507,9 @@ INSERT INTO threads ( git_sha, git_branch, git_origin_url, + used_connector_ids, memory_mode -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING "#, ) @@ -547,6 +549,7 @@ ON CONFLICT(id) DO NOTHING .bind(metadata.git_sha.as_deref()) .bind(metadata.git_branch.as_deref()) .bind(metadata.git_origin_url.as_deref()) + .bind(used_connector_ids_json(&metadata.used_connector_ids)?) .bind("enabled") .execute(self.pool.as_ref()) .await?; @@ -568,6 +571,19 @@ ON CONFLICT(id) DO NOTHING Ok(result.rows_affected() > 0) } + pub async fn set_thread_used_connector_ids( + &self, + thread_id: ThreadId, + used_connector_ids: &[String], + ) -> anyhow::Result { + let result = sqlx::query("UPDATE threads SET used_connector_ids = ? WHERE id = ?") + .bind(used_connector_ids_json(used_connector_ids)?) + .bind(thread_id.to_string()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + pub async fn update_thread_title( &self, thread_id: ThreadId, @@ -712,8 +728,9 @@ INSERT INTO threads ( git_sha, git_branch, git_origin_url, + used_connector_ids, memory_mode -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET rollout_path = excluded.rollout_path, created_at = excluded.created_at, @@ -740,7 +757,8 @@ ON CONFLICT(id) DO UPDATE SET archived_at = excluded.archived_at, git_sha = COALESCE(threads.git_sha, excluded.git_sha), git_branch = COALESCE(threads.git_branch, excluded.git_branch), - git_origin_url = COALESCE(threads.git_origin_url, excluded.git_origin_url) + git_origin_url = COALESCE(threads.git_origin_url, excluded.git_origin_url), + used_connector_ids = threads.used_connector_ids "#, ) .bind(metadata.id.to_string()) @@ -779,6 +797,7 @@ ON CONFLICT(id) DO UPDATE SET .bind(metadata.git_sha.as_deref()) .bind(metadata.git_branch.as_deref()) .bind(metadata.git_origin_url.as_deref()) + .bind(used_connector_ids_json(&metadata.used_connector_ids)?) .bind(creation_memory_mode.unwrap_or("enabled")) .execute(self.pool.as_ref()) .await?; @@ -942,11 +961,17 @@ SELECT threads.archived_at, threads.git_sha, threads.git_branch, - threads.git_origin_url + threads.git_origin_url, + threads.used_connector_ids "#, ); } +fn used_connector_ids_json(used_connector_ids: &[String]) -> anyhow::Result { + serde_json::to_string(used_connector_ids) + .map_err(|err| anyhow::anyhow!("failed to serialize used connector ids: {err}")) +} + pub(super) fn extract_memory_mode(items: &[RolloutItem]) -> Option { items.iter().rev().find_map(|item| match item { RolloutItem::SessionMeta(meta_line) => meta_line.meta.memory_mode.clone(), @@ -1144,6 +1169,51 @@ mod tests { assert_eq!(memory_mode, "disabled"); } + #[tokio::test] + async fn used_connector_ids_are_updated_explicitly_and_preserved_by_upsert() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string()) + .await + .expect("state db should initialize"); + let thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id"); + let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone()); + + runtime + .upsert_thread(&metadata) + .await + .expect("initial insert should succeed"); + runtime + .set_thread_used_connector_ids( + thread_id, + &[ + "connector_calendar".to_string(), + "connector_drive".to_string(), + ], + ) + .await + .expect("used connector update should succeed"); + + metadata.title = "updated title".to_string(); + runtime + .upsert_thread(&metadata) + .await + .expect("upsert should succeed"); + + let persisted = runtime + .get_thread(thread_id) + .await + .expect("thread should load") + .expect("thread should exist"); + assert_eq!( + persisted.used_connector_ids, + vec![ + "connector_calendar".to_string(), + "connector_drive".to_string() + ] + ); + } + #[tokio::test] async fn list_threads_updated_after_returns_oldest_changes_first() { let codex_home = unique_temp_dir(); diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index 612328f4c7..6de57c5556 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -295,12 +295,21 @@ async fn apply_metadata_update( metadata.git_branch = branch; metadata.git_origin_url = origin_url; } + if let Some(used_connector_ids) = patch.used_connector_ids { + metadata.used_connector_ids = used_connector_ids; + } state_db .upsert_thread(&metadata) .await .map_err(|err| ThreadStoreError::Internal { message: format!("failed to update thread metadata for {thread_id}: {err}"), })?; + state_db + .set_thread_used_connector_ids(thread_id, &metadata.used_connector_ids) + .await + .map_err(|err| ThreadStoreError::Internal { + message: format!("failed to update used connector ids for {thread_id}: {err}"), + })?; if let Some(memory_mode) = patch.memory_mode { state_db .set_thread_memory_mode(thread_id, memory_mode_as_str(memory_mode)) diff --git a/codex-rs/thread-store/src/types.rs b/codex-rs/thread-store/src/types.rs index 1e26e5dc77..50b5c50a03 100644 --- a/codex-rs/thread-store/src/types.rs +++ b/codex-rs/thread-store/src/types.rs @@ -529,6 +529,8 @@ pub struct ThreadMetadataPatch { pub git_info: Option, /// Thread memory behavior. pub memory_mode: Option, + /// Connector IDs used by this thread, in first-use order. + pub used_connector_ids: Option>, } impl ThreadMetadataPatch { @@ -606,6 +608,9 @@ impl ThreadMetadataPatch { if next.memory_mode.is_some() { self.memory_mode = next.memory_mode; } + if next.used_connector_ids.is_some() { + self.used_connector_ids = next.used_connector_ids; + } } pub fn is_empty(&self) -> bool { @@ -631,6 +636,7 @@ impl ThreadMetadataPatch { && self.first_user_message.is_none() && self.git_info.is_none() && self.memory_mode.is_none() + && self.used_connector_ids.is_none() } }