diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 52860de1d4..a066ce4ac0 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -7,7 +7,6 @@ use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; use crate::events::CodexTurnEventRequest; -use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; @@ -29,6 +28,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; @@ -254,6 +254,16 @@ 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()), + rpc_transport: AppServerRpcTransport::Stdio, + experimental_api_enabled: None, + } +} + fn sample_runtime_metadata() -> CodexRuntimeMetadata { CodexRuntimeMetadata { codex_rs_version: "0.1.0".to_string(), diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 8cfeb7a354..647ef055be 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, @@ -134,7 +127,6 @@ 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) submission_type: Option, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, @@ -375,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 02366f7028..c4dc7cc8e7 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -79,6 +79,14 @@ pub struct TurnTokenUsageFact { pub token_usage: TokenUsage, } +#[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 4fdec9f7bf..41b5c64813 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -12,6 +12,7 @@ pub use facts::InputError; 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 77943b0aeb..785920607b 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::AnalyticsJsonRpcError; use crate::facts::AppMentionedInput; 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 3c7664656d..cb1fcd14fc 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -299,12 +299,23 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { assert_eq!(event["event_params"]["thread_id"], thread.id); assert_eq!(event["event_params"]["turn_id"], turn.id); assert_eq!( - event["event_params"]["product_client_id"], + event["event_params"]["app_server_client"]["product_client_id"], DEFAULT_CLIENT_NAME ); assert_eq!(event["event_params"]["model"], "mock-model"); assert_eq!(event["event_params"]["model_provider"], "mock_provider"); assert_eq!(event["event_params"]["sandbox_policy"], "read_only"); + 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"]["status"], "completed"); assert!(event["event_params"]["started_at"].as_u64().is_some()); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c44a57375d..11480fcb43 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -651,6 +651,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, @@ -1076,6 +1077,14 @@ impl TurnContext { } } +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), @@ -1137,6 +1146,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>, @@ -1161,6 +1171,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, } } @@ -1534,6 +1545,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 { @@ -4090,6 +4103,7 @@ impl Session { turn_kind: NonSteerableTurnKind::Compact, }); } + None => return Err(SteerInputError::NoActiveTurn(input)), } let mut turn_state = active_turn.turn_state.lock().await; diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index f0bded7187..615ca14e62 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::ToolInfo; @@ -1872,6 +1873,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, @@ -1974,6 +1976,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, @@ -2323,6 +2326,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, @@ -2586,6 +2590,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, @@ -2690,6 +2695,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, @@ -3531,6 +3537,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 {