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 ca8487b792c9..daa897a4b801 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1760,6 +1760,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-1".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1818,6 +1819,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-1".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1920,6 +1922,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1971,6 +1974,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -2023,6 +2027,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -2086,6 +2091,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -2154,6 +2160,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -2232,6 +2239,7 @@ mod tests { })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-image".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -2344,6 +2352,7 @@ mod tests { }), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".into()), + started_at: None, reason: TurnAbortReason::Replaced, completed_at: None, duration_ms: None, @@ -2583,6 +2592,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3151,6 +3161,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3193,6 +3204,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-b".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3249,6 +3261,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3291,6 +3304,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-b".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3479,6 +3493,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3501,6 +3516,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3513,6 +3529,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-b".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3551,6 +3568,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3573,6 +3591,7 @@ mod tests { }), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-a".into()), + started_at: None, reason: TurnAbortReason::Replaced, completed_at: None, duration_ms: None, @@ -3616,6 +3635,7 @@ mod tests { }), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-compact".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3878,6 +3898,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3943,6 +3964,7 @@ mod tests { }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3998,6 +4020,7 @@ mod tests { RolloutItem::ResponseItem(hook_prompt), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -4079,6 +4102,7 @@ mod tests { }), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -4244,6 +4268,7 @@ mod tests { let complete_changes = builder.handle_rollout_item_with_changes(&RolloutItem::EventMsg( EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: Some(20), duration_ms: Some(123), @@ -4324,6 +4349,7 @@ mod tests { })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), + started_at: None, last_agent_message: None, completed_at: Some(20), duration_ms: Some(123), diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 829aa3f12655..f7051bdb2441 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2191,6 +2191,7 @@ mod tests { fn turn_complete_event(turn_id: &str) -> TurnCompleteEvent { TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: Some(TEST_TURN_COMPLETED_AT), duration_ms: Some(TEST_TURN_DURATION_MS), @@ -2201,6 +2202,7 @@ mod tests { fn turn_aborted_event(turn_id: &str) -> TurnAbortedEvent { TurnAbortedEvent { turn_id: Some(turn_id.to_string()), + started_at: None, reason: codex_protocol::protocol::TurnAbortReason::Interrupted, completed_at: Some(TEST_TURN_COMPLETED_AT), duration_ms: Some(TEST_TURN_DURATION_MS), diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 8337575e5265..efdd2c9eed4a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -2268,6 +2268,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re "type": "event_msg", "payload": serde_json::to_value(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(interrupted_turn_id.to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/agent/control/residency_tests.rs b/codex-rs/core/src/agent/control/residency_tests.rs index cf043971e269..abc85a74f97a 100644 --- a/codex-rs/core/src/agent/control/residency_tests.rs +++ b/codex-rs/core/src/agent/control/residency_tests.rs @@ -159,6 +159,7 @@ async fn mark_thread_completed(thread: &CodexThread) { turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn.sub_id.clone(), + started_at: None, last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, @@ -178,6 +179,7 @@ async fn mark_thread_interrupted(thread: &CodexThread) { turn.as_ref(), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(turn.sub_id.clone()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 355f45d9af2e..8b34c37b5eb3 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -349,6 +349,7 @@ async fn on_event_updates_status_from_task_started() { async fn on_event_updates_status_from_task_complete() { let status = agent_status_from_event(&EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-1".to_string(), + started_at: None, last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, @@ -373,6 +374,7 @@ async fn on_event_updates_status_from_error() { async fn on_event_updates_status_from_turn_aborted() { let status = agent_status_from_event(&EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -2025,6 +2027,7 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() { tester_turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: tester_turn.sub_id.clone(), + started_at: None, last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, @@ -2112,6 +2115,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() { tester_turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: tester_turn.sub_id.clone(), + started_at: None, last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index 5cb2251a4a38..6a6997472ba6 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -56,6 +56,7 @@ async fn forward_events_filters_private_events_before_blocked_send_is_cancelled( id: "full".to_string(), msg: EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 81fae5716b33..7fb22342f0a5 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -1177,6 +1177,7 @@ mod tests { id: turn_id.to_string(), msg: EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: last_agent_message.map(str::to_string), completed_at: None, duration_ms: None, @@ -1190,6 +1191,7 @@ mod tests { id: turn_id.to_string(), msg: EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(turn_id.to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 22a11ee5f19f..651e822cbba6 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -2229,6 +2229,7 @@ async fn guardian_reused_trunk_ignores_stale_prior_turn_completion() -> anyhow:: id: "stale-turn".to_string(), msg: EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "stale-turn".to_string(), + started_at: None, last_agent_message: Some( "{\"risk_level\":\"high\",\"user_authorization\":\"low\",\"outcome\":\"deny\",\"rationale\":\"stale guardian rationale\"}" .to_string(), diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 460ae21d1c39..01005643a3bb 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -97,6 +97,7 @@ fn completed_user_turn_rollout( codex_protocol::protocol::TurnCompleteEvent { turn_id, last_agent_message: None, + started_at: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, @@ -265,6 +266,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif codex_protocol::protocol::TurnCompleteEvent { turn_id, last_agent_message: None, + started_at: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, @@ -339,6 +341,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -373,6 +376,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: rolled_back_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -452,6 +456,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -549,6 +554,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -579,6 +585,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: second_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -598,6 +605,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: standalone_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -675,6 +683,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -696,6 +705,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: assistant_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -768,6 +778,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: only_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -821,6 +832,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: user_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -840,6 +852,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: standalone_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -897,6 +910,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1226,6 +1240,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: current_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1306,6 +1321,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1418,6 +1434,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1446,6 +1463,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu RolloutItem::EventMsg(EventMsg::TurnAborted( codex_protocol::protocol::TurnAbortedEvent { turn_id: None, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1540,6 +1558,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1568,6 +1587,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo RolloutItem::EventMsg(EventMsg::TurnAborted( codex_protocol::protocol::TurnAbortedEvent { turn_id: Some(unmatched_abort_turn_id), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1577,6 +1597,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: current_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1667,6 +1688,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -1836,6 +1858,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index b7cc02fadcdf..c17b06b9b3ef 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -486,6 +486,7 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted let EventMsg::TurnAborted(TurnAbortedEvent { turn_id, reason, + started_at, completed_at, duration_ms, }) = second.msg @@ -494,6 +495,7 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted }; assert_eq!(turn_id, Some(tc.sub_id.clone())); assert_eq!(reason, TurnAbortReason::Interrupted); + assert!(started_at.is_some()); assert!(completed_at.is_some()); assert!(duration_ms.is_some()); } @@ -3134,6 +3136,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { codex_protocol::protocol::TurnCompleteEvent { turn_id, last_agent_message: None, + started_at: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, @@ -3333,6 +3336,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context RolloutItem::ResponseItem(turn_one_assistant.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3362,6 +3366,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context RolloutItem::ResponseItem(turn_two_assistant), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: rolled_back_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3451,6 +3456,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio RolloutItem::ResponseItem(assistant_message("turn 1 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: first_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3475,6 +3481,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio }), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: compact_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3507,6 +3514,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio RolloutItem::ResponseItem(assistant_message("turn 2 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: rolled_back_turn_id, + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3580,6 +3588,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::ResponseItem(assistant_message("turn 1 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-1".to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3607,6 +3616,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::ResponseItem(assistant_message("turn 2 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-2".to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -3634,6 +3644,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::ResponseItem(assistant_message("turn 3 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-3".to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 7a930bb5789c..f3bc02f5cf8f 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -740,6 +740,7 @@ impl Session { turn_context.config.memories.use_memories, turn_had_memory_citation, ); + let started_at = turn_context.turn_timing_state.started_at_unix_secs().await; let (completed_at, duration_ms) = turn_context .turn_timing_state .completed_at_and_duration_ms() @@ -756,6 +757,7 @@ impl Session { EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(turn_context.sub_id.clone()), reason, + started_at, completed_at, duration_ms, }) @@ -769,6 +771,7 @@ impl Session { EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_context.sub_id.clone(), last_agent_message, + started_at, completed_at, duration_ms, time_to_first_token_ms, @@ -877,6 +880,11 @@ impl Session { } } + let started_at = task + .turn_context + .turn_timing_state + .started_at_unix_secs() + .await; let (completed_at, duration_ms) = task .turn_context .turn_timing_state @@ -891,6 +899,7 @@ impl Session { let event = EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(task.turn_context.sub_id.clone()), reason, + started_at, completed_at, duration_ms, }); diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index de37484f8bee..66fbe42984cf 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1786,6 +1786,7 @@ fn truncate_before_nth_user_message( struct SnapshotTurnState { ends_mid_turn: bool, active_turn_id: Option, + active_turn_started_at: Option, active_turn_start_index: Option, } @@ -1805,6 +1806,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState { return SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }; } @@ -1812,6 +1814,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState { return SnapshotTurnState { ends_mid_turn: true, active_turn_id, + active_turn_started_at: active_turn_snapshot.and_then(|turn| turn.started_at), active_turn_start_index: builder.active_turn_start_index(), }; } @@ -1823,6 +1826,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState { return SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }; }; @@ -1838,6 +1842,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState { ) }), active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, } } @@ -1865,6 +1870,7 @@ fn fork_history_from_snapshot( append_interrupted_boundary( history, snapshot_state.active_turn_id, + snapshot_state.active_turn_started_at, interrupted_marker, ) } else { @@ -1880,11 +1886,13 @@ fn fork_history_from_snapshot( fn append_interrupted_boundary( history: InitialHistory, turn_id: Option, + started_at: Option, interrupted_marker: InterruptedTurnHistoryMarker, ) -> InitialHistory { let aborted_event = RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id, reason: TurnAbortReason::Interrupted, + started_at, completed_at: None, duration_ms: None, })); diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index d29d6d04e5d1..7d00e9a9ce08 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -199,6 +199,7 @@ fn truncates_before_requested_user_message() { &SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -224,6 +225,7 @@ fn truncates_before_requested_user_message() { &SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -248,6 +250,7 @@ fn out_of_range_truncation_drops_only_unfinished_suffix_mid_turn() { &SnapshotTurnState { ends_mid_turn: true, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -299,6 +302,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() { SnapshotTurnState { ends_mid_turn: true, active_turn_id: Some("turn-2".to_string()), + active_turn_started_at: None, active_turn_start_index: Some(2), }, ); @@ -340,6 +344,7 @@ async fn ignores_session_prefix_messages_when_truncating() { &SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -1379,6 +1384,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() { append_interrupted_boundary( committed_history, /*turn_id*/ None, + /*started_at*/ None, InterruptedTurnHistoryMarker::ContextualUser, ) .get_rollout_items() @@ -1389,6 +1395,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() { RolloutItem::ResponseItem(contextual_user_interrupted_marker()), RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: None, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1401,6 +1408,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() { append_interrupted_boundary( InitialHistory::New, /*turn_id*/ None, + /*started_at*/ None, InterruptedTurnHistoryMarker::ContextualUser, ) .get_rollout_items() @@ -1410,6 +1418,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() { RolloutItem::ResponseItem(contextual_user_interrupted_marker()), RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: None, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1429,6 +1438,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() { append_interrupted_boundary( committed_history, /*turn_id*/ None, + /*started_at*/ None, InterruptedTurnHistoryMarker::Disabled, ) .get_rollout_items() @@ -1438,6 +1448,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() { RolloutItem::ResponseItem(user_msg("hello")), RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: None, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1450,6 +1461,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() { append_interrupted_boundary( InitialHistory::New, /*turn_id*/ None, + /*started_at*/ None, InterruptedTurnHistoryMarker::Disabled, ) .get_rollout_items() @@ -1458,6 +1470,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() { serde_json::to_value(vec![RolloutItem::EventMsg(EventMsg::TurnAborted( TurnAbortedEvent { turn_id: None, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1475,6 +1488,7 @@ fn interrupted_snapshot_is_not_mid_turn() { RolloutItem::ResponseItem(contextual_user_interrupted_marker()), RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1486,6 +1500,7 @@ fn interrupted_snapshot_is_not_mid_turn() { SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -1532,6 +1547,7 @@ fn completed_legacy_event_history_is_not_mid_turn() { SnapshotTurnState { ends_mid_turn: false, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -1556,6 +1572,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() { SnapshotTurnState { ends_mid_turn: true, active_turn_id: None, + active_turn_started_at: None, active_turn_start_index: None, }, ); @@ -1642,6 +1659,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor let interrupted_abort_json = serde_json::to_value(RolloutItem::EventMsg( EventMsg::TurnAborted(TurnAbortedEvent { turn_id: expected_turn_id, + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, @@ -1729,6 +1747,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { SnapshotTurnState { ends_mid_turn: true, active_turn_id: Some("turn-explicit".to_string()), + active_turn_started_at: None, active_turn_start_index: Some(1), }, ); @@ -1761,6 +1780,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { item, RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(turn_id), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs index 16d10a827728..c0f8819c83eb 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -82,6 +82,7 @@ fn turn_started(turn_id: &str) -> RolloutItem { fn turn_completed(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, 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 7d50d567b936..620e85498945 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1558,6 +1558,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_without_encrypted_s child_turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: child_turn.sub_id.clone(), + started_at: None, last_agent_message: Some("done".to_string()), completed_at: None, duration_ms: None, @@ -2007,6 +2008,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() first_turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: first_turn.sub_id.clone(), + started_at: None, last_agent_message: Some("first done".to_string()), completed_at: None, duration_ms: None, @@ -2048,6 +2050,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() second_turn.as_ref(), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: second_turn.sub_id.clone(), + started_at: None, last_agent_message: Some("second done".to_string()), completed_at: None, duration_ms: None, @@ -2210,6 +2213,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() { aborted_turn.as_ref(), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(aborted_turn.sub_id.clone()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 3e135abf4d9b..f88bda56db22 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -74,6 +74,7 @@ fn resume_history( RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id, last_agent_message: None, + started_at: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, diff --git a/codex-rs/external-agent-sessions/src/export.rs b/codex-rs/external-agent-sessions/src/export.rs index 992770f85547..4352f74f7580 100644 --- a/codex-rs/external-agent-sessions/src/export.rs +++ b/codex-rs/external-agent-sessions/src/export.rs @@ -81,8 +81,13 @@ fn rollout_items_from_messages(messages: Vec) -> Vec { - if let Some(turn_id) = current_turn.take() { - items.push(turn_complete_item(turn_id, /*completed_at*/ None)); + let started_at = message.timestamp; + if let Some((turn_id, previous_started_at)) = current_turn.take() { + items.push(turn_complete_item( + turn_id, + previous_started_at, + /*completed_at*/ None, + )); } user_turn_count += 1; let turn_id = format!("external-import-turn-{user_turn_count}"); @@ -90,7 +95,7 @@ fn rollout_items_from_messages(messages: Vec) -> Vec) -> Vec { if current_turn.is_none() { @@ -125,10 +130,10 @@ fn rollout_items_from_messages(messages: Vec) -> Vec RolloutItem { })) } -fn turn_complete_item(turn_id: String, completed_at: Option) -> RolloutItem { +fn turn_complete_item( + turn_id: String, + started_at: Option, + completed_at: Option, +) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id, last_agent_message: None, + started_at, completed_at, duration_ms: None, time_to_first_token_ms: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 57a36c3312c9..0935bea46f04 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1955,6 +1955,10 @@ pub struct ContextCompactedEvent; pub struct TurnCompleteEvent { pub turn_id: String, pub last_agent_message: Option, + /// Unix timestamp (in seconds) when the turn started. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "number | null", optional)] + pub started_at: Option, /// Unix timestamp (in seconds) when the turn completed. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(type = "number | null", optional)] @@ -4105,6 +4109,10 @@ pub struct Chunk { pub struct TurnAbortedEvent { pub turn_id: Option, pub reason: TurnAbortReason, + /// Unix timestamp (in seconds) when the turn started. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "number | null", optional)] + pub started_at: Option, /// Unix timestamp (in seconds) when the turn was aborted. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(type = "number | null", optional)] diff --git a/codex-rs/rollout/src/persistence_metrics_tests.rs b/codex-rs/rollout/src/persistence_metrics_tests.rs index c5f6d6c9c873..d7a8e80cf680 100644 --- a/codex-rs/rollout/src/persistence_metrics_tests.rs +++ b/codex-rs/rollout/src/persistence_metrics_tests.rs @@ -51,6 +51,7 @@ fn turn_started(turn_id: &str) -> RolloutItem { fn turn_complete(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None, @@ -61,6 +62,7 @@ fn turn_complete(turn_id: &str) -> RolloutItem { fn turn_aborted(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(turn_id.to_string()), + started_at: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 627b001c494e..dd5891633e62 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -538,6 +538,7 @@ mod tests { )), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-1".to_string(), + started_at: None, last_agent_message: None, completed_at: None, duration_ms: None,