From d664a6f7805430f65b740d12689a2e27595b8a1f Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Fri, 17 Apr 2026 10:15:34 -0700 Subject: [PATCH] [codex-analytics] guardian review TTFT plumbing and emission --- .../src/protocol/thread_history.rs | 16 +++++++++++++++ .../app-server/src/bespoke_event_handling.rs | 1 + codex-rs/core/src/agent/control_tests.rs | 3 +++ .../src/codex/rollout_reconstruction_tests.rs | 20 +++++++++++++++++++ codex-rs/core/src/codex_tests.rs | 10 ++++++++++ codex-rs/core/src/guardian/review.rs | 1 + codex-rs/core/src/guardian/review_session.rs | 15 ++++++++++++-- codex-rs/core/src/guardian/tests.rs | 4 ++++ codex-rs/core/src/tasks/mod.rs | 5 +++++ .../src/tools/handlers/multi_agents_tests.rs | 3 +++ codex-rs/core/src/turn_timing.rs | 18 +++++++++++++---- codex-rs/core/tests/suite/resume_warning.rs | 1 + codex-rs/protocol/src/protocol.rs | 4 ++++ codex-rs/tui/src/app/app_server_adapter.rs | 2 ++ .../tui/src/chatwidget/tests/exec_flow.rs | 5 +++++ .../tui/src/chatwidget/tests/plan_mode.rs | 4 ++++ .../tui/src/chatwidget/tests/review_mode.rs | 2 ++ .../src/chatwidget/tests/slash_commands.rs | 5 +++++ .../src/chatwidget/tests/status_and_layout.rs | 3 +++ 19 files changed, 116 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 9e94515dd9..a390d33392 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1332,6 +1332,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -1406,6 +1407,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]; @@ -1729,6 +1731,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -2235,6 +2238,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), @@ -2272,6 +2276,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -2324,6 +2329,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), @@ -2361,6 +2367,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -2535,6 +2542,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), @@ -2553,6 +2561,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::AgentMessage(AgentMessageEvent { message: "still in b".into(), @@ -2564,6 +2573,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -2598,6 +2608,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), @@ -2654,6 +2665,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]; @@ -2899,6 +2911,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), EventMsg::Error(ErrorEvent { message: "request-level failure".into(), @@ -2958,6 +2971,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ]; @@ -3009,6 +3023,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]; @@ -3057,6 +3072,7 @@ mod tests { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]; diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 4fddee4a11..d5195f5960 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3042,6 +3042,7 @@ mod tests { last_agent_message: None, completed_at: Some(TEST_TURN_COMPLETED_AT), duration_ms: Some(TEST_TURN_DURATION_MS), + time_to_first_token_ms: None, } } diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 6fc74b30e8..2e800ea198 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -275,6 +275,7 @@ async fn on_event_updates_status_from_task_complete() { last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })); let expected = AgentStatus::Completed(Some("done".to_string())); assert_eq!(status, Some(expected)); @@ -1225,6 +1226,7 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() { last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ) .await; @@ -1311,6 +1313,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() { last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ) .await; diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index 753244ac2b..f3d62a090c 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -148,6 +148,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), ]; @@ -215,6 +216,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -242,6 +244,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( @@ -311,6 +314,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -400,6 +404,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -426,6 +431,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -443,6 +449,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( @@ -515,6 +522,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -534,6 +542,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( @@ -601,6 +610,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( @@ -650,6 +660,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), // Standalone task turn (no UserMessage) should not consume rollback skips. @@ -667,6 +678,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( @@ -720,6 +732,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -876,6 +889,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), ]; @@ -945,6 +959,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), ]; @@ -1046,6 +1061,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -1153,6 +1169,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -1186,6 +1203,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), ]; @@ -1268,6 +1286,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( @@ -1418,6 +1437,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 6de8909e6d..6e4e870e48 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1381,6 +1381,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }, )), ]; @@ -1566,6 +1567,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { @@ -1591,6 +1593,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]) .await; @@ -1668,6 +1671,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { @@ -1686,6 +1690,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { @@ -1713,6 +1718,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]) .await; @@ -1759,6 +1765,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { @@ -1782,6 +1789,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { @@ -1805,6 +1813,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ]) .await; @@ -4785,6 +4794,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() EventMsg::TurnComplete(TurnCompleteEvent { turn_id, last_agent_message: None, + time_to_first_token_ms: None, .. }) if turn_id == tc.sub_id )); diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index b53db08991..be53e33d97 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -282,6 +282,7 @@ impl GuardianReviewAnalyticsResult { result.had_prior_review_context = Some(metadata.had_prior_review_context); result.reviewed_action_truncated = metadata.reviewed_action_truncated; result.token_usage = metadata.token_usage; + result.time_to_first_token_ms = metadata.time_to_first_token_ms; } result diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 270b9dd151..558116c42d 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -74,6 +74,7 @@ pub(crate) struct GuardianReviewSessionMetadata { pub(crate) had_prior_review_context: bool, pub(crate) reviewed_action_truncated: bool, pub(crate) token_usage: Option, + pub(crate) time_to_first_token_ms: Option, } pub(crate) struct GuardianReviewSessionParams { @@ -623,6 +624,7 @@ async fn run_review_on_session( had_prior_review_context: had_prior_review_context(&prompt_mode), reviewed_action_truncated: false, token_usage: None, + time_to_first_token_ms: None, }; if send_followup_reminder { append_guardian_followup_reminder(review_session).await; @@ -701,8 +703,13 @@ async fn run_review_on_session( }; guardian_metadata.reviewed_action_truncated = reviewed_action_truncated; - let outcome = - wait_for_guardian_review(review_session, deadline, params.external_cancel.as_ref()).await; + let outcome = wait_for_guardian_review( + review_session, + deadline, + params.external_cancel.as_ref(), + &mut guardian_metadata, + ) + .await; if matches!(outcome.0, GuardianReviewSessionOutcome::Completed(_)) { if outcome.2 && let Some(total_token_usage) = review_session.codex.session.total_token_usage().await @@ -745,6 +752,7 @@ async fn wait_for_guardian_review( review_session: &GuardianReviewSession, deadline: tokio::time::Instant, external_cancel: Option<&CancellationToken>, + metadata: &mut GuardianReviewSessionMetadata, ) -> (GuardianReviewSessionOutcome, bool, bool) { let timeout = tokio::time::sleep_until(deadline); tokio::pin!(timeout); @@ -770,6 +778,9 @@ async fn wait_for_guardian_review( match event { Ok(event) => match event.msg { EventMsg::TurnComplete(turn_complete) => { + metadata.time_to_first_token_ms = turn_complete + .time_to_first_token_ms + .and_then(|ms| u64::try_from(ms).ok()); if turn_complete.last_agent_message.is_none() && let Some(error_message) = last_error_message { diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 3aca90255d..19e1520210 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -959,6 +959,10 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() assert_eq!(metadata.guardian_model, "gpt-5.4"); assert_eq!(metadata.guardian_reasoning_effort.as_deref(), Some("low")); assert!(!metadata.had_prior_review_context); + assert!( + metadata.time_to_first_token_ms.is_some(), + "guardian review metadata should capture TTFT when the nested turn completes" + ); let request = request_log.single_request(); let mut settings = Settings::clone_current(); diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index f17017316e..692a5919a3 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -535,11 +535,16 @@ impl Session { .turn_timing_state .completed_at_and_duration_ms() .await; + let time_to_first_token_ms = turn_context + .turn_timing_state + .time_to_first_token_ms() + .await; let event = EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_context.sub_id.clone(), last_agent_message, completed_at, duration_ms, + time_to_first_token_ms, }); self.send_event(turn_context.as_ref(), event).await; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index e35d6a06fb..7c55be087d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1154,6 +1154,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ) .await; @@ -1631,6 +1632,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() last_agent_message: Some("first done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ) .await; @@ -1659,6 +1661,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() last_agent_message: Some("second done".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), ) .await; diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index 6c47d3b528..a5ce8df8bd 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -74,6 +74,13 @@ impl TurnTimingState { (completed_at, duration_ms) } + pub(crate) async fn time_to_first_token_ms(&self) -> Option { + let state = self.state.lock().await; + state + .time_to_first_token() + .map(|duration| i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)) + } + pub(crate) async fn record_ttft_for_response_event( &self, event: &ResponseEvent, @@ -102,14 +109,17 @@ fn now_unix_timestamp_secs() -> i64 { } impl TurnTimingStateInner { + fn time_to_first_token(&self) -> Option { + Some(self.first_token_at?.duration_since(self.started_at?)) + } + fn record_turn_ttft(&mut self) -> Option { if self.first_token_at.is_some() { return None; } - let started_at = self.started_at?; - let first_token_at = Instant::now(); - self.first_token_at = Some(first_token_at); - Some(first_token_at.duration_since(started_at)) + self.started_at?; + self.first_token_at = Some(Instant::now()); + self.time_to_first_token() } fn record_turn_ttfm(&mut self) -> Option { diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index f9ad64c771..70c9014747 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -69,6 +69,7 @@ fn resume_history( last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })), ], rollout_path: rollout_path.to_path_buf(), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 7d20fadeb4..73045ba734 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2057,6 +2057,10 @@ pub struct TurnCompleteEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(type = "number | null", optional)] pub duration_ms: Option, + /// Duration between turn start and the first model token in milliseconds, if known. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "number | null", optional)] + pub time_to_first_token_ms: Option, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index 09b8ce06cd..2107aef4b7 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -762,6 +762,7 @@ fn append_terminal_turn_events(events: &mut Vec, turn: &Turn, include_fai last_agent_message: None, completed_at: turn.completed_at, duration_ms: turn.duration_ms, + time_to_first_token_ms: None, }), }), TurnStatus::Interrupted => events.push(Event { @@ -793,6 +794,7 @@ fn append_terminal_turn_events(events: &mut Vec, turn: &Turn, include_fai last_agent_message: None, completed_at: turn.completed_at, duration_ms: turn.duration_ms, + time_to_first_token_ms: None, }), }); } diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 53f16a39d1..261fd47f3f 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -654,6 +654,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() { last_agent_message: Some("Final response.".into()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -699,6 +700,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -766,6 +768,7 @@ async fn unified_exec_waiting_multiple_empty_snapshots() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -846,6 +849,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -1350,6 +1354,7 @@ async fn turn_complete_keeps_unified_exec_processes() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index f3746fdbf0..2cd2790ee6 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -546,6 +546,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { last_agent_message: Some("Plan details".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })]); let popup = render_bottom_popup(&chat, /*width*/ 80); @@ -572,6 +573,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com last_agent_message: Some("Plan details".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, })]); let replay_popup = render_bottom_popup(&chat, /*width*/ 80); assert!( @@ -586,6 +588,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com last_agent_message: Some("Plan details".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -609,6 +612,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com last_agent_message: Some("Plan details".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); let duplicate_popup = render_bottom_popup(&chat, /*width*/ 80); diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index c42ec4fbac..9a05320aa6 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -234,6 +234,7 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -255,6 +256,7 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 2afb959349..27cd96e388 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -225,6 +225,7 @@ async fn slash_copy_state_tracks_turn_complete_final_reply() { last_agent_message: Some("Final reply **markdown**".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -257,6 +258,7 @@ async fn slash_copy_state_tracks_plan_item_completion() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -343,6 +345,7 @@ async fn slash_copy_state_is_preserved_during_running_task() { last_agent_message: Some("Previous completed reply".to_string()), completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); chat.on_task_started(); @@ -373,6 +376,7 @@ async fn slash_copy_tracks_replayed_legacy_agent_message_when_turn_complete_omit last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); let _ = drain_insert_history(&mut rx); @@ -410,6 +414,7 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text( last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); let _ = drain_insert_history(&mut rx); diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index e59928ff1c..a294e3aadf 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -961,6 +961,7 @@ async fn status_line_branch_refreshes_after_turn_complete() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -1311,6 +1312,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); @@ -2251,6 +2253,7 @@ printf 'fenced within fenced\n' last_agent_message: None, completed_at: None, duration_ms: None, + time_to_first_token_ms: None, }), }); for lines in drain_insert_history(&mut rx) {