diff --git a/codex-rs/core/src/responses_metadata.rs b/codex-rs/core/src/responses_metadata.rs index af2326968a..64264c2599 100644 --- a/codex-rs/core/src/responses_metadata.rs +++ b/codex-rs/core/src/responses_metadata.rs @@ -26,6 +26,7 @@ use crate::client::X_OPENAI_SUBAGENT_HEADER; pub(crate) const INSTALLATION_ID_KEY: &str = "installation_id"; pub(crate) const SESSION_ID_KEY: &str = "session_id"; pub(crate) const THREAD_ID_KEY: &str = "thread_id"; +pub(crate) const AGENT_NAME_KEY: &str = "agent_name"; pub(crate) const TURN_ID_KEY: &str = "turn_id"; pub(crate) const WINDOW_ID_KEY: &str = "window_id"; pub(crate) const REQUEST_KIND_KEY: &str = "request_kind"; @@ -55,6 +56,7 @@ const RESERVED_METADATA_KEYS: &[&str] = &[ X_CODEX_INSTALLATION_ID_HEADER, SESSION_ID_KEY, THREAD_ID_KEY, + AGENT_NAME_KEY, TURN_ID_KEY, WINDOW_ID_KEY, X_CODEX_WINDOW_ID_HEADER, @@ -203,6 +205,7 @@ pub struct CodexResponsesMetadata { pub(crate) installation_id: String, pub(crate) session_id: String, pub(crate) thread_id: String, + pub(crate) agent_name: Option, pub(crate) turn_id: Option, pub(crate) routing_hint: Option, pub(crate) window_id: String, @@ -236,6 +239,7 @@ impl CodexResponsesMetadata { installation_id, session_id, thread_id, + agent_name: None, turn_id: None, routing_hint: None, window_id, @@ -354,6 +358,9 @@ impl CodexResponsesMetadata { installation_id: has_request_identity.then_some(self.installation_id.as_str()), session_id: has_turn_identity.then_some(self.session_id.as_str()), thread_id: has_turn_identity.then_some(self.thread_id.as_str()), + agent_name: has_turn_identity + .then_some(self.agent_name.as_deref()) + .flatten(), turn_id: has_turn_identity .then_some(self.turn_id.as_deref()) .flatten(), @@ -474,6 +481,8 @@ struct CodexTurnMetadataPayload<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] thread_id: Option<&'a str>, #[serde(default, skip_serializing_if = "Option::is_none")] + agent_name: Option<&'a str>, + #[serde(default, skip_serializing_if = "Option::is_none")] turn_id: Option<&'a str>, #[serde(default, skip_serializing_if = "Option::is_none")] window_id: Option<&'a str>, diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 5b5d928c75..81d4912327 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -12,6 +12,7 @@ use std::sync::atomic::Ordering; use serde_json::Value; use tokio::task::JoinHandle; +use crate::responses_metadata::AGENT_NAME_KEY; use crate::responses_metadata::CodexResponsesMetadata; use crate::responses_metadata::CodexResponsesRequestKind; use crate::responses_metadata::PARENT_TURN_ID_KEY; @@ -27,6 +28,7 @@ use codex_git_utils::get_git_remote_urls_assume_git_repo; use codex_git_utils::get_git_repo_root; use codex_git_utils::get_has_changes_in_repo; use codex_git_utils::get_head_commit_hash; +use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; @@ -100,6 +102,7 @@ pub(crate) struct TurnMetadataState { repo_root: Option, session_id: String, thread_id: String, + agent_name: String, forked_from_thread_id: Option, parent_thread_id: Option, parent_turn_id: OnceLock, @@ -151,11 +154,16 @@ impl TurnMetadataState { ); let sandbox_mode = Some(permission_profile_policy_tag(permission_profile, cwd.as_path()).to_string()); + let agent_name = session_source + .get_agent_path() + .unwrap_or_else(AgentPath::root) + .to_string(); Self { cwd, repo_root, session_id, thread_id, + agent_name, forked_from_thread_id, parent_thread_id, parent_turn_id: OnceLock::new(), @@ -190,6 +198,7 @@ impl TurnMetadataState { let Value::Object(mut metadata) = responses_metadata.turn_metadata_value()? else { return None; }; + metadata.remove(AGENT_NAME_KEY); metadata.remove(PARENT_TURN_ID_KEY); metadata.remove(ROOT_TURN_ID_KEY); metadata.insert( @@ -343,6 +352,7 @@ impl TurnMetadataState { } CodexResponsesMetadata { turn_id: Some(self.turn_id.clone()), + agent_name: Some(self.agent_name.clone()), forked_from_thread_id: self.forked_from_thread_id, parent_thread_id: self.parent_thread_id, parent_turn_id: self.parent_turn_id.get().cloned(), diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index ef416c1bda..53bc1351fe 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -22,6 +22,7 @@ use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; use codex_analytics::CompactionTrigger; use codex_models_manager::model_info::model_info_from_slug; +use codex_protocol::AgentPath; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::SessionSource; @@ -260,6 +261,7 @@ fn turn_metadata_state_includes_sandbox_metadata() { assert_eq!(auto_review_enabled, Some(true)); assert_eq!(session_id, Some("session-a")); assert_eq!(thread_id, Some("thread-a")); + assert_eq!(json["agent_name"].as_str(), Some("/root")); assert!(json.get("forked_from_thread_id").is_none()); assert!(json.get("parent_thread_id").is_none()); assert!(json.get("subagent_kind").is_none()); @@ -317,7 +319,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() { &SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, - agent_path: None, + agent_path: Some(AgentPath::root().join("worker").expect("agent path")), agent_nickname: None, agent_role: None, }), @@ -340,6 +342,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() { Some("22222222-2222-4222-8222-222222222222") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn")); + assert_eq!(json["agent_name"].as_str(), Some("/root/worker")); } #[test] @@ -384,6 +387,9 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() { Some("33333333-3333-4333-8333-333333333333") ); assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn")); + // V1 subagents have no canonical agent path and are intentionally unsupported by + // agent-name-addressed history and notes; their metadata falls back to the root agent. + assert_eq!(json["agent_name"].as_str(), Some("/root")); } #[test] @@ -424,6 +430,7 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with Some("44444444-4444-4444-8444-444444444444") ); assert_eq!(json["subagent_kind"].as_str(), Some(subagent_kind)); + assert_eq!(json["agent_name"].as_str(), Some("/root")); } } @@ -483,12 +490,14 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta( let header = test_turn_metadata_header(&state); let header_json: Value = serde_json::from_str(&header).expect("json"); + assert_eq!(header_json["agent_name"].as_str(), Some("/root")); assert!(header_json.get("model").is_none()); assert!(header_json.get("reasoning_effort").is_none()); let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); + assert!(meta.get("agent_name").is_none()); assert!(meta.get("request_kind").is_none()); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); @@ -694,6 +703,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( ), ("session_id".to_string(), "client-supplied".to_string()), ("thread_id".to_string(), "client-supplied".to_string()), + ("agent_name".to_string(), "client-supplied".to_string()), ("installation_id".to_string(), "client-supplied".to_string()), ( "x-codex-installation-id".to_string(), @@ -769,6 +779,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( assert_eq!(json["session_id"].as_str(), Some("session-a")); assert_eq!(json["thread_id"].as_str(), Some("thread-a")); assert!(json.get(LEGACY_CODE_MODE_TOOL_NAMES_KEY).is_none()); + assert_eq!(json["agent_name"].as_str(), Some("/root")); assert_eq!( json[TOOL_NAMESPACES_INFO_KEY], serde_json::json!({ diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap index c693e4ca42..921d7c39da 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap @@ -12,7 +12,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s - "thread_id": "", - "turn_id": "", - "x-codex-installation-id": "", -- "x-codex-turn-metadata": "{\"installation_id\":\"\",\"session_id\":\"\",\"thread_id\":\"\",\"turn_id\":\"\",\"window_id\":\":0\",\"request_kind\":\"turn\",\"root_turn_id\":\"\",\"sandbox\":\"\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":}", +- "x-codex-turn-metadata": "{\"installation_id\":\"\",\"session_id\":\"\",\"thread_id\":\"\",\"agent_name\":\"/root\",\"turn_id\":\"\",\"window_id\":\":0\",\"request_kind\":\"turn\",\"root_turn_id\":\"\",\"sandbox\":\"\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":}", - "x-codex-window-id": ":0" - }, - "include": [ diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap index d40c967e2c..78897d13d9 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap @@ -12,7 +12,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses - "thread_id": "", - "turn_id": "", - "x-codex-installation-id": "", -- "x-codex-turn-metadata": "{\"installation_id\":\"\",\"session_id\":\"\",\"thread_id\":\"\",\"turn_id\":\"\",\"window_id\":\":0\",\"request_kind\":\"turn\",\"root_turn_id\":\"\",\"sandbox\":\"\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":}", +- "x-codex-turn-metadata": "{\"installation_id\":\"\",\"session_id\":\"\",\"thread_id\":\"\",\"agent_name\":\"/root\",\"turn_id\":\"\",\"window_id\":\":0\",\"request_kind\":\"turn\",\"root_turn_id\":\"\",\"sandbox\":\"\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":}", - "x-codex-window-id": ":0" - }, - "include": [