From 48a7e70eec88e7da71cec7b8410375a9687d4a4e Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Thu, 2 Apr 2026 22:06:55 -0700 Subject: [PATCH 1/2] [codex-analytics] add queued submission metadata --- .../analytics/src/analytics_client_tests.rs | 90 ++++++++++++++++++- .../schema/json/ClientRequest.json | 18 ++++ .../codex_app_server_protocol.schemas.json | 18 ++++ .../codex_app_server_protocol.v2.schemas.json | 18 ++++ .../schema/json/v2/TurnStartParams.json | 18 ++++ .../schema/typescript/SubmissionType.ts | 5 ++ .../schema/typescript/index.ts | 1 + .../schema/typescript/v2/TurnStartParams.ts | 4 + .../app-server-protocol/src/protocol/v2.rs | 7 ++ .../app-server/src/codex_message_processor.rs | 3 +- .../src/message_processor/tracing_tests.rs | 1 + .../app-server/tests/suite/v2/turn_start.rs | 2 + codex-rs/core/src/codex.rs | 42 ++++++++- codex-rs/core/src/codex_tests.rs | 3 + codex-rs/core/src/guardian/review_session.rs | 1 + codex-rs/core/tests/common/test_codex.rs | 1 + codex-rs/core/tests/suite/apply_patch_cli.rs | 7 ++ codex-rs/core/tests/suite/approvals.rs | 1 + codex-rs/core/tests/suite/client.rs | 2 + codex-rs/core/tests/suite/code_mode.rs | 1 + .../tests/suite/collaboration_instructions.rs | 2 + codex-rs/core/tests/suite/compact.rs | 7 ++ codex-rs/core/tests/suite/exec_policy.rs | 2 + codex-rs/core/tests/suite/image_rollout.rs | 2 + codex-rs/core/tests/suite/items.rs | 5 ++ codex-rs/core/tests/suite/json_result.rs | 1 + codex-rs/core/tests/suite/live_reload.rs | 1 + codex-rs/core/tests/suite/model_switching.rs | 14 +++ .../core/tests/suite/model_visible_layout.rs | 5 ++ codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + .../core/tests/suite/models_etag_responses.rs | 1 + codex-rs/core/tests/suite/personality.rs | 13 +++ codex-rs/core/tests/suite/prompt_caching.rs | 5 ++ codex-rs/core/tests/suite/remote_models.rs | 4 + .../core/tests/suite/request_permissions.rs | 1 + .../tests/suite/request_permissions_tool.rs | 1 + .../core/tests/suite/request_user_input.rs | 2 + codex-rs/core/tests/suite/rmcp_client.rs | 6 ++ .../tests/suite/safety_check_downgrade.rs | 4 + codex-rs/core/tests/suite/shell_snapshot.rs | 4 + codex-rs/core/tests/suite/skill_approval.rs | 1 + codex-rs/core/tests/suite/skills.rs | 1 + codex-rs/core/tests/suite/sqlite_state.rs | 1 + codex-rs/core/tests/suite/tool_harness.rs | 5 ++ codex-rs/core/tests/suite/tool_parallelism.rs | 2 + codex-rs/core/tests/suite/truncation.rs | 1 + codex-rs/core/tests/suite/unified_exec.rs | 26 ++++++ codex-rs/core/tests/suite/user_shell_cmd.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 14 +++ .../core/tests/suite/websocket_fallback.rs | 1 + codex-rs/exec/src/lib.rs | 1 + codex-rs/protocol/src/protocol.rs | 25 ++++++ codex-rs/tui/src/app.rs | 2 + codex-rs/tui/src/app_command.rs | 6 ++ codex-rs/tui/src/app_server_session.rs | 3 + codex-rs/tui/src/chatwidget.rs | 17 +++- .../chatwidget/tests/composer_submission.rs | 23 +++++ 57 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 3a0230113c..f661615b53 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -28,6 +28,7 @@ use crate::facts::SkillInvokedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSubmissionType; use crate::reducer::AnalyticsReducer; use crate::reducer::normalize_path_for_skill_id; use crate::reducer::skill_id_for_local_skill; @@ -231,7 +232,7 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { turn_id: turn_id.to_string(), thread_id: "thread-2".to_string(), num_input_images: 1, - submission_type: None, + submission_type: Some(TurnSubmissionType::Default), model: "gpt-5".to_string(), model_provider: "openai".to_string(), sandbox_policy: SandboxPolicy::new_read_only_policy(), @@ -904,7 +905,7 @@ fn turn_event_serializes_expected_shape() { thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), product_client_id: Some("codex-tui".to_string()), - submission_type: None, + submission_type: Some(TurnSubmissionType::Default), model: Some("gpt-5".to_string()), model_provider: "openai".to_string(), sandbox_policy: Some("read_only"), @@ -950,7 +951,7 @@ fn turn_event_serializes_expected_shape() { "thread_id": "thread-2", "turn_id": "turn-2", "product_client_id": "codex-tui", - "submission_type": null, + "submission_type": "default", "model": "gpt-5", "model_provider": "openai", "sandbox_policy": "read_only", @@ -1038,6 +1039,89 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["total_tokens"], json!(321)); } +#[tokio::test] +async fn queued_submission_type_emits_queued_turn_event() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Initialize { + connection_id: 7, + params: InitializeParams { + client_info: ClientInfo { + name: "codex-tui".to_string(), + title: None, + version: "1.0.0".to_string(), + }, + capabilities: None, + }, + product_client_id: "codex-tui".to_string(), + runtime: CodexRuntimeMetadata { + codex_rs_version: "0.1.0".to_string(), + runtime_os: "macos".to_string(), + runtime_os_version: "15.3.1".to_string(), + runtime_arch: "aarch64".to_string(), + }, + rpc_transport: AppServerRpcTransport::Stdio, + }, + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Request { + connection_id: 7, + request_id: RequestId::Integer(3), + request: Box::new(sample_turn_start_request("thread-2", /*request_id*/ 3)), + }, + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Response { + connection_id: 7, + response: Box::new(sample_turn_start_response("turn-2", /*request_id*/ 3)), + }, + &mut out, + ) + .await; + let mut resolved_config = sample_turn_resolved_config("turn-2"); + resolved_config.submission_type = Some(TurnSubmissionType::Queued); + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new( + resolved_config, + ))), + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_started_notification( + "thread-2", "turn-2", + ))), + &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"]["submission_type"], json!("queued")); +} + #[tokio::test] async fn turn_does_not_emit_without_required_prerequisites() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7c94419844..a001a69997 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2473,6 +2473,13 @@ }, "type": "object" }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TextElement": { "properties": { "byteRange": { @@ -3241,6 +3248,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index acc274aaff..0771cb0e82 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12065,6 +12065,13 @@ } ] }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -14616,6 +14623,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 2421630d43..8af7e0d716 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9920,6 +9920,13 @@ } ] }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12471,6 +12478,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index cad1d8b5bc..982cf96634 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -354,6 +354,13 @@ ], "type": "object" }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TextElement": { "properties": { "byteRange": { @@ -595,6 +602,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts b/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts new file mode 100644 index 0000000000..7c32c07052 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SubmissionType = "prompt" | "prompt_queued"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 09c388337f..9b53219ea5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -64,6 +64,7 @@ export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; +export type { SubmissionType } from "./SubmissionType"; export type { ThreadId } from "./ThreadId"; export type { Tool } from "./Tool"; export type { Verbosity } from "./Verbosity"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 8f57a5e68b..9697838d9c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -6,6 +6,7 @@ import type { Personality } from "../Personality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; import type { ServiceTier } from "../ServiceTier"; +import type { SubmissionType } from "../SubmissionType"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -45,6 +46,9 @@ personality?: Personality | null, /** * this turn. */ outputSchema?: JsonValue | null, /** + * Metadata describing how the prompt was submitted. + */ +submissionType?: SubmissionType | null, /** * EXPERIMENTAL - Set a pre-set collaboration mode. * Takes precedence over model, reasoning_effort, and developer instructions if set. * diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 538293f1f5..9c17e68a43 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -79,6 +79,7 @@ use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; use codex_protocol::protocol::SkillScope as CoreSkillScope; use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TokenUsage as CoreTokenUsage; use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope; @@ -3981,6 +3982,11 @@ pub struct TurnStartParams { #[ts(optional = nullable)] pub output_schema: Option, + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub submission_type: Option, + /// EXPERIMENTAL - Set a pre-set collaboration mode. /// Takes precedence over model, reasoning_effort, and developer instructions if set. /// @@ -8316,6 +8322,7 @@ mod tests { service_tier: None, effort: None, summary: None, + submission_type: None, output_schema: None, collaboration_mode: None, personality: None, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 18a8c0b6b0..827b7c74cd 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6429,9 +6429,10 @@ impl CodexMessageProcessor { .submit_core_op( &request_id, thread.as_ref(), - Op::UserInput { + Op::UserInputWithMetadata { items: mapped_items, final_output_json_schema: params.output_schema, + submission_type: params.submission_type, }, ) .await; diff --git a/codex-rs/app-server/src/message_processor/tracing_tests.rs b/codex-rs/app-server/src/message_processor/tracing_tests.rs index d110f4feea..5b0168bbea 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -611,6 +611,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { effort: None, summary: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }, 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 a8552b32d9..f3697c40ce 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1472,6 +1472,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { summary: Some(ReasoningSummary::Auto), service_tier: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }) @@ -1505,6 +1506,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { summary: Some(ReasoningSummary::Auto), service_tier: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 1fa969ed84..69dbded68b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -54,6 +54,7 @@ use codex_analytics::AnalyticsEventsClient; use codex_analytics::AppInvocation; use codex_analytics::InvocationType; use codex_analytics::TurnResolvedConfigFact; +use codex_analytics::TurnSubmissionType; use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; @@ -114,6 +115,7 @@ use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; @@ -877,6 +879,7 @@ pub(crate) struct TurnContext { pub(crate) current_date: Option, pub(crate) timezone: Option, pub(crate) app_server_client_name: Option, + pub(crate) submission_type: Option, pub(crate) developer_instructions: Option, pub(crate) compact_prompt: Option, pub(crate) user_instructions: Option, @@ -987,6 +990,7 @@ impl TurnContext { current_date: self.current_date.clone(), timezone: self.timezone.clone(), app_server_client_name: self.app_server_client_name.clone(), + submission_type: self.submission_type, developer_instructions: self.developer_instructions.clone(), compact_prompt: self.compact_prompt.clone(), user_instructions: self.user_instructions.clone(), @@ -1072,6 +1076,13 @@ impl TurnContext { } } +fn turn_submission_type(submission_type: SubmissionType) -> TurnSubmissionType { + match submission_type { + SubmissionType::Prompt => TurnSubmissionType::Default, + SubmissionType::PromptQueued => TurnSubmissionType::Queued, + } +} + fn local_time_context() -> (String, String) { match iana_time_zone::get_timezone() { Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), @@ -1240,6 +1251,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) final_output_json_schema: Option>, pub(crate) personality: Option, pub(crate) app_server_client_name: Option, + pub(crate) submission_type: Option, } impl Session { @@ -1392,6 +1404,7 @@ impl Session { network: Option, environment: Arc, sub_id: String, + submission_type: Option, js_repl: Arc, skills_outcome: Arc, ) -> TurnContext { @@ -1455,6 +1468,7 @@ impl Session { current_date: Some(current_date), timezone: Some(timezone), app_server_client_name: session_configuration.app_server_client_name.clone(), + submission_type, developer_instructions: session_configuration.developer_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), @@ -2447,6 +2461,7 @@ impl Session { sub_id, session_configuration, updates.final_output_json_schema, + updates.submission_type, sandbox_policy_changed, ) .await) @@ -2457,6 +2472,7 @@ impl Session { sub_id: String, session_configuration: SessionConfiguration, final_output_json_schema: Option>, + submission_type: Option, sandbox_policy_changed: bool, ) -> Arc { let per_turn_config = Self::build_per_turn_config(&session_configuration); @@ -2522,6 +2538,7 @@ impl Session { .map(StartedNetworkProxy::proxy), Arc::clone(&self.services.environment), sub_id, + submission_type, Arc::clone(&self.js_repl), skills_outcome, ); @@ -2634,6 +2651,7 @@ impl Session { sub_id, session_configuration, /*final_output_json_schema*/ None, + /*submission_type*/ None, /*sandbox_policy_changed*/ false, ) .await @@ -4522,7 +4540,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv .await; false } - Op::UserInput { .. } | Op::UserTurn { .. } => { + Op::UserInput { .. } | Op::UserInputWithMetadata { .. } | Op::UserTurn { .. } => { handlers::user_input_or_turn(&sess, sub.id.clone(), sub.op).await; false } @@ -4780,6 +4798,7 @@ mod handlers { items, collaboration_mode, personality, + submission_type, } => { let collaboration_mode = collaboration_mode.or_else(|| { Some(CollaborationMode { @@ -4805,9 +4824,22 @@ mod handlers { final_output_json_schema: Some(final_output_json_schema), personality, app_server_client_name: None, + submission_type, }, ) } + Op::UserInputWithMetadata { + items, + final_output_json_schema, + submission_type, + } => ( + items, + SessionSettingsUpdate { + final_output_json_schema: Some(final_output_json_schema), + submission_type, + ..Default::default() + }, + ), Op::UserInput { items, final_output_json_schema, @@ -5620,6 +5652,7 @@ async fn spawn_review_thread( current_date: parent_turn_context.current_date.clone(), timezone: parent_turn_context.timezone.clone(), app_server_client_name: parent_turn_context.app_server_client_name.clone(), + submission_type: None, developer_instructions: None, user_instructions: None, compact_prompt: parent_turn_context.compact_prompt.clone(), @@ -6249,7 +6282,12 @@ async fn track_turn_resolved_config_analytics( matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. }) }) .count(), - submission_type: None, + submission_type: Some( + turn_context + .submission_type + .map(turn_submission_type) + .unwrap_or(TurnSubmissionType::Default), + ), 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 986e711348..68000565e2 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2735,6 +2735,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { /*network*/ None, environment, "turn_id".to_string(), + /*submission_type*/ None, Arc::clone(&js_repl), skills_outcome, ); @@ -3103,6 +3104,7 @@ async fn user_turn_updates_approvals_reviewer() { final_output_json_schema: None, collaboration_mode: None, personality: config.personality, + submission_type: None, }, ) .await; @@ -3572,6 +3574,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( /*network*/ None, environment, "turn_id".to_string(), + /*submission_type*/ None, Arc::clone(&js_repl), skills_outcome, )); diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 2a63e5b547..2dd5cff883 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -523,6 +523,7 @@ async fn run_review_on_session( final_output_json_schema: Some(params.schema.clone()), collaboration_mode: None, personality: params.personality, + submission_type: None, }) .await }), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index a347488c2d..2705782bcb 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -738,6 +738,7 @@ impl TestCodex { service_tier, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index a8b429c0dd..c6a1b22970 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -368,6 +368,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -978,6 +979,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1060,6 +1062,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1212,6 +1215,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1277,6 +1281,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1350,6 +1355,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1423,6 +1429,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index f7874eaf45..837c1c331c 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -597,6 +597,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 231ab1a5dc..0ca06c9da6 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1562,6 +1562,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; @@ -1676,6 +1677,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 37ee27dd68..43ae44c760 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -2338,6 +2338,7 @@ text( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index df8d6e4fe4..42ae909c85 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -191,6 +191,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -307,6 +308,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu collaboration_mode: Some(turn_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 35fe9d07e7..f63a0be647 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1667,6 +1667,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .unwrap(); @@ -1758,6 +1759,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit first user turn"); @@ -1783,6 +1785,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit second user turn"); @@ -1894,6 +1897,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit pre-resume turn"); @@ -1943,6 +1947,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit resumed user turn"); @@ -3148,6 +3153,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit first user turn"); @@ -3173,6 +3179,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit second user turn"); diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index fb055c970f..6de4141e6c 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -59,6 +59,7 @@ async fn submit_user_turn( service_tier: None, collaboration_mode, personality: None, + submission_type: None, }) .await?; Ok(()) @@ -140,6 +141,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 8195bd0a86..0594e48f31 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -130,6 +130,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -214,6 +215,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index f949cd4b97..d5c748454d 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -530,6 +530,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result< service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -607,6 +608,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -716,6 +718,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -903,6 +906,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -1017,6 +1021,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 3b6f3e3f9c..027e34edbd 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -88,6 +88,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 663cf47488..647d840c2e 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -69,6 +69,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 4cb84e8b66..8551b7131b 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -134,6 +134,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -171,6 +172,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -231,6 +233,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -268,6 +271,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -410,6 +414,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -431,6 +436,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -539,6 +545,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -560,6 +567,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -671,6 +679,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -692,6 +701,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -805,6 +815,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -834,6 +845,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -989,6 +1001,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1048,6 +1061,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 13e2819141..4284cdc099 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -129,6 +129,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -153,6 +154,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -232,6 +234,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -256,6 +259,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -366,6 +370,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |event| { diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 0e043bd9f6..19e9b3f0fc 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -104,6 +104,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index daf3e89462..9fbbcdb93c 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -110,6 +110,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 22870cae73..7b2afe9301 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -111,6 +111,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -162,6 +163,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -220,6 +222,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -284,6 +287,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -336,6 +340,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -374,6 +379,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -441,6 +447,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -479,6 +486,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -559,6 +567,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -597,6 +606,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -714,6 +724,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; @@ -833,6 +844,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -871,6 +883,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 1bcd068614..22585eb898 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -712,6 +712,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -825,6 +826,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -846,6 +848,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -951,6 +954,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -972,6 +976,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index c0776e1ea7..cbdee255c0 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -181,6 +181,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -244,6 +245,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -404,6 +406,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -623,6 +626,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 2cfd1cf6f7..6e25adb228 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -202,6 +202,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 14506f4a41..d44f47d931 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -153,6 +153,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 8e30b37c21..952c696653 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -153,6 +153,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul }, }), personality: None, + submission_type: None, }) .await?; @@ -264,6 +265,7 @@ where service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 4262a6aab0..17ec92debe 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -138,6 +138,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -309,6 +310,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -514,6 +516,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -630,6 +633,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -793,6 +797,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1041,6 +1046,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index 51a88ef16a..ef8c086275 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -53,6 +53,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -152,6 +153,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -238,6 +240,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -288,6 +291,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 0e044f3922..960ebdb945 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -172,6 +172,7 @@ async fn run_snapshot_command_with_options( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -263,6 +264,7 @@ async fn run_shell_command_snapshot_with_options( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -334,6 +336,7 @@ async fn run_tool_turn_on_harness( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -568,6 +571,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index d77d736f94..15d096268a 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -59,6 +59,7 @@ async fn submit_turn_with_policies( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 388618b56a..690de66893 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -82,6 +82,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index f35152e185..e3a0a8e54c 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -404,6 +404,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 9594195a5b..296b5c4c09 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -93,6 +93,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -164,6 +165,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -245,6 +247,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -341,6 +344,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -445,6 +449,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index faff0b2e09..16b0a2edd7 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -50,6 +50,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -367,6 +368,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 3f4a9e5a64..5e4c980228 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -496,6 +496,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 2f72277086..8039f39855 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -200,6 +200,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -334,6 +335,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -417,6 +419,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -503,6 +506,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -601,6 +605,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -681,6 +686,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -762,6 +768,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -897,6 +904,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1039,6 +1047,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1204,6 +1213,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1317,6 +1327,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1440,6 +1451,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1535,6 +1547,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1621,6 +1634,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1757,6 +1771,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1930,6 +1945,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<() service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2012,6 +2028,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2105,6 +2122,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2214,6 +2232,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2354,6 +2373,7 @@ PY service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; // This is a worst case scenario for the truncate logic. @@ -2473,6 +2493,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2574,6 +2595,7 @@ PY service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2675,6 +2697,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2784,6 +2807,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2884,6 +2908,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -3024,6 +3049,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index f6abb5d621..c3ff787392 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -185,6 +185,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index f16d3c2b18..f9ac77748c 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -164,6 +164,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -273,6 +274,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -414,6 +416,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5 summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -520,6 +523,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -612,6 +616,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -712,6 +717,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -823,6 +829,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -925,6 +932,7 @@ await codex.emitImage(out); service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1045,6 +1053,7 @@ console.log(out.type); summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1138,6 +1147,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1214,6 +1224,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1295,6 +1306,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1425,6 +1437,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1500,6 +1513,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index e7f33df148..79f00a3961 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -164,6 +164,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 5609fd0344..16d5b2335c 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -686,6 +686,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { effort: default_effort, summary: None, personality: None, + submission_type: None, output_schema, collaboration_mode: None, }, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e0903fd5eb..e4874d7519 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -101,6 +101,13 @@ pub const REALTIME_CONVERSATION_OPEN_TAG: &str = ""; pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = ""; pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:"; +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum SubmissionType { + Prompt, + PromptQueued, +} + /// Submission Queue Entry - requests from user #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct Submission { @@ -244,6 +251,19 @@ pub enum Op { final_output_json_schema: Option, }, + /// Same transport shape as [`Op::UserInput`], but preserves submission + /// metadata for analytics. + UserInputWithMetadata { + /// User input items, see `InputItem` + items: Vec, + /// Optional JSON Schema used to constrain the final assistant message for this turn. + #[serde(skip_serializing_if = "Option::is_none")] + final_output_json_schema: Option, + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + submission_type: Option, + }, + /// Similar to [`Op::UserInput`], but contains additional context required /// for a turn of a [`crate::codex_thread::CodexThread`]. UserTurn { @@ -299,6 +319,10 @@ pub enum Op { /// Optional personality override for this turn. #[serde(skip_serializing_if = "Option::is_none")] personality: Option, + + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + submission_type: Option, }, /// Inter-agent communication that should be recorded as assistant history @@ -579,6 +603,7 @@ impl Op { Self::RealtimeConversationText(_) => "realtime_conversation_text", Self::RealtimeConversationClose => "realtime_conversation_close", Self::UserInput { .. } => "user_input", + Self::UserInputWithMetadata { .. } => "user_input", Self::UserTurn { .. } => "user_turn", Self::InterAgentCommunication { .. } => "inter_agent_communication", Self::OverrideTurnContext { .. } => "override_turn_context", diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 759bf52c7c..32e15a7705 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2229,6 +2229,7 @@ impl App { final_output_json_schema, collaboration_mode, personality, + submission_type, } => { let mut should_start_turn = true; if let Some(turn_id) = self.active_turn_id_for_thread(thread_id).await { @@ -2311,6 +2312,7 @@ impl App { *service_tier, collaboration_mode.clone(), *personality, + *submission_type, final_output_json_schema.clone(), ) .await?; diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 0646cc297d..4a83d1ab95 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -17,6 +17,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SubmissionType; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -51,6 +52,7 @@ pub(crate) enum AppCommandView<'a> { final_output_json_schema: &'a Option, collaboration_mode: &'a Option, personality: &'a Option, + submission_type: &'a Option, }, OverrideTurnContext { cwd: &'a Option, @@ -152,6 +154,7 @@ impl AppCommand { final_output_json_schema: Option, collaboration_mode: Option, personality: Option, + submission_type: Option, ) -> Self { Self(Op::UserTurn { items, @@ -166,6 +169,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, }) } @@ -311,6 +315,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, } => AppCommandView::UserTurn { items, cwd, @@ -324,6 +329,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, }, Op::OverrideTurnContext { cwd, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index c0be4a94f6..3e6be5671f 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -82,6 +82,7 @@ use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget as CoreReviewTarget; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionNetworkProxyRuntime; +use codex_protocol::protocol::SubmissionType; use color_eyre::eyre::ContextCompat; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; @@ -408,6 +409,7 @@ impl AppServerSession { service_tier: Option>, collaboration_mode: Option, personality: Option, + submission_type: Option, output_schema: Option, ) -> Result { let request_id = self.next_request_id(); @@ -426,6 +428,7 @@ impl AppServerSession { effort, summary, personality, + submission_type, output_schema, collaboration_mode, }, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 63b201d111..4976ade348 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -193,6 +193,7 @@ use codex_protocol::protocol::ReviewTarget; use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata; #[cfg(test)] use codex_protocol::protocol::StreamErrorEvent; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TerminalInteractionEvent; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; @@ -5568,6 +5569,14 @@ impl ChatWidget { } fn submit_user_message(&mut self, user_message: UserMessage) { + self.submit_user_message_with_type(user_message, /*submission_type*/ None); + } + + fn submit_user_message_with_type( + &mut self, + user_message: UserMessage, + submission_type: Option, + ) { if !self.is_session_configured() { tracing::warn!("cannot submit user message before session is configured; queueing"); self.queued_user_messages.push_front(user_message); @@ -5783,6 +5792,7 @@ impl ChatWidget { /*final_output_json_schema*/ None, collaboration_mode, personality, + submission_type, ); if !self.submit_op(op) { @@ -7240,8 +7250,13 @@ impl ChatWidget { if self.bottom_pane.is_task_running() { return; } + let submission_type = if self.rejected_steers_queue.is_empty() { + Some(SubmissionType::PromptQueued) + } else { + None + }; if let Some(user_message) = self.pop_next_queued_user_message() { - self.submit_user_message(user_message); + self.submit_user_message_with_type(user_message, submission_type); } // Update the list to reflect the remaining queued messages (if any). self.refresh_pending_input_preview(); diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index a5678a6b93..149f44a281 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -640,6 +640,29 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() { assert_eq!(chat.active_collaboration_mode_kind(), expected_mode); } +#[tokio::test] +async fn queued_user_message_marks_prompt_queued_submission_type() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.thread_id = Some(ThreadId::new()); + chat.queued_user_messages.push_back(UserMessage { + text: "queued follow up".to_string(), + local_images: Vec::new(), + remote_image_urls: Vec::new(), + text_elements: Vec::new(), + mention_bindings: Vec::new(), + }); + + chat.maybe_send_next_queued_input(); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { + submission_type: Some(SubmissionType::PromptQueued), + .. + } => {} + other => panic!("expected queued prompt submission, got {other:?}"), + } +} + #[tokio::test] async fn remap_placeholders_uses_attachment_labels() { let placeholder_one = "[Image #1]"; From f750e127f4ac7780b2a61a7e482285725fd5d1fe Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Fri, 3 Apr 2026 11:35:46 -0700 Subject: [PATCH 2/2] [codex-analytics] add steering metadata --- .../analytics/src/analytics_client_tests.rs | 193 +++++++++++++++++- codex-rs/analytics/src/client.rs | 11 + codex-rs/analytics/src/events.rs | 38 ++++ codex-rs/analytics/src/facts.rs | 33 +++ codex-rs/analytics/src/lib.rs | 3 + codex-rs/analytics/src/reducer.rs | 32 ++- codex-rs/core/src/codex.rs | 156 ++++++++++++-- 7 files changed, 450 insertions(+), 16 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index f661615b53..98545180ef 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -7,6 +7,7 @@ use crate::events::CodexPluginEventRequest; 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; @@ -14,10 +15,12 @@ use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; +use crate::events::codex_turn_steer_event_params; use crate::facts::AnalyticsFact; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; +use crate::facts::CodexTurnSteerEvent; use crate::facts::CustomAnalyticsFact; use crate::facts::InvocationType; use crate::facts::PluginState; @@ -28,6 +31,9 @@ use crate::facts::SkillInvokedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSteerInput; +use crate::facts::TurnSteerRejectionReason; +use crate::facts::TurnSteerResult; use crate::facts::TurnSubmissionType; use crate::reducer::AnalyticsReducer; use crate::reducer::normalize_path_for_skill_id; @@ -921,7 +927,7 @@ fn turn_event_serializes_expected_shape() { is_first_turn: true, status: Some(TurnStatus::Completed), turn_error: None, - steer_count: None, + steer_count: Some(0), total_tool_call_count: None, shell_command_count: None, file_change_count: None, @@ -967,7 +973,7 @@ fn turn_event_serializes_expected_shape() { "is_first_turn": true, "status": "completed", "turn_error": null, - "steer_count": null, + "steer_count": 0, "total_tool_call_count": null, "shell_command_count": null, "file_change_count": null, @@ -989,6 +995,90 @@ fn turn_event_serializes_expected_shape() { ); } +#[test] +fn turn_steer_event_serializes_expected_shape() { + let tracking = TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }; + let event = TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest { + event_type: "codex_turn_steer_event", + event_params: codex_turn_steer_event_params( + &tracking, + CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 2, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at: 1_716_000_123, + }, + ), + }); + + let payload = serde_json::to_value(&event).expect("serialize turn steer event"); + + assert_eq!( + payload, + json!({ + "event_type": "codex_turn_steer_event", + "event_params": { + "thread_id": "thread-2", + "expected_turn_id": "turn-2", + "accepted_turn_id": "turn-2", + "product_client_id": originator().value, + "num_input_images": 2, + "result": "accepted", + "rejection_reason": null, + "created_at": 1_716_000_123 + } + }) + ); +} + +#[test] +fn rejected_turn_steer_event_serializes_expected_shape() { + let tracking = TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-3".to_string(), + turn_id: "turn-3".to_string(), + }; + let event = TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest { + event_type: "codex_turn_steer_event", + event_params: codex_turn_steer_event_params( + &tracking, + CodexTurnSteerEvent { + expected_turn_id: Some("turn-expected".to_string()), + accepted_turn_id: None, + num_input_images: 1, + result: TurnSteerResult::Rejected, + rejection_reason: Some(TurnSteerRejectionReason::ExpectedTurnMismatch), + created_at: 1_716_000_124, + }, + ), + }); + + let payload = serde_json::to_value(&event).expect("serialize rejected turn steer event"); + + assert_eq!( + payload, + json!({ + "event_type": "codex_turn_steer_event", + "event_params": { + "thread_id": "thread-3", + "expected_turn_id": "turn-expected", + "accepted_turn_id": null, + "product_client_id": originator().value, + "num_input_images": 1, + "result": "rejected", + "rejection_reason": "expected_turn_mismatch", + "created_at": 1_716_000_124 + } + }) + ); +} + #[tokio::test] async fn turn_lifecycle_emits_turn_event() { let mut reducer = AnalyticsReducer::default(); @@ -1026,6 +1116,7 @@ async fn turn_lifecycle_emits_turn_event() { ); 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)); assert_eq!(payload["event_params"]["created_at"], json!(455)); assert_eq!(payload["event_params"]["completed_at"], json!(456)); assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); @@ -1039,6 +1130,104 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["total_tokens"], json!(321)); } +#[tokio::test] +async fn accepted_steers_increment_turn_steer_count() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + ingest_turn_prerequisites( + &mut reducer, + &mut out, + /*include_initialize*/ true, + /*include_resolved_config*/ true, + /*include_started*/ true, + /*include_token_usage*/ false, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 0, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at: 1, + }, + })), + &mut out, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: None, + accepted_turn_id: None, + num_input_images: 0, + result: TurnSteerResult::Rejected, + rejection_reason: Some(TurnSteerRejectionReason::NoActiveTurn), + created_at: 2, + }, + })), + &mut out, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 1, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at: 3, + }, + })), + &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; + + let turn_event = out + .iter() + .find(|event| matches!(event, TrackEventRequest::TurnEvent(_))) + .expect("turn event should be emitted"); + let payload = serde_json::to_value(turn_event).expect("serialize turn event"); + assert_eq!(payload["event_params"]["steer_count"], json!(2)); +} + #[tokio::test] async fn queued_submission_type_emits_queued_turn_event() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index a4dc58de36..6fb61d3946 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -6,6 +6,7 @@ use crate::facts::AnalyticsFact; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; +use crate::facts::CodexTurnSteerEvent; use crate::facts::CustomAnalyticsFact; use crate::facts::PluginState; use crate::facts::PluginStateChangedInput; @@ -13,6 +14,7 @@ use crate::facts::SkillInvocation; use crate::facts::SkillInvokedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; +use crate::facts::TurnSteerInput; use crate::reducer::AnalyticsReducer; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponse; @@ -189,6 +191,15 @@ impl AnalyticsEventsClient { )); } + pub fn track_turn_steer(&self, tracking: TrackEventsContext, turn_steer: CodexTurnSteerEvent) { + self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer( + TurnSteerInput { + tracking, + turn_steer, + }, + ))); + } + pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) { self.record_fact(AnalyticsFact::Custom( CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput { diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 3ae13b0a53..73b05efc01 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -1,8 +1,11 @@ use crate::facts::AppInvocation; +use crate::facts::CodexTurnSteerEvent; use crate::facts::InvocationType; use crate::facts::PluginState; use crate::facts::TrackEventsContext; use crate::facts::TurnStatus; +use crate::facts::TurnSteerRejectionReason; +use crate::facts::TurnSteerResult; use crate::facts::TurnSubmissionType; use codex_app_server_protocol::CodexErrorInfo; use codex_login::default_client::originator; @@ -39,6 +42,7 @@ pub(crate) enum TrackEventRequest { AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), TurnEvent(Box), + TurnSteer(CodexTurnSteerEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -170,6 +174,24 @@ pub(crate) struct CodexTurnEventRequest { pub(crate) event_params: CodexTurnEventParams, } +#[derive(Serialize)] +pub(crate) struct CodexTurnSteerEventParams { + pub(crate) thread_id: String, + pub(crate) expected_turn_id: Option, + pub(crate) accepted_turn_id: Option, + pub(crate) product_client_id: Option, + pub(crate) num_input_images: usize, + pub(crate) result: TurnSteerResult, + pub(crate) rejection_reason: Option, + pub(crate) created_at: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexTurnSteerEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexTurnSteerEventParams, +} + #[derive(Serialize)] pub(crate) struct CodexPluginMetadata { pub(crate) plugin_id: Option, @@ -261,6 +283,22 @@ pub(crate) fn codex_plugin_used_metadata( } } +pub(crate) fn codex_turn_steer_event_params( + tracking: &TrackEventsContext, + turn_steer: CodexTurnSteerEvent, +) -> CodexTurnSteerEventParams { + CodexTurnSteerEventParams { + thread_id: tracking.thread_id.clone(), + expected_turn_id: turn_steer.expected_turn_id, + accepted_turn_id: turn_steer.accepted_turn_id, + product_client_id: Some(originator().value), + num_input_images: turn_steer.num_input_images, + result: turn_steer.result, + rejection_reason: turn_steer.rejection_reason, + created_at: turn_steer.created_at, + } +} + pub(crate) fn thread_source_name(thread_source: &SessionSource) -> Option<&'static str> { match thread_source { SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec => Some("user"), diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 1c14135581..67b935d3ca 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -72,6 +72,33 @@ pub enum TurnStatus { Interrupted, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerResult { + Accepted, + Rejected, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerRejectionReason { + NoActiveTurn, + ExpectedTurnMismatch, + NonSteerableReview, + NonSteerableCompact, + EmptyInput, +} + +#[derive(Clone)] +pub struct CodexTurnSteerEvent { + pub expected_turn_id: Option, + pub accepted_turn_id: Option, + pub num_input_images: usize, + pub result: TurnSteerResult, + pub rejection_reason: Option, + pub created_at: u64, +} + #[derive(Clone, Debug)] pub struct SkillInvocation { pub skill_name: String, @@ -119,6 +146,7 @@ pub(crate) enum AnalyticsFact { pub(crate) enum CustomAnalyticsFact { TurnResolvedConfig(Box), + TurnSteer(TurnSteerInput), SkillInvoked(SkillInvokedInput), AppMentioned(AppMentionedInput), AppUsed(AppUsedInput), @@ -126,6 +154,11 @@ pub(crate) enum CustomAnalyticsFact { PluginStateChanged(PluginStateChangedInput), } +pub(crate) struct TurnSteerInput { + pub tracking: TrackEventsContext, + pub turn_steer: CodexTurnSteerEvent, +} + pub(crate) struct SkillInvokedInput { pub tracking: TrackEventsContext, pub invocations: Vec, diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index f74b9dc716..49fcf34f80 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -6,11 +6,14 @@ mod reducer; pub use client::AnalyticsEventsClient; pub use events::AppServerRpcTransport; pub use facts::AppInvocation; +pub use facts::CodexTurnSteerEvent; pub use facts::InvocationType; pub use facts::SkillInvocation; pub use facts::TrackEventsContext; pub use facts::TurnResolvedConfigFact; pub use facts::TurnStatus; +pub use facts::TurnSteerRejectionReason; +pub use facts::TurnSteerResult; pub use facts::TurnSubmissionType; pub use facts::build_track_events_context; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index c32d043c90..ad2624095e 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -7,6 +7,7 @@ use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; use crate::events::CodexTurnEventParams; use crate::events::CodexTurnEventRequest; +use crate::events::CodexTurnSteerEventRequest; use crate::events::SkillInvocationEventParams; use crate::events::SkillInvocationEventRequest; use crate::events::ThreadInitializationMode; @@ -16,6 +17,7 @@ use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; +use crate::events::codex_turn_steer_event_params; use crate::events::plugin_state_event_type; use crate::events::thread_source_name; use crate::facts::AnalyticsFact; @@ -28,6 +30,8 @@ use crate::facts::PluginUsedInput; use crate::facts::SkillInvokedInput; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSteerInput; +use crate::facts::TurnSteerResult; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponse; use codex_app_server_protocol::CodexErrorInfo; @@ -86,6 +90,7 @@ struct TurnState { created_at: Option, token_usage: Option, completed: Option, + steer_count: usize, } impl AnalyticsReducer { @@ -126,6 +131,9 @@ impl AnalyticsReducer { CustomAnalyticsFact::TurnResolvedConfig(input) => { self.ingest_turn_resolved_config(*input, out); } + CustomAnalyticsFact::TurnSteer(input) => { + self.ingest_turn_steer(input, out); + } CustomAnalyticsFact::SkillInvoked(input) => { self.ingest_skill_invoked(input, out).await; } @@ -210,6 +218,7 @@ impl AnalyticsReducer { created_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.thread_id = Some(thread_id); turn_state.num_input_images = Some(num_input_images); @@ -363,6 +372,7 @@ impl AnalyticsReducer { created_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.connection_id = Some(connection_id); turn_state.thread_id = Some(pending_request.thread_id); @@ -388,6 +398,7 @@ impl AnalyticsReducer { created_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.created_at = u64::try_from(notification.created_at).ok(); } @@ -400,6 +411,7 @@ impl AnalyticsReducer { created_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.token_usage = Some(notification.token_usage.last); } @@ -415,6 +427,7 @@ impl AnalyticsReducer { created_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.completed = Some(CompletedTurnState { status: analytics_turn_status(notification.turn.status), @@ -465,6 +478,23 @@ impl AnalyticsReducer { )); } + fn ingest_turn_steer(&mut self, input: TurnSteerInput, out: &mut Vec) { + let TurnSteerInput { + tracking, + turn_steer, + } = input; + if matches!(turn_steer.result, TurnSteerResult::Accepted) + && let Some(accepted_turn_id) = turn_steer.accepted_turn_id.as_ref() + && let Some(turn_state) = self.turns.get_mut(accepted_turn_id) + { + turn_state.steer_count += 1; + } + out.push(TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest { + event_type: "codex_turn_steer_event", + event_params: codex_turn_steer_event_params(&tracking, turn_steer), + })); + } + fn maybe_emit_turn_event(&mut self, turn_id: &str, out: &mut Vec) { let Some(turn_state) = self.turns.get(turn_id) else { return; @@ -550,7 +580,7 @@ fn codex_turn_event_params( is_first_turn, status: completed.status, turn_error: completed.turn_error, - steer_count: None, + steer_count: Some(turn_state.steer_count), total_tool_call_count: None, shell_command_count: None, file_change_count: None, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 69dbded68b..a220b9593f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -5,6 +5,8 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicU64; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; use crate::agent::AgentControl; use crate::agent::AgentStatus; @@ -52,8 +54,12 @@ use chrono::Local; use chrono::Utc; use codex_analytics::AnalyticsEventsClient; use codex_analytics::AppInvocation; +use codex_analytics::CodexTurnSteerEvent; use codex_analytics::InvocationType; +use codex_analytics::TrackEventsContext; use codex_analytics::TurnResolvedConfigFact; +use codex_analytics::TurnSteerRejectionReason; +use codex_analytics::TurnSteerResult; use codex_analytics::TurnSubmissionType; use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; @@ -237,6 +243,18 @@ impl SteerInputError { }, } } + + fn to_turn_steer_rejection_reason(&self) -> TurnSteerRejectionReason { + match self { + Self::NoActiveTurn(_) => TurnSteerRejectionReason::NoActiveTurn, + Self::ExpectedTurnMismatch { .. } => TurnSteerRejectionReason::ExpectedTurnMismatch, + Self::ActiveTurnNotSteerable { turn_kind } => match turn_kind { + NonSteerableTurnKind::Review => TurnSteerRejectionReason::NonSteerableReview, + NonSteerableTurnKind::Compact => TurnSteerRejectionReason::NonSteerableCompact, + }, + Self::EmptyInput => TurnSteerRejectionReason::EmptyInput, + } + } } /// Notes from the previous real user turn. @@ -4027,47 +4045,159 @@ impl Session { input: Vec, expected_turn_id: Option<&str>, ) -> Result { + let created_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let thread_id = self.conversation_id.to_string(); + let fallback_tracking = || { + build_track_events_context( + String::new(), + thread_id.clone(), + expected_turn_id.unwrap_or_default().to_string(), + ) + }; + let track_rejection = |tracking: TrackEventsContext, + expected_turn_id: Option, + rejection_reason: TurnSteerRejectionReason, + num_input_images: usize| { + self.services.analytics_events_client.track_turn_steer( + tracking, + CodexTurnSteerEvent { + expected_turn_id, + accepted_turn_id: None, + num_input_images, + result: TurnSteerResult::Rejected, + rejection_reason: Some(rejection_reason), + created_at, + }, + ); + }; + if input.is_empty() { - return Err(SteerInputError::EmptyInput); + let err = SteerInputError::EmptyInput; + track_rejection( + fallback_tracking(), + expected_turn_id.map(str::to_string), + err.to_turn_steer_rejection_reason(), + /*num_input_images*/ 0, + ); + return Err(err); } + let num_input_images = input + .iter() + .filter(|item| matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })) + .count(); + let mut active = self.active_turn.lock().await; let Some(active_turn) = active.as_mut() else { - return Err(SteerInputError::NoActiveTurn(input)); + let err = SteerInputError::NoActiveTurn(input); + track_rejection( + fallback_tracking(), + expected_turn_id.map(str::to_string), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); }; - let Some((active_turn_id, _)) = active_turn.tasks.first() else { - return Err(SteerInputError::NoActiveTurn(input)); + let Some((active_turn_id, task)) = active_turn.tasks.first() else { + let err = SteerInputError::NoActiveTurn(input); + track_rejection( + fallback_tracking(), + expected_turn_id.map(str::to_string), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); }; + let active_turn_id = active_turn_id.clone(); + let tracking = build_track_events_context( + task.turn_context.model_info.slug.clone(), + thread_id.clone(), + task.turn_context.sub_id.clone(), + ); if let Some(expected_turn_id) = expected_turn_id && expected_turn_id != active_turn_id { - return Err(SteerInputError::ExpectedTurnMismatch { + let err = SteerInputError::ExpectedTurnMismatch { expected: expected_turn_id.to_string(), - actual: active_turn_id.clone(), - }); + actual: active_turn_id, + }; + track_rejection( + tracking, + Some(expected_turn_id.to_string()), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); } match active_turn.tasks.first().map(|(_, task)| task.kind) { Some(crate::state::TaskKind::Regular) => {} Some(crate::state::TaskKind::Review) => { - return Err(SteerInputError::ActiveTurnNotSteerable { + let err = SteerInputError::ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind::Review, - }); + }; + track_rejection( + tracking, + expected_turn_id + .map(str::to_string) + .or(Some(active_turn_id.clone())), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); } Some(crate::state::TaskKind::Compact) => { - return Err(SteerInputError::ActiveTurnNotSteerable { + let err = SteerInputError::ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind::Compact, - }); + }; + track_rejection( + tracking, + expected_turn_id + .map(str::to_string) + .or(Some(active_turn_id.clone())), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); + } + None => { + let err = SteerInputError::NoActiveTurn(input); + track_rejection( + fallback_tracking(), + expected_turn_id.map(str::to_string), + err.to_turn_steer_rejection_reason(), + num_input_images, + ); + return Err(err); } - None => return Err(SteerInputError::NoActiveTurn(input)), } let mut turn_state = active_turn.turn_state.lock().await; turn_state.push_pending_input(input.into()); turn_state.accept_mailbox_delivery_for_current_turn(); - Ok(active_turn_id.clone()) + drop(turn_state); + drop(active); + + let expected_turn_id = expected_turn_id + .map(str::to_string) + .unwrap_or_else(|| active_turn_id.clone()); + self.services.analytics_events_client.track_turn_steer( + tracking, + CodexTurnSteerEvent { + expected_turn_id: Some(expected_turn_id), + accepted_turn_id: Some(active_turn_id.clone()), + num_input_images, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at, + }, + ); + Ok(active_turn_id) } /// Returns the input if there was no task running to inject into.