From 8f8de7844f9a95ba0f07b9ae584b5c08b3173dd6 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 22 Jun 2026 09:05:27 -0700 Subject: [PATCH] fix(core): restore thread_source in x-codex-turn-metadata (#29455) ## Description Restore `thread_source` in `x-codex-turn-metadata`. Inadvertently removed `thread_source` from `x-codex-turn-metadata` in https://github.com/openai/codex/pull/27122 - didn't realize it was a top-level thread app-server API field, not passed in `responsesapi_client_metadata`. This also reserves the key so `responsesapi_client_metadata` cannot override it. --- .../tests/suite/v2/client_metadata.rs | 14 ++++++++++---- codex-rs/core/src/responses_metadata.rs | 8 ++++++++ codex-rs/core/src/session/review.rs | 8 ++++++-- codex-rs/core/src/session/turn_context.rs | 1 + codex-rs/core/src/turn_metadata.rs | 5 +++++ codex-rs/core/src/turn_metadata_tests.rs | 19 ++++++++++++++++++- 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/client_metadata.rs b/codex-rs/app-server/tests/suite/v2/client_metadata.rs index e55cd0c24..ffe7a5362 100644 --- a/codex-rs/app-server/tests/suite/v2/client_metadata.rs +++ b/codex-rs/app-server/tests/suite/v2/client_metadata.rs @@ -63,7 +63,10 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp - .send_thread_start_request(ThreadStartParams::default()) + .send_thread_start_request(ThreadStartParams { + thread_source: Some(ThreadSource::Feature("automation".to_string())), + ..Default::default() + }) .await?; let thread_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -75,7 +78,6 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result let client_metadata = HashMap::from([ ("fiber_run_id".to_string(), "fiber-start-123".to_string()), ("origin".to_string(), "gaas".to_string()), - ("thread_source".to_string(), "client-supplied".to_string()), ]); let turn_req = mcp .send_turn_start_request(TurnStartParams { @@ -110,7 +112,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result .expect("x-codex-turn-metadata header should be present"); assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123")); assert_eq!(metadata["origin"].as_str(), Some("gaas")); - assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied")); + assert_eq!(metadata["thread_source"].as_str(), Some("automation")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("installation_id").is_some()); assert!(metadata.get("session_id").is_some()); @@ -561,7 +563,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp - .send_thread_start_request(ThreadStartParams::default()) + .send_thread_start_request(ThreadStartParams { + thread_source: Some(ThreadSource::Feature("automation".to_string())), + ..Default::default() + }) .await?; let thread_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -619,6 +624,7 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body .expect("websocket x-codex-turn-metadata client metadata should be present"); assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123")); assert_eq!(metadata["origin"].as_str(), Some("gaas")); + assert_eq!(metadata["thread_source"].as_str(), Some("automation")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("session_id").is_some()); assert_eq!( diff --git a/codex-rs/core/src/responses_metadata.rs b/codex-rs/core/src/responses_metadata.rs index 6e86ed8eb..1852c4bea 100644 --- a/codex-rs/core/src/responses_metadata.rs +++ b/codex-rs/core/src/responses_metadata.rs @@ -10,6 +10,7 @@ use codex_protocol::ThreadId; use codex_protocol::protocol::InternalSessionSource; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::ThreadSource; use codex_utils_string::to_ascii_json_string; use http::HeaderMap as ApiHeaderMap; use http::HeaderValue; @@ -34,6 +35,7 @@ pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms"; pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id"; pub(crate) const PARENT_THREAD_ID_KEY: &str = "parent_thread_id"; pub(crate) const SUBAGENT_KIND_KEY: &str = "subagent_kind"; +pub(crate) const THREAD_SOURCE_KEY: &str = "thread_source"; pub(crate) const SANDBOX_KEY: &str = "sandbox"; pub(crate) const WORKSPACES_KEY: &str = "workspaces"; @@ -56,6 +58,7 @@ const RESERVED_METADATA_KEYS: &[&str] = &[ FORKED_FROM_THREAD_ID_KEY, PARENT_THREAD_ID_KEY, SUBAGENT_KIND_KEY, + THREAD_SOURCE_KEY, SANDBOX_KEY, WORKSPACES_KEY, ]; @@ -143,6 +146,7 @@ pub struct CodexResponsesMetadata { pub(crate) parent_thread_id: Option, pub(crate) subagent_header: Option, pub(crate) subagent_kind: Option, + pub(crate) thread_source: Option, pub(crate) sandbox: Option, pub(crate) workspaces: BTreeMap, pub(crate) turn_started_at_unix_ms: Option, @@ -167,6 +171,7 @@ impl CodexResponsesMetadata { parent_thread_id: None, subagent_header: None, subagent_kind: None, + thread_source: None, sandbox: None, workspaces: BTreeMap::new(), turn_started_at_unix_ms: None, @@ -268,6 +273,7 @@ impl CodexResponsesMetadata { forked_from_thread_id: self.forked_from_thread_id, parent_thread_id: self.parent_thread_id, subagent_kind: self.subagent_kind.as_deref(), + thread_source: self.thread_source.as_ref(), sandbox: self.sandbox.as_deref(), workspaces: non_empty_workspaces(&self.workspaces), turn_started_at_unix_ms: self.turn_started_at_unix_ms, @@ -354,6 +360,8 @@ struct CodexTurnMetadataPayload<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] subagent_kind: Option<&'a str>, #[serde(default, skip_serializing_if = "Option::is_none")] + thread_source: Option<&'a ThreadSource>, + #[serde(default, skip_serializing_if = "Option::is_none")] sandbox: Option<&'a str>, #[serde(default, skip_serializing_if = "Option::is_none")] workspaces: Option<&'a BTreeMap>, diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 7fccc4d19..8986ffd05 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -75,9 +75,12 @@ pub(super) async fn spawn_review_thread( .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); let session_source = parent_turn_context.session_source.clone(); - let forked_from_thread_id = { + let (forked_from_thread_id, thread_source) = { let state = sess.state.lock().await; - state.session_configuration.forked_from_thread_id + ( + state.session_configuration.forked_from_thread_id, + state.session_configuration.thread_source.clone(), + ) }; let per_turn_config = Arc::new(per_turn_config); @@ -88,6 +91,7 @@ pub(super) async fn spawn_review_thread( forked_from_thread_id, parent_turn_context.parent_thread_id, &session_source, + thread_source, review_turn_id.clone(), #[allow(deprecated)] parent_turn_context.cwd.clone(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 2fb02e7fc..b546355f7 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -536,6 +536,7 @@ impl Session { session_configuration.forked_from_thread_id, session_configuration.parent_thread_id, &session_configuration.session_source, + session_configuration.thread_source.clone(), sub_id.clone(), cwd.clone(), &session_configuration.permission_profile(), diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index d13c08181..0a1f781b3 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -25,6 +25,7 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::ThreadSource; use codex_utils_absolute_path::AbsolutePathBuf; const MODEL_KEY: &str = "model"; @@ -91,6 +92,7 @@ pub(crate) struct TurnMetadataState { parent_thread_id: Option, subagent_header: Option, subagent_kind: Option, + thread_source: Option, turn_id: String, sandbox: Option, enriched_workspaces: Arc>>>, @@ -108,6 +110,7 @@ impl TurnMetadataState { forked_from_thread_id: Option, parent_thread_id: Option, session_source: &SessionSource, + thread_source: Option, turn_id: String, cwd: AbsolutePathBuf, permission_profile: &PermissionProfile, @@ -132,6 +135,7 @@ impl TurnMetadataState { parent_thread_id, subagent_header: subagent_header_value(session_source), subagent_kind: subagent_metadata_kind(session_source), + thread_source, turn_id, sandbox, enriched_workspaces: Arc::new(RwLock::new(None)), @@ -225,6 +229,7 @@ impl TurnMetadataState { parent_thread_id: self.parent_thread_id, subagent_header: self.subagent_header.clone(), subagent_kind: self.subagent_kind.clone(), + thread_source: self.thread_source.clone(), sandbox: self.sandbox.clone(), workspaces: self.current_workspaces(), turn_started_at_unix_ms: self.current_turn_started_at_unix_ms(), diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index 5e6d1f32b..83649b918 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -13,6 +13,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::ThreadSource; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::PathBufExt; use core_test_support::PathExt; @@ -187,6 +188,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -229,6 +231,7 @@ fn turn_metadata_state_includes_root_fork_lineage() { Some(source_thread_id), /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -267,6 +270,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() { agent_nickname: None, agent_role: None, }), + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -305,6 +309,7 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() { agent_nickname: None, agent_role: None, }), + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -349,6 +354,7 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with /*forked_from_thread_id*/ None, Some(parent_thread_id), &SessionSource::SubAgent(subagent_source), + /*thread_source*/ None, "turn-a".to_string(), cwd.clone(), &permission_profile, @@ -380,6 +386,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() { /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -409,6 +416,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta( /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -457,6 +465,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -509,6 +518,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() { /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -562,6 +572,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( agent_nickname: None, agent_role: None, }), + Some(ThreadSource::Feature("automation".to_string())), "turn-a".to_string(), cwd, &permission_profile, @@ -637,7 +648,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( Some("55555555-5555-4555-8555-555555555555") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn")); - assert_eq!(json["thread_source"].as_str(), Some("client-supplied")); + assert_eq!(json["thread_source"].as_str(), Some("automation")); assert_eq!(json["turn_id"].as_str(), Some("turn-a")); assert!(json.get("request_kind").is_none()); assert!(json.get(WINDOW_ID_KEY).is_none()); @@ -650,6 +661,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( let model_request_json: Value = serde_json::from_str(&model_request_header).expect("model request json"); assert_eq!(model_request_json["request_kind"].as_str(), Some("turn")); + assert_eq!( + model_request_json["thread_source"].as_str(), + Some("automation") + ); assert_eq!( model_request_json[INSTALLATION_ID_KEY].as_str(), Some("installation-a") @@ -679,6 +694,7 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() { /*forked_from_thread_id*/ None, /*parent_thread_id*/ None, &SessionSource::Exec, + /*thread_source*/ None, "turn-a".to_string(), cwd, &permission_profile, @@ -741,6 +757,7 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() { agent_nickname: None, agent_role: None, }), + /*thread_source*/ None, "turn-a".to_string(), repo_path, &permission_profile,