diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 80d33fb92d..77268cefb7 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -8,7 +8,6 @@ use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; use crate::events::CodexTurnEventRequest; use crate::events::CodexTurnSteerEventRequest; -use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; @@ -30,6 +29,7 @@ use crate::facts::PluginUsedInput; use crate::facts::SkillInvocation; use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; @@ -75,6 +75,7 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use pretty_assertions::assert_eq; use serde_json::json; @@ -248,6 +249,9 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { thread_id: "thread-2".to_string(), num_input_images: 1, submission_type: Some(TurnSubmissionType::Default), + ephemeral: false, + session_source: SessionSource::Exec, + initialization_mode: ThreadInitializationMode::New, model: "gpt-5".to_string(), model_provider: "openai".to_string(), sandbox_policy: SandboxPolicy::new_read_only_policy(), @@ -266,8 +270,8 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { CodexAppServerClientMetadata { product_client_id: "codex-tui".to_string(), - client_name: Some("codex-tui".to_string()), - client_version: Some("1.0.0".to_string()), + client_name: "codex-tui".to_string(), + client_version: "1.0.0".to_string(), rpc_transport: AppServerRpcTransport::Stdio, experimental_api_enabled: None, } @@ -549,8 +553,8 @@ fn thread_initialized_event_serializes_expected_shape() { thread_id: "thread-0".to_string(), app_server_client: CodexAppServerClientMetadata { product_client_id: DEFAULT_ORIGINATOR.to_string(), - client_name: Some("codex-tui".to_string()), - client_version: Some("1.0.0".to_string()), + client_name: "codex-tui".to_string(), + client_version: "1.0.0".to_string(), rpc_transport: AppServerRpcTransport::Stdio, experimental_api_enabled: Some(true), }, @@ -1087,8 +1091,14 @@ fn turn_event_serializes_expected_shape() { event_params: crate::events::CodexTurnEventParams { thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), - product_client_id: "codex-tui".to_string(), + app_server_client: sample_app_server_client_metadata(), + runtime: sample_runtime_metadata(), submission_type: Some(TurnSubmissionType::Default), + ephemeral: false, + thread_source: Some("user".to_string()), + initialization_mode: ThreadInitializationMode::New, + subagent_source: None, + parent_thread_id: None, model: Some("gpt-5".to_string()), model_provider: "openai".to_string(), sandbox_policy: Some("read_only"), @@ -1125,51 +1135,97 @@ fn turn_event_serializes_expected_shape() { })); let payload = serde_json::to_value(&event).expect("serialize turn event"); - + assert_eq!(payload["event_type"], json!("codex_turn_event")); + assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( - payload, + payload["event_params"]["app_server_client"], json!({ - "event_type": "codex_turn_event", - "event_params": { - "thread_id": "thread-2", - "turn_id": "turn-2", - "product_client_id": "codex-tui", - "submission_type": "default", - "model": "gpt-5", - "model_provider": "openai", - "sandbox_policy": "read_only", - "reasoning_effort": "high", - "reasoning_summary": "detailed", - "service_tier": "flex", - "approval_policy": "on-request", - "approvals_reviewer": "guardian_subagent", - "sandbox_network_access": true, - "collaboration_mode": "plan", - "personality": "pragmatic", - "num_input_images": 2, - "is_first_turn": true, - "status": "completed", - "turn_error": null, - "steer_count": 0, - "total_tool_call_count": null, - "shell_command_count": null, - "file_change_count": null, - "mcp_tool_call_count": null, - "dynamic_tool_call_count": null, - "subagent_tool_call_count": null, - "web_search_count": null, - "image_generation_count": null, - "input_tokens": null, - "cached_input_tokens": null, - "output_tokens": null, - "reasoning_output_tokens": null, - "total_tokens": null, - "duration_ms": 1234, - "started_at": 455, - "completed_at": 456 - } + "product_client_id": "codex-tui", + "client_name": "codex-tui", + "client_version": "1.0.0", + "rpc_transport": "stdio", + "experimental_api_enabled": null, }) ); + assert_eq!( + payload["event_params"]["runtime"], + json!({ + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64", + }) + ); + assert!(payload["event_params"].get("product_client_id").is_none()); + assert_eq!(payload["event_params"]["submission_type"], json!("default")); + assert_eq!(payload["event_params"]["ephemeral"], json!(false)); + assert_eq!(payload["event_params"]["thread_source"], json!("user")); + assert_eq!(payload["event_params"]["initialization_mode"], json!("new")); + assert_eq!(payload["event_params"]["subagent_source"], json!(null)); + assert_eq!(payload["event_params"]["parent_thread_id"], json!(null)); + assert_eq!(payload["event_params"]["model"], json!("gpt-5")); + assert_eq!(payload["event_params"]["model_provider"], json!("openai")); + assert_eq!( + payload["event_params"]["sandbox_policy"], + json!("read_only") + ); + assert_eq!(payload["event_params"]["reasoning_effort"], json!("high")); + assert_eq!( + payload["event_params"]["reasoning_summary"], + json!("detailed") + ); + assert_eq!(payload["event_params"]["service_tier"], json!("flex")); + assert_eq!( + payload["event_params"]["approval_policy"], + json!("on-request") + ); + assert_eq!( + payload["event_params"]["approvals_reviewer"], + json!("guardian_subagent") + ); + assert_eq!( + payload["event_params"]["sandbox_network_access"], + json!(true) + ); + assert_eq!(payload["event_params"]["collaboration_mode"], json!("plan")); + assert_eq!(payload["event_params"]["personality"], json!("pragmatic")); + assert_eq!(payload["event_params"]["num_input_images"], json!(2)); + assert_eq!(payload["event_params"]["is_first_turn"], json!(true)); + assert_eq!(payload["event_params"]["status"], json!("completed")); + assert_eq!(payload["event_params"]["turn_error"], json!(null)); + assert_eq!(payload["event_params"]["steer_count"], json!(0)); + assert_eq!( + payload["event_params"]["total_tool_call_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["shell_command_count"], json!(null)); + assert_eq!(payload["event_params"]["file_change_count"], json!(null)); + assert_eq!(payload["event_params"]["mcp_tool_call_count"], json!(null)); + assert_eq!( + payload["event_params"]["dynamic_tool_call_count"], + json!(null) + ); + assert_eq!( + payload["event_params"]["subagent_tool_call_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["web_search_count"], json!(null)); + assert_eq!( + payload["event_params"]["image_generation_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["cached_input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["output_tokens"], json!(null)); + assert_eq!( + payload["event_params"]["reasoning_output_tokens"], + json!(null) + ); + assert_eq!(payload["event_params"]["total_tokens"], json!(null)); + assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); + assert_eq!(payload["event_params"]["started_at"], json!(455)); + assert_eq!(payload["event_params"]["completed_at"], json!(456)); } #[test] @@ -1478,9 +1534,30 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( - payload["event_params"]["product_client_id"], - json!("codex-tui") + payload["event_params"]["app_server_client"], + json!({ + "product_client_id": "codex-tui", + "client_name": "codex-tui", + "client_version": "1.0.0", + "rpc_transport": "stdio", + "experimental_api_enabled": null, + }) ); + assert_eq!( + payload["event_params"]["runtime"], + json!({ + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64", + }) + ); + assert!(payload["event_params"].get("product_client_id").is_none()); + assert_eq!(payload["event_params"]["ephemeral"], json!(false)); + assert_eq!(payload["event_params"]["thread_source"], json!("user")); + assert_eq!(payload["event_params"]["initialization_mode"], json!("new")); + assert_eq!(payload["event_params"]["subagent_source"], json!(null)); + assert_eq!(payload["event_params"]["parent_thread_id"], json!(null)); assert_eq!(payload["event_params"]["num_input_images"], json!(1)); assert_eq!(payload["event_params"]["status"], json!("completed")); assert_eq!(payload["event_params"]["steer_count"], json!(0)); @@ -1678,6 +1755,73 @@ async fn queued_submission_type_emits_queued_turn_event() { assert_eq!(payload["event_params"]["submission_type"], json!("queued")); } +#[tokio::test] +async fn turn_event_includes_subagent_thread_metadata() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + ingest_turn_prerequisites( + &mut reducer, + &mut out, + /*include_initialize*/ true, + /*include_resolved_config*/ false, + /*include_started*/ true, + /*include_token_usage*/ false, + ) + .await; + + let mut resolved_config = sample_turn_resolved_config("turn-2"); + resolved_config.ephemeral = true; + let parent_thread_id = + codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111") + .expect("valid thread id"); + resolved_config.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: Some("worker".to_string()), + agent_role: None, + }); + resolved_config.initialization_mode = ThreadInitializationMode::Forked; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new( + resolved_config, + ))), + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_completed_notification( + "thread-2", + "turn-2", + AppServerTurnStatus::Completed, + /*codex_error_info*/ None, + ))), + &mut out, + ) + .await; + + assert_eq!(out.len(), 1); + let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); + assert_eq!(payload["event_params"]["ephemeral"], json!(true)); + assert_eq!(payload["event_params"]["thread_source"], json!("subagent")); + assert_eq!( + payload["event_params"]["initialization_mode"], + json!("forked") + ); + assert_eq!( + payload["event_params"]["subagent_source"], + json!("thread_spawn") + ); + assert_eq!( + payload["event_params"]["parent_thread_id"], + json!("11111111-1111-1111-1111-111111111111") + ); +} + #[tokio::test] async fn turn_does_not_emit_without_required_prerequisites() { let mut reducer = AnalyticsReducer::default(); @@ -1703,12 +1847,7 @@ async fn turn_does_not_emit_without_required_prerequisites() { &mut out, ) .await; - assert_eq!(out.len(), 1); - let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); - assert_eq!( - payload["event_params"]["product_client_id"], - json!(originator().value) - ); + assert!(out.is_empty()); let mut reducer = AnalyticsReducer::default(); let mut out = Vec::new(); diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 23e6cccbe4..e1c854f9ad 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -3,6 +3,7 @@ use crate::facts::CodexTurnSteerEvent; use crate::facts::InvocationType; use crate::facts::PluginState; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnStatus; use crate::facts::TurnSteerRejectionReason; @@ -23,14 +24,6 @@ pub enum AppServerRpcTransport { InProcess, } -#[derive(Clone, Copy, Debug, Serialize)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ThreadInitializationMode { - New, - Forked, - Resumed, -} - #[derive(Serialize)] pub(crate) struct TrackEventsRequest { pub(crate) events: Vec, @@ -73,8 +66,8 @@ pub(crate) struct SkillInvocationEventParams { #[derive(Clone, Serialize)] pub(crate) struct CodexAppServerClientMetadata { pub(crate) product_client_id: String, - pub(crate) client_name: Option, - pub(crate) client_version: Option, + pub(crate) client_name: String, + pub(crate) client_version: String, pub(crate) rpc_transport: AppServerRpcTransport, pub(crate) experimental_api_enabled: Option, } @@ -134,8 +127,14 @@ pub(crate) struct CodexAppUsedEventRequest { pub(crate) struct CodexTurnEventParams { pub(crate) thread_id: String, pub(crate) turn_id: String, - pub(crate) product_client_id: String, + pub(crate) app_server_client: CodexAppServerClientMetadata, + pub(crate) runtime: CodexRuntimeMetadata, pub(crate) submission_type: Option, + pub(crate) ephemeral: bool, + pub(crate) thread_source: Option, + pub(crate) initialization_mode: ThreadInitializationMode, + pub(crate) subagent_source: Option, + pub(crate) parent_thread_id: Option, pub(crate) model: Option, pub(crate) model_provider: String, pub(crate) sandbox_policy: Option<&'static str>, @@ -330,8 +329,8 @@ pub(crate) fn subagent_thread_started_event_request( thread_id: input.thread_id, app_server_client: CodexAppServerClientMetadata { product_client_id: input.product_client_id, - client_name: Some(input.client_name), - client_version: Some(input.client_version), + client_name: input.client_name, + client_version: input.client_version, rpc_transport: AppServerRpcTransport::InProcess, experimental_api_enabled: None, }, @@ -368,3 +367,27 @@ fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option _ => None, } } + +pub(crate) fn turn_subagent_source_name(thread_source: &SessionSource) -> Option { + match thread_source { + SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)), + SessionSource::Cli + | SessionSource::VSCode + | SessionSource::Exec + | SessionSource::Mcp + | SessionSource::Custom(_) + | SessionSource::Unknown => None, + } +} + +pub(crate) fn turn_parent_thread_id(thread_source: &SessionSource) -> Option { + match thread_source { + SessionSource::SubAgent(subagent_source) => subagent_parent_thread_id(subagent_source), + SessionSource::Cli + | SessionSource::VSCode + | SessionSource::Exec + | SessionSource::Mcp + | SessionSource::Custom(_) + | SessionSource::Unknown => None, + } +} diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index a9b019d841..19b5bf8f48 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -14,6 +14,7 @@ use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::SubAgentSource; use serde::Serialize; @@ -44,6 +45,9 @@ pub struct TurnResolvedConfigFact { pub thread_id: String, pub num_input_images: usize, pub submission_type: Option, + pub ephemeral: bool, + pub session_source: SessionSource, + pub initialization_mode: ThreadInitializationMode, pub model: String, pub model_provider: String, pub sandbox_policy: SandboxPolicy, @@ -65,6 +69,14 @@ pub enum TurnSubmissionType { Queued, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ThreadInitializationMode { + New, + Forked, + Resumed, +} + #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum TurnStatus { diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index 69eab1da09..eb4fe48683 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -10,6 +10,7 @@ pub use facts::CodexTurnSteerEvent; pub use facts::InvocationType; pub use facts::SkillInvocation; pub use facts::SubAgentThreadStartedInput; +pub use facts::ThreadInitializationMode; pub use facts::TrackEventsContext; pub use facts::TurnResolvedConfigFact; pub use facts::TurnStatus; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 998e3b40ed..ae37e44777 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -10,7 +10,6 @@ use crate::events::CodexTurnEventRequest; use crate::events::CodexTurnSteerEventRequest; use crate::events::SkillInvocationEventParams; use crate::events::SkillInvocationEventRequest; -use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; @@ -21,6 +20,8 @@ use crate::events::codex_turn_steer_event_params; use crate::events::plugin_state_event_type; use crate::events::subagent_thread_started_event_request; use crate::events::thread_source_name; +use crate::events::turn_parent_thread_id; +use crate::events::turn_subagent_source_name; use crate::facts::AnalyticsFact; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; @@ -31,6 +32,7 @@ use crate::facts::PluginStateChangedInput; use crate::facts::PluginUsedInput; use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; @@ -174,8 +176,8 @@ impl AnalyticsReducer { ConnectionState { app_server_client: CodexAppServerClientMetadata { product_client_id, - client_name: Some(params.client_info.name), - client_version: Some(params.client_info.version), + client_name: params.client_info.name, + client_version: params.client_info.version, rpc_transport, experimental_api_enabled: params .capabilities @@ -572,16 +574,24 @@ impl AnalyticsReducer { { return; } - let product_client_id = turn_state + let connection_metadata = turn_state .connection_id .and_then(|connection_id| self.connections.get(&connection_id)) - .map(|connection_state| connection_state.app_server_client.product_client_id.clone()) - .unwrap_or_else(|| originator().value); + .map(|connection_state| { + ( + connection_state.app_server_client.clone(), + connection_state.runtime.clone(), + ) + }); + let Some((app_server_client, runtime)) = connection_metadata else { + return; + }; out.push(TrackEventRequest::TurnEvent(Box::new( CodexTurnEventRequest { event_type: "codex_turn_event", event_params: codex_turn_event_params( - product_client_id, + app_server_client, + runtime, turn_id.to_string(), turn_state, ), @@ -592,7 +602,8 @@ impl AnalyticsReducer { } fn codex_turn_event_params( - product_client_id: String, + app_server_client: CodexAppServerClientMetadata, + runtime: CodexRuntimeMetadata, turn_id: String, turn_state: &TurnState, ) -> CodexTurnEventParams { @@ -610,6 +621,9 @@ fn codex_turn_event_params( thread_id: _resolved_thread_id, num_input_images: _resolved_num_input_images, submission_type, + ephemeral, + session_source, + initialization_mode, model, model_provider, sandbox_policy, @@ -627,8 +641,14 @@ fn codex_turn_event_params( CodexTurnEventParams { thread_id, turn_id, - product_client_id, + app_server_client, + runtime, submission_type, + ephemeral, + thread_source: thread_source_name(&session_source).map(str::to_string), + initialization_mode, + subagent_source: turn_subagent_source_name(&session_source), + parent_thread_id: turn_parent_thread_id(&session_source), model: Some(model), model_provider, sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 48ce9849e1..e87bb3bcab 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -9165,6 +9165,7 @@ mod tests { reasoning_effort: None, personality: None, session_source: SessionSource::Cli, + initialization_mode: codex_analytics::ThreadInitializationMode::New, }; assert_eq!( diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 0a2cf27568..db723323ec 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -404,6 +404,17 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { assert_eq!(event["event_params"]["model_provider"], "mock_provider"); assert_eq!(event["event_params"]["sandbox_policy"], "read_only"); assert_eq!(event["event_params"]["submission_type"], "default"); + assert_eq!(event["event_params"]["ephemeral"], false); + assert_eq!(event["event_params"]["thread_source"], "user"); + assert_eq!(event["event_params"]["initialization_mode"], "new"); + assert_eq!( + event["event_params"]["subagent_source"], + serde_json::Value::Null + ); + assert_eq!( + event["event_params"]["parent_thread_id"], + serde_json::Value::Null + ); assert_eq!(event["event_params"]["num_input_images"], 1); assert_eq!(event["event_params"]["is_first_turn"], true); assert_eq!(event["event_params"]["status"], "completed"); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 3bb1fb2ac1..329499b72f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -52,6 +52,7 @@ use codex_analytics::AppInvocation; use codex_analytics::CodexTurnSteerEvent; use codex_analytics::InvocationType; use codex_analytics::SubAgentThreadStartedInput; +use codex_analytics::ThreadInitializationMode; use codex_analytics::TrackEventsContext; use codex_analytics::TurnResolvedConfigFact; use codex_analytics::TurnSteerRejectionReason; @@ -688,6 +689,7 @@ impl Codex { app_server_client_name: None, app_server_client_version: None, session_source, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools, persist_extended_history, inherited_shell_snapshot, @@ -1138,6 +1140,14 @@ fn turn_submission_type(submission_type: SubmissionType) -> TurnSubmissionType { } } +fn thread_initialization_mode(initial_history: &InitialHistory) -> ThreadInitializationMode { + match initial_history { + InitialHistory::New => ThreadInitializationMode::New, + InitialHistory::Forked(_) => ThreadInitializationMode::Forked, + InitialHistory::Resumed(_) => ThreadInitializationMode::Resumed, + } +} + fn local_time_context() -> (String, String) { match iana_time_zone::get_timezone() { Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), @@ -1199,6 +1209,7 @@ pub(crate) struct SessionConfiguration { app_server_client_version: Option, /// Source of the session (cli, vscode, exec, mcp, ...) session_source: SessionSource, + thread_initialization_mode: ThreadInitializationMode, dynamic_tools: Vec, persist_extended_history: bool, inherited_shell_snapshot: Option>, @@ -1223,6 +1234,7 @@ impl SessionConfiguration { reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, session_source: self.session_source.clone(), + initialization_mode: self.thread_initialization_mode, } } @@ -1599,6 +1611,8 @@ impl Session { session_configuration.collaboration_mode.model(), session_configuration.provider ); + session_configuration.thread_initialization_mode = + thread_initialization_mode(&initial_history); let forked_from_id = initial_history.forked_from_id(); let (conversation_id, rollout_params) = match &initial_history { @@ -6535,6 +6549,10 @@ async fn track_turn_resolved_config_analytics( turn_context: &TurnContext, input: &[UserInput], ) { + let thread_config = { + let state = sess.state.lock().await; + state.session_configuration.thread_config_snapshot() + }; let is_first_turn = { let mut state = sess.state.lock().await; state.take_next_turn_is_first() @@ -6556,6 +6574,9 @@ async fn track_turn_resolved_config_analytics( .map(turn_submission_type) .unwrap_or(TurnSubmissionType::Default), ), + ephemeral: thread_config.ephemeral, + session_source: thread_config.session_source, + initialization_mode: thread_config.initialization_mode, model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(), sandbox_policy: turn_context.sandbox_policy.get().clone(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 055b83b952..5c828db59a 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -13,6 +13,7 @@ use crate::function_tool::FunctionCallError; use crate::shell::default_user_shell; use crate::tools::format_exec_output_str; +use codex_analytics::ThreadInitializationMode; use codex_features::Features; use codex_login::CodexAuth; use codex_mcp::mcp_connection_manager::ToolInfo; @@ -1875,6 +1876,7 @@ async fn set_rate_limits_retains_previous_credits() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -1977,6 +1979,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2326,6 +2329,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2592,6 +2596,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2696,6 +2701,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -3538,6 +3544,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools, persist_extended_history: false, inherited_shell_snapshot: None, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 9727cc208a..4a2f5dae24 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -3,6 +3,7 @@ use crate::codex::Codex; use crate::codex::SteerInputError; use crate::config::ConstraintResult; use crate::file_watcher::WatchRegistration; +use codex_analytics::ThreadInitializationMode; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::Personality; @@ -42,6 +43,7 @@ pub struct ThreadConfigSnapshot { pub reasoning_effort: Option, pub personality: Option, pub session_source: SessionSource, + pub initialization_mode: ThreadInitializationMode, } pub struct CodexThread {