From e84f54294aa2f05fffd616e28293070a58383e02 Mon Sep 17 00:00:00 2001 From: nicholasclark-openai Date: Thu, 19 Mar 2026 11:18:05 -0700 Subject: [PATCH] Include session id in MCP turn metadata Co-authored-by: Codex --- codex-rs/core/src/codex.rs | 4 ++++ codex-rs/core/src/codex_tests.rs | 2 ++ codex-rs/core/src/mcp_tool_call_tests.rs | 28 +++++++++++++++--------- codex-rs/core/src/turn_metadata.rs | 8 +++++++ codex-rs/core/src/turn_metadata_tests.rs | 3 +++ codex-rs/core/tests/suite/search_tool.rs | 4 ++++ 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8197c8cb64..931300cf4e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1285,6 +1285,7 @@ impl Session { #[allow(clippy::too_many_arguments)] fn make_turn_context( + conversation_id: ThreadId, auth_manager: Option>, session_telemetry: &SessionTelemetry, provider: ModelProviderInfo, @@ -1335,6 +1336,7 @@ impl Session { let cwd = session_configuration.cwd.clone(); let turn_metadata_state = Arc::new(TurnMetadataState::new( + conversation_id.to_string(), sub_id.clone(), cwd.clone(), session_configuration.sandbox_policy.get(), @@ -2391,6 +2393,7 @@ impl Session { .skills_for_config(&per_turn_config), ); let mut turn_context: TurnContext = Self::make_turn_context( + self.conversation_id, Some(Arc::clone(&self.services.auth_manager)), &self.services.session_telemetry, session_configuration.provider.clone(), @@ -5217,6 +5220,7 @@ async fn spawn_review_thread( let per_turn_config = Arc::new(per_turn_config); let review_turn_id = sub_id.to_string(); let turn_metadata_state = Arc::new(TurnMetadataState::new( + sess.conversation_id.to_string(), review_turn_id.clone(), parent_turn_context.cwd.clone(), parent_turn_context.sandbox_policy.get(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 787ad399b1..c7cbcf2252 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2513,6 +2513,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let skills_outcome = Arc::new(services.skills_manager.skills_for_config(&per_turn_config)); let turn_context = Session::make_turn_context( + conversation_id, Some(Arc::clone(&auth_manager)), &session_telemetry, session_configuration.provider.clone(), @@ -3307,6 +3308,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( let skills_outcome = Arc::new(services.skills_manager.skills_for_config(&per_turn_config)); let turn_context = Arc::new(Session::make_turn_context( + conversation_id, Some(Arc::clone(&auth_manager)), &session_telemetry, session_configuration.provider.clone(), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 99aa0e0c69..5537e680ed 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -441,7 +441,14 @@ fn sanitize_mcp_tool_result_for_model_preserves_image_when_supported() { #[tokio::test] async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() { - let (_session, turn_context) = make_session_and_context().await; + let (_, turn_context) = make_session_and_context().await; + let expected_turn_metadata = serde_json::from_str::( + &turn_context + .turn_metadata_state + .current_header_value() + .expect("turn metadata header"), + ) + .expect("turn metadata json"); let meta = build_mcp_tool_call_request_meta(&turn_context, "custom_server", /*metadata*/ None) @@ -450,17 +457,21 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() { assert_eq!( meta, serde_json::json!({ - crate::X_CODEX_TURN_METADATA_HEADER: { - "turn_id": turn_context.sub_id, - "sandbox": "workspace-write", - }, + crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, }) ); } #[tokio::test] async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps_meta() { - let (_session, turn_context) = make_session_and_context().await; + let (_, turn_context) = make_session_and_context().await; + let expected_turn_metadata = serde_json::from_str::( + &turn_context + .turn_metadata_state + .current_header_value() + .expect("turn metadata header"), + ) + .expect("turn metadata json"); let metadata = McpToolApprovalMetadata { annotations: None, connector_id: Some("calendar".to_string()), @@ -487,10 +498,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps Some(&metadata), ), Some(serde_json::json!({ - crate::X_CODEX_TURN_METADATA_HEADER: { - "turn_id": turn_context.sub_id, - "sandbox": "workspace-write", - }, + crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, MCP_TOOL_CODEX_APPS_META_KEY: { "resource_uri": "connector://calendar/tools/calendar_create_event", "contains_mcp_source": true, diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 2545355825..3a4bac011d 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -53,6 +53,8 @@ impl From for TurnMetadataWorkspace { #[derive(Clone, Debug, Serialize, Default)] pub(crate) struct TurnMetadataBag { + #[serde(default, skip_serializing_if = "Option::is_none")] + session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] turn_id: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] @@ -68,6 +70,7 @@ impl TurnMetadataBag { } fn build_turn_metadata_bag( + session_id: Option, turn_id: Option, sandbox: Option, repo_root: Option, @@ -81,6 +84,7 @@ fn build_turn_metadata_bag( } TurnMetadataBag { + session_id, turn_id, workspaces, sandbox, @@ -104,6 +108,7 @@ pub async fn build_turn_metadata_header(cwd: &Path, sandbox: Option<&str>) -> Op } build_turn_metadata_bag( + /*session_id*/ None, /*turn_id*/ None, sandbox.map(ToString::to_string), repo_root, @@ -128,6 +133,7 @@ pub(crate) struct TurnMetadataState { impl TurnMetadataState { pub(crate) fn new( + session_id: String, turn_id: String, cwd: PathBuf, sandbox_policy: &SandboxPolicy, @@ -136,6 +142,7 @@ impl TurnMetadataState { let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned()); let sandbox = Some(sandbox_tag(sandbox_policy, windows_sandbox_level).to_string()); let base_metadata = build_turn_metadata_bag( + Some(session_id), Some(turn_id), sandbox, /*repo_root*/ None, @@ -194,6 +201,7 @@ impl TurnMetadataState { }; let enriched_metadata = build_turn_metadata_bag( + state.base_metadata.session_id.clone(), state.base_metadata.turn_id.clone(), state.base_metadata.sandbox.clone(), Some(repo_root), diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index 5124213de3..5da26563f6 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -67,6 +67,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { let sandbox_policy = SandboxPolicy::new_read_only_policy(); let state = TurnMetadataState::new( + "session-a".to_string(), "turn-a".to_string(), cwd, &sandbox_policy, @@ -76,7 +77,9 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { let header = state.current_header_value().expect("header"); let json: Value = serde_json::from_str(&header).expect("json"); let sandbox_name = json.get("sandbox").and_then(Value::as_str); + let session_id = json.get("session_id").and_then(Value::as_str); let expected_sandbox = sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled); assert_eq!(sandbox_name, Some(expected_sandbox)); + assert_eq!(session_id, Some("session-a")); } diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 35071287ae..2b8cffa3de 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -446,6 +446,10 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() - "connector_id": "calendar", })) ); + assert_eq!( + apps_tool_call.pointer("/params/_meta/x-codex-turn-metadata/session_id"), + Some(&json!(test.session_configured.session_id.to_string())) + ); assert!( apps_tool_call .pointer("/params/_meta/x-codex-turn-metadata/turn_id")