From 7fd7c9053fd53409d70b79ec4b4a891acfe6cc76 Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Mon, 4 May 2026 14:49:31 -0700 Subject: [PATCH] [codex-analytics] add core item timing production --- codex-rs/app-server-client/src/lib.rs | 2 + .../schema/json/ServerNotification.json | 14 +++- .../codex_app_server_protocol.schemas.json | 14 +++- .../codex_app_server_protocol.v2.schemas.json | 14 +++- .../json/v2/ItemCompletedNotification.json | 8 ++- .../json/v2/ItemStartedNotification.json | 8 ++- .../v2/ItemCompletedNotification.ts | 6 +- .../typescript/v2/ItemStartedNotification.ts | 6 +- .../src/protocol/event_mapping.rs | 19 +++++ .../src/protocol/thread_history.rs | 11 +++ .../app-server-protocol/src/protocol/v2.rs | 6 ++ .../app-server/src/bespoke_event_handling.rs | 17 +++++ codex-rs/core/src/session/mod.rs | 3 + codex-rs/core/src/tasks/user_shell.rs | 5 ++ codex-rs/core/src/tools/events.rs | 3 + codex-rs/core/src/tools/handlers/dynamic.rs | 5 ++ .../handlers/multi_agents/close_agent.rs | 4 ++ .../handlers/multi_agents/resume_agent.rs | 3 + .../tools/handlers/multi_agents/send_input.rs | 3 + .../src/tools/handlers/multi_agents/spawn.rs | 3 + .../src/tools/handlers/multi_agents/wait.rs | 4 ++ .../handlers/multi_agents_v2/close_agent.rs | 4 ++ .../handlers/multi_agents_v2/message_tool.rs | 3 + .../tools/handlers/multi_agents_v2/spawn.rs | 3 + .../tools/handlers/multi_agents_v2/wait.rs | 3 + codex-rs/core/src/turn_timing.rs | 2 +- codex-rs/core/tests/suite/items.rs | 41 ++++++++++- ...event_processor_with_jsonl_output_tests.rs | 1 + .../tests/event_processor_with_json_output.rs | 25 +++++++ codex-rs/protocol/src/dynamic_tools.rs | 2 + codex-rs/protocol/src/protocol.rs | 72 +++++++++++++++++++ codex-rs/tui/src/app/tests.rs | 2 + .../tui/src/chatwidget/tests/app_server.rs | 11 +++ codex-rs/tui/src/chatwidget/tests/helpers.rs | 11 +++ .../src/chatwidget/tests/history_replay.rs | 1 + .../tui/src/chatwidget/tests/review_mode.rs | 3 + .../src/chatwidget/tests/slash_commands.rs | 1 + 37 files changed, 332 insertions(+), 11 deletions(-) diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 539a1684c54d..1eb16166ac92 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1139,6 +1139,7 @@ mod tests { ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { thread_id: "thread".to_string(), turn_id: "turn".to_string(), + completed_at_ms: 0, item: codex_app_server_protocol::ThreadItem::AgentMessage { id: "item".to_string(), text: text.to_string(), @@ -2012,6 +2013,7 @@ mod tests { codex_app_server_protocol::ItemCompletedNotification { thread_id: "thread".to_string(), turn_id: "turn".to_string(), + completed_at_ms: 0, item: codex_app_server_protocol::ThreadItem::AgentMessage { id: "item".to_string(), text: "hello".to_string(), diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 82914f3a6f22..8b1d158313e8 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1932,6 +1932,11 @@ }, "ItemCompletedNotification": { "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, "item": { "$ref": "#/definitions/ThreadItem" }, @@ -1943,6 +1948,7 @@ } }, "required": [ + "completedAtMs", "item", "threadId", "turnId" @@ -2030,6 +2036,11 @@ "item": { "$ref": "#/definitions/ThreadItem" }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, "threadId": { "type": "string" }, @@ -2039,6 +2050,7 @@ }, "required": [ "item", + "startedAtMs", "threadId", "turnId" ], @@ -5896,4 +5908,4 @@ } ], "title": "ServerNotification" -} \ No newline at end of file +} 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 6c2ad69e1b21..edcca850c23a 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 @@ -10109,6 +10109,11 @@ "ItemCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, "item": { "$ref": "#/definitions/v2/ThreadItem" }, @@ -10120,6 +10125,7 @@ } }, "required": [ + "completedAtMs", "item", "threadId", "turnId" @@ -10213,6 +10219,11 @@ "item": { "$ref": "#/definitions/v2/ThreadItem" }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, "threadId": { "type": "string" }, @@ -10222,6 +10233,7 @@ }, "required": [ "item", + "startedAtMs", "threadId", "turnId" ], @@ -18261,4 +18273,4 @@ }, "title": "CodexAppServerProtocol", "type": "object" -} \ No newline at end of file +} 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 150b221adf13..e58a8451f209 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 @@ -6762,6 +6762,11 @@ "ItemCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, "item": { "$ref": "#/definitions/ThreadItem" }, @@ -6773,6 +6778,7 @@ } }, "required": [ + "completedAtMs", "item", "threadId", "turnId" @@ -6866,6 +6872,11 @@ "item": { "$ref": "#/definitions/ThreadItem" }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, "threadId": { "type": "string" }, @@ -6875,6 +6886,7 @@ }, "required": [ "item", + "startedAtMs", "threadId", "turnId" ], @@ -16146,4 +16158,4 @@ }, "title": "CodexAppServerProtocolV2", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 0831483a327f..4ec0e10bc9f8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1370,6 +1370,11 @@ } }, "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, "item": { "$ref": "#/definitions/ThreadItem" }, @@ -1381,10 +1386,11 @@ } }, "required": [ + "completedAtMs", "item", "threadId", "turnId" ], "title": "ItemCompletedNotification", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 16bfeece144a..868498935680 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1373,6 +1373,11 @@ "item": { "$ref": "#/definitions/ThreadItem" }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, "threadId": { "type": "string" }, @@ -1382,9 +1387,10 @@ }, "required": [ "item", + "startedAtMs", "threadId", "turnId" ], "title": "ItemStartedNotification", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ItemCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ItemCompletedNotification.ts index 96122204b43c..25ced4a0750f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ItemCompletedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ItemCompletedNotification.ts @@ -3,4 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ThreadItem } from "./ThreadItem"; -export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string, }; +export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string, +/** + * Unix timestamp (in milliseconds) when this item lifecycle completed. + */ +completedAtMs: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ItemStartedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ItemStartedNotification.ts index 5cf1e7b91881..9ec8af09e9f3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ItemStartedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ItemStartedNotification.ts @@ -3,4 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ThreadItem } from "./ThreadItem"; -export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string, }; +export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string, +/** + * Unix timestamp (in milliseconds) when this item lifecycle started. + */ +startedAtMs: number, }; diff --git a/codex-rs/app-server-protocol/src/protocol/event_mapping.rs b/codex-rs/app-server-protocol/src/protocol/event_mapping.rs index 2880ce73f320..609ca83a5ddd 100644 --- a/codex-rs/app-server-protocol/src/protocol/event_mapping.rs +++ b/codex-rs/app-server-protocol/src/protocol/event_mapping.rs @@ -69,6 +69,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id: response.turn_id, item, + completed_at_ms: response.completed_at_ms, }) } EventMsg::CollabAgentSpawnBegin(begin_event) => { @@ -87,6 +88,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + started_at_ms: begin_event.started_at_ms, }) } EventMsg::CollabAgentSpawnEnd(end_event) => { @@ -125,6 +127,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + completed_at_ms: end_event.completed_at_ms, }) } EventMsg::CollabAgentInteractionBegin(begin_event) => { @@ -144,6 +147,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + started_at_ms: begin_event.started_at_ms, }) } EventMsg::CollabAgentInteractionEnd(end_event) => { @@ -171,6 +175,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + completed_at_ms: end_event.completed_at_ms, }) } EventMsg::CollabWaitingBegin(begin_event) => { @@ -194,6 +199,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + started_at_ms: begin_event.started_at_ms, }) } EventMsg::CollabWaitingEnd(end_event) => { @@ -229,6 +235,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + completed_at_ms: end_event.completed_at_ms, }) } EventMsg::CollabCloseBegin(begin_event) => { @@ -247,6 +254,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + started_at_ms: begin_event.started_at_ms, }) } EventMsg::CollabCloseEnd(end_event) => { @@ -279,6 +287,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + completed_at_ms: end_event.completed_at_ms, }) } EventMsg::CollabResumeBegin(begin_event) => { @@ -297,6 +306,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + started_at_ms: begin_event.started_at_ms, }) } EventMsg::CollabResumeEnd(end_event) => { @@ -329,6 +339,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item, + completed_at_ms: end_event.completed_at_ms, }) } EventMsg::AgentMessageContentDelta(event) => { @@ -378,6 +389,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item: item_started_event.item.into(), + started_at_ms: item_started_event.started_at_ms, }) } EventMsg::ItemCompleted(item_completed_event) => { @@ -385,6 +397,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item: item_completed_event.item.into(), + completed_at_ms: item_completed_event.completed_at_ms, }) } EventMsg::PatchApplyUpdated(event) => { @@ -400,6 +413,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item: build_command_execution_begin_item(&exec_command_begin_event), + started_at_ms: exec_command_begin_event.started_at_ms, }) } EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => { @@ -428,6 +442,7 @@ pub fn item_event_to_server_notification( thread_id, turn_id, item: build_command_execution_end_item(&exec_command_end_event), + completed_at_ms: exec_command_end_event.completed_at_ms, }) } _ => unreachable!("unsupported item event"), @@ -480,6 +495,7 @@ mod tests { fn collab_resume_begin_maps_to_item_started_resume_agent() { let event = CollabResumeBeginEvent { call_id: "call-1".to_string(), + started_at_ms: 123, sender_thread_id: ThreadId::new(), receiver_thread_id: ThreadId::new(), receiver_agent_nickname: None, @@ -496,6 +512,7 @@ mod tests { ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: event.started_at_ms, item: ThreadItem::CollabAgentToolCall { id: event.call_id, tool: CollabAgentTool::ResumeAgent, @@ -515,6 +532,7 @@ mod tests { fn collab_resume_end_maps_to_item_completed_resume_agent() { let event = CollabResumeEndEvent { call_id: "call-2".to_string(), + completed_at_ms: 456, sender_thread_id: ThreadId::new(), receiver_thread_id: ThreadId::new(), receiver_agent_nickname: None, @@ -533,6 +551,7 @@ mod tests { ItemCompletedNotification { thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), + completed_at_ms: event.completed_at_ms, item: ThreadItem::CollabAgentToolCall { id: event.call_id, tool: CollabAgentTool::ResumeAgent, 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 57eeedec34d0..6228d754d6f5 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1356,6 +1356,7 @@ mod tests { id: "user-item-id".to_string(), content: Vec::new(), }), + started_at_ms: 0, }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: turn_id.to_string(), @@ -1820,6 +1821,7 @@ mod tests { call_id: "exec-1".into(), process_id: Some("pid-1".into()), turn_id: "turn-1".into(), + completed_at_ms: 0, command: vec!["echo".into(), "hello world".into()], cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { @@ -1983,6 +1985,7 @@ mod tests { codex_protocol::dynamic_tools::DynamicToolCallRequest { call_id: "dyn-1".into(), turn_id: "turn-1".into(), + started_at_ms: 0, namespace: Some("codex_app".into()), tool: "lookup_ticket".into(), arguments: serde_json::json!({"id":"ABC-123"}), @@ -1991,6 +1994,7 @@ mod tests { EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { call_id: "dyn-1".into(), turn_id: "turn-1".into(), + completed_at_ms: 0, namespace: Some("codex_app".into()), tool: "lookup_ticket".into(), arguments: serde_json::json!({"id":"ABC-123"}), @@ -2046,6 +2050,7 @@ mod tests { call_id: "exec-declined".into(), process_id: Some("pid-2".into()), turn_id: "turn-1".into(), + completed_at_ms: 0, command: vec!["ls".into()], cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }], @@ -2293,6 +2298,7 @@ mod tests { call_id: "exec-late".into(), process_id: Some("pid-42".into()), turn_id: "turn-a".into(), + completed_at_ms: 0, command: vec!["echo".into(), "done".into()], cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { @@ -2384,6 +2390,7 @@ mod tests { call_id: "exec-unknown-turn".into(), process_id: Some("pid-42".into()), turn_id: "turn-missing".into(), + completed_at_ms: 0, command: vec!["echo".into(), "done".into()], cwd: test_path_buf("/tmp").abs(), parsed_cmd: vec![ParsedCommand::Unknown { @@ -2732,6 +2739,7 @@ mod tests { }), EventMsg::CollabResumeEnd(codex_protocol::protocol::CollabResumeEndEvent { call_id: "resume-1".into(), + completed_at_ms: 0, sender_thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000001") .expect("valid sender thread id"), receiver_thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000002") @@ -2788,6 +2796,7 @@ mod tests { }), EventMsg::CollabAgentSpawnEnd(codex_protocol::protocol::CollabAgentSpawnEndEvent { call_id: "spawn-1".into(), + completed_at_ms: 0, sender_thread_id, new_thread_id: Some(spawned_thread_id), new_agent_nickname: Some("Scout".into()), @@ -2849,6 +2858,7 @@ mod tests { EventMsg::CollabAgentInteractionBegin( codex_protocol::protocol::CollabAgentInteractionBeginEvent { call_id: "send-1".into(), + started_at_ms: 0, sender_thread_id: sender, receiver_thread_id: receiver, prompt: "new task".into(), @@ -2857,6 +2867,7 @@ mod tests { EventMsg::CollabAgentInteractionEnd( codex_protocol::protocol::CollabAgentInteractionEndEvent { call_id: "send-1".into(), + completed_at_ms: 0, sender_thread_id: sender, receiver_thread_id: receiver, receiver_agent_nickname: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index aea80029a87c..053895c2bbf9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -6928,6 +6928,9 @@ pub struct ItemStartedNotification { pub item: ThreadItem, pub thread_id: String, pub turn_id: String, + /// Unix timestamp (in milliseconds) when this item lifecycle started. + #[ts(type = "number")] + pub started_at_ms: i64, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -6990,6 +6993,9 @@ pub struct ItemCompletedNotification { pub item: ThreadItem, pub thread_id: String, pub turn_id: String, + /// Unix timestamp (in milliseconds) when this item lifecycle completed. + #[ts(type = "number")] + pub completed_at_ms: i64, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index cc1a11c01faa..89b5d265790e 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -116,6 +116,8 @@ use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; use tokio::sync::Mutex; use tokio::sync::oneshot; use tracing::error; @@ -816,6 +818,7 @@ pub(crate) async fn apply_bespoke_event_handling( let notification = ItemStartedNotification { thread_id: conversation_id.to_string(), turn_id: turn_id.clone(), + started_at_ms: request.started_at_ms, item, }; outgoing @@ -968,6 +971,7 @@ pub(crate) async fn apply_bespoke_event_handling( let started = ItemStartedNotification { thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), + started_at_ms: now_unix_timestamp_ms(), item: item.clone(), }; outgoing @@ -976,6 +980,7 @@ pub(crate) async fn apply_bespoke_event_handling( let completed = ItemCompletedNotification { thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), item, }; outgoing @@ -1025,6 +1030,7 @@ pub(crate) async fn apply_bespoke_event_handling( let started = ItemStartedNotification { thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), + started_at_ms: now_unix_timestamp_ms(), item: item.clone(), }; outgoing @@ -1033,6 +1039,7 @@ pub(crate) async fn apply_bespoke_event_handling( let completed = ItemCompletedNotification { thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), item, }; outgoing @@ -1368,6 +1375,7 @@ async fn start_command_execution_item( let notification = ItemStartedNotification { thread_id: conversation_id.to_string(), turn_id, + started_at_ms: now_unix_timestamp_ms(), item: ThreadItem::CommandExecution { id: item_id, command, @@ -1427,6 +1435,7 @@ async fn complete_command_execution_item( let notification = ItemCompletedNotification { thread_id: conversation_id.to_string(), turn_id, + completed_at_ms: now_unix_timestamp_ms(), item, }; outgoing @@ -1474,6 +1483,7 @@ pub(crate) async fn maybe_emit_hook_prompt_item_completed( let notification = ItemCompletedNotification { thread_id: conversation_id.to_string(), turn_id: turn_id.to_string(), + completed_at_ms: now_unix_timestamp_ms(), item: ThreadItem::HookPrompt { id: hook_prompt.id, fragments: hook_prompt @@ -2077,6 +2087,13 @@ async fn on_command_execution_request_approval_response( } } +fn now_unix_timestamp_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as i64) + .unwrap_or_default() +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index b697b8e623ba..46d942f71011 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -41,6 +41,7 @@ use crate::session_prefix::format_subagent_notification_message; use crate::skills::SkillRenderSideEffects; use crate::skills_load_input_from_config; use crate::turn_metadata::TurnMetadataState; +use crate::turn_timing::now_unix_timestamp_ms; use async_channel::Receiver; use async_channel::Sender; use chrono::Local; @@ -1652,6 +1653,7 @@ impl Session { thread_id: self.conversation_id, turn_id: turn_context.sub_id.clone(), item: item.clone(), + started_at_ms: now_unix_timestamp_ms(), }), ) .await; @@ -1669,6 +1671,7 @@ impl Session { thread_id: self.conversation_id, turn_id: turn_context.sub_id.clone(), item, + completed_at_ms: now_unix_timestamp_ms(), }), ) .await; diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 23cd076404df..683856b90e6e 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -21,6 +21,7 @@ use crate::session::turn_context::TurnContext; use crate::state::TaskKind; use crate::tools::format_exec_output_str; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; +use crate::turn_timing::now_unix_timestamp_ms; use crate::user_shell_command::user_shell_command_record_item; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; @@ -164,6 +165,7 @@ pub(crate) async fn execute_user_shell_command( call_id: call_id.clone(), process_id: None, turn_id: turn_context.sub_id.clone(), + started_at_ms: now_unix_timestamp_ms(), command: display_command.clone(), cwd: cwd.clone(), parsed_cmd: parsed_cmd.clone(), @@ -236,6 +238,7 @@ pub(crate) async fn execute_user_shell_command( call_id, process_id: None, turn_id: turn_context.sub_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), command: display_command.clone(), cwd: cwd.clone(), parsed_cmd: parsed_cmd.clone(), @@ -260,6 +263,7 @@ pub(crate) async fn execute_user_shell_command( call_id: call_id.clone(), process_id: None, turn_id: turn_context.sub_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), command: display_command.clone(), cwd: cwd.clone(), parsed_cmd: parsed_cmd.clone(), @@ -304,6 +308,7 @@ pub(crate) async fn execute_user_shell_command( call_id, process_id: None, turn_id: turn_context.sub_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), command: display_command, cwd, parsed_cmd, diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 6469a4984eb5..f97e51fd8ad0 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -3,6 +3,7 @@ use crate::session::session::Session; use crate::session::turn_context::TurnContext; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::sandboxing::ToolError; +use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; @@ -77,6 +78,7 @@ pub(crate) async fn emit_exec_command_begin( call_id: ctx.call_id.to_string(), process_id: process_id.map(str::to_owned), turn_id: ctx.turn.sub_id.clone(), + started_at_ms: now_unix_timestamp_ms(), command: command.to_vec(), cwd: cwd.clone(), parsed_cmd: parsed_cmd.to_vec(), @@ -472,6 +474,7 @@ async fn emit_exec_end( call_id: ctx.call_id.to_string(), process_id: exec_input.process_id.map(str::to_owned), turn_id: ctx.turn.sub_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), command: exec_input.command.to_vec(), cwd: exec_input.cwd.clone(), parsed_cmd: exec_input.parsed_cmd.to_vec(), diff --git a/codex-rs/core/src/tools/handlers/dynamic.rs b/codex-rs/core/src/tools/handlers/dynamic.rs index b7e07090dc78..eab1f0f8087e 100644 --- a/codex-rs/core/src/tools/handlers/dynamic.rs +++ b/codex-rs/core/src/tools/handlers/dynamic.rs @@ -7,6 +7,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::dynamic_tools::DynamicToolCallRequest; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::FunctionCallOutputContentItem; @@ -102,9 +103,11 @@ async fn request_dynamic_tool( } let started_at = Instant::now(); + let started_at_ms = now_unix_timestamp_ms(); let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest { call_id: call_id.clone(), turn_id: turn_id.clone(), + started_at_ms, namespace: namespace.clone(), tool: tool.clone(), arguments: arguments.clone(), @@ -116,6 +119,7 @@ async fn request_dynamic_tool( Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { call_id, turn_id, + completed_at_ms: now_unix_timestamp_ms(), namespace, tool, arguments, @@ -127,6 +131,7 @@ async fn request_dynamic_tool( None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { call_id, turn_id, + completed_at_ms: now_unix_timestamp_ms(), namespace, tool, arguments, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs index 8c00b0a13cb7..0b308bb09ea8 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs @@ -1,4 +1,5 @@ use super::*; +use crate::turn_timing::now_unix_timestamp_ms; pub(crate) struct Handler; @@ -34,6 +35,7 @@ impl ToolHandler for Handler { &turn, CollabCloseBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, } @@ -54,6 +56,7 @@ impl ToolHandler for Handler { &turn, CollabCloseEndEvent { call_id: call_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, receiver_agent_nickname: receiver_agent.agent_nickname.clone(), @@ -75,6 +78,7 @@ impl ToolHandler for Handler { &turn, CollabCloseEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, receiver_agent_nickname: receiver_agent.agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs index 2d4f2c3f47e8..59a5038934c5 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs @@ -1,5 +1,6 @@ use super::*; use crate::agent::next_thread_spawn_depth; +use crate::turn_timing::now_unix_timestamp_ms; use std::sync::Arc; pub(crate) struct Handler; @@ -46,6 +47,7 @@ impl ToolHandler for Handler { &turn, CollabResumeBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, receiver_agent_nickname: receiver_agent.agent_nickname.clone(), @@ -101,6 +103,7 @@ impl ToolHandler for Handler { &turn, CollabResumeEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, receiver_agent_nickname: receiver_agent.agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs index 4ae3240cb384..1feb21b83996 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs @@ -1,5 +1,6 @@ use super::*; use crate::agent::control::render_input_preview; +use crate::turn_timing::now_unix_timestamp_ms; pub(crate) struct Handler; @@ -45,6 +46,7 @@ impl ToolHandler for Handler { &turn, CollabAgentInteractionBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, prompt: prompt.clone(), @@ -67,6 +69,7 @@ impl ToolHandler for Handler { &turn, CollabAgentInteractionEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, receiver_agent_nickname: receiver_agent.agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index e36ee171d35c..bc5dcd6929f0 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -6,6 +6,7 @@ use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; +use crate::turn_timing::now_unix_timestamp_ms; pub(crate) struct Handler; @@ -50,6 +51,7 @@ impl ToolHandler for Handler { &turn, CollabAgentSpawnBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, prompt: prompt.clone(), model: args.model.clone().unwrap_or_default(), @@ -146,6 +148,7 @@ impl ToolHandler for Handler { &turn, CollabAgentSpawnEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, new_thread_id, new_agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs index 77fa5f83a240..49b85dbfb3ef 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs @@ -1,5 +1,6 @@ use super::*; use crate::agent::status::is_final; +use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::error::CodexErr; use futures::FutureExt; use futures::StreamExt; @@ -73,6 +74,7 @@ impl ToolHandler for Handler { .send_event( &turn, CollabWaitingBeginEvent { + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_ids: receiver_thread_ids.clone(), receiver_agents: receiver_agents.clone(), @@ -105,6 +107,7 @@ impl ToolHandler for Handler { CollabWaitingEndEvent { sender_thread_id: session.conversation_id, call_id: call_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), agent_statuses: build_wait_agent_statuses( &statuses, &receiver_agents, @@ -173,6 +176,7 @@ impl ToolHandler for Handler { CollabWaitingEndEvent { sender_thread_id: session.conversation_id, call_id, + completed_at_ms: now_unix_timestamp_ms(), agent_statuses, statuses: statuses_by_id, } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs index 8074f7fe04a7..c0a1bcbc53ce 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs @@ -1,4 +1,5 @@ use super::*; +use crate::turn_timing::now_unix_timestamp_ms; pub(crate) struct Handler; @@ -43,6 +44,7 @@ impl ToolHandler for Handler { &turn, CollabCloseBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, } @@ -63,6 +65,7 @@ impl ToolHandler for Handler { &turn, CollabCloseEndEvent { call_id: call_id.clone(), + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, receiver_agent_nickname: receiver_agent.agent_nickname.clone(), @@ -87,6 +90,7 @@ impl ToolHandler for Handler { &turn, CollabCloseEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id: agent_id, receiver_agent_nickname: receiver_agent.agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index a42cde8f62fe..12e443b8142b 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -5,6 +5,7 @@ use super::*; use crate::tools::context::FunctionToolOutput; +use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::protocol::InterAgentCommunication; #[derive(Clone, Copy, PartialEq, Eq)] @@ -97,6 +98,7 @@ async fn handle_message_submission( &turn, CollabAgentInteractionBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, prompt: prompt.clone(), @@ -132,6 +134,7 @@ async fn handle_message_submission( &turn, CollabAgentInteractionEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_id, receiver_agent_nickname: receiver_agent.agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 509366de2f9e..184ea36c510a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -5,6 +5,7 @@ use crate::agent::control::render_input_preview; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; +use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::AgentPath; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::Op; @@ -49,6 +50,7 @@ impl ToolHandler for Handler { &turn, CollabAgentSpawnBeginEvent { call_id: call_id.clone(), + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, prompt: prompt.clone(), model: args.model.clone().unwrap_or_default(), @@ -168,6 +170,7 @@ impl ToolHandler for Handler { &turn, CollabAgentSpawnEndEvent { call_id, + completed_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, new_thread_id, new_agent_nickname, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs index 778c57be2136..b86e237f5300 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs @@ -1,4 +1,5 @@ use super::*; +use crate::turn_timing::now_unix_timestamp_ms; use std::collections::HashMap; use std::time::Duration; use tokio::time::Instant; @@ -48,6 +49,7 @@ impl ToolHandler for Handler { .send_event( &turn, CollabWaitingBeginEvent { + started_at_ms: now_unix_timestamp_ms(), sender_thread_id: session.conversation_id, receiver_thread_ids: Vec::new(), receiver_agents: Vec::new(), @@ -71,6 +73,7 @@ impl ToolHandler for Handler { CollabWaitingEndEvent { sender_thread_id: session.conversation_id, call_id, + completed_at_ms: now_unix_timestamp_ms(), agent_statuses: Vec::new(), statuses: HashMap::new(), } diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index 54911dcd13d0..74c3c59d8033 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -107,7 +107,7 @@ fn now_unix_timestamp_secs() -> i64 { now_unix_timestamp_ms() / 1000 } -fn now_unix_timestamp_ms() -> i64 { +pub(crate) fn now_unix_timestamp_ms() -> i64 { let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 2e60823c0c15..9530675acdf4 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -303,6 +303,15 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { }) .await?; + let started = wait_for_event_match(&codex, |ev| match ev { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::WebSearch(item), + started_at_ms, + .. + }) => Some((item.clone(), *started_at_ms)), + _ => None, + }) + .await; let begin = wait_for_event_match(&codex, |ev| match ev { EventMsg::WebSearchBegin(event) => Some(event.clone()), _ => None, @@ -311,16 +320,20 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { let completed = wait_for_event_match(&codex, |ev| match ev { EventMsg::ItemCompleted(ItemCompletedEvent { item: TurnItem::WebSearch(item), + completed_at_ms, .. - }) => Some(item.clone()), + }) => Some((item.clone(), *completed_at_ms)), _ => None, }) .await; assert_eq!(begin.call_id, "web-search-1"); - assert_eq!(completed.id, begin.call_id); + assert_eq!(started.0.id, begin.call_id); + assert!(started.1 > 0); + assert_eq!(completed.0.id, begin.call_id); + assert!(completed.1 > 0); assert_eq!( - completed.action, + completed.0.action, WebSearchAction::Search { query: Some("weather seattle".to_string()), queries: None, @@ -369,11 +382,29 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { }) .await?; + let started = wait_for_event_match(&codex, |ev| match ev { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ImageGeneration(item), + started_at_ms, + .. + }) => Some((item.clone(), *started_at_ms)), + _ => None, + }) + .await; let begin = wait_for_event_match(&codex, |ev| match ev { EventMsg::ImageGenerationBegin(event) => Some(event.clone()), _ => None, }) .await; + let completed = wait_for_event_match(&codex, |ev| match ev { + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::ImageGeneration(item), + completed_at_ms, + .. + }) => Some((item.clone(), *completed_at_ms)), + _ => None, + }) + .await; let end = wait_for_event_match(&codex, |ev| match ev { EventMsg::ImageGenerationEnd(event) => Some(event.clone()), _ => None, @@ -381,6 +412,10 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { .await; assert_eq!(begin.call_id, call_id); + assert_eq!(started.0.id, call_id); + assert!(started.1 > 0); + assert_eq!(completed.0.id, call_id); + assert!(completed.1 > 0); assert_eq!(end.call_id, call_id); assert_eq!(end.status, "completed"); assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string())); diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index 2a26ec3c7e89..f83b54504ad3 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -20,6 +20,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index f6f6d35f2151..5eda08c13de1 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -181,6 +181,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() { item: command_item, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); assert_eq!( started, @@ -216,6 +217,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); assert_eq!( @@ -250,6 +252,7 @@ fn empty_reasoning_items_are_ignored() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -274,6 +277,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -295,6 +299,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -327,6 +332,7 @@ fn reasoning_items_emit_summary_not_raw_content() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -362,6 +368,7 @@ fn web_search_completion_preserves_query_and_action() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -399,6 +406,7 @@ fn web_search_start_and_completion_reuse_item_id() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( @@ -413,6 +421,7 @@ fn web_search_start_and_completion_reuse_item_id() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -472,6 +481,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( ItemCompletedNotification { @@ -492,6 +502,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -559,6 +570,7 @@ fn mcp_tool_call_failure_sets_failed_status() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -604,6 +616,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( ItemCompletedNotification { @@ -627,6 +640,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -695,6 +709,7 @@ fn collab_spawn_begin_and_end_emit_item_events() { }, thread_id: "thread-parent".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( ItemCompletedNotification { @@ -717,6 +732,7 @@ fn collab_spawn_begin_and_end_emit_item_events() { }, thread_id: "thread-parent".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -795,6 +811,7 @@ fn file_change_completion_maps_change_kinds() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -845,6 +862,7 @@ fn file_change_declined_maps_to_failed_status() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -882,6 +900,7 @@ fn agent_message_item_updates_final_message() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -916,6 +935,7 @@ fn agent_message_item_started_is_ignored() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); assert_eq!( @@ -940,6 +960,7 @@ fn reasoning_item_completed_uses_synthetic_id() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -1296,6 +1317,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, })); assert_eq!( started, @@ -1378,6 +1400,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -1426,6 +1449,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); @@ -1470,6 +1494,7 @@ fn failed_turn_clears_stale_final_message() { }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, }, )); diff --git a/codex-rs/protocol/src/dynamic_tools.rs b/codex-rs/protocol/src/dynamic_tools.rs index 2bee24972b2a..da2ef6e02ce9 100644 --- a/codex-rs/protocol/src/dynamic_tools.rs +++ b/codex-rs/protocol/src/dynamic_tools.rs @@ -23,6 +23,8 @@ pub struct DynamicToolCallRequest { pub call_id: String, pub turn_id: String, #[serde(default)] + pub started_at_ms: i64, + #[serde(default)] pub namespace: Option, pub tool: String, pub arguments: JsonValue, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 95f61eba8805..a84685cb9111 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1828,6 +1828,7 @@ pub struct ItemStartedEvent { pub thread_id: ThreadId, pub turn_id: String, pub item: TurnItem, + pub started_at_ms: i64, } impl HasLegacyEvent for ItemStartedEvent { @@ -1854,6 +1855,15 @@ pub struct ItemCompletedEvent { pub thread_id: ThreadId, pub turn_id: String, pub item: TurnItem, + // Old rollout files may contain ItemCompleted events for PlanItem without + // this field. Default to 0 so those persisted rollouts still deserialize + // after tightening the core event contract. + #[serde(default = "default_item_completed_at_ms")] + pub completed_at_ms: i64, +} + +const fn default_item_completed_at_ms() -> i64 { + 0 } pub trait HasLegacyEvent { @@ -2348,6 +2358,8 @@ pub struct DynamicToolCallResponseEvent { pub call_id: String, /// Turn ID that this dynamic tool call belongs to. pub turn_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Dynamic tool namespace, when one was provided. #[serde(default)] pub namespace: Option, @@ -3058,6 +3070,8 @@ pub struct ExecCommandBeginEvent { pub process_id: Option, /// Turn ID that this command belongs to. pub turn_id: String, + #[serde(default)] + pub started_at_ms: i64, /// The command to be executed. pub command: Vec, /// The command's working directory if not the default cwd for the agent. @@ -3082,6 +3096,8 @@ pub struct ExecCommandEndEvent { pub process_id: Option, /// Turn ID that this command belongs to. pub turn_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// The command that was executed. pub command: Vec, /// The command's working directory if not the default cwd for the agent. @@ -3750,6 +3766,8 @@ pub enum TurnAbortReason { pub struct CollabAgentSpawnBeginEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub started_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the @@ -3789,6 +3807,8 @@ pub struct CollabAgentStatusEntry { pub struct CollabAgentSpawnEndEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the newly spawned agent, if it was created. @@ -3814,6 +3834,8 @@ pub struct CollabAgentSpawnEndEvent { pub struct CollabAgentInteractionBeginEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub started_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -3827,6 +3849,8 @@ pub struct CollabAgentInteractionBeginEvent { pub struct CollabAgentInteractionEndEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -3846,6 +3870,8 @@ pub struct CollabAgentInteractionEndEvent { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)] pub struct CollabWaitingBeginEvent { + #[serde(default)] + pub started_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receivers. @@ -3863,6 +3889,8 @@ pub struct CollabWaitingEndEvent { pub sender_thread_id: ThreadId, /// ID of the waiting call. pub call_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Optional receiver metadata paired with final statuses. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub agent_statuses: Vec, @@ -3874,6 +3902,8 @@ pub struct CollabWaitingEndEvent { pub struct CollabCloseBeginEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub started_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -3884,6 +3914,8 @@ pub struct CollabCloseBeginEvent { pub struct CollabCloseEndEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -3903,6 +3935,8 @@ pub struct CollabCloseEndEvent { pub struct CollabResumeBeginEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub started_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -3919,6 +3953,8 @@ pub struct CollabResumeBeginEvent { pub struct CollabResumeEndEvent { /// Identifier for the collab tool call. pub call_id: String, + #[serde(default)] + pub completed_at_ms: i64, /// Thread ID of the sender. pub sender_thread_id: ThreadId, /// Thread ID of the receiver. @@ -4596,6 +4632,7 @@ mod tests { queries: None, }, }), + started_at_ms: 0, }; let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false); @@ -4612,6 +4649,7 @@ mod tests { thread_id: ThreadId::new(), turn_id: "turn-1".into(), item: TurnItem::UserMessage(UserMessageItem::new(&[])), + started_at_ms: 0, }; assert!( @@ -4633,6 +4671,7 @@ mod tests { result: String::new(), saved_path: None, }), + started_at_ms: 0, }; let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false); @@ -4648,6 +4687,7 @@ mod tests { let event = ItemStartedEvent { thread_id: ThreadId::new(), turn_id: "turn-1".into(), + started_at_ms: 0, item: TurnItem::FileChange(FileChangeItem { id: "patch-1".into(), changes: [( @@ -4683,6 +4723,7 @@ mod tests { let event = ItemStartedEvent { thread_id: ThreadId::new(), turn_id: "turn-1".into(), + started_at_ms: 0, item: TurnItem::McpToolCall(McpToolCallItem { id: "mcp-1".into(), server: "server".into(), @@ -4724,6 +4765,7 @@ mod tests { result: "Zm9v".into(), saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()), }), + completed_at_ms: 0, }; let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false); @@ -4748,6 +4790,7 @@ mod tests { let event = ItemCompletedEvent { thread_id: ThreadId::new(), turn_id: "turn-1".into(), + completed_at_ms: 0, item: TurnItem::FileChange(FileChangeItem { id: "patch-1".into(), changes: [( @@ -4785,6 +4828,7 @@ mod tests { let event = ItemCompletedEvent { thread_id: ThreadId::new(), turn_id: "turn-1".into(), + completed_at_ms: 0, item: TurnItem::McpToolCall(McpToolCallItem { id: "mcp-1".into(), server: "server".into(), @@ -4821,6 +4865,34 @@ mod tests { } } + #[test] + fn item_started_event_requires_started_at_ms() { + let mut value = serde_json::to_value(ItemStartedEvent { + thread_id: ThreadId::new(), + turn_id: "turn-1".into(), + item: TurnItem::UserMessage(UserMessageItem::new(&[])), + started_at_ms: 123, + }) + .unwrap(); + value.as_object_mut().unwrap().remove("started_at_ms"); + + assert!(serde_json::from_value::(value).is_err()); + } + + #[test] + fn item_completed_event_defaults_missing_completed_at_ms() { + let mut value = serde_json::to_value(ItemCompletedEvent { + thread_id: ThreadId::new(), + turn_id: "turn-1".into(), + item: TurnItem::UserMessage(UserMessageItem::new(&[])), + completed_at_ms: 123, + }) + .unwrap(); + value.as_object_mut().unwrap().remove("completed_at_ms"); + + let event = serde_json::from_value::(value).unwrap(); + assert_eq!(event.completed_at_ms, 0); + } #[test] fn rollback_failed_error_does_not_affect_turn_status() { let event = ErrorEvent { diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 949aa581a4b6..dacfabcb7276 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2634,6 +2634,7 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: thread_id.to_string(), turn_id: "turn-approval".to_string(), + started_at_ms: 0, item: ThreadItem::FileChange { id: "patch-approval".to_string(), changes: vec![FileUpdateChange { @@ -4766,6 +4767,7 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() { codex_app_server_protocol::ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: ThreadItem::CollabAgentToolCall { id: "wait-1".to_string(), tool: codex_app_server_protocol::CollabAgentTool::Wait, diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 13b86e2afbfd..a486c0e0ea15 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -16,6 +16,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "call-spawn".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, @@ -34,6 +35,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "call-spawn".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, @@ -90,6 +92,7 @@ async fn live_app_server_user_message_item_completed_does_not_duplicate_rendered ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::UserMessage { id: "user-1".to_string(), content: vec![AppServerUserInput::Text { @@ -135,6 +138,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::AgentMessage { id: "msg-1".to_string(), text: "Yes. What do you need?".to_string(), @@ -287,6 +291,7 @@ async fn live_app_server_file_change_item_started_preserves_changes() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: AppServerThreadItem::FileChange { id: "patch-1".to_string(), changes: vec![FileUpdateChange { @@ -320,6 +325,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: AppServerThreadItem::CommandExecution { id: "cmd-1".to_string(), command: command.clone(), @@ -341,6 +347,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::CommandExecution { id: "cmd-1".to_string(), command, @@ -396,6 +403,7 @@ async fn live_app_server_collab_wait_items_render_history() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "wait-1".to_string(), tool: AppServerCollabAgentTool::Wait, @@ -418,6 +426,7 @@ async fn live_app_server_collab_wait_items_render_history() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "wait-1".to_string(), tool: AppServerCollabAgentTool::Wait, @@ -471,6 +480,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "spawn-1".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, @@ -490,6 +500,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::CollabAgentToolCall { id: "spawn-1".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 2631e506599f..8f3d37e76cae 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -659,6 +659,7 @@ pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) { .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), + completed_at_ms: 0, item: AppServerThreadItem::Reasoning { id: "reasoning-1".to_string(), summary: Vec::new(), @@ -677,6 +678,7 @@ pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Int .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), + started_at_ms: 0, item: AppServerThreadItem::EnteredReviewMode { id: "review-start".to_string(), review: review.into(), @@ -705,6 +707,7 @@ pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) { .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), + completed_at_ms: 0, item: AppServerThreadItem::ExitedReviewMode { id: "review-end".to_string(), review: String::new(), @@ -761,6 +764,7 @@ pub(super) fn handle_patch_apply_begin( ServerNotification::ItemStarted(ItemStartedNotification { thread_id: thread_id(chat), turn_id: turn_id.into(), + started_at_ms: 0, item: AppServerThreadItem::FileChange { id: call_id.into(), changes: file_update_changes_from_tui(changes), @@ -782,6 +786,7 @@ pub(super) fn handle_patch_apply_end( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: thread_id(chat), turn_id: turn_id.into(), + completed_at_ms: 0, item: AppServerThreadItem::FileChange { id: call_id.into(), changes: file_update_changes_from_tui(changes), @@ -801,6 +806,7 @@ pub(super) fn handle_view_image_tool_call( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: thread_id(chat), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::ImageView { id: call_id.into(), path, @@ -820,6 +826,7 @@ pub(super) fn handle_image_generation_end( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: thread_id(chat), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::ImageGeneration { id: call_id.into(), status: "completed".to_string(), @@ -977,6 +984,7 @@ pub(super) fn handle_exec_begin(chat: &mut ChatWidget, item: AppServerThreadItem .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), + started_at_ms: 0, item, }), /*replay_kind*/ None, @@ -1016,6 +1024,7 @@ pub(super) fn complete_assistant_message( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::AgentMessage { id: item_id.to_string(), text: text.to_string(), @@ -1058,6 +1067,7 @@ pub(super) fn complete_user_message_for_inputs( ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::UserMessage { id: item_id.to_string(), content, @@ -1199,6 +1209,7 @@ pub(super) fn handle_exec_end(chat: &mut ChatWidget, item: AppServerThreadItem) .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), + completed_at_ms: 0, item, }), /*replay_kind*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index d801870bca0f..180412cae7c7 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -801,6 +801,7 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::Reasoning { id: "reasoning-1".to_string(), summary: vec!["Summary only".to_string()], diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index 8b851676045d..0924d724fae5 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -154,6 +154,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { ServerNotification::ItemStarted(ItemStartedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + started_at_ms: 0, item: review_mode_item.clone(), }), /*replay_kind*/ None, @@ -166,6 +167,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: review_mode_item, }), /*replay_kind*/ None, @@ -176,6 +178,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::UserMessage { id: "review-prompt".to_string(), content: vec![AppServerUserInput::Text { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 84cc6e84a576..283237737554 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1149,6 +1149,7 @@ async fn slash_copy_state_tracks_plan_item_completion() { ServerNotification::ItemCompleted(ItemCompletedNotification { thread_id: String::new(), turn_id: "turn-1".to_string(), + completed_at_ms: 0, item: AppServerThreadItem::Plan { id: "plan-1".to_string(), text: plan_text.clone(),