From a797dd7c6f9b201818a18d23921eb3d1ef3c1314 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 23 Oct 2026 19:40:02 +0000 Subject: [PATCH] Expose lifecycle timestamps on app-server thread items (#57172) ## What changed - Add nullable `startedAtMs` and `completedAtMs` fields to `ThreadItem` in live notifications and turn history. Values are Unix milliseconds when recorded. - Preserve recorded item times through history reconstruction, pagination, and resume, including command start times recorded during approval. - Separate the Rust item payload into `ThreadItemPayload` while keeping the JSON payload flat, and update consumers, schemas, and generated TypeScript and Python types. ## Testing Extend coverage for timestamp serialization, history reconstruction, paginated reads and resume, and persisted Python SDK items. GitOrigin-RevId: ecbcee95af52db7c807b46a6e9deb30e5fba1f80 --- codex-rs/analytics/src/client.rs | 29 +- .../analytics/src/client_product_tests.rs | 29 +- codex-rs/analytics/src/reducer.rs | 184 +++-- .../src/tests/suite/reducer_ordering_tests.rs | 38 +- .../src/tests/suite/threads_tests.rs | 25 +- .../analytics/src/tests/suite/tools_tests.rs | 137 ++-- .../src/tests/suite/turns/events_tests.rs | 41 +- codex-rs/analytics/src/tests/support.rs | 33 +- codex-rs/app-server-client/src/lib.rs | 33 +- .../schema/json/ServerNotification.json | 24 +- .../codex_app_server_protocol.schemas.json | 24 +- .../codex_app_server_protocol.v2.schemas.json | 24 +- .../json/v2/ItemCompletedNotification.json | 24 +- .../json/v2/ItemStartedNotification.json | 24 +- .../schema/json/v2/ReviewStartResponse.json | 24 +- .../schema/json/v2/ThreadForkResponse.json | 24 +- .../json/v2/ThreadItemsListResponse.json | 24 +- .../schema/json/v2/ThreadListResponse.json | 24 +- .../json/v2/ThreadMetadataUpdateResponse.json | 24 +- .../schema/json/v2/ThreadReadResponse.json | 24 +- .../schema/json/v2/ThreadResumeResponse.json | 24 +- .../schema/json/v2/ThreadRevertResponse.json | 24 +- .../schema/json/v2/ThreadStartResponse.json | 24 +- .../json/v2/ThreadStartedNotification.json | 24 +- .../json/v2/ThreadTurnsListResponse.json | 24 +- .../json/v2/ThreadUnarchiveResponse.json | 24 +- .../json/v2/TurnCompletedNotification.json | 24 +- .../schema/json/v2/TurnStartResponse.json | 24 +- .../json/v2/TurnStartedNotification.json | 24 +- .../app-server-exports-experimental.json.zst | Bin 168306 -> 168429 bytes .../app-server-exports-stable.json.zst | Bin 161018 -> 161065 bytes .../schema/typescript/v2/ThreadItem.ts | 15 +- .../src/protocol/event_mapping.rs | 166 ++-- .../src/protocol/item_builders.rs | 28 +- .../src/protocol/item_builders_tests.rs | 7 +- .../src/protocol/thread_history.rs | 773 ++++++++++++------ .../src/protocol/thread_history_projection.rs | 4 +- .../thread_history_projection_tests.rs | 26 +- .../src/protocol/turn_items_view.rs | 16 +- .../src/protocol/v2/item.rs | 210 +++-- .../src/protocol/v2/tests.rs | 78 +- .../src/protocol/v2/thread.rs | 10 + .../src/protocol/v2/thread_data.rs | 10 + codex-rs/app-server-test-client/src/lib.rs | 27 +- codex-rs/app-server/README.md | 5 + .../app-server/src/bespoke_event_handling.rs | 218 +++-- codex-rs/app-server/src/in_process.rs | 22 +- codex-rs/app-server/src/notification_media.rs | 52 +- codex-rs/app-server/src/request_processors.rs | 2 +- .../request_processors/thread_lifecycle.rs | 1 + .../request_processors/thread_processor.rs | 33 +- .../thread_processor_tests.rs | 5 +- .../request_processors/thread_read_state.rs | 8 +- .../thread_resume_redaction.rs | 36 +- .../src/request_processors/turn_processor.rs | 5 +- codex-rs/app-server/src/thread_state.rs | 25 +- .../app-server/tests/suite/v2/analytics.rs | 14 +- .../app-server/tests/suite/v2/compaction.rs | 14 +- .../v2/config_requirements_exec_tests.rs | 6 +- .../tests/suite/v2/daybreak_access.rs | 6 +- .../tests/suite/v2/dynamic_tools.rs | 28 +- .../tests/suite/v2/external_agent_config.rs | 32 +- .../suite/v2/external_agent_import_sync.rs | 13 +- .../app-server/tests/suite/v2/guardian_v2.rs | 6 +- .../tests/suite/v2/imagegen_extension.rs | 42 +- .../app-server/tests/suite/v2/mcp_tool.rs | 32 +- .../app-server/tests/suite/v2/plan_item.rs | 18 +- .../tests/suite/v2/realtime_conversation.rs | 30 +- codex-rs/app-server/tests/suite/v2/review.rs | 23 +- .../tests/suite/v2/safety_check_downgrade.rs | 16 +- codex-rs/app-server/tests/suite/v2/sleep.rs | 33 +- .../app-server/tests/suite/v2/thread_fork.rs | 14 +- .../tests/suite/v2/thread_inject_items.rs | 13 +- .../app-server/tests/suite/v2/thread_queue.rs | 20 +- .../app-server/tests/suite/v2/thread_read.rs | 129 ++- .../tests/suite/v2/thread_resume.rs | 56 +- .../tests/suite/v2/thread_shell_command.rs | 36 +- .../tests/suite/v2/thread_timeline.rs | 6 +- .../tests/suite/v2/thread_unsubscribe.rs | 5 +- .../app-server/tests/suite/v2/turn_start.rs | 125 +-- .../app-server/tests/suite/v2/turn_steer.rs | 6 +- .../app-server/tests/suite/v2/view_image.rs | 5 +- .../app-server/tests/suite/v2/web_search.rs | 21 +- codex-rs/core/src/session/mod.rs | 8 + codex-rs/core/tests/suite/guardian_retry.rs | 12 +- .../core/tests/suite/realtime_conversation.rs | 2 +- .../unified_exec_launch_failure_tests.rs | 9 +- .../src/event_processor_with_human_output.rs | 46 +- ...event_processor_with_human_output_tests.rs | 21 +- .../src/event_processor_with_jsonl_output.rs | 48 +- ...event_processor_with_jsonl_output_tests.rs | 26 +- codex-rs/exec/src/lib.rs | 3 +- codex-rs/exec/src/lib_tests.rs | 42 +- .../tests/event_processor_with_json_output.rs | 371 +++++---- .../src/sessions/export.rs | 12 +- codex-rs/protocol/src/protocol.rs | 28 + .../thread-store/src/local/thread_history.rs | 57 +- .../src/local/thread_history/realtime.rs | 34 +- .../src/local/thread_history/search.rs | 53 +- .../local/thread_history_materialization.rs | 2 +- .../thread_history_materialization_tests.rs | 14 +- codex-rs/thread-store/src/types.rs | 2 +- codex-rs/tui/src/app.rs | 26 +- codex-rs/tui/src/app/activity_groups_tests.rs | 26 +- codex-rs/tui/src/app/agent_status_feed.rs | 48 +- .../tui/src/app/agent_status_feed_tests.rs | 52 +- .../tui/src/app/agents_overview_details.rs | 8 +- codex-rs/tui/src/app/agents_overview_tests.rs | 42 +- .../tui/src/app/agents_overview_threads.rs | 11 +- codex-rs/tui/src/app/app_server_events.rs | 6 +- codex-rs/tui/src/app/history_completion.rs | 6 +- .../tui/src/app/history_completion_tests.rs | 31 +- codex-rs/tui/src/app/history_pagination.rs | 16 +- .../tui/src/app/history_pagination_tests.rs | 25 +- codex-rs/tui/src/app/native_history_tests.rs | 2 +- .../src/app/owned_transcript_input_tests.rs | 13 +- .../tui/src/app/pending_interactive_replay.rs | 8 +- codex-rs/tui/src/app/realtime_delivery.rs | 31 +- codex-rs/tui/src/app/reasoning_replay.rs | 8 +- codex-rs/tui/src/app/replay_filter.rs | 4 +- codex-rs/tui/src/app/safety_buffering.rs | 7 +- codex-rs/tui/src/app/tests.rs | 206 ++--- .../src/app/tests/active_reconnect_tests.rs | 2 +- .../app/tests/browsing_pagination_tests.rs | 2 +- codex-rs/tui/src/app/tests/buffered_replay.rs | 233 +++--- .../src/app/tests/history_hydration_tests.rs | 6 +- .../app/tests/navigation_reconnect_tests.rs | 2 +- .../app/tests/pagination_completion_tests.rs | 4 +- .../tui/src/app/tests/patch_approval_tests.rs | 45 +- .../tui/src/app/tests/realtime_requests.rs | 99 ++- .../src/app/tests/reasoning_resume_tests.rs | 29 +- .../tui/src/app/tests/safety_buffering.rs | 8 +- .../app/tests/session_lifecycle_requests.rs | 12 +- .../tui/src/app/tests/startup_frame_tests.rs | 2 +- .../src/app/tests/streaming_interleave_e2e.rs | 8 +- codex-rs/tui/src/app/tests/thread_usage.rs | 2 +- .../app/tests/unavailable_commands_tests.rs | 2 +- .../tests/user_verification_routes_tests.rs | 29 +- codex-rs/tui/src/app/thread_events.rs | 169 ++-- codex-rs/tui/src/app/thread_routing.rs | 10 +- codex-rs/tui/src/app/thread_title.rs | 13 +- codex-rs/tui/src/app/thread_title_tests.rs | 27 +- codex-rs/tui/src/app/transcript_export.rs | 20 +- .../tui/src/app/transcript_export_tests.rs | 17 +- codex-rs/tui/src/app/turn_tips.rs | 12 +- codex-rs/tui/src/app_backtrack.rs | 78 +- codex-rs/tui/src/app_server_session.rs | 39 +- .../tui/src/app_server_session/history.rs | 13 +- codex-rs/tui/src/chatwidget.rs | 2 +- .../tui/src/chatwidget/command_lifecycle.rs | 16 +- .../tui/src/chatwidget/dynamic_activity.rs | 8 +- codex-rs/tui/src/chatwidget/interrupts.rs | 16 +- codex-rs/tui/src/chatwidget/protocol.rs | 53 +- codex-rs/tui/src/chatwidget/realtime.rs | 34 +- .../tui/src/chatwidget/realtime_navigation.rs | 2 +- codex-rs/tui/src/chatwidget/realtime_tests.rs | 43 +- .../realtime_tests/caption_replay.rs | 39 +- .../realtime_tests/handoff_privacy.rs | 8 +- .../src/chatwidget/realtime_tests/handoffs.rs | 2 +- .../realtime_tests/speech_recovery.rs | 8 +- codex-rs/tui/src/chatwidget/replay.rs | 108 +-- codex-rs/tui/src/chatwidget/session_flow.rs | 8 +- codex-rs/tui/src/chatwidget/tests.rs | 2 +- .../tui/src/chatwidget/tests/app_server.rs | 193 ++--- .../src/chatwidget/tests/compaction_tests.rs | 25 +- .../tests/completion_styling_tests.rs | 19 +- .../tests/computer_activity_tests.rs | 8 +- .../tests/dynamic_activity_tests.rs | 4 +- .../tui/src/chatwidget/tests/exec_flow.rs | 49 +- codex-rs/tui/src/chatwidget/tests/helpers.rs | 180 ++-- .../chatwidget/tests/history_projection.rs | 52 +- .../src/chatwidget/tests/history_replay.rs | 104 +-- .../tui/src/chatwidget/tests/plan_mode.rs | 37 +- .../tests/question_notifications_tests.rs | 13 +- .../src/chatwidget/tests/questions_tests.rs | 28 +- .../tests/reasoning_status_tests.rs | 17 +- .../tui/src/chatwidget/tests/review_mode.rs | 42 +- .../src/chatwidget/tests/slash_commands.rs | 26 +- .../src/chatwidget/tests/status_and_layout.rs | 4 +- .../tests/subagent_activity_tests.rs | 25 +- .../chatwidget/tests/tool_activity_tests.rs | 4 +- codex-rs/tui/src/chatwidget/tool_lifecycle.rs | 54 +- .../tui/src/clipboard_copy/worker_tests.rs | 13 +- codex-rs/tui/src/dynamic_tools.rs | 91 +-- codex-rs/tui/src/history_cell/dynamic.rs | 10 +- .../tui/src/history_cell/dynamic_tests.rs | 4 +- codex-rs/tui/src/lib_tests.rs | 5 +- codex-rs/tui/src/multi_agents.rs | 40 +- .../tui/src/pager_overlay/transcript_tests.rs | 2 +- codex-rs/tui/src/resume_picker.rs | 21 +- .../src/resume_picker_transcript_preview.rs | 12 +- .../resume_picker_transcript_preview_tests.rs | 4 +- .../tui/src/temporary_structured_request.rs | 4 +- .../src/temporary_structured_request_tests.rs | 15 +- codex-rs/tui/src/thread_transcript.rs | 26 +- .../src/thread_transcript/computer_groups.rs | 21 +- .../computer_groups_tests.rs | 23 +- .../thread_transcript/exploration_groups.rs | 13 +- .../tui/src/thread_transcript/other_items.rs | 54 +- .../thread_transcript/other_items_tests.rs | 22 +- codex-rs/tui/src/thread_transcript/tools.rs | 18 +- .../tui/src/thread_transcript/tools_tests.rs | 20 +- .../src/transcript_view/mutations_tests.rs | 11 +- sdk/python/scripts/update_sdk_artifacts.py | 7 + .../src/openai_codex/generated/v2_all.py | 289 ++++++- sdk/python/tests/test_app_server_inputs.py | 13 +- 206 files changed, 4691 insertions(+), 3089 deletions(-) diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index aef8abef13..b0a8705b1b 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -436,22 +436,29 @@ impl AnalyticsEventsClient { completed_at_ms: i64, ) { let thread_id = item.sender_thread_id.to_string(); - let completed_item = TurnItem::CollabAgentToolCall(item.clone()).into(); + let completed_item = codex_app_server_protocol::ThreadItemPayload::from( + TurnItem::CollabAgentToolCall(item.clone()), + ); item.status = CollabAgentToolCallStatus::InProgress; - self.track_notification(&ServerNotification::ItemStarted(ItemStartedNotification { - item: TurnItem::CollabAgentToolCall(item).into(), - thread_id: thread_id.clone(), - turn_id: turn_id.clone(), - started_at_ms, - })); + self.track_notification(&ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.clone(), + turn_id.clone(), + codex_app_server_protocol::ThreadItemPayload::from(TurnItem::CollabAgentToolCall( + item, + )), + started_at_ms, + ), + )); self.track_notification(&ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: completed_item, + ItemCompletedNotification::new( thread_id, turn_id, + completed_item, + Some(started_at_ms), completed_at_ms, - }, + ), )); } @@ -935,7 +942,7 @@ fn session_event_to_analytics_notification( match ¬ification { ServerNotification::ItemStarted(ItemStartedNotification { item, .. }) | ServerNotification::ItemCompleted(ItemCompletedNotification { item, .. }) => { - tracked_tool_item_id(item)?; + tracked_tool_item_id(&item.payload)?; } _ => {} } diff --git a/codex-rs/analytics/src/client_product_tests.rs b/codex-rs/analytics/src/client_product_tests.rs index 029dea7fd1..641510cd55 100644 --- a/codex-rs/analytics/src/client_product_tests.rs +++ b/codex-rs/analytics/src/client_product_tests.rs @@ -22,7 +22,7 @@ use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnStartedNotification; use codex_login::AuthManager; use codex_protocol::ThreadId; @@ -238,11 +238,10 @@ async fn buffered_tool_events_preserve_attribution_or_drop_it_on_queue_overflow( turn: started.turn, })); // Collaborator completions wait in the reducer for subsequent sampling evidence. - let completed = ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 2, - item: ThreadItem::CollabAgentToolCall { + let completed = ItemCompletedNotification::new( + thread_id.to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: "item".to_string(), tool: CollabAgentTool::SendMessage, status: CollabAgentToolCallStatus::Failed, @@ -253,13 +252,17 @@ async fn buffered_tool_events_preserve_attribution_or_drop_it_on_queue_overflow( reasoning_effort: None, agents_states: Default::default(), }, - }; - client.track_notification(&ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 1, - item: completed.item.clone(), - })); + /*started_at_ms*/ None, + /*completed_at_ms*/ 2, + ); + client.track_notification(&ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.to_string(), + "turn-1".to_string(), + completed.item.payload.clone(), + /*started_at_ms*/ 1, + ), + )); client.track_notification(&ServerNotification::ItemCompleted(completed)); let measurement = PluginMeasurementsInput { thread_id: thread_id.to_string(), diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index cef0a71216..7d0bf4058e 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -154,7 +154,7 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerResponse; use codex_app_server_protocol::SubAgentActivityKind; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput; use codex_app_server_protocol::WebSearchAction; @@ -535,36 +535,36 @@ struct TurnToolCounts { } impl TurnToolCounts { - fn record(&mut self, item: &ThreadItem) { + fn record(&mut self, item: &ThreadItemPayload) { match item { - ThreadItem::CommandExecution { .. } => self.shell_command += 1, - ThreadItem::FileChange { .. } => self.file_change += 1, - ThreadItem::McpToolCall { .. } => self.mcp_tool_call += 1, - ThreadItem::DynamicToolCall { .. } => self.dynamic_tool_call += 1, - ThreadItem::SubAgentActivity { + ThreadItemPayload::CommandExecution { .. } => self.shell_command += 1, + ThreadItemPayload::FileChange { .. } => self.file_change += 1, + ThreadItemPayload::McpToolCall { .. } => self.mcp_tool_call += 1, + ThreadItemPayload::DynamicToolCall { .. } => self.dynamic_tool_call += 1, + ThreadItemPayload::SubAgentActivity { kind: SubAgentActivityKind::Completed, .. } => return, - ThreadItem::CollabAgentToolCall { id, .. } - | ThreadItem::SubAgentActivity { id, .. } => { + ThreadItemPayload::CollabAgentToolCall { id, .. } + | ThreadItemPayload::SubAgentActivity { id, .. } => { if !self.subagent_tool_call_ids.insert(id.clone()) { return; } self.subagent_tool_call += 1; } - ThreadItem::WebSearch(_) => self.web_search += 1, - ThreadItem::ImageGeneration(_) => self.image_generation += 1, - ThreadItem::UserMessage { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::AgentMessage { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::ImageView { .. } - | ThreadItem::Sleep(_) - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => return, + ThreadItemPayload::WebSearch(_) => self.web_search += 1, + ThreadItemPayload::ImageGeneration(_) => self.image_generation += 1, + ThreadItemPayload::UserMessage { .. } + | ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::AgentMessage { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::Sleep(_) + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => return, } self.total += 1; } @@ -2060,15 +2060,17 @@ impl AnalyticsReducer { })); } ServerNotification::ItemStarted(notification) => { - let Some(item_id) = tracked_tool_item_id(¬ification.item) else { + let Some(item_id) = tracked_tool_item_id(¬ification.item.payload) else { return; }; let Some(started_at_ms) = option_i64_to_u64(Some(notification.started_at_ms)) else { return; }; - let model_context = match ¬ification.item { - ThreadItem::CommandExecution { model_context, .. } => model_context.clone(), + let model_context = match ¬ification.item.payload { + ThreadItemPayload::CommandExecution { model_context, .. } => { + model_context.clone() + } _ => None, }; self.turn_state(notification.turn_id.clone()) @@ -2079,7 +2081,7 @@ impl AnalyticsReducer { turn_id: notification.turn_id.clone(), item_id: item_id.to_string(), }; - let cua_identity = cua_mcp_tool_identity(¬ification.item); + let cua_identity = cua_mcp_tool_identity(¬ification.item.payload); if cua_identity.is_some() && self .turns @@ -2148,7 +2150,10 @@ impl AnalyticsReducer { )); } ServerNotification::ItemCompleted(notification) => { - if matches!(notification.item, ThreadItem::SubAgentActivity { .. }) { + if matches!( + notification.item.payload, + ThreadItemPayload::SubAgentActivity { .. } + ) { let Some(turn_state) = self.turns.get_mut(¬ification.turn_id) else { tracing::warn!( thread_id = %notification.thread_id, @@ -2157,10 +2162,10 @@ impl AnalyticsReducer { ); return; }; - turn_state.tool_counts.record(¬ification.item); + turn_state.tool_counts.record(¬ification.item.payload); return; } - let Some(item_id) = tracked_tool_item_id(¬ification.item) else { + let Some(item_id) = tracked_tool_item_id(¬ification.item.payload) else { return; }; let key = ToolItemKey { @@ -2168,7 +2173,7 @@ impl AnalyticsReducer { turn_id: notification.turn_id.clone(), item_id: item_id.to_string(), }; - let is_cua = cua_mcp_tool_identity(¬ification.item).is_some(); + let is_cua = cua_mcp_tool_identity(¬ification.item.payload).is_some(); if is_cua && self .turns @@ -2177,18 +2182,20 @@ impl AnalyticsReducer { { return; } - let elicitation_type = - if matches!(notification.item, ThreadItem::McpToolCall { .. }) { - // Producers enqueue known classifications before completion on - // the same analytics queue, so completed keys need no history. - self.pending_mcp_tool_elicitations - .iter() - .position(|(pending, _)| pending == &key) - .and_then(|index| self.pending_mcp_tool_elicitations.remove(index)) - .map(|(_, elicitation_type)| elicitation_type) - } else { - None - }; + let elicitation_type = if matches!( + notification.item.payload, + ThreadItemPayload::McpToolCall { .. } + ) { + // Producers enqueue known classifications before completion on + // the same analytics queue, so completed keys need no history. + self.pending_mcp_tool_elicitations + .iter() + .position(|(pending, _)| pending == &key) + .and_then(|index| self.pending_mcp_tool_elicitations.remove(index)) + .map(|(_, elicitation_type)| elicitation_type) + } else { + None + }; let Some(turn_state) = self.turns.get_mut(¬ification.turn_id) else { tracing::warn!( thread_id = %notification.thread_id, @@ -2198,7 +2205,7 @@ impl AnalyticsReducer { ); return; }; - turn_state.tool_counts.record(¬ification.item); + turn_state.tool_counts.record(¬ification.item.payload); let Some((started_at_ms, model_context)) = self.tool_items_started_at_ms.remove(&key) else { @@ -2222,7 +2229,7 @@ impl AnalyticsReducer { if let Some(mut event) = tool_item_event(ToolItemEventInput { thread_id: ¬ification.thread_id, turn_id: ¬ification.turn_id, - item: ¬ification.item, + item: ¬ification.item.payload, model_context: model_context.as_ref(), started_at_ms, completed_at_ms, @@ -2245,12 +2252,14 @@ impl AnalyticsReducer { .and_then(|config| config.turn_metadata.root_turn_id()); // Fast collaborator tools can complete before their sampling response. // Keep the event until that response can supply its originating ID. - let emission = - if matches!(¬ification.item, ThreadItem::CollabAgentToolCall { .. }) { - ToolEventEmission::AwaitResponse - } else { - ToolEventEmission::ImmediateUnlessCorrelated - }; + let emission = if matches!( + ¬ification.item.payload, + ThreadItemPayload::CollabAgentToolCall { .. } + ) { + ToolEventEmission::AwaitResponse + } else { + ToolEventEmission::ImmediateUnlessCorrelated + }; self.record_tool_event( ¬ification.thread_id, ¬ification.turn_id, @@ -2903,32 +2912,34 @@ fn warn_missing_analytics_context( ); } -pub(crate) fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> { +pub(crate) fn tracked_tool_item_id(item: &ThreadItemPayload) -> Option<&str> { match item { - ThreadItem::CommandExecution { id, .. } - | ThreadItem::FileChange { id, .. } - | ThreadItem::McpToolCall { id, .. } - | ThreadItem::DynamicToolCall { id, .. } - | ThreadItem::CollabAgentToolCall { id, .. } => Some(id), - ThreadItem::WebSearch(item) => Some(&item.id), - ThreadItem::ImageGeneration(item) => Some(&item.id), - ThreadItem::UserMessage { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::AgentMessage { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::SubAgentActivity { .. } - | ThreadItem::ImageView { .. } - | ThreadItem::Sleep(_) - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => None, + ThreadItemPayload::CommandExecution { id, .. } + | ThreadItemPayload::FileChange { id, .. } + | ThreadItemPayload::McpToolCall { id, .. } + | ThreadItemPayload::DynamicToolCall { id, .. } + | ThreadItemPayload::CollabAgentToolCall { id, .. } => Some(id), + ThreadItemPayload::WebSearch(item) => Some(&item.id), + ThreadItemPayload::ImageGeneration(item) => Some(&item.id), + ThreadItemPayload::UserMessage { .. } + | ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::AgentMessage { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::SubAgentActivity { .. } + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::Sleep(_) + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => None, } } -fn cua_mcp_tool_identity(item: &ThreadItem) -> Option<(&str, &str, Option<&str>, Option<&str>)> { - let ThreadItem::McpToolCall { +fn cua_mcp_tool_identity( + item: &ThreadItemPayload, +) -> Option<(&str, &str, Option<&str>, Option<&str>)> { + let ThreadItemPayload::McpToolCall { server, tool, plugin_id, @@ -3037,7 +3048,7 @@ fn item_review_summary_key(pending_review: &PendingReviewState) -> Option { thread_id: &'a str, turn_id: &'a str, - item: &'a ThreadItem, + item: &'a ThreadItemPayload, model_context: Option<&'a ModelInvocationContext>, started_at_ms: u64, completed_at_ms: u64, @@ -3063,7 +3074,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { elicitation_type, } = input; match item { - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { id, sandbox_type, plugin_id, @@ -3122,7 +3133,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, )) } - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id, changes, status, @@ -3160,7 +3171,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, })) } - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { id, server, tool, @@ -3209,7 +3220,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, )) } - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { id, tool, status, @@ -3256,7 +3267,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, )) } - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -3323,7 +3334,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, )) } - ThreadItem::WebSearch(item) => { + ThreadItemPayload::WebSearch(item) => { let base = tool_item_base( thread_id, turn_id, @@ -3353,7 +3364,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option { }, })) } - ThreadItem::ImageGeneration(item) => { + ThreadItemPayload::ImageGeneration(item) => { let (terminal_status, failure_kind) = image_generation_outcome(item.status.as_str()); let base = tool_item_base( thread_id, @@ -4235,11 +4246,10 @@ mod tests { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: keys[2].thread_id.clone(), - turn_id: keys[2].turn_id.clone(), - completed_at_ms: 1_000, - item: ThreadItem::McpToolCall { + ItemCompletedNotification::new( + keys[2].thread_id.clone(), + keys[2].turn_id.clone(), + ThreadItemPayload::McpToolCall { id: keys[2].item_id.clone(), server: "server".to_string(), tool: "tool".to_string(), @@ -4254,7 +4264,9 @@ mod tests { error: None, duration_ms: None, }, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut events, ) diff --git a/codex-rs/analytics/src/tests/suite/reducer_ordering_tests.rs b/codex-rs/analytics/src/tests/suite/reducer_ordering_tests.rs index 1275543ce4..d6e8c65f2a 100644 --- a/codex-rs/analytics/src/tests/suite/reducer_ordering_tests.rs +++ b/codex-rs/analytics/src/tests/suite/reducer_ordering_tests.rs @@ -308,16 +308,16 @@ async fn completed_background_tool_item_emits_after_turn_event() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - started_at_ms: 998, - item: sample_command_execution_item( + ItemStartedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + sample_command_execution_item( CommandExecutionStatus::InProgress, /*exit_code*/ None, /*duration_ms*/ None, ), - }, + /*started_at_ms*/ 998, + ), ))), &mut out, ) @@ -343,16 +343,17 @@ async fn completed_background_tool_item_emits_after_turn_event() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: 1_000, - item: sample_command_execution_item( + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + sample_command_execution_item( CommandExecutionStatus::Completed, Some(0), Some(1), ), - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut out, ) @@ -430,16 +431,17 @@ async fn item_completed_without_turn_state_does_not_create_turn_state() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: 1_000, - item: sample_command_execution_item( + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + sample_command_execution_item( CommandExecutionStatus::Completed, Some(0), Some(1), ), - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut out, ) diff --git a/codex-rs/analytics/src/tests/suite/threads_tests.rs b/codex-rs/analytics/src/tests/suite/threads_tests.rs index 49eb280e7c..b0c4cc0f08 100644 --- a/codex-rs/analytics/src/tests/suite/threads_tests.rs +++ b/codex-rs/analytics/src/tests/suite/threads_tests.rs @@ -830,16 +830,16 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-subagent".to_string(), - turn_id: "turn-subagent".to_string(), - started_at_ms: 1_000, - item: sample_command_execution_item( + ItemStartedNotification::new( + "thread-subagent".to_string(), + "turn-subagent".to_string(), + sample_command_execution_item( CommandExecutionStatus::InProgress, /*exit_code*/ None, /*duration_ms*/ None, ), - }, + /*started_at_ms*/ 1_000, + ), ))), &mut events, ) @@ -847,16 +847,17 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-subagent".to_string(), - turn_id: "turn-subagent".to_string(), - completed_at_ms: 1_042, - item: sample_command_execution_item( + ItemCompletedNotification::new( + "thread-subagent".to_string(), + "turn-subagent".to_string(), + sample_command_execution_item( CommandExecutionStatus::Completed, Some(0), Some(42), ), - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_042, + ), ))), &mut events, ) diff --git a/codex-rs/analytics/src/tests/suite/tools_tests.rs b/codex-rs/analytics/src/tests/suite/tools_tests.rs index 41e9497696..9ca6b21d45 100644 --- a/codex-rs/analytics/src/tests/suite/tools_tests.rs +++ b/codex-rs/analytics/src/tests/suite/tools_tests.rs @@ -47,7 +47,7 @@ use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::McpToolCallAppContext; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; use codex_protocol::protocol::ThreadSource; use codex_protocol::sandbox::SandboxType; @@ -64,9 +64,9 @@ fn sample_command_execution_item_with_actions( plugin_id: Option<&str>, script_path: Option<&str>, sandbox_type: Option, -) -> ThreadItem { +) -> ThreadItemPayload { let mut item = sample_command_execution_item(status, exit_code, duration_ms); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { command_actions: item_command_actions, plugin_id: item_plugin_id, script_path: item_script_path, @@ -233,7 +233,7 @@ async fn cua_tool_call_lifecycle_requires_a_start_and_deduplicates_replay() { .await; events.clear(); - let item = |status| ThreadItem::McpToolCall { + let item = |status| ThreadItemPayload::McpToolCall { id: "cua-call".into(), server: "codex_apps".into(), tool: "js".into(), @@ -254,18 +254,19 @@ async fn cua_tool_call_lifecycle_requires_a_start_and_deduplicates_replay() { error: None, duration_ms: None, }; - let started = ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-2".into(), - turn_id: "turn-2".into(), - started_at_ms: 1_000, - item: item(McpToolCallStatus::InProgress), - }); - let completed = ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-2".into(), - turn_id: "turn-2".into(), - completed_at_ms: 1_100, - item: item(McpToolCallStatus::Completed), - }); + let started = ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-2".into(), + "turn-2".into(), + item(McpToolCallStatus::InProgress), + /*started_at_ms*/ 1_000, + )); + let completed = ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-2".into(), + "turn-2".into(), + item(McpToolCallStatus::Completed), + Some(1_000), + /*completed_at_ms*/ 1_100, + )); for notification in [ completed.clone(), // Missing start must not poison later deduplication. started.clone(), @@ -330,17 +331,18 @@ async fn item_lifecycle_notifications_publish_command_execution_event() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 1_000, - item: { + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + { let mut item = sample_command_execution_item( CommandExecutionStatus::InProgress, /*exit_code*/ None, /*duration_ms*/ None, ); - if let ThreadItem::CommandExecution { model_context, .. } = &mut item { + if let ThreadItemPayload::CommandExecution { model_context, .. } = + &mut item + { *model_context = Some(codex_protocol::items::ModelInvocationContext { model_slug: model.to_string(), @@ -349,7 +351,8 @@ async fn item_lifecycle_notifications_publish_command_execution_event() { } item }, - }, + /*started_at_ms*/ 1_000, + ), ))), &mut events, ) @@ -363,11 +366,10 @@ async fn item_lifecycle_notifications_publish_command_execution_event() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 1_045, - item: sample_command_execution_item_with_actions( + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + sample_command_execution_item_with_actions( CommandExecutionStatus::Completed, Some(0), Some(42), @@ -394,7 +396,9 @@ async fn item_lifecycle_notifications_publish_command_execution_event() { Some("scripts/run.py"), Some(SandboxType::WindowsMxc), ), - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_045, + ), ))), &mut events, ) @@ -466,7 +470,7 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli &mut events, ) .await; - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: "call-1".into(), tool: CollabAgentTool::SendMessage, status: CollabAgentToolCallStatus::Failed, @@ -480,12 +484,12 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-1".into(), - turn_id: "turn-1".into(), - started_at_ms: 1_000, - item: item.clone(), - }, + ItemStartedNotification::new( + "thread-1".into(), + "turn-1".into(), + item.clone(), + /*started_at_ms*/ 1_000, + ), ))), &mut events, ) @@ -494,12 +498,13 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli sampling_response("turn-1", "response-1", &["call-1"]), )); let completion = AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-1".into(), - turn_id: "turn-1".into(), - completed_at_ms: 1_010, + ItemCompletedNotification::new( + "thread-1".into(), + "turn-1".into(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_010, + ), ))); let facts = if response_first { [response, completion] @@ -785,7 +790,7 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c ("retry", "calendar", None), ("denied", "drive", Some(ElicitationType::Approval)), ] { - let item = ThreadItem::McpToolCall { + let item = ThreadItemPayload::McpToolCall { id: item_id.to_string(), server: "server".to_string(), tool: "search".to_string(), @@ -809,12 +814,12 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - started_at_ms: 998, - item: item.clone(), - }, + ItemStartedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + item.clone(), + /*started_at_ms*/ 998, + ), ))), &mut events, ) @@ -851,12 +856,13 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: 1_000, + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut events, ) @@ -966,7 +972,7 @@ async fn image_generation_events_preserve_transparent_background_metadata() { ) .await; - let item = ThreadItem::ImageGeneration(ImageGenerationItem { + let item = ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: "image-1".to_string(), status: status.to_string(), revised_prompt: None, @@ -984,12 +990,12 @@ async fn image_generation_events_preserve_transparent_background_metadata() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - started_at_ms: 998, - item: item.clone(), - }, + ItemStartedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + item.clone(), + /*started_at_ms*/ 998, + ), ))), &mut out, ) @@ -997,12 +1003,13 @@ async fn image_generation_events_preserve_transparent_background_metadata() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: 1_000, + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut out, ) diff --git a/codex-rs/analytics/src/tests/suite/turns/events_tests.rs b/codex-rs/analytics/src/tests/suite/turns/events_tests.rs index 959dfe72c4..06896368ad 100644 --- a/codex-rs/analytics/src/tests/suite/turns/events_tests.rs +++ b/codex-rs/analytics/src/tests/suite/turns/events_tests.rs @@ -34,7 +34,7 @@ use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::SubAgentActivityKind; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadRealtimeStartedNotification; use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; use codex_app_server_protocol::WebSearchItem; @@ -474,7 +474,7 @@ async fn turn_event_counts_completed_tool_items() { ) .await; - let mcp_tool_call_item = |status, duration_ms| ThreadItem::McpToolCall { + let mcp_tool_call_item = |status, duration_ms| ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "server".to_string(), tool: "search".to_string(), @@ -497,13 +497,13 @@ async fn turn_event_counts_completed_tool_items() { }; let completed_tool_items = vec![ sample_command_execution_item(CommandExecutionStatus::Completed, Some(0), Some(1)), - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id: "file-change-1".to_string(), changes: Vec::new(), status: PatchApplyStatus::Completed, }, mcp_tool_call_item(McpToolCallStatus::Completed, Some(2)), - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { id: "dynamic-1".to_string(), // A name collision with code-mode exec must not change the event boundary. namespace: Some("custom".to_string()), @@ -514,7 +514,7 @@ async fn turn_event_counts_completed_tool_items() { success: Some(true), duration_ms: Some(3), }, - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id: "collab-1".to_string(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::Completed, @@ -525,7 +525,7 @@ async fn turn_event_counts_completed_tool_items() { reasoning_effort: None, agents_states: Default::default(), }, - ThreadItem::SubAgentActivity { + ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "sub-agent-activity-1".to_string(), @@ -533,7 +533,7 @@ async fn turn_event_counts_completed_tool_items() { agent_thread_id: "thread-child".to_string(), agent_path: "/root/child".to_string(), }, - ThreadItem::SubAgentActivity { + ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "sub-agent-activity-completed".to_string(), @@ -541,13 +541,13 @@ async fn turn_event_counts_completed_tool_items() { agent_thread_id: "thread-child".to_string(), agent_path: "/root/child".to_string(), }, - ThreadItem::WebSearch(WebSearchItem { + ThreadItemPayload::WebSearch(WebSearchItem { id: "web-1".to_string(), query: "codex".to_string(), action: None, results: None, }), - ThreadItem::ImageGeneration(ImageGenerationItem { + ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: "image-1".to_string(), status: "completed".to_string(), revised_prompt: None, @@ -567,12 +567,12 @@ async fn turn_event_counts_completed_tool_items() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - started_at_ms: 998, - item: item.clone(), - }, + ItemStartedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + item.clone(), + /*started_at_ms*/ 998, + ), ))), &mut out, ) @@ -583,12 +583,13 @@ async fn turn_event_counts_completed_tool_items() { reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: 1_000, + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_000, + ), ))), &mut out, ) diff --git a/codex-rs/analytics/src/tests/support.rs b/codex-rs/analytics/src/tests/support.rs index e32fe28aca..8fdfcfd98a 100644 --- a/codex-rs/analytics/src/tests/support.rs +++ b/codex-rs/analytics/src/tests/support.rs @@ -31,7 +31,7 @@ use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::SessionSource as AppServerSessionSource; use codex_app_server_protocol::Thread; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadSource as AppServerThreadSource; use codex_app_server_protocol::ThreadStartResponse; @@ -545,17 +545,17 @@ pub(super) async fn ingest_completed_command_execution_item( reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 1_000, - item: sample_command_execution_item_with_id( + ItemStartedNotification::new( + thread_id.to_string(), + "turn-1".to_string(), + sample_command_execution_item_with_id( item_id, CommandExecutionStatus::InProgress, /*exit_code*/ None, /*duration_ms*/ None, ), - }, + /*started_at_ms*/ 1_000, + ), ))), events, ) @@ -563,17 +563,18 @@ pub(super) async fn ingest_completed_command_execution_item( reducer .ingest( AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 1_042, - item: sample_command_execution_item_with_id( + ItemCompletedNotification::new( + thread_id.to_string(), + "turn-1".to_string(), + sample_command_execution_item_with_id( item_id, CommandExecutionStatus::Completed, Some(0), Some(42), ), - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1_042, + ), ))), events, ) @@ -640,7 +641,7 @@ pub(super) fn sample_command_execution_item( status: CommandExecutionStatus, exit_code: Option, duration_ms: Option, -) -> ThreadItem { +) -> ThreadItemPayload { sample_command_execution_item_with_id("item-1", status, exit_code, duration_ms) } @@ -649,8 +650,8 @@ pub(super) fn sample_command_execution_item_with_id( status: CommandExecutionStatus, exit_code: Option, duration_ms: Option, -) -> ThreadItem { - ThreadItem::CommandExecution { +) -> ThreadItemPayload { + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: id.to_string(), diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index e038e439f6..4d42819eb4 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1134,19 +1134,22 @@ mod tests { } fn item_completed_notification(text: &str) -> ServerNotification { - 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(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, - }) + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + "thread".to_string(), + "turn".to_string(), + codex_app_server_protocol::ThreadItemPayload::AgentMessage { + id: "item".to_string(), + text: text.to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ) } fn turn_completed_notification() -> ServerNotification { @@ -1882,8 +1885,8 @@ mod tests { } ServerNotification::ItemCompleted(notification) if matches!( - ¬ification.item, - codex_app_server_protocol::ThreadItem::AgentMessage { text, .. } + ¬ification.item.payload, + codex_app_server_protocol::ThreadItemPayload::AgentMessage { text, .. } if text == "hello" ) => { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index cf708bc436..7e3ef7ffbb 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4643,6 +4643,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -5545,7 +5546,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadNameUpdatedNotification": { "properties": { 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 317837bdf1..669752dbfe 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 @@ -22316,6 +22316,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -23218,7 +23219,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadItemEntry": { "properties": { 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 fac312d9ac..0ce520a0d4 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 @@ -20030,6 +20030,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -20932,7 +20933,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadItemEntry": { "properties": { 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 f822591d7d..a1a5a52866 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -797,6 +797,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1699,7 +1700,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "UserInput": { "oneOf": [ 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 d766bf8d55..29c048b1fd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -797,6 +797,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1699,7 +1700,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "UserInput": { "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index e8c2d6984b..40d9228061 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1001,6 +1001,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1903,7 +1904,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "Turn": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index acf5e470eb..9226add882 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1615,6 +1615,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2517,7 +2518,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json index 41a14aa338..9938cc5e54 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json @@ -797,6 +797,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1699,7 +1700,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadItemEntry": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 9a8ddee000..f84e2cc358 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadReadState": { "description": "The first unread position in a durable local thread. A null position means read. The opaque revision guards edits against activity or marks the caller has not seen.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 27399f7544..fdd60d2bab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 09b124e5a7..7c2a3312b7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadReadState": { "description": "The first unread position in a durable local thread. A null position means read. The opaque revision guards edits against activity or marks the caller has not seen.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 612c30e99b..7e6b864f17 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1667,6 +1667,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2569,7 +2570,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index 62277c4ad1..fa8c7eaf1d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 8c26333295..0ea67f6b20 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1615,6 +1615,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2517,7 +2518,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index f2f9e9dce8..ce122a67a0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json index da152655cc..b19cc193d6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json @@ -1001,6 +1001,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1903,7 +1904,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "Turn": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 37830b79ec..87d6af83f9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1406,6 +1406,7 @@ "type": "string" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -2308,7 +2309,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadSection": { "description": "An independently persisted, user-visible thread section.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 98f9204915..ac0e869aa6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1001,6 +1001,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1903,7 +1904,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "Turn": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 6e161a29f5..011a41907f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1001,6 +1001,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1903,7 +1904,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "Turn": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 5e5c05fd08..f7df365791 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1001,6 +1001,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -1903,7 +1904,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "Turn": { "properties": { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 68f61805ed0aca2f4bc279f47190ecdf62fbb09a..3a60dfafeb2163ae72e4b2e49e9aeeea547c0a47 100644 GIT binary patch delta 123989 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
WhJco49hH!SMtMh(O! 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

f5H%~ z;~<5?jsrLWY_qPTxslk_yzUj1Q(8n%!`Os`9+on6Jou?*qFC^AWF;?^=lr_)lA4%T z_VS5`m)Hp(E?1VLXDNqx1aavc#ARF0QX1ltPo(53yJFJ3bg@*LcTm|sp7zjn63_;? zUYWKm)tqQG0e^tK;y*)Bp0tzKfALx~!)8Q@d4*=kfYq5UP=1nmg1Q!+MTG!SE`tA!6NQP;vT7$kPo5cWX?Na*!GH4 z@L8)_35*g{A@@o|v7~^xSIjk|Yqh57e~7)}cZr9Qfte(7EkYx?t-We&dRp@a(xh-3 zt&p!gZNtbVT2`8NP%IOj8!lft;tPI>hueH5k2DD7zMWKZc1k4({YNDBXep5$|Bds^(|-CAgq!@ko2j;E8+3|s Og?BdHj2TD)TB{7dVG-j1 delta 123822 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�&^`#E&RWFmuz(Z+YsFH_q; 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 z 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%0Drn{zd*&^qk61uWH`I@Ucqr3}&c%^76j(Hdpc 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 L-3_cJxabNDeCTUo diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 1f132df8da..5338795c96 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -31,7 +31,18 @@ import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { SubAgentActivityKind } from "./SubAgentActivityKind"; import type { UserInput } from "./UserInput"; -export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, +/** + * A thread item and its recorded lifecycle times. The payload stays flat on the wire. + */ +export type ThreadItem = { +/** + * Unix timestamp in milliseconds when the item started, if recorded. + */ +startedAtMs: number | null, +/** + * Unix timestamp in milliseconds when the item completed, if recorded. + */ +completedAtMs: number | null, } & ({ "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** * Trusted first-party plugin id when this command resolves to one plugin script. */ @@ -130,4 +141,4 @@ model: string | null, /** * Resolved reasoning effort at sub-agent creation, when known. */ -reasoningEffort: ReasoningEffort | null, id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +reasoningEffort: ReasoningEffort | null, id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }); 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 e0fcfb6759..9ba6a32d70 100644 --- a/codex-rs/app-server-protocol/src/protocol/event_mapping.rs +++ b/codex-rs/app-server-protocol/src/protocol/event_mapping.rs @@ -17,7 +17,7 @@ use crate::protocol::v2::ReasoningSummaryPartAddedNotification; use crate::protocol::v2::ReasoningSummaryTextDeltaNotification; use crate::protocol::v2::ReasoningTextDeltaNotification; use crate::protocol::v2::TerminalInteractionNotification; -use crate::protocol::v2::ThreadItem; +use crate::protocol::v2::ThreadItemPayload; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::protocol::EventMsg; use std::collections::HashMap; @@ -42,7 +42,7 @@ pub fn item_event_to_server_notification( DynamicToolCallStatus::Failed }; let duration_ms = i64::try_from(response.duration.as_millis()).ok(); - let item = ThreadItem::DynamicToolCall { + let item = ThreadItemPayload::DynamicToolCall { id: response.call_id, namespace: response.namespace, tool: response.tool, @@ -68,15 +68,16 @@ pub fn item_event_to_server_notification( success: Some(response.success), duration_ms, }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, - turn_id: response.turn_id, + response.turn_id, item, - completed_at_ms: response.completed_at_ms, - }) + /*started_at_ms*/ None, + response.completed_at_ms, + )) } EventMsg::CollabAgentSpawnBegin(begin_event) => { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: begin_event.call_id, tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::InProgress, @@ -87,12 +88,12 @@ pub fn item_event_to_server_notification( reasoning_effort: Some(begin_event.reasoning_effort), agents_states: HashMap::new(), }; - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, item, - started_at_ms: begin_event.started_at_ms, - }) + begin_event.started_at_ms, + )) } EventMsg::CollabAgentSpawnEnd(end_event) => { let has_receiver = end_event.new_thread_id.is_some(); @@ -115,7 +116,7 @@ pub fn item_event_to_server_notification( } None => (Vec::new(), HashMap::new()), }; - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: end_event.call_id, tool: CollabAgentTool::SpawnAgent, status, @@ -126,16 +127,17 @@ pub fn item_event_to_server_notification( reasoning_effort: Some(end_event.reasoning_effort), agents_states, }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: end_event.completed_at_ms, - }) + /*started_at_ms*/ None, + end_event.completed_at_ms, + )) } EventMsg::CollabAgentInteractionBegin(begin_event) => { let receiver_thread_ids = vec![begin_event.receiver_thread_id.to_string()]; - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: begin_event.call_id, tool: CollabAgentTool::SendInput, status: CollabAgentToolCallStatus::InProgress, @@ -146,12 +148,12 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states: HashMap::new(), }; - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, item, - started_at_ms: begin_event.started_at_ms, - }) + begin_event.started_at_ms, + )) } EventMsg::CollabAgentInteractionEnd(end_event) => { let status = match &end_event.status { @@ -163,7 +165,7 @@ pub fn item_event_to_server_notification( }; let receiver_id = end_event.receiver_thread_id.to_string(); let received_status = CollabAgentState::from(end_event.status); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: end_event.call_id, tool: CollabAgentTool::SendInput, status, @@ -174,15 +176,16 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states: [(receiver_id, received_status)].into_iter().collect(), }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: end_event.completed_at_ms, - }) + /*started_at_ms*/ None, + end_event.completed_at_ms, + )) } EventMsg::SubAgentActivity(activity) => { - let item = ThreadItem::SubAgentActivity { + let item = ThreadItemPayload::SubAgentActivity { model: activity.model, reasoning_effort: activity.reasoning_effort, id: activity.event_id, @@ -190,12 +193,13 @@ pub fn item_event_to_server_notification( agent_thread_id: activity.agent_thread_id.to_string(), agent_path: String::from(activity.agent_path), }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: activity.occurred_at_ms, - }) + /*started_at_ms*/ None, + activity.occurred_at_ms, + )) } EventMsg::CollabWaitingBegin(begin_event) => { let receiver_thread_ids = begin_event @@ -203,7 +207,7 @@ pub fn item_event_to_server_notification( .iter() .map(ToString::to_string) .collect(); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: begin_event.call_id, tool: CollabAgentTool::Wait, status: CollabAgentToolCallStatus::InProgress, @@ -214,12 +218,12 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states: HashMap::new(), }; - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, item, - started_at_ms: begin_event.started_at_ms, - }) + begin_event.started_at_ms, + )) } EventMsg::CollabWaitingEnd(end_event) => { let status = if end_event.statuses.values().any(|status| { @@ -239,7 +243,7 @@ pub fn item_event_to_server_notification( .iter() .map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone()))) .collect(); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: end_event.call_id, tool: CollabAgentTool::Wait, status, @@ -250,15 +254,16 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states, }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: end_event.completed_at_ms, - }) + /*started_at_ms*/ None, + end_event.completed_at_ms, + )) } EventMsg::CollabCloseBegin(begin_event) => { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: begin_event.call_id, tool: CollabAgentTool::CloseAgent, status: CollabAgentToolCallStatus::InProgress, @@ -269,12 +274,12 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states: HashMap::new(), }; - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, item, - started_at_ms: begin_event.started_at_ms, - }) + begin_event.started_at_ms, + )) } EventMsg::CollabCloseEnd(end_event) => { let status = match &end_event.status { @@ -291,7 +296,7 @@ pub fn item_event_to_server_notification( )] .into_iter() .collect(); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: end_event.call_id, tool: CollabAgentTool::CloseAgent, status, @@ -302,15 +307,16 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states, }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: end_event.completed_at_ms, - }) + /*started_at_ms*/ None, + end_event.completed_at_ms, + )) } EventMsg::CollabResumeBegin(begin_event) => { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: begin_event.call_id, tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::InProgress, @@ -321,12 +327,12 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states: HashMap::new(), }; - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, item, - started_at_ms: begin_event.started_at_ms, - }) + begin_event.started_at_ms, + )) } EventMsg::CollabResumeEnd(end_event) => { let status = match &end_event.status { @@ -343,7 +349,7 @@ pub fn item_event_to_server_notification( )] .into_iter() .collect(); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: end_event.call_id, tool: CollabAgentTool::ResumeAgent, status, @@ -354,12 +360,13 @@ pub fn item_event_to_server_notification( reasoning_effort: None, agents_states, }; - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, item, - completed_at_ms: end_event.completed_at_ms, - }) + /*started_at_ms*/ None, + end_event.completed_at_ms, + )) } EventMsg::AgentMessageContentDelta(event) => { let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } = @@ -404,20 +411,21 @@ pub fn item_event_to_server_notification( }) } EventMsg::ItemStarted(item_started_event) => { - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, - item: item_started_event.item.into(), - started_at_ms: item_started_event.started_at_ms, - }) + ThreadItemPayload::from(item_started_event.item), + item_started_event.started_at_ms, + )) } EventMsg::ItemCompleted(item_completed_event) => { - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, - item: item_completed_event.item.into(), - completed_at_ms: item_completed_event.completed_at_ms, - }) + ThreadItemPayload::from(item_completed_event.item), + item_completed_event.started_at_ms, + item_completed_event.completed_at_ms, + )) } EventMsg::PatchApplyUpdated(event) => { ServerNotification::FileChangePatchUpdated(FileChangePatchUpdatedNotification { @@ -428,12 +436,12 @@ pub fn item_event_to_server_notification( }) } EventMsg::ExecCommandBegin(exec_command_begin_event) => { - ServerNotification::ItemStarted(ItemStartedNotification { + ServerNotification::ItemStarted(ItemStartedNotification::new( thread_id, turn_id, - item: build_command_execution_begin_item(&exec_command_begin_event), - started_at_ms: exec_command_begin_event.started_at_ms, - }) + build_command_execution_begin_item(&exec_command_begin_event), + exec_command_begin_event.started_at_ms, + )) } EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => { let item_id = exec_command_output_delta_event.call_id; @@ -457,12 +465,13 @@ pub fn item_event_to_server_notification( }) } EventMsg::ExecCommandEnd(exec_command_end_event) => { - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( thread_id, turn_id, - item: build_command_execution_end_item(&exec_command_end_event), - completed_at_ms: exec_command_end_event.completed_at_ms, - }) + build_command_execution_end_item(&exec_command_end_event), + /*started_at_ms*/ None, + exec_command_end_event.completed_at_ms, + )) } _ => unreachable!("unsupported item event"), } @@ -528,11 +537,10 @@ mod tests { ); assert_item_started_server_notification( notification, - ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: event.started_at_ms, - item: ThreadItem::CollabAgentToolCall { + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: event.call_id, tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::InProgress, @@ -543,7 +551,8 @@ mod tests { reasoning_effort: None, agents_states: HashMap::new(), }, - }, + event.started_at_ms, + ), ); } @@ -567,11 +576,10 @@ mod tests { ); assert_item_completed_server_notification( notification, - ItemCompletedNotification { - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - completed_at_ms: event.completed_at_ms, - item: ThreadItem::CollabAgentToolCall { + ItemCompletedNotification::new( + "thread-2".to_string(), + "turn-2".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: event.call_id, tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::Failed, @@ -587,7 +595,9 @@ mod tests { .into_iter() .collect(), }, - }, + /*started_at_ms*/ None, + event.completed_at_ms, + ), ); } diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 1ace0e8a6e..3f100534d7 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -21,7 +21,7 @@ use crate::protocol::v2::ItemGuardianApprovalReviewCompletedNotification; use crate::protocol::v2::ItemGuardianApprovalReviewStartedNotification; use crate::protocol::v2::PatchApplyStatus; use crate::protocol::v2::PatchChangeKind; -use crate::protocol::v2::ThreadItem; +use crate::protocol::v2::ThreadItemPayload; use codex_protocol::ThreadId; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; @@ -69,34 +69,34 @@ pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String { pub fn build_file_change_approval_request_item( payload: &ApplyPatchApprovalRequestEvent, -) -> ThreadItem { - ThreadItem::FileChange { +) -> ThreadItemPayload { + ThreadItemPayload::FileChange { id: payload.call_id.clone(), changes: convert_patch_changes(&payload.changes), status: PatchApplyStatus::InProgress, } } -pub fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItem { - ThreadItem::FileChange { +pub fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItemPayload { + ThreadItemPayload::FileChange { id: payload.call_id.clone(), changes: convert_patch_changes(&payload.changes), status: PatchApplyStatus::InProgress, } } -pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem { - ThreadItem::FileChange { +pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItemPayload { + ThreadItemPayload::FileChange { id: payload.call_id.clone(), changes: convert_patch_changes(&payload.changes), status: (&payload.status).into(), } } -pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem { +pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItemPayload { let presentation = CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd); - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: payload.call_id.clone(), @@ -114,7 +114,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th } } -pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItem { +pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItemPayload { let aggregated_output = if payload.aggregated_output.is_empty() { None } else { @@ -124,7 +124,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread let presentation = CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd); - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: payload.call_id.clone(), @@ -193,7 +193,7 @@ fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> pub fn build_item_from_guardian_event( assessment: &GuardianAssessmentEvent, status: CommandExecutionStatus, -) -> Option { +) -> Option { match &assessment.action { GuardianAssessmentAction::Command { command, cwd, .. } => { let id = assessment.target_item_id.as_ref()?; @@ -201,7 +201,7 @@ pub fn build_item_from_guardian_event( let command_actions = vec![CommandAction::Unknown { command: command.clone(), }]; - Some(ThreadItem::CommandExecution { + Some(ThreadItemPayload::CommandExecution { id: id.clone(), model_context: assessment.model_context.clone(), sandbox_type: None, @@ -241,7 +241,7 @@ pub fn build_item_from_guardian_event( .map(|parsed| CommandAction::from_core_with_cwd(parsed, cwd)) .collect() }; - Some(ThreadItem::CommandExecution { + Some(ThreadItemPayload::CommandExecution { id: id.clone(), model_context: assessment.model_context.clone(), sandbox_type: None, diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index f08082a8d9..e9509d7899 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::protocol::thread_history::build_turns_from_rollout_items; +use crate::protocol::v2::ThreadItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::GuardianAssessmentStatus; @@ -126,7 +127,11 @@ fn guardian_stdin_reviews_preserve_parent_command_history() { assert_eq!(expected.len(), 2); assert_eq!( expected[0].items, - vec![build_command_execution_begin_item(&begin)], + [ThreadItem { + payload: build_command_execution_begin_item(&begin), + started_at_ms: Some(begin.started_at_ms), + completed_at_ms: None, + }], ); assert!(expected[1].items.is_empty()); 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 418a9fb1bb..077dfd5e0d 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -17,6 +17,7 @@ use crate::protocol::v2::McpToolCallError; use crate::protocol::v2::McpToolCallResult; use crate::protocol::v2::McpToolCallStatus; use crate::protocol::v2::ThreadItem; +use crate::protocol::v2::ThreadItemPayload; use crate::protocol::v2::Turn; use crate::protocol::v2::TurnError as V2TurnError; use crate::protocol::v2::TurnError; @@ -53,6 +54,7 @@ use codex_protocol::protocol::McpToolCallBeginEvent; use codex_protocol::protocol::McpToolCallEndEvent; use codex_protocol::protocol::PatchApplyBeginEvent; use codex_protocol::protocol::PatchApplyEndEvent; +use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::TurnAbortedEvent; use codex_protocol::protocol::TurnCompleteEvent; @@ -95,13 +97,52 @@ pub fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec { builder.finish() } +#[derive(Default)] +struct ItemTimes { + started_at_ms: Option, + completed_at_ms: Option, +} + +impl ItemTimes { + // Legacy events deserialize an omitted integer timestamp as zero. Leave the + // item untimed in that case so replay never shows a date in 1970. + fn legacy_start(started_at_ms: i64) -> Self { + Self { + started_at_ms: (started_at_ms != 0).then_some(started_at_ms), + ..Self::default() + } + } + + fn legacy_completion(completed_at_ms: i64) -> Self { + Self { + completed_at_ms: (completed_at_ms != 0).then_some(completed_at_ms), + ..Self::default() + } + } +} + +fn history_items_with_times( + items: Vec, + times: &HashMap, +) -> Vec { + items + .into_iter() + .map(|payload| { + let times = times.get(payload.id()); + ThreadItem { + started_at_ms: times.and_then(|t| t.started_at_ms), + completed_at_ms: times.and_then(|t| t.completed_at_ms), + payload, + } + }) + .collect() +} + /// A materialized `ThreadItem` snapshot that changed while handling one input. #[derive(Debug, Clone, PartialEq)] pub struct ThreadHistoryItemChange { pub turn_id: String, pub item: ThreadItem, - pub started_at_ms: Option, - pub completed_at_ms: Option, } /// Turn metadata for history projection and snapshots that do not need items. @@ -362,7 +403,19 @@ impl ThreadHistoryBuilder { /// This function should handle all EventMsg variants that can be persisted in a rollout file. /// See `persisted_rollout_item` in `codex-rs/rollout/src/policy.rs`. pub fn handle_event(&mut self, event: &EventMsg) { + self.handle_event_for_history_mode(event, ThreadHistoryMode::Legacy); + } + + /// Paginated live history uses canonical items when Core's compatibility + /// events omit their IDs. Legacy rollouts have only the compatibility events. + pub fn handle_event_for_history_mode(&mut self, event: &EventMsg, mode: ThreadHistoryMode) { match event { + EventMsg::UserMessage(_) + | EventMsg::AgentMessage(_) + | EventMsg::AgentReasoning(_) + | EventMsg::AgentReasoningRawContent(_) + | EventMsg::ContextCompacted(_) + if mode == ThreadHistoryMode::Paginated => {} EventMsg::UserMessage(payload) => self.handle_user_message(payload), EventMsg::AgentMessage(payload) => self.handle_agent_message(payload), EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload), @@ -410,8 +463,8 @@ impl ThreadHistoryBuilder { EventMsg::ContextCompacted(payload) => self.handle_context_compacted(payload), EventMsg::EnteredReviewMode(payload) => self.handle_entered_review_mode(payload), EventMsg::ExitedReviewMode(payload) => self.handle_exited_review_mode(payload), - EventMsg::ItemStarted(payload) => self.handle_item_started(payload), - EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload), + EventMsg::ItemStarted(payload) => self.handle_item_started(payload, mode), + EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload, mode), EventMsg::HookStarted(_) | EventMsg::HookCompleted(_) => {} EventMsg::Error(payload) => self.handle_error(payload), EventMsg::TokenCount(_) => {} @@ -475,7 +528,19 @@ impl ThreadHistoryBuilder { debug_assert!(self.active_change_set.is_none()); self.active_change_set = Some(ThreadHistoryChangeSet::default()); handle(self); - self.active_change_set.take().unwrap_or_default() + let mut changes = self.active_change_set.take().unwrap_or_default(); + // Canonical events can record times after updating the payload. Publish + // the same lifecycle times a turn snapshot would return after this event. + for change in &mut changes.changed_items { + if let Some(times) = self + .turn_mut(&change.turn_id) + .and_then(|turn| turn.item_times.get(change.item.id())) + { + change.item.started_at_ms = times.started_at_ms; + change.item.completed_at_ms = times.completed_at_ms; + } + } + changes } fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) { @@ -494,7 +559,7 @@ impl ThreadHistoryBuilder { return; }; - self.push_item_in_current_turn(ThreadItem::HookPrompt { + self.push_item_in_current_turn(ThreadItemPayload::HookPrompt { id: hook_prompt.id, fragments: hook_prompt .fragments @@ -516,7 +581,7 @@ impl ThreadHistoryBuilder { } let id = self.next_item_id(); let content = self.build_user_inputs(payload); - self.push_item_in_current_turn(ThreadItem::UserMessage { + self.push_item_in_current_turn(ThreadItemPayload::UserMessage { id, client_id: payload.client_id.clone(), content, @@ -529,7 +594,7 @@ impl ThreadHistoryBuilder { } let id = self.next_item_id(); - self.push_item_in_current_turn(ThreadItem::AgentMessage { + self.push_item_in_current_turn(ThreadItemPayload::AgentMessage { id, text: payload.message.clone(), phase: payload.phase.clone(), @@ -548,7 +613,7 @@ impl ThreadHistoryBuilder { let existing_item_change = { let tracking_changes = self.is_tracking_changes(); let turn = self.ensure_turn(); - if let Some(ThreadItem::Reasoning { summary, .. }) = turn.items.last_mut() { + if let Some(ThreadItemPayload::Reasoning { summary, .. }) = turn.items.last_mut() { summary.push(payload.text.clone()); let changed_item = if tracking_changes { turn.items @@ -572,7 +637,7 @@ impl ThreadHistoryBuilder { // Otherwise, create a new reasoning item. let id = self.next_item_id(); - self.push_item_in_current_turn(ThreadItem::Reasoning { + self.push_item_in_current_turn(ThreadItemPayload::Reasoning { id, summary: vec![payload.text.clone()], content: Vec::new(), @@ -588,7 +653,7 @@ impl ThreadHistoryBuilder { let existing_item_change = { let tracking_changes = self.is_tracking_changes(); let turn = self.ensure_turn(); - if let Some(ThreadItem::Reasoning { content, .. }) = turn.items.last_mut() { + if let Some(ThreadItemPayload::Reasoning { content, .. }) = turn.items.last_mut() { content.push(payload.text.clone()); let changed_item = if tracking_changes { turn.items @@ -612,19 +677,62 @@ impl ThreadHistoryBuilder { // Otherwise, create a new reasoning item. let id = self.next_item_id(); - self.push_item_in_current_turn(ThreadItem::Reasoning { + self.push_item_in_current_turn(ThreadItemPayload::Reasoning { id, summary: Vec::new(), content: vec![payload.text.clone()], }); } - fn handle_item_started(&mut self, payload: &ItemStartedEvent) { + fn handle_item_started(&mut self, payload: &ItemStartedEvent, mode: ThreadHistoryMode) { self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item); + self.record_item_times( + &payload.turn_id, + &payload.item, + Some(payload.started_at_ms), + /*completed_at_ms*/ None, + mode, + ); } - fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) { - self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item); + fn handle_item_completed(&mut self, payload: &ItemCompletedEvent, mode: ThreadHistoryMode) { + if mode == ThreadHistoryMode::Paginated && has_idless_legacy_copy(&payload.item) { + let item = ThreadItemPayload::from(payload.item.clone()); + if !matches!(&item, ThreadItemPayload::AgentMessage { text, .. } if text.is_empty()) { + self.upsert_item_in_turn_id(&payload.turn_id, item); + } + } else { + self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item); + } + self.record_item_times( + &payload.turn_id, + &payload.item, + payload.started_at_ms, + (payload.completed_at_ms != 0).then_some(payload.completed_at_ms), + mode, + ); + } + + fn record_item_times( + &mut self, + turn_id: &str, + item: &codex_protocol::items::TurnItem, + started_at_ms: Option, + completed_at_ms: Option, + mode: ThreadHistoryMode, + ) { + // The compatibility events for these items assign synthetic IDs. A matching + // ID string alone cannot establish that a timestamp belongs to that item. + let keeps_item_id = mode == ThreadHistoryMode::Paginated || !has_idless_legacy_copy(item); + if keeps_item_id && let Some(turn) = self.turn_mut(turn_id) { + turn.record_item_times( + &item.id(), + ItemTimes { + started_at_ms, + completed_at_ms, + }, + ); + } } fn handle_materialized_item_lifecycle( @@ -660,7 +768,7 @@ impl ThreadHistoryBuilder { }; if should_upsert { - let item = ThreadItem::from(item.clone()); + let item = ThreadItemPayload::from(item.clone()); if is_review_mode_item { self.upsert_review_mode_item(Some(turn_id), item); } else { @@ -670,7 +778,7 @@ impl ThreadHistoryBuilder { } fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) { - let item = ThreadItem::WebSearch(WebSearchItem { + let item = ThreadItemPayload::WebSearch(WebSearchItem { id: payload.call_id.clone(), query: String::new(), action: None, @@ -680,7 +788,7 @@ impl ThreadHistoryBuilder { } fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) { - let item = ThreadItem::WebSearch(WebSearchItem { + let item = ThreadItemPayload::WebSearch(WebSearchItem { id: payload.call_id.clone(), query: payload.query.clone(), action: Some(web_search_action_from_core(payload.action.clone())), @@ -691,7 +799,8 @@ impl ThreadHistoryBuilder { fn handle_exec_command_begin(&mut self, payload: &ExecCommandBeginEvent) { let item = build_command_execution_begin_item(payload); - self.upsert_item_in_turn_id(&payload.turn_id, item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(Some(&payload.turn_id), item, times); } fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent) { @@ -701,7 +810,8 @@ impl ThreadHistoryBuilder { // background exit watcher when that process finally exits. By then, a // newer user turn may already have started. Route by event turn_id so // replay preserves the original turn association. - self.upsert_item_in_turn_id(&payload.turn_id, item); + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(Some(&payload.turn_id), item, times); } fn handle_guardian_assessment(&mut self, payload: &GuardianAssessmentEvent) { @@ -711,25 +821,32 @@ impl ThreadHistoryBuilder { CommandExecutionStatus::Declined } GuardianAssessmentStatus::TimedOut => CommandExecutionStatus::Failed, + // Approval hands the command back to execution, which supplies its completion time. GuardianAssessmentStatus::Approved => return, }; let Some(item) = build_item_from_guardian_event(payload, status) else { return; }; - if payload.turn_id.is_empty() { - self.upsert_item_in_current_turn(item); - } else { - self.upsert_item_in_turn_id(&payload.turn_id, item); - } + self.upsert_item( + (!payload.turn_id.is_empty()).then_some(payload.turn_id.as_str()), + item, + ItemTimes { + completed_at_ms: payload.completed_at_ms, + ..ItemTimes::legacy_start(payload.started_at_ms) + }, + ); } fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent) { - let item = build_file_change_approval_request_item(payload); - if payload.turn_id.is_empty() { - self.upsert_item_in_current_turn(item); - } else { - self.upsert_item_in_turn_id(&payload.turn_id, item); - } + // An approval can be the first event in the history for this file change. + self.upsert_item( + (!payload.turn_id.is_empty()).then_some(payload.turn_id.as_str()), + build_file_change_approval_request_item(payload), + ItemTimes { + started_at_ms: Some(payload.started_at_ms), + completed_at_ms: None, + }, + ); } fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) { @@ -754,7 +871,7 @@ impl ThreadHistoryBuilder { &mut self, payload: &codex_protocol::dynamic_tools::DynamicToolCallRequest, ) { - let item = ThreadItem::DynamicToolCall { + let item = ThreadItemPayload::DynamicToolCall { id: payload.call_id.clone(), namespace: payload.namespace.clone(), tool: payload.tool.clone(), @@ -764,11 +881,11 @@ impl ThreadHistoryBuilder { success: None, duration_ms: None, }; - if payload.turn_id.is_empty() { - self.upsert_item_in_current_turn(item); - } else { - self.upsert_item_in_turn_id(&payload.turn_id, item); - } + self.upsert_item( + (!payload.turn_id.is_empty()).then_some(payload.turn_id.as_str()), + item, + ItemTimes::legacy_start(payload.started_at_ms), + ); } fn handle_dynamic_tool_call_response(&mut self, payload: &DynamicToolCallResponseEvent) { @@ -778,7 +895,7 @@ impl ThreadHistoryBuilder { DynamicToolCallStatus::Failed }; let duration_ms = i64::try_from(payload.duration.as_millis()).ok(); - let item = ThreadItem::DynamicToolCall { + let item = ThreadItemPayload::DynamicToolCall { id: payload.call_id.clone(), namespace: payload.namespace.clone(), tool: payload.tool.clone(), @@ -788,15 +905,15 @@ impl ThreadHistoryBuilder { success: Some(payload.success), duration_ms, }; - if payload.turn_id.is_empty() { - self.upsert_item_in_current_turn(item); - } else { - self.upsert_item_in_turn_id(&payload.turn_id, item); - } + self.upsert_item( + (!payload.turn_id.is_empty()).then_some(payload.turn_id.as_str()), + item, + ItemTimes::legacy_completion(payload.completed_at_ms), + ); } fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) { - let item = ThreadItem::McpToolCall { + let item = ThreadItemPayload::McpToolCall { id: payload.call_id.clone(), server: payload.invocation.server.clone(), tool: payload.invocation.tool.clone(), @@ -854,7 +971,7 @@ impl ThreadHistoryBuilder { }), ), }; - let item = ThreadItem::McpToolCall { + let item = ThreadItemPayload::McpToolCall { id: payload.call_id.clone(), server: payload.invocation.server.clone(), tool: payload.invocation.tool.clone(), @@ -890,7 +1007,7 @@ impl ThreadHistoryBuilder { } fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) { - let item = ThreadItem::ImageView { + let item = ThreadItemPayload::ImageView { id: payload.call_id.clone(), path: payload.path.clone().into(), }; @@ -898,7 +1015,7 @@ impl ThreadHistoryBuilder { } fn handle_image_generation_begin(&mut self, payload: &ImageGenerationBeginEvent) { - let item = ThreadItem::ImageGeneration(ImageGenerationItem { + let item = ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: payload.call_id.clone(), status: String::new(), revised_prompt: None, @@ -916,7 +1033,7 @@ impl ThreadHistoryBuilder { } fn handle_image_generation_end(&mut self, payload: &ImageGenerationEndEvent) { - let item = ThreadItem::ImageGeneration(ImageGenerationItem { + let item = ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: payload.call_id.clone(), status: payload.status.clone(), revised_prompt: payload.revised_prompt.clone(), @@ -935,7 +1052,7 @@ impl ThreadHistoryBuilder { &mut self, payload: &codex_protocol::protocol::CollabAgentSpawnBeginEvent, ) { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::InProgress, @@ -946,7 +1063,8 @@ impl ThreadHistoryBuilder { reasoning_effort: Some(payload.reasoning_effort.clone()), agents_states: HashMap::new(), }; - self.upsert_item_in_current_turn(item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_agent_spawn_end( @@ -970,7 +1088,7 @@ impl ThreadHistoryBuilder { } None => (Vec::new(), HashMap::new()), }; - self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::SpawnAgent, status, @@ -980,14 +1098,16 @@ impl ThreadHistoryBuilder { model: Some(payload.model.clone()), reasoning_effort: Some(payload.reasoning_effort.clone()), agents_states, - }); + }; + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_agent_interaction_begin( &mut self, payload: &codex_protocol::protocol::CollabAgentInteractionBeginEvent, ) { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::SendInput, status: CollabAgentToolCallStatus::InProgress, @@ -998,7 +1118,8 @@ impl ThreadHistoryBuilder { reasoning_effort: None, agents_states: HashMap::new(), }; - self.upsert_item_in_current_turn(item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_agent_interaction_end( @@ -1011,7 +1132,7 @@ impl ThreadHistoryBuilder { }; let receiver_id = payload.receiver_thread_id.to_string(); let received_status = CollabAgentState::from(payload.status.clone()); - self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::SendInput, status, @@ -1021,28 +1142,32 @@ impl ThreadHistoryBuilder { model: None, reasoning_effort: None, agents_states: [(receiver_id, received_status)].into_iter().collect(), - }); + }; + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_sub_agent_activity( &mut self, payload: &codex_protocol::protocol::SubAgentActivityEvent, ) { - self.upsert_item_in_current_turn(ThreadItem::SubAgentActivity { + let item = ThreadItemPayload::SubAgentActivity { model: payload.model.clone(), reasoning_effort: payload.reasoning_effort.clone(), id: payload.event_id.clone(), kind: payload.kind.into(), agent_thread_id: payload.agent_thread_id.to_string(), agent_path: String::from(payload.agent_path.clone()), - }); + }; + let times = ItemTimes::legacy_completion(payload.occurred_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_waiting_begin( &mut self, payload: &codex_protocol::protocol::CollabWaitingBeginEvent, ) { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::Wait, status: CollabAgentToolCallStatus::InProgress, @@ -1057,7 +1182,8 @@ impl ThreadHistoryBuilder { reasoning_effort: None, agents_states: HashMap::new(), }; - self.upsert_item_in_current_turn(item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_waiting_end( @@ -1081,7 +1207,7 @@ impl ThreadHistoryBuilder { .iter() .map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone()))) .collect(); - self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::Wait, status, @@ -1091,14 +1217,16 @@ impl ThreadHistoryBuilder { model: None, reasoning_effort: None, agents_states, - }); + }; + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_close_begin( &mut self, payload: &codex_protocol::protocol::CollabCloseBeginEvent, ) { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::CloseAgent, status: CollabAgentToolCallStatus::InProgress, @@ -1109,7 +1237,8 @@ impl ThreadHistoryBuilder { reasoning_effort: None, agents_states: HashMap::new(), }; - self.upsert_item_in_current_turn(item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_close_end(&mut self, payload: &codex_protocol::protocol::CollabCloseEndEvent) { @@ -1124,7 +1253,7 @@ impl ThreadHistoryBuilder { )] .into_iter() .collect(); - self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::CloseAgent, status, @@ -1134,14 +1263,16 @@ impl ThreadHistoryBuilder { model: None, reasoning_effort: None, agents_states, - }); + }; + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_resume_begin( &mut self, payload: &codex_protocol::protocol::CollabResumeBeginEvent, ) { - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::InProgress, @@ -1152,7 +1283,8 @@ impl ThreadHistoryBuilder { reasoning_effort: None, agents_states: HashMap::new(), }; - self.upsert_item_in_current_turn(item); + let times = ItemTimes::legacy_start(payload.started_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_collab_resume_end( @@ -1170,7 +1302,7 @@ impl ThreadHistoryBuilder { )] .into_iter() .collect(); - self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: payload.call_id.clone(), tool: CollabAgentTool::ResumeAgent, status, @@ -1180,12 +1312,14 @@ impl ThreadHistoryBuilder { model: None, reasoning_effort: None, agents_states, - }); + }; + let times = ItemTimes::legacy_completion(payload.completed_at_ms); + self.upsert_item(/*turn_id*/ None, item, times); } fn handle_context_compacted(&mut self, _payload: &ContextCompactedEvent) { let id = self.next_item_id(); - self.push_item_in_current_turn(ThreadItem::ContextCompaction { id }); + self.push_item_in_current_turn(ThreadItemPayload::ContextCompaction { id }); } fn handle_entered_review_mode( @@ -1202,7 +1336,7 @@ impl ThreadHistoryBuilder { .unwrap_or_else(|| self.next_item_id()); self.upsert_review_mode_item( payload.turn_id.as_deref(), - ThreadItem::EnteredReviewMode { id, review }, + ThreadItemPayload::EnteredReviewMode { id, review }, ); } @@ -1217,11 +1351,11 @@ impl ThreadHistoryBuilder { .unwrap_or_else(|| self.next_item_id()); self.upsert_review_mode_item( payload.turn_id.as_deref(), - ThreadItem::ExitedReviewMode { id, review }, + ThreadItemPayload::ExitedReviewMode { id, review }, ); } - fn upsert_review_mode_item(&mut self, turn_id: Option<&str>, item: ThreadItem) { + fn upsert_review_mode_item(&mut self, turn_id: Option<&str>, item: ThreadItemPayload) { let Some(turn_id) = turn_id else { self.upsert_item_in_current_turn(item); return; @@ -1425,6 +1559,7 @@ impl ThreadHistoryBuilder { root_turn_id: None, items: Vec::new(), item_index: TurnItemIndex::default(), + item_times: HashMap::new(), error: None, status: TurnStatus::Completed, started_at: None, @@ -1450,7 +1585,7 @@ impl ThreadHistoryBuilder { unreachable!("current turn must exist after initialization"); } - fn push_item_in_current_turn(&mut self, item: ThreadItem) { + fn push_item_in_current_turn(&mut self, item: ThreadItemPayload) { let tracking_changes = self.is_tracking_changes(); let changed_item = { let turn = self.ensure_turn(); @@ -1463,46 +1598,40 @@ impl ThreadHistoryBuilder { } } - fn upsert_item_in_turn_id(&mut self, turn_id: &str, item: ThreadItem) { - let tracking_changes = self.is_tracking_changes(); - if let Some(turn) = self.current_turn.as_mut() - && turn.id == turn_id - { - let changed_item = { - let item = turn.item_index.upsert(&mut turn.items, item); - tracking_changes.then(|| (turn.id.clone(), item.clone())) - }; - if let Some((turn_id, item)) = changed_item { - self.record_changed_item(turn_id, item); - } - return; - } - - if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) { - let changed_item = { - let item = turn.item_index.upsert(&mut turn.items, item); - tracking_changes.then(|| (turn.id.clone(), item.clone())) - }; - if let Some((turn_id, item)) = changed_item { - self.record_changed_item(turn_id, item); - } - return; - } - - warn!( - item_id = item.id(), - "dropping turn-scoped item for unknown turn id `{turn_id}`" - ); + fn turn_mut(&mut self, turn_id: &str) -> Option<&mut PendingTurn> { + self.current_turn + .as_mut() + .filter(|turn| turn.id == turn_id) + .or_else(|| self.turns.iter_mut().find(|turn| turn.id == turn_id)) } - fn upsert_item_in_current_turn(&mut self, item: ThreadItem) { + fn upsert_item_in_turn_id(&mut self, turn_id: &str, item: ThreadItemPayload) { + self.upsert_item(Some(turn_id), item, ItemTimes::default()); + } + + fn upsert_item_in_current_turn(&mut self, item: ThreadItemPayload) { + self.upsert_item(/*turn_id*/ None, item, ItemTimes::default()); + } + + fn upsert_item(&mut self, turn_id: Option<&str>, item: ThreadItemPayload, times: ItemTimes) { let tracking_changes = self.is_tracking_changes(); - let changed_item = { - let turn = self.ensure_turn(); - let item = turn.item_index.upsert(&mut turn.items, item); - tracking_changes.then(|| (turn.id.clone(), item.clone())) + let turn = if let Some(turn_id) = turn_id { + let Some(turn) = self.turn_mut(turn_id) else { + warn!( + item_id = item.id(), + "dropping turn-scoped item for unknown turn id `{turn_id}`" + ); + return; + }; + turn + } else { + self.ensure_turn() }; - if let Some((turn_id, item)) = changed_item { + turn.record_item_times(item.id(), times); + let item = turn.item_index.upsert(&mut turn.items, item); + if tracking_changes { + let turn_id = turn.id.clone(); + let item = item.clone(); self.record_changed_item(turn_id, item); } } @@ -1511,14 +1640,11 @@ impl ThreadHistoryBuilder { self.active_change_set.is_some() } - fn record_changed_item(&mut self, turn_id: String, item: ThreadItem) { + fn record_changed_item(&mut self, turn_id: String, item: ThreadItemPayload) { if let Some(change_set) = self.active_change_set.as_mut() { change_set.changed_items.push(ThreadHistoryItemChange { turn_id, - item, - // Legacy events used by ThreadHistoryBuilder don't have timestamps - started_at_ms: None, - completed_at_ms: None, + item: ThreadItem::untimed(item), }); } } @@ -1636,6 +1762,18 @@ impl ThreadHistoryBuilder { } } +/// These items get synthetic IDs when reconstructed from compatibility events. +fn has_idless_legacy_copy(item: &codex_protocol::items::TurnItem) -> bool { + use codex_protocol::items::TurnItem; + matches!( + item, + TurnItem::UserMessage(_) + | TurnItem::AgentMessage(_) + | TurnItem::Reasoning(_) + | TurnItem::ContextCompaction(_) + ) +} + fn convert_dynamic_tool_content_items( items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem], ) -> Vec { @@ -1667,7 +1805,7 @@ struct TurnItemIndex { } impl TurnItemIndex { - fn push(&mut self, items: &mut Vec, item: ThreadItem) { + fn push(&mut self, items: &mut Vec, item: ThreadItemPayload) { if let Some(positions) = &mut self.positions { positions .entry(item.id().to_string()) @@ -1676,7 +1814,11 @@ impl TurnItemIndex { items.push(item); } - fn upsert<'a>(&mut self, items: &'a mut Vec, item: ThreadItem) -> &'a ThreadItem { + fn upsert<'a>( + &mut self, + items: &'a mut Vec, + item: ThreadItemPayload, + ) -> &'a ThreadItemPayload { if self.positions.is_none() && items.len() >= TURN_ITEM_INDEX_THRESHOLD { let mut positions = HashMap::with_capacity(items.len()); for (index, existing) in items.iter().enumerate() { @@ -1703,8 +1845,9 @@ impl TurnItemIndex { struct PendingTurn { id: String, root_turn_id: Option, - items: Vec, + items: Vec, item_index: TurnItemIndex, + item_times: HashMap, error: Option, status: TurnStatus, started_at: Option, @@ -1721,6 +1864,16 @@ struct PendingTurn { } impl PendingTurn { + fn record_item_times(&mut self, item_id: &str, times: ItemTimes) { + if times.started_at_ms.is_some() || times.completed_at_ms.is_some() { + let recorded = self.item_times.entry(item_id.to_owned()).or_default(); + // Canonical and compatibility events can describe the same item. + // Each fills in only the part of its lifecycle we have yet to observe. + recorded.started_at_ms = recorded.started_at_ms.or(times.started_at_ms); + recorded.completed_at_ms = recorded.completed_at_ms.or(times.completed_at_ms); + } + } + fn opened_explicitly(mut self) -> Self { self.opened_explicitly = true; self @@ -1737,10 +1890,11 @@ impl PendingTurn { } fn snapshot(&self, items_view: TurnItemsView) -> Turn { + let items = items_view.project_items(&self.items); Turn { id: self.id.clone(), root_turn_id: self.root_turn_id.clone(), - items: items_view.project_items(&self.items), + items: history_items_with_times(items, &self.item_times), items_view, error: self.error.clone(), status: self.status.clone(), @@ -1756,7 +1910,7 @@ impl From for Turn { Self { id: value.id, root_turn_id: value.root_turn_id, - items: value.items, + items: history_items_with_times(value.items, &value.item_times), items_view: TurnItemsView::Full, error: value.error, status: value.status, @@ -1829,8 +1983,8 @@ mod tests { use std::time::Duration; use uuid::Uuid; - fn indexed_sleep_item(id: &str, duration_ms: u64) -> ThreadItem { - ThreadItem::Sleep(CoreSleepItem { + fn indexed_sleep_item(id: &str, duration_ms: u64) -> ThreadItemPayload { + ThreadItemPayload::Sleep(CoreSleepItem { id: id.to_string(), duration_ms, }) @@ -1864,6 +2018,62 @@ mod tests { } } + #[test] + fn paginated_snapshot_keeps_reasoning_and_compaction_lifecycle() { + use codex_protocol::items::ContextCompactionItem; + use codex_protocol::items::ReasoningItem; + use codex_protocol::protocol::HasLegacyEvent; + + let mode = ThreadHistoryMode::Paginated; + let mut builder = ThreadHistoryBuilder::new(); + let turn_id = "turn-1".to_string(); + builder.handle_event_for_history_mode( + &EventMsg::TurnStarted(TurnStartedEvent { + turn_id: turn_id.clone(), + turn_attribution: None, + root_turn_id: None, + trace_id: None, + started_at: None, + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + mode, + ); + let mut expected = Vec::new(); + for item in [ + CoreTurnItem::Reasoning(ReasoningItem { + id: "reasoning".into(), + summary_text: vec!["summary".into()], + raw_content: vec!["content".into()], + }), + CoreTurnItem::ContextCompaction(ContextCompactionItem { + id: "compaction".into(), + }), + ] { + let started = ItemStartedEvent { + thread_id: ThreadId::default(), + turn_id: turn_id.clone(), + item: item.clone(), + started_at_ms: 10, + }; + builder.handle_event_for_history_mode(&EventMsg::ItemStarted(started), mode); + let completed = ItemCompletedEvent { + thread_id: ThreadId::default(), + turn_id: turn_id.clone(), + item, + started_at_ms: Some(10), + completed_at_ms: 20, + }; + expected.push(ThreadItem::from(&completed)); + let copies = completed.as_legacy_events(/*show_raw_agent_reasoning*/ true); + builder.handle_event_for_history_mode(&EventMsg::ItemCompleted(completed), mode); + for copy in copies { + builder.handle_event_for_history_mode(©, mode); + } + } + assert_eq!(builder.active_turn_snapshot().unwrap().items, expected); + } + #[test] fn builds_multiple_turns_with_reasoning_items() { let events = vec![ @@ -1917,8 +2127,8 @@ mod tests { assert_eq!(first.status, TurnStatus::Completed); assert_eq!(first.items.len(), 3); assert_eq!( - first.items[0], - ThreadItem::UserMessage { + first.items[0].payload, + ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![ @@ -1936,8 +2146,8 @@ mod tests { } ); assert_eq!( - first.items[1], - ThreadItem::AgentMessage { + first.items[1].payload, + ThreadItemPayload::AgentMessage { id: "item-2".into(), text: "Hi there".into(), phase: None, @@ -1947,8 +2157,8 @@ mod tests { } ); assert_eq!( - first.items[2], - ThreadItem::Reasoning { + first.items[2].payload, + ThreadItemPayload::Reasoning { id: "item-3".into(), summary: vec!["thinking".into()], content: vec!["full reasoning".into()], @@ -1960,8 +2170,8 @@ mod tests { assert_ne!(first.id, second.id); assert_eq!(second.items.len(), 2); assert_eq!( - second.items[0], - ThreadItem::UserMessage { + second.items[0].payload, + ThreadItemPayload::UserMessage { id: "item-4".into(), client_id: None, content: vec![UserInput::Text { @@ -1971,8 +2181,8 @@ mod tests { } ); assert_eq!( - second.items[1], - ThreadItem::AgentMessage { + second.items[1].payload, + ThreadItemPayload::AgentMessage { id: "item-5".into(), text: "Reply two".into(), phase: None, @@ -2019,13 +2229,13 @@ mod tests { assert_eq!(turns[0].id, "turn-1"); assert_eq!( - turns[0].items, - vec![ - ThreadItem::EnteredReviewMode { + turns[0].item_payloads().collect::>(), + [ + &ThreadItemPayload::EnteredReviewMode { id: "entered-review".into(), review: "Review requested.".into(), }, - ThreadItem::ExitedReviewMode { + &ThreadItemPayload::ExitedReviewMode { id: "exited-review".into(), review: REVIEW_FALLBACK_MESSAGE.into(), }, @@ -2082,13 +2292,13 @@ mod tests { assert_eq!(turns[0].id, "turn-1"); assert_eq!( - turns[0].items, - vec![ - ThreadItem::EnteredReviewMode { + turns[0].item_payloads().collect::>(), + [ + &ThreadItemPayload::EnteredReviewMode { id: "entered-review".into(), review: "Review requested.".into(), }, - ThreadItem::ExitedReviewMode { + &ThreadItemPayload::ExitedReviewMode { id: "exited-review".into(), review: REVIEW_FALLBACK_MESSAGE.into(), }, @@ -2121,8 +2331,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items[0], - ThreadItem::UserMessage { + turns[0].items[0].payload, + ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![ @@ -2172,8 +2382,8 @@ mod tests { let turns = build_turns_from_rollout_items(&events); assert_eq!( - turns[0].items[0], - ThreadItem::UserMessage { + turns[0].items[0].payload, + ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![ @@ -2246,8 +2456,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 1); assert_eq!( - turns[0].items[0], - ThreadItem::UserMessage { + turns[0].items[0].payload, + ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -2303,12 +2513,19 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items, - vec![ThreadItem::Sleep(CoreSleepItem { + turns[0].item_payloads().collect::>(), + [&ThreadItemPayload::Sleep(CoreSleepItem { id: "sleep-1".to_string(), duration_ms: 1_000, })] ); + assert_eq!( + ( + turns[0].items[0].started_at_ms, + turns[0].items[0].completed_at_ms + ), + (Some(0), Some(1_000)) + ); } #[test] @@ -2329,7 +2546,10 @@ mod tests { builder.handle_thread_rollback(&ThreadRolledBackEvent { num_turns: 1 }); expected[0] = indexed_sleep_item("0", /*duration_ms*/ 2); builder.upsert_item_in_turn_id("retained", expected[0].clone()); - assert_eq!(builder.finish()[0].items, expected); + assert_eq!( + builder.finish()[0].item_payloads().collect::>(), + expected.iter().collect::>() + ); } #[test] @@ -2396,8 +2616,8 @@ mod tests { let turns = build_turns_from_rollout_items(&items); assert_eq!( - turns[0].items, - vec![ThreadItem::ImageGeneration(ImageGenerationItem { + turns[0].item_payloads().collect::>(), + [&ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: "image-1".to_string(), status: "completed".to_string(), revised_prompt: Some("A blue square".to_string()), @@ -2511,8 +2731,10 @@ mod tests { .collect::>(); assert_eq!( - build_turns_from_rollout_items(&items[..2])[0].items, - vec![ThreadItem::CommandExecution { + build_turns_from_rollout_items(&items[..2])[0] + .item_payloads() + .collect::>(), + [&ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-1".to_string(), @@ -2538,8 +2760,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items, - vec![ThreadItem::CommandExecution { + turns[0].item_payloads().collect::>(), + [&ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-1".to_string(), @@ -2618,14 +2840,14 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( turns[0].items, - vec![ThreadItem::UserMessage { + [ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: Some("client-message-1".to_string()), content: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), }], - }] + })] ); } @@ -2656,8 +2878,8 @@ mod tests { let turns = build_turns_from_rollout_items(&items); assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items[0], - ThreadItem::AgentMessage { + turns[0].items[0].payload, + ThreadItemPayload::AgentMessage { id: "item-1".into(), text: "Final reply".into(), phase: Some(CoreMessagePhase::FinalAnswer), @@ -2734,15 +2956,15 @@ mod tests { duration_ms: None, items_view: TurnItemsView::Full, items: vec![ - ThreadItem::UserMessage { + ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { text: "generate an image".into(), text_elements: Vec::new(), }], - }, - ThreadItem::ImageGeneration(ImageGenerationItem { + }), + ThreadItem::untimed(ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: "ig_123".into(), status: "completed".into(), revised_prompt: Some("final prompt".into()), @@ -2763,7 +2985,7 @@ mod tests { }], imagegen_request_id: None, generation_id: None, - }), + })), ], } ); @@ -2808,16 +3030,16 @@ mod tests { assert_eq!(turn.items.len(), 4); assert_eq!( - turn.items[1], - ThreadItem::Reasoning { + turn.items[1].payload, + ThreadItemPayload::Reasoning { id: "item-2".into(), summary: vec!["first summary".into()], content: vec!["first content".into()], } ); assert_eq!( - turn.items[3], - ThreadItem::Reasoning { + turn.items[3].payload, + ThreadItemPayload::Reasoning { id: "item-4".into(), summary: vec!["second summary".into()], content: Vec::new(), @@ -2880,8 +3102,8 @@ mod tests { assert_eq!(first_turn.status, TurnStatus::Interrupted); assert_eq!(first_turn.items.len(), 2); assert_eq!( - first_turn.items[0], - ThreadItem::UserMessage { + first_turn.items[0].payload, + ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -2891,8 +3113,8 @@ mod tests { } ); assert_eq!( - first_turn.items[1], - ThreadItem::AgentMessage { + first_turn.items[1].payload, + ThreadItemPayload::AgentMessage { id: "item-2".into(), text: "Working...".into(), phase: None, @@ -2906,8 +3128,8 @@ mod tests { assert_eq!(second_turn.status, TurnStatus::Completed); assert_eq!(second_turn.items.len(), 2); assert_eq!( - second_turn.items[0], - ThreadItem::UserMessage { + second_turn.items[0].payload, + ThreadItemPayload::UserMessage { id: "item-3".into(), client_id: None, content: vec![UserInput::Text { @@ -2917,8 +3139,8 @@ mod tests { } ); assert_eq!( - second_turn.items[1], - ThreadItem::AgentMessage { + second_turn.items[1].payload, + ThreadItemPayload::AgentMessage { id: "item-4".into(), text: "Second attempt complete.".into(), phase: None, @@ -2992,9 +3214,9 @@ mod tests { assert_eq!(turns[0].status, TurnStatus::Completed); assert_eq!(turns[1].status, TurnStatus::Completed); assert_eq!( - turns[0].items, - vec![ - ThreadItem::UserMessage { + turns[0].item_payloads().collect::>(), + [ + &ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -3002,7 +3224,7 @@ mod tests { text_elements: Vec::new(), }], }, - ThreadItem::AgentMessage { + &ThreadItemPayload::AgentMessage { id: "item-2".into(), text: "A1".into(), phase: None, @@ -3013,9 +3235,9 @@ mod tests { ] ); assert_eq!( - turns[1].items, - vec![ - ThreadItem::UserMessage { + turns[1].item_payloads().collect::>(), + [ + &ThreadItemPayload::UserMessage { id: "item-3".into(), client_id: None, content: vec![UserInput::Text { @@ -3023,7 +3245,7 @@ mod tests { text_elements: Vec::new(), }], }, - ThreadItem::AgentMessage { + &ThreadItemPayload::AgentMessage { id: "item-4".into(), text: "A3".into(), phase: None, @@ -3127,9 +3349,9 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].id, "turn-a"); assert_eq!( - turns[0].items, - vec![ - ThreadItem::UserMessage { + turns[0].item_payloads().collect::>(), + [ + &ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -3137,7 +3359,7 @@ mod tests { text_elements: Vec::new(), }], }, - ThreadItem::UserMessage { + &ThreadItemPayload::UserMessage { id: "item-2".into(), client_id: None, content: vec![UserInput::Text { @@ -3230,8 +3452,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 4); assert_eq!( - turns[0].items[1], - ThreadItem::WebSearch(WebSearchItem { + turns[0].items[1].payload, + ThreadItemPayload::WebSearch(WebSearchItem { id: "search-1".into(), query: "codex".into(), action: Some(WebSearchAction::Search { @@ -3246,8 +3468,8 @@ mod tests { }) ); assert_eq!( - turns[0].items[2], - ThreadItem::CommandExecution { + turns[0].items[2].payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-1".into(), @@ -3267,8 +3489,8 @@ mod tests { } ); assert_eq!( - turns[0].items[3], - ThreadItem::McpToolCall { + turns[0].items[3].payload, + ThreadItemPayload::McpToolCall { id: "mcp-1".into(), server: "docs".into(), tool: "lookup".into(), @@ -3338,8 +3560,8 @@ mod tests { let turns = build_turns_from_rollout_items(&items); assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items[0], - ThreadItem::McpToolCall { + turns[0].items[0].payload, + ThreadItemPayload::McpToolCall { id: "mcp-1".into(), server: "docs".into(), tool: "lookup".into(), @@ -3434,8 +3656,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); assert_eq!( - turns[0].items[1], - ThreadItem::DynamicToolCall { + turns[0].items[1].payload, + ThreadItemPayload::DynamicToolCall { id: "dyn-1".into(), namespace: Some("codex_app".into()), tool: "lookup_ticket".into(), @@ -3521,8 +3743,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 3); assert_eq!( - turns[0].items[1], - ThreadItem::CommandExecution { + turns[0].items[1].payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-declined".into(), @@ -3542,8 +3764,8 @@ mod tests { } ); assert_eq!( - turns[0].items[2], - ThreadItem::FileChange { + turns[0].items[2].payload, + ThreadItemPayload::FileChange { id: "patch-declined".into(), changes: vec![FileUpdateChange { path: "README.md".into(), @@ -3632,9 +3854,14 @@ mod tests { let turns = build_turns_from_rollout_items(&items); assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); + let item = &turns[0].items[1]; assert_eq!( - turns[0].items[1], - ThreadItem::CommandExecution { + (item.started_at_ms, item.completed_at_ms), + (Some(1_000), Some(1_042)) + ); + assert_eq!( + item.payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "guardian-exec".into(), @@ -3709,8 +3936,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); assert_eq!( - turns[0].items[1], - ThreadItem::CommandExecution { + turns[0].items[1].payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "guardian-execve".into(), @@ -3808,7 +4035,9 @@ mod tests { let mut expected = build_turns_from_rollout_items(&items); expected[0] .items - .push(ThreadItem::from(completion.item.clone())); + .push(ThreadItem::untimed(ThreadItemPayload::from( + completion.item.clone(), + ))); let legacy = completion.as_legacy_events(/*show_raw_agent_reasoning*/ false); let serialized = serde_json::to_string(&legacy[0]).unwrap(); @@ -3917,8 +4146,8 @@ mod tests { assert_eq!(turns[0].items.len(), 2); assert_eq!(turns[1].items.len(), 1); assert_eq!( - turns[0].items[1], - ThreadItem::CommandExecution { + turns[0].items[1].payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-late".into(), @@ -4028,8 +4257,8 @@ mod tests { assert_eq!(turns[0].items.len(), 1); assert_eq!(turns[1].items.len(), 1); assert_eq!( - turns[1].items[0], - ThreadItem::UserMessage { + turns[1].items[0].payload, + ThreadItemPayload::UserMessage { id: "item-2".into(), client_id: None, content: vec![UserInput::Text { @@ -4222,9 +4451,9 @@ mod tests { assert_eq!(snapshot.id, turn_id); assert_eq!(snapshot.status, TurnStatus::InProgress); assert_eq!( - snapshot.items, - vec![ - ThreadItem::UserMessage { + snapshot.item_payloads().collect::>(), + [ + &ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -4232,7 +4461,7 @@ mod tests { text_elements: Vec::new(), }], }, - ThreadItem::FileChange { + &ThreadItemPayload::FileChange { id: "patch-call".into(), changes: vec![FileUpdateChange { path: "README.md".into(), @@ -4270,7 +4499,7 @@ mod tests { EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "patch-call".into(), turn_id: turn_id.to_string(), - started_at_ms: 0, + started_at_ms: 1_250, changes: [( PathBuf::from("README.md"), codex_protocol::protocol::FileChange::Add { @@ -4293,10 +4522,11 @@ mod tests { .expect("active turn snapshot"); assert_eq!(snapshot.id, turn_id); assert_eq!(snapshot.status, TurnStatus::InProgress); + assert_eq!(snapshot.items[1].started_at_ms, Some(1_250)); assert_eq!( - snapshot.items, - vec![ - ThreadItem::UserMessage { + snapshot.item_payloads().collect::>(), + [ + &ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { @@ -4304,7 +4534,7 @@ mod tests { text_elements: Vec::new(), }], }, - ThreadItem::FileChange { + &ThreadItemPayload::FileChange { id: "patch-call".into(), changes: vec![FileUpdateChange { path: "README.md".into(), @@ -4469,14 +4699,14 @@ mod tests { id: "turn-a".into(), root_turn_id: None, items_view: TurnItemsView::Full, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { text: "first".into(), text_elements: Vec::new(), }], - }], + })], status: TurnStatus::Failed, error: Some(TurnError { misalignment: None, @@ -4495,14 +4725,14 @@ mod tests { id: "turn-b".into(), root_turn_id: None, items_view: TurnItemsView::Full, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-2".into(), client_id: None, content: vec![UserInput::Text { text: "second".into(), text_elements: Vec::new(), }], - }], + })], status: TurnStatus::InProgress, error: None, started_at: Some(30), @@ -4658,7 +4888,7 @@ mod tests { }), EventMsg::CollabResumeEnd(codex_protocol::protocol::CollabResumeEndEvent { call_id: "resume-1".into(), - completed_at_ms: 0, + completed_at_ms: 2_000, 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") @@ -4676,9 +4906,14 @@ mod tests { let turns = build_turns_from_rollout_items(&items); assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); + let item = &turns[0].items[1]; assert_eq!( - turns[0].items[1], - ThreadItem::CollabAgentToolCall { + (item.started_at_ms, item.completed_at_ms), + (None, Some(2_000)) + ); + assert_eq!( + item.payload, + ThreadItemPayload::CollabAgentToolCall { id: "resume-1".into(), tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::Completed, @@ -4737,8 +4972,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); assert_eq!( - turns[0].items[1], - ThreadItem::CollabAgentToolCall { + turns[0].items[1].payload, + ThreadItemPayload::CollabAgentToolCall { id: "spawn-1".into(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::Completed, @@ -4809,8 +5044,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); assert_eq!( - turns[0].items[1], - ThreadItem::CollabAgentToolCall { + turns[0].items[1].payload, + ThreadItemPayload::CollabAgentToolCall { id: "send-1".into(), tool: CollabAgentTool::SendInput, status: CollabAgentToolCallStatus::Completed, @@ -4921,14 +5156,14 @@ mod tests { completed_at: None, duration_ms: None, items_view: TurnItemsView::Full, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), }], - }], + })], } ); } @@ -5042,14 +5277,14 @@ mod tests { id: "turn-a".into(), root_turn_id: None, items_view: TurnItemsView::Full, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: None, content: vec![UserInput::Text { text: "retry me".into(), text_elements: Vec::new(), }], - }], + })], status: TurnStatus::Failed, error: Some(TurnError { misalignment: None, @@ -5107,8 +5342,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].items.len(), 2); assert_eq!( - turns[0].items[1], - ThreadItem::HookPrompt { + turns[0].items[1].payload, + ThreadItemPayload::HookPrompt { id: turns[0].items[1].id().to_string(), fragments: vec![ crate::protocol::v2::HookPromptFragment { @@ -5133,7 +5368,11 @@ mod tests { "hook-run-1", )], }); - let expected_item = ThreadItem::from(hook_prompt.clone()); + let expected_item = ThreadItem { + payload: ThreadItemPayload::from(hook_prompt.clone()), + started_at_ms: Some(10), + completed_at_ms: Some(20), + }; let mut builder = ThreadHistoryBuilder::new(); builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent { turn_attribution: None, @@ -5144,18 +5383,18 @@ mod tests { model_context_window: None, collaboration_mode_kind: Default::default(), })); - builder.handle_event(&EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), - turn_id: "turn-a".into(), - item: hook_prompt, - started_at_ms: Some(0), - completed_at_ms: 0, - })); + let changes = + builder.handle_event_with_changes(&EventMsg::ItemCompleted(ItemCompletedEvent { + thread_id: ThreadId::new(), + turn_id: "turn-a".into(), + item: hook_prompt, + started_at_ms: Some(10), + completed_at_ms: 20, + })); - assert_eq!( - builder.active_turn_snapshot().expect("active turn").items, - vec![expected_item] - ); + let snapshot = builder.active_turn_snapshot().expect("active turn"); + assert_eq!(snapshot.items, [expected_item]); + assert_eq!(changes.changed_items[0].item, snapshot.items[0]); } #[test] @@ -5180,7 +5419,7 @@ mod tests { started_at_ms: Some(1), completed_at_ms: 2, }; - let expected_spawn = ThreadItem::from(spawn.item.clone()); + let expected_spawn = ThreadItemPayload::from(spawn.item.clone()); let legacy_spawn = spawn .as_legacy_events(/*show_raw_agent_reasoning*/ false) .pop() @@ -5191,7 +5430,7 @@ mod tests { panic!("expected item completed"); }; assert_eq!( - (notification.item, notification.completed_at_ms), + (notification.item.payload, notification.completed_at_ms), (expected_spawn.clone(), 2) ); for spawn in [EventMsg::ItemCompleted(spawn), legacy_spawn] { @@ -5236,10 +5475,10 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items, - vec![ - expected_spawn.clone(), - ThreadItem::SubAgentActivity { + turns[0].item_payloads().collect::>(), + [ + &expected_spawn, + &ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "child-turn-completed".into(), @@ -5314,16 +5553,14 @@ mod tests { ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: "rollout-0".into(), - item: ThreadItem::UserMessage { + item: ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "item-1".into(), client_id: Some("client-message-1".into()), content: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), }], - }, - started_at_ms: None, - completed_at_ms: None, + }) }], changed_turns: vec![ThreadHistoryTurnMetadata { turn_id: "rollout-0".into(), @@ -5364,7 +5601,7 @@ mod tests { ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: "rollout-0".into(), - item: ThreadItem::WebSearch(WebSearchItem { + item: ThreadItem::untimed(ThreadItemPayload::WebSearch(WebSearchItem { id: "search-1".into(), query: "codex".into(), action: Some(WebSearchAction::Search { @@ -5372,9 +5609,7 @@ mod tests { queries: None, }), results: None, - }), - started_at_ms: None, - completed_at_ms: None, + })) }], changed_turns: Vec::new(), removed_turn_ids: Vec::new(), @@ -5401,13 +5636,11 @@ mod tests { ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: "rollout-0".into(), - item: ThreadItem::Reasoning { + item: ThreadItem::untimed(ThreadItemPayload::Reasoning { id: "item-1".into(), summary: vec!["summary".into()], content: vec!["raw content".into()], - }, - started_at_ms: None, - completed_at_ms: None, + }) }], changed_turns: Vec::new(), removed_turn_ids: Vec::new(), @@ -5510,7 +5743,7 @@ mod tests { ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: "rollout-0".into(), - item: ThreadItem::WebSearch(WebSearchItem { + item: ThreadItem::untimed(ThreadItemPayload::WebSearch(WebSearchItem { id: "search-1".into(), query: "codex".into(), action: Some(WebSearchAction::Search { @@ -5518,9 +5751,7 @@ mod tests { queries: None, }), results: None, - }), - started_at_ms: None, - completed_at_ms: None, + })) }], changed_turns: vec![ThreadHistoryTurnMetadata { turn_id: "rollout-0".into(), diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs index 756f42e3ab..32d61bf82c 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs @@ -78,9 +78,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet { RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: event.turn_id.clone(), - item: ThreadItem::from(event.item.clone()), - started_at_ms: event.started_at_ms, - completed_at_ms: (event.completed_at_ms != 0).then_some(event.completed_at_ms), + item: ThreadItem::from(event), }], ..Default::default() }, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs index a2ca09c6b7..9517d459ab 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs @@ -19,7 +19,7 @@ use pretty_assertions::assert_eq; use std::collections::BTreeMap; use super::*; -use crate::protocol::v2::ThreadItem; +use crate::protocol::v2::ThreadItemPayload; use crate::protocol::v2::TurnError; #[test] @@ -145,18 +145,22 @@ fn projects_completed_canonical_turn_items() { user_changes.changed_items, vec![ThreadHistoryItemChange { turn_id: "turn-1".to_string(), - item: ThreadItem::from(user_item), - started_at_ms: Some(100), - completed_at_ms: Some(123), + item: ThreadItem { + payload: ThreadItemPayload::from(user_item), + started_at_ms: Some(100), + completed_at_ms: Some(123) + } }] ); assert_eq!( agent_changes.changed_items, vec![ThreadHistoryItemChange { turn_id: "turn-1".to_string(), - item: ThreadItem::from(agent_item), - started_at_ms: Some(100), - completed_at_ms: Some(123), + item: ThreadItem { + payload: ThreadItemPayload::from(agent_item), + started_at_ms: Some(100), + completed_at_ms: Some(123) + } }] ); } @@ -187,9 +191,11 @@ fn projects_optional_completed_item_lifecycle_timestamps() { ThreadHistoryChangeSet { changed_items: vec![ThreadHistoryItemChange { turn_id: "turn-1".to_string(), - item: ThreadItem::from(item.clone()), - started_at_ms, - completed_at_ms: expected_completed_at_ms, + item: ThreadItem { + payload: ThreadItemPayload::from(item.clone()), + started_at_ms, + completed_at_ms: expected_completed_at_ms + } }], ..Default::default() } diff --git a/codex-rs/app-server-protocol/src/protocol/turn_items_view.rs b/codex-rs/app-server-protocol/src/protocol/turn_items_view.rs index 7af9fe50ce..593a6ca560 100644 --- a/codex-rs/app-server-protocol/src/protocol/turn_items_view.rs +++ b/codex-rs/app-server-protocol/src/protocol/turn_items_view.rs @@ -1,25 +1,25 @@ //! Projects a turn's items without copying payloads excluded by the requested view. -use super::v2::ThreadItem; +use super::v2::ThreadItemPayload; use super::v2::TurnItemsView; +use std::borrow::Borrow; impl TurnItemsView { /// Copies only the items included in this view, preserving summary selection order. - pub fn project_items(self, items: &[ThreadItem]) -> Vec { + pub fn project_items + Clone>(self, items: &[T]) -> Vec { match self { Self::NotLoaded => Vec::new(), Self::Full => items.to_vec(), Self::Summary => { let first_user_message = items .iter() - .find(|item| matches!(item, ThreadItem::UserMessage { .. })); - let final_agent_message = items - .iter() - .rev() - .find(|item| matches!(item, ThreadItem::AgentMessage { .. })); + .find(|item| matches!((*item).borrow(), ThreadItemPayload::UserMessage { .. })); + let final_agent_message = items.iter().rev().find(|item| { + matches!((*item).borrow(), ThreadItemPayload::AgentMessage { .. }) + }); match (first_user_message, final_agent_message) { (Some(user_message), Some(agent_message)) - if user_message.id() != agent_message.id() => + if user_message.borrow().id() != agent_message.borrow().id() => { vec![user_message.clone(), agent_message.clone()] } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index bde4c2aa87..8997b722e2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -231,11 +231,72 @@ impl CommandAction { } } +/// A thread item and its recorded lifecycle times. The payload stays flat on the wire. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadItem { + #[serde(flatten)] + pub payload: ThreadItemPayload, + /// Unix timestamp in milliseconds when the item started, if recorded. + #[serde(default)] + #[ts(type = "number | null")] + pub started_at_ms: Option, + /// Unix timestamp in milliseconds when the item completed, if recorded. + #[serde(default)] + #[ts(type = "number | null")] + pub completed_at_ms: Option, +} + +impl ThreadItem { + /// Creates an item with no recorded start or completion time. + pub fn untimed(payload: ThreadItemPayload) -> Self { + Self { + payload, + started_at_ms: None, + completed_at_ms: None, + } + } + + fn completed( + payload: ThreadItemPayload, + started_at_ms: Option, + completed_at_ms: i64, + ) -> Self { + Self { + payload, + started_at_ms, + // Older rollout events default a missing completion time to zero. + completed_at_ms: (completed_at_ms != 0).then_some(completed_at_ms), + } + } + + pub fn id(&self) -> &str { + self.payload.id() + } +} + +impl From<&codex_protocol::protocol::ItemCompletedEvent> for ThreadItem { + fn from(event: &codex_protocol::protocol::ItemCompletedEvent) -> Self { + ThreadItem::completed( + ThreadItemPayload::from(event.item.clone()), + event.started_at_ms, + event.completed_at_ms, + ) + } +} + +impl std::borrow::Borrow for ThreadItem { + fn borrow(&self) -> &ThreadItemPayload { + &self.payload + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] #[ts(export_to = "v2/")] -pub enum ThreadItem { +pub enum ThreadItemPayload { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] UserMessage { @@ -453,28 +514,28 @@ pub struct HookPromptFragment { pub hook_run_id: String, } -impl ThreadItem { +impl ThreadItemPayload { pub fn id(&self) -> &str { match self { - ThreadItem::UserMessage { id, .. } - | ThreadItem::HookPrompt { id, .. } - | ThreadItem::AgentMessage { id, .. } - | ThreadItem::FunctionCallOutput { id, .. } - | ThreadItem::Plan { id, .. } - | ThreadItem::Reasoning { id, .. } - | ThreadItem::CommandExecution { id, .. } - | ThreadItem::FileChange { id, .. } - | ThreadItem::McpToolCall { id, .. } - | ThreadItem::DynamicToolCall { id, .. } - | ThreadItem::CollabAgentToolCall { id, .. } - | ThreadItem::SubAgentActivity { id, .. } - | ThreadItem::ImageView { id, .. } - | ThreadItem::EnteredReviewMode { id, .. } - | ThreadItem::ExitedReviewMode { id, .. } - | ThreadItem::ContextCompaction { id, .. } => id, - ThreadItem::WebSearch(item) => &item.id, - ThreadItem::Sleep(item) => &item.id, - ThreadItem::ImageGeneration(item) => &item.id, + Self::UserMessage { id, .. } + | Self::HookPrompt { id, .. } + | Self::AgentMessage { id, .. } + | Self::FunctionCallOutput { id, .. } + | Self::Plan { id, .. } + | Self::Reasoning { id, .. } + | Self::CommandExecution { id, .. } + | Self::FileChange { id, .. } + | Self::McpToolCall { id, .. } + | Self::DynamicToolCall { id, .. } + | Self::CollabAgentToolCall { id, .. } + | Self::SubAgentActivity { id, .. } + | Self::ImageView { id, .. } + | Self::EnteredReviewMode { id, .. } + | Self::ExitedReviewMode { id, .. } + | Self::ContextCompaction { id, .. } => id, + Self::WebSearch(item) => &item.id, + Self::Sleep(item) => &item.id, + Self::ImageGeneration(item) => &item.id, } } } @@ -872,15 +933,15 @@ pub(crate) fn web_search_action_from_core( } } -impl From for ThreadItem { +impl From for ThreadItemPayload { fn from(value: CoreTurnItem) -> Self { match value { - CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage { + CoreTurnItem::UserMessage(user) => Self::UserMessage { id: user.id, client_id: user.client_id, content: user.content.into_iter().map(UserInput::from).collect(), }, - CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt { + CoreTurnItem::HookPrompt(hook_prompt) => Self::HookPrompt { id: hook_prompt.id, fragments: hook_prompt .fragments @@ -896,7 +957,7 @@ impl From for ThreadItem { CoreAgentMessageContent::Text { text } => text, }) .collect::(); - ThreadItem::AgentMessage { + Self::AgentMessage { id: agent.id, text, phase: agent.phase, @@ -905,17 +966,17 @@ impl From for ThreadItem { questions: agent.questions, } } - CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput { + CoreTurnItem::FunctionCallOutput(output) => Self::FunctionCallOutput { id: output.id, name: output.name, namespace: output.namespace, output: output.output, }, - CoreTurnItem::Plan(plan) => ThreadItem::Plan { + CoreTurnItem::Plan(plan) => Self::Plan { id: plan.id, text: plan.text, }, - CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning { + CoreTurnItem::Reasoning(reasoning) => Self::Reasoning { id: reasoning.id, summary: reasoning.summary_text, content: reasoning.raw_content, @@ -926,7 +987,7 @@ impl From for ThreadItem { &command.parsed_cmd, &command.cwd, ); - ThreadItem::CommandExecution { + Self::CommandExecution { id: command.id, model_context: command.model_context, sandbox_type: command.sandbox_type, @@ -947,7 +1008,7 @@ impl From for ThreadItem { .and_then(|duration| i64::try_from(duration.as_millis()).ok()), } } - CoreTurnItem::DynamicToolCall(call) => ThreadItem::DynamicToolCall { + CoreTurnItem::DynamicToolCall(call) => Self::DynamicToolCall { id: call.id, namespace: call.namespace, tool: call.tool, @@ -964,7 +1025,7 @@ impl From for ThreadItem { .duration .and_then(|duration| i64::try_from(duration.as_millis()).ok()), }, - CoreTurnItem::CollabAgentToolCall(call) => ThreadItem::CollabAgentToolCall { + CoreTurnItem::CollabAgentToolCall(call) => Self::CollabAgentToolCall { id: call.id, tool: call.tool.into(), status: call.status.into(), @@ -983,7 +1044,7 @@ impl From for ThreadItem { .map(|(thread_id, status)| (thread_id.to_string(), status.into())) .collect(), }, - CoreTurnItem::SubAgentActivity(activity) => ThreadItem::SubAgentActivity { + CoreTurnItem::SubAgentActivity(activity) => Self::SubAgentActivity { model: activity.model, reasoning_effort: activity.reasoning_effort, id: activity.id, @@ -991,46 +1052,44 @@ impl From for ThreadItem { agent_thread_id: activity.agent_thread_id.to_string(), agent_path: String::from(activity.agent_path), }, - CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch(WebSearchItem { + CoreTurnItem::WebSearch(search) => Self::WebSearch(WebSearchItem { id: search.id, query: search.query, action: Some(web_search_action_from_core(search.action)), results: search.results, }), - CoreTurnItem::ImageView(image) => ThreadItem::ImageView { + CoreTurnItem::ImageView(image) => Self::ImageView { id: image.id, path: image.path.into(), }, CoreTurnItem::Extension(extension) => match extension { - ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item), - ExtensionItem::Sleep(item) => ThreadItem::Sleep(item), - ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(item), + ExtensionItem::ImageGeneration(item) => Self::ImageGeneration(item), + ExtensionItem::Sleep(item) => Self::Sleep(item), + ExtensionItem::WebSearch(item) => Self::WebSearch(item), }, - CoreTurnItem::ImageGeneration(image) => { - ThreadItem::ImageGeneration(ImageGenerationItem { - id: image.id, - status: image.status, - revised_prompt: image.revised_prompt, - image: GeneratedImageRef::Inline { - result: image.result, - }, - transparent_background: None, - failure: None, - saved_path: image.saved_path, - input_images: Vec::new(), - imagegen_request_id: None, - generation_id: None, - }) - } - CoreTurnItem::EnteredReviewMode(review) => ThreadItem::EnteredReviewMode { + CoreTurnItem::ImageGeneration(image) => Self::ImageGeneration(ImageGenerationItem { + id: image.id, + status: image.status, + revised_prompt: image.revised_prompt, + image: GeneratedImageRef::Inline { + result: image.result, + }, + transparent_background: None, + failure: None, + saved_path: image.saved_path, + input_images: Vec::new(), + imagegen_request_id: None, + generation_id: None, + }), + CoreTurnItem::EnteredReviewMode(review) => Self::EnteredReviewMode { id: review.id, review: review.user_facing_hint, }, - CoreTurnItem::ExitedReviewMode(review) => ThreadItem::ExitedReviewMode { + CoreTurnItem::ExitedReviewMode(review) => Self::ExitedReviewMode { id: review.id, review: review_output_text(review.review_output.as_ref()), }, - CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange { + CoreTurnItem::FileChange(file_change) => Self::FileChange { id: file_change.id, changes: convert_patch_changes(&file_change.changes), status: file_change @@ -1044,7 +1103,7 @@ impl From for ThreadItem { .duration .and_then(|duration| i64::try_from(duration.as_millis()).ok()); - ThreadItem::McpToolCall { + Self::McpToolCall { id: mcp.id, server: mcp.server, tool: mcp.tool, @@ -1067,7 +1126,7 @@ impl From for ThreadItem { } } CoreTurnItem::ContextCompaction(compaction) => { - ThreadItem::ContextCompaction { id: compaction.id } + Self::ContextCompaction { id: compaction.id } } } } @@ -1353,6 +1412,25 @@ pub struct ItemStartedNotification { #[ts(type = "number")] pub started_at_ms: i64, } +impl ItemStartedNotification { + pub fn new( + thread_id: String, + turn_id: String, + payload: ThreadItemPayload, + started_at_ms: i64, + ) -> Self { + Self { + thread_id, + turn_id, + started_at_ms, + item: ThreadItem { + payload, + started_at_ms: Some(started_at_ms), + completed_at_ms: None, + }, + } + } +} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -1431,6 +1509,22 @@ pub struct ItemCompletedNotification { #[ts(type = "number")] pub completed_at_ms: i64, } +impl ItemCompletedNotification { + pub fn new( + thread_id: String, + turn_id: String, + payload: ThreadItemPayload, + started_at_ms: Option, + completed_at_ms: i64, + ) -> Self { + Self { + thread_id, + turn_id, + completed_at_ms, + item: ThreadItem::completed(payload, started_at_ms, completed_at_ms), + } + } +} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index d268de69fb..bc722c514d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -461,8 +461,12 @@ fn thread_items_list_round_trips() { let response = ThreadItemsListResponse { data: vec![ThreadItemEntry { turn_id: "turn_456".to_string(), - item: ThreadItem::ContextCompaction { - id: "item_1".to_string(), + item: ThreadItem { + payload: ThreadItemPayload::ContextCompaction { + id: "item_1".to_string(), + }, + started_at_ms, + completed_at_ms, }, started_at_ms, completed_at_ms, @@ -476,7 +480,7 @@ fn thread_items_list_round_trips() { json!({ "data": [{ "turnId": "turn_456", - "item": {"type": "contextCompaction", "id": "item_1"}, + "item": {"type": "contextCompaction", "id": "item_1", "startedAtMs": started_at_ms, "completedAtMs": completed_at_ms}, "startedAtMs": started_at_ms, "completedAtMs": completed_at_ms, }], @@ -3164,8 +3168,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(user_item), - ThreadItem::UserMessage { + ThreadItemPayload::from(user_item), + ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: Some("client-message-1".to_string()), content: vec![ @@ -3218,8 +3222,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(agent_item), - ThreadItem::AgentMessage { + ThreadItemPayload::from(agent_item), + ThreadItemPayload::AgentMessage { id: "agent-1".to_string(), text: "Hello world".to_string(), phase: None, @@ -3249,8 +3253,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(agent_item_with_phase), - ThreadItem::AgentMessage { + ThreadItemPayload::from(agent_item_with_phase), + ThreadItemPayload::AgentMessage { id: "agent-2".to_string(), text: "final".to_string(), phase: Some(MessagePhase::FinalAnswer), @@ -3268,7 +3272,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { } ); - let async_item = ThreadItem::from(TurnItem::AgentMessage(AgentMessageItem { + let async_item = ThreadItemPayload::from(TurnItem::AgentMessage(AgentMessageItem { id: "async-1".to_string(), content: vec![AgentMessageContent::Text { text: "Which?".to_string(), @@ -3288,13 +3292,13 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { "memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}] }) ); - let old_item: ThreadItem = serde_json::from_value(json!({ + let old_item: ThreadItemPayload = serde_json::from_value(json!({ "type": "agentMessage", "id": "old-1", "text": "An old message" })) .unwrap(); assert!(matches!( old_item, - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { questions: None, .. } @@ -3307,8 +3311,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(reasoning_item), - ThreadItem::Reasoning { + ThreadItemPayload::from(reasoning_item), + ThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec!["line one".to_string(), "line two".to_string()], content: vec![], @@ -3345,8 +3349,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(command_item), - ThreadItem::CommandExecution { + ThreadItemPayload::from(command_item), + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "exec-1".to_string(), @@ -3391,8 +3395,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(dynamic_tool_call_item), - ThreadItem::DynamicToolCall { + ThreadItemPayload::from(dynamic_tool_call_item), + ThreadItemPayload::DynamicToolCall { id: "dynamic-1".to_string(), namespace: Some("apps".to_string()), tool: "lookup".to_string(), @@ -3432,8 +3436,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(collab_item), - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::from(collab_item), + ThreadItemPayload::CollabAgentToolCall { id: "collab-1".to_string(), tool: CollabAgentTool::SendInput, status: CollabAgentToolCallStatus::Completed, @@ -3466,8 +3470,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(sub_agent_activity_item), - ThreadItem::SubAgentActivity { + ThreadItemPayload::from(sub_agent_activity_item), + ThreadItemPayload::SubAgentActivity { model: Some("gpt-5".into()), reasoning_effort: Some(ReasoningEffort::High), id: "activity-1".to_string(), @@ -3484,7 +3488,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { "agent_path": "/root/worker", })) .unwrap(); - let old_wire_activity: ThreadItem = serde_json::from_value(json!({ + let old_wire_activity: ThreadItemPayload = serde_json::from_value(json!({ "type": "subAgentActivity", "id": "old-activity", "kind": "started", @@ -3493,7 +3497,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { })) .unwrap(); assert_eq!( - ThreadItem::from(TurnItem::SubAgentActivity(old_activity)), + ThreadItemPayload::from(TurnItem::SubAgentActivity(old_activity)), old_wire_activity ); @@ -3526,14 +3530,14 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }; assert_eq!( - ThreadItem::from(search_item), - ThreadItem::WebSearch(expected_search_item.clone()) + ThreadItemPayload::from(search_item), + ThreadItemPayload::WebSearch(expected_search_item.clone()) ); assert_eq!( - ThreadItem::from(TurnItem::Extension( + ThreadItemPayload::from(TurnItem::Extension( codex_extension_items::ExtensionItem::WebSearch(expected_search_item.clone()), )), - ThreadItem::WebSearch(expected_search_item) + ThreadItemPayload::WebSearch(expected_search_item) ); let image_view_item = TurnItem::ImageView(ImageViewItem { @@ -3542,8 +3546,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(image_view_item), - ThreadItem::ImageView { + ThreadItemPayload::from(image_view_item), + ThreadItemPayload::ImageView { id: "view-image-1".to_string(), path: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp/view-image.png").abs()), } @@ -3566,8 +3570,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(file_change_item), - ThreadItem::FileChange { + ThreadItemPayload::from(file_change_item), + ThreadItemPayload::FileChange { id: "patch-1".to_string(), changes: vec![FileUpdateChange { path: "README.md".to_string(), @@ -3598,8 +3602,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(mcp_tool_call_item), - ThreadItem::McpToolCall { + ThreadItemPayload::from(mcp_tool_call_item), + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "server".to_string(), tool: "tool".to_string(), @@ -3647,8 +3651,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { }); assert_eq!( - ThreadItem::from(completed_mcp_tool_call_item), - ThreadItem::McpToolCall { + ThreadItemPayload::from(completed_mcp_tool_call_item), + ThreadItemPayload::McpToolCall { id: "mcp-2".to_string(), server: "server".to_string(), tool: "tool".to_string(), @@ -3672,7 +3676,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { #[test] fn mcp_tool_call_app_context_serializes_connector_id() { - let item = ThreadItem::McpToolCall { + let item = ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "codex_apps".to_string(), tool: "calendar.create_event".to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 79422eba09..b31dfdf093 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -1845,6 +1845,16 @@ pub struct ThreadItemEntry { pub completed_at_ms: Option, } +impl From for ThreadItem { + fn from(entry: ThreadItemEntry) -> Self { + Self { + started_at_ms: entry.item.started_at_ms.or(entry.started_at_ms), + completed_at_ms: entry.item.completed_at_ms.or(entry.completed_at_ms), + ..entry.item + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index 015d813462..dd7c0d854a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -1,6 +1,7 @@ use super::CodexErrorInfo; use super::ThreadEnvironment; use super::ThreadItem; +use super::ThreadItemPayload; use super::ThreadStatus; use super::TurnStatus; use crate::JsonSchema; @@ -409,6 +410,15 @@ pub struct Turn { pub duration_ms: Option, } +impl Turn { + /// Iterates over the item payloads in this turn. + pub fn item_payloads( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator { + self.items.iter().map(|item| &item.payload) + } +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 5db6d881ae..ae04387445 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -57,7 +57,7 @@ use codex_app_server_protocol::ThreadDecrementElicitationParams; use codex_app_server_protocol::ThreadDecrementElicitationResponse; use codex_app_server_protocol::ThreadIncrementElicitationParams; use codex_app_server_protocol::ThreadIncrementElicitationResponse; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadResumeParams; @@ -1456,13 +1456,13 @@ struct CodexClient { command_item_started: bool, helper_done_seen: bool, turn_completed_before_helper_done: bool, - unexpected_items_before_helper_done: Vec, + unexpected_items_before_helper_done: Vec, last_turn_status: Option, last_turn_error_message: Option, } fn item_started_before_helper_done_is_unexpected( - item: &ThreadItem, + item: &ThreadItemPayload, command_item_started: bool, helper_done_seen: bool, ) -> bool { @@ -1470,7 +1470,7 @@ fn item_started_before_helper_done_is_unexpected( return false; } - !matches!(item, ThreadItem::UserMessage { .. }) + !matches!(item, ThreadItemPayload::UserMessage { .. }) } impl CodexClient { @@ -1816,28 +1816,31 @@ impl CodexClient { std::io::stdout().flush().ok(); } ServerNotification::ItemStarted(payload) => { - if matches!(payload.item, ThreadItem::CommandExecution { .. }) { + if matches!( + payload.item.payload, + ThreadItemPayload::CommandExecution { .. } + ) { if self.command_item_started && !self.helper_done_seen { self.unexpected_items_before_helper_done - .push(payload.item.clone()); + .push(payload.item.payload.clone()); } self.command_item_started = true; } else if item_started_before_helper_done_is_unexpected( - &payload.item, + &payload.item.payload, self.command_item_started, self.helper_done_seen, ) { self.unexpected_items_before_helper_done - .push(payload.item.clone()); + .push(payload.item.payload.clone()); } - println!("\n< item started: {:?}", payload.item); + println!("\n< item started: {:?}", payload.item.payload); } ServerNotification::ItemCompleted(payload) => { - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { status, aggregated_output, .. - } = payload.item.clone() + } = payload.item.payload.clone() { self.command_execution_statuses.push(status); if let Some(aggregated_output) = aggregated_output { @@ -1845,7 +1848,7 @@ impl CodexClient { self.command_execution_outputs.push(aggregated_output); } } - println!("< item completed: {:?}", payload.item); + println!("< item completed: {:?}", payload.item.payload); } ServerNotification::TurnCompleted(payload) => { if payload.turn.id == turn_id { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 97d91065f4..269e649e72 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -611,6 +611,11 @@ execution and sandbox policies. # Item history anchors +Live notifications and turn history return the same `ThreadItem`. Each item has +`startedAtMs` and `completedAtMs` beside `type` and `id`. Each timestamp is in +Unix milliseconds, or `null` when the producer has not recorded it. Clients +reading older servers may also encounter items that omit these fields. + `thread/items/list` accepts an optional nullable `cursor`: either an opaque string from a previous response or an item anchor such as `{"type":"item","itemId":"item-123"}`. An item anchor resumes exclusively after diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index d0d363e401..8e5c1b9ade 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -54,6 +54,7 @@ use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::StrictReviewRequiredNotification; use codex_app_server_protocol::ThreadGoalUpdatedNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadRealtimeClosedNotification; use codex_app_server_protocol::ThreadRealtimeErrorNotification; use codex_app_server_protocol::ThreadRealtimeItemAddedNotification; @@ -156,6 +157,7 @@ pub(crate) async fn apply_bespoke_event_handling( id: event_turn_id, msg, } = event; + let approval_starts_command = msg.command_approval_start().is_some(); match msg { EventMsg::TurnStarted(payload) => { // While not technically necessary as it was already done on TurnComplete, be extra cautios and abort any pending server requests. @@ -165,7 +167,8 @@ pub(crate) async fn apply_bespoke_event_handling( .await; let turn = { let state = thread_state.lock().await; - let mut turn = state.active_turn_snapshot().unwrap_or_else(|| Turn { + let turn = state.active_turn_metadata_snapshot().map(Turn::from); + turn.unwrap_or_else(|| Turn { id: payload.turn_id.clone(), root_turn_id: payload.root_turn_id.clone(), items: Vec::new(), @@ -175,10 +178,7 @@ pub(crate) async fn apply_bespoke_event_handling( started_at: payload.started_at, completed_at: None, duration_ms: None, - }); - turn.items.clear(); - turn.items_view = TurnItemsView::NotLoaded; - turn + }) }; let notification = TurnStartedNotification { thread_id: conversation_id.to_string(), @@ -300,7 +300,7 @@ pub(crate) async fn apply_bespoke_event_handling( &assessment, CommandExecutionStatus::InProgress, ) { - Some(ThreadItem::CommandExecution { + Some(ThreadItemPayload::CommandExecution { id, model_context, plugin_id, @@ -327,20 +327,15 @@ pub(crate) async fn apply_bespoke_event_handling( } else { assessment.turn_id.clone() }; - if assessment.status == codex_protocol::protocol::GuardianAssessmentStatus::InProgress + if approval_starts_command && let Some((target_item_id, completion_item)) = pending_command_execution.as_ref() { start_command_execution_item( &conversation_id, assessment_turn_id.clone(), target_item_id.clone(), - completion_item.model_context.clone(), - completion_item.plugin_id.clone(), - completion_item.script_path.clone(), - completion_item.command.clone(), - completion_item.cwd.clone(), - completion_item.command_actions.clone(), - CommandExecutionSource::Agent, + assessment.started_at_ms, + completion_item, &outgoing, &thread_state, ) @@ -393,9 +388,8 @@ pub(crate) async fn apply_bespoke_event_handling( assessment_turn_id, target_item_id, completion_item, - /*process_id*/ None, - CommandExecutionSource::Agent, completion_status, + assessment.completed_at_ms, &outgoing, &thread_state, ) @@ -760,20 +754,13 @@ pub(crate) async fn apply_bespoke_event_handling( Some(completion_item), ), }; - if approval_id.is_none() - && let Some(completion_item) = completion_item.as_ref() - { + if approval_starts_command && let Some(completion_item) = completion_item.as_ref() { start_command_execution_item( &conversation_id, event_turn_id.clone(), call_id.clone(), - completion_item.model_context.clone(), - completion_item.plugin_id.clone(), - completion_item.script_path.clone(), - completion_item.command.clone(), - completion_item.cwd.clone(), - completion_item.command_actions.clone(), - CommandExecutionSource::Agent, + started_at_ms, + completion_item, &outgoing, &thread_state, ) @@ -1088,13 +1075,12 @@ pub(crate) async fn apply_bespoke_event_handling( EventMsg::ItemStarted(event) => { let should_emit = match &event.item { // Approval and guardian flows can emit the command start notification before core - // emits the canonical item. Reuse the same set to suppress that duplicate. + // emits the canonical item. Keep the first observed start and suppress that duplicate. CoreTurnItem::CommandExecution(item) => thread_state .lock() .await .turn_summary - .command_execution_started - .insert(item.id.clone()), + .record_command_start(item.id.clone(), event.started_at_ms), _ => true, }; let dynamic_tool_call_params = match &event.item { @@ -1129,12 +1115,12 @@ pub(crate) async fn apply_bespoke_event_handling( }); } } - EventMsg::ItemCompleted(event) => { + EventMsg::ItemCompleted(mut event) => { apply_canonical_item_completed_side_effects( &thread_manager, &thread_watch_manager, &thread_state, - &event.item, + &mut event, ) .await; let mut notification = item_event_to_server_notification( @@ -1408,16 +1394,18 @@ async fn apply_canonical_item_completed_side_effects( thread_manager: &Arc, thread_watch_manager: &ThreadWatchManager, thread_state: &Arc>, - item: &CoreTurnItem, + event: &mut codex_protocol::protocol::ItemCompletedEvent, ) { - match item { + match &event.item { CoreTurnItem::CommandExecution(item) => { - thread_state + let observed_start = thread_state .lock() .await .turn_summary - .command_execution_started + .command_start_times .remove(&item.id); + // A client can already be displaying the command's approval-time start. + event.started_at_ms = observed_start.or(event.started_at_ms); } CoreTurnItem::SubAgentActivity(activity) if activity.kind == SubAgentActivityKind::Interrupted => @@ -1450,50 +1438,42 @@ async fn remove_missing_thread_watch( } } -#[allow(clippy::too_many_arguments)] async fn start_command_execution_item( conversation_id: &ThreadId, turn_id: String, item_id: String, - model_context: Option, - plugin_id: Option, - script_path: Option, - command: String, - cwd: LegacyAppPathString, - command_actions: Vec, - source: CommandExecutionSource, + started_at_ms: i64, + details: &CommandExecutionCompletionItem, outgoing: &ThreadScopedOutgoingMessageSender, thread_state: &Arc>, ) -> bool { - let first_start = { - let mut state = thread_state.lock().await; - state - .turn_summary - .command_execution_started - .insert(item_id.clone()) - }; + let first_start = thread_state + .lock() + .await + .turn_summary + .record_command_start(item_id.clone(), started_at_ms); if first_start { - let notification = ItemStartedNotification { - thread_id: conversation_id.to_string(), + let notification = ItemStartedNotification::new( + conversation_id.to_string(), turn_id, - started_at_ms: now_unix_timestamp_ms(), - item: ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { id: item_id, - model_context, + model_context: details.model_context.clone(), sandbox_type: None, - plugin_id, - script_path, - command, - cwd, + plugin_id: details.plugin_id.clone(), + script_path: details.script_path.clone(), + command: details.command.clone(), + cwd: details.cwd.clone(), process_id: None, - source, + source: CommandExecutionSource::Agent, status: CommandExecutionStatus::InProgress, - command_actions, + command_actions: details.command_actions.clone(), aggregated_output: None, exit_code: None, duration_ms: None, }, - }; + started_at_ms, + ); outgoing .send_server_notification(ServerNotification::ItemStarted(notification)) .await; @@ -1507,23 +1487,22 @@ async fn complete_command_execution_item( turn_id: String, item_id: String, completion_item: CommandExecutionCompletionItem, - process_id: Option, - source: CommandExecutionSource, status: CommandExecutionStatus, + completed_at_ms: Option, outgoing: &ThreadScopedOutgoingMessageSender, thread_state: &Arc>, ) { - let should_emit = thread_state + let Some(started_at_ms) = thread_state .lock() .await .turn_summary - .command_execution_started - .remove(&item_id); - if !should_emit { + .command_start_times + .remove(&item_id) + else { return; - } + }; - let item = ThreadItem::CommandExecution { + let item = ThreadItemPayload::CommandExecution { id: item_id, model_context: completion_item.model_context, sandbox_type: None, @@ -1531,20 +1510,22 @@ async fn complete_command_execution_item( script_path: completion_item.script_path, command: completion_item.command, cwd: completion_item.cwd, - process_id, - source, + process_id: None, + source: CommandExecutionSource::Agent, status, command_actions: completion_item.command_actions, aggregated_output: None, exit_code: None, duration_ms: None, }; - let notification = ItemCompletedNotification { - thread_id: conversation_id.to_string(), + let notification = ItemCompletedNotification::new( + conversation_id.to_string(), turn_id, - completed_at_ms: now_unix_timestamp_ms(), item, - }; + Some(started_at_ms), + // Guardian supplies the decision time used on resume. Manual approvals are timed here. + completed_at_ms.unwrap_or_else(now_unix_timestamp_ms), + ); outgoing .send_server_notification(ServerNotification::ItemCompleted(notification)) .await; @@ -2132,8 +2113,8 @@ async fn on_command_execution_request_approval_response( let state = thread_state.lock().await; state .turn_summary - .command_execution_started - .contains(&item_id) + .command_start_times + .contains_key(&item_id) } else { false } @@ -2148,9 +2129,8 @@ async fn on_command_execution_request_approval_response( event_turn_id.clone(), item_id.clone(), completion_item, - /*process_id*/ None, - CommandExecutionSource::Agent, status, + /*completed_at_ms*/ None, &outgoing, &thread_state, ) @@ -2551,13 +2531,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), - completion_item.model_context.clone(), - completion_item.plugin_id.clone(), - completion_item.script_path.clone(), - completion_item.command.clone(), - completion_item.cwd.clone(), - completion_item.command_actions.clone(), - CommandExecutionSource::Agent, + /*started_at_ms*/ 1_000, + &completion_item, &outgoing, &thread_state, ) @@ -2567,11 +2542,12 @@ mod tests { let msg = recv_broadcast_notification(&mut rx).await?; match msg { ServerNotification::ItemStarted(payload) => { + assert_eq!(payload.item.started_at_ms, Some(1_000)); assert_eq!(payload.thread_id, conversation_id.to_string()); assert_eq!(payload.turn_id, "turn-1"); assert_eq!( - payload.item, - ThreadItem::CommandExecution { + payload.item.payload, + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -2596,13 +2572,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), - completion_item.model_context.clone(), - completion_item.plugin_id.clone(), - completion_item.script_path.clone(), - completion_item.command.clone(), - completion_item.cwd.clone(), - completion_item.command_actions.clone(), - CommandExecutionSource::Agent, + /*started_at_ms*/ 1_100, + &completion_item, &outgoing, &thread_state, ) @@ -2633,13 +2604,8 @@ mod tests { &conversation_id, "turn-1".to_string(), "cmd-1".to_string(), - completion_item.model_context.clone(), - completion_item.plugin_id.clone(), - completion_item.script_path.clone(), - completion_item.command.clone(), - completion_item.cwd.clone(), - completion_item.command_actions.clone(), - CommandExecutionSource::Agent, + /*started_at_ms*/ 1_000, + &completion_item, &outgoing, &thread_state, ) @@ -2651,9 +2617,8 @@ mod tests { "turn-1".to_string(), "cmd-1".to_string(), completion_item, - /*process_id*/ None, - CommandExecutionSource::Agent, CommandExecutionStatus::Declined, + /*completed_at_ms*/ None, &outgoing, &thread_state, ) @@ -2662,13 +2627,13 @@ mod tests { let completed = recv_broadcast_notification(&mut rx).await?; match completed { ServerNotification::ItemCompleted(payload) => { - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, plugin_id, script_path, status, .. - } = payload.item + } = payload.item.payload else { bail!("expected command execution completion"); }; @@ -2685,9 +2650,8 @@ mod tests { "turn-1".to_string(), "cmd-1".to_string(), command_execution_completion_item("printf hi"), - /*process_id*/ None, - CommandExecutionSource::Agent, CommandExecutionStatus::Declined, + /*completed_at_ms*/ None, &outgoing, &thread_state, ) @@ -2750,13 +2714,13 @@ mod tests { match first { ServerNotification::ItemStarted(payload) => { assert_eq!(payload.turn_id, "turn-guardian-approved"); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, plugin_id, script_path, status, .. - } = payload.item + } = payload.item.payload else { bail!("expected command execution item"); }; @@ -2822,13 +2786,13 @@ mod tests { match fourth { ServerNotification::ItemStarted(payload) => { assert_eq!(payload.turn_id, "turn-guardian-denied"); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, plugin_id, script_path, status, .. - } = payload.item + } = payload.item.payload else { bail!("expected command execution item"); }; @@ -2878,13 +2842,14 @@ mod tests { let seventh = recv_broadcast_notification(&mut rx).await?; match seventh { ServerNotification::ItemCompleted(payload) => { - let ThreadItem::CommandExecution { + assert_eq!(payload.item.completed_at_ms, Some(1_042)); + let ThreadItemPayload::CommandExecution { id, plugin_id, script_path, status, .. - } = payload.item + } = payload.item.payload else { bail!("expected command execution completion"); }; @@ -3360,8 +3325,10 @@ mod tests { }; assert_eq!( payload, - ItemCompletedNotification { - item: ThreadItem::SubAgentActivity { + ItemCompletedNotification::new( + conversation_id.to_string(), + "turn-1".to_string(), + ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "activity-1".to_string(), @@ -3369,10 +3336,9 @@ mod tests { agent_thread_id: child_thread_id_string, agent_path: "/root/worker".to_string(), }, - thread_id: conversation_id.to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 42, - } + Some(42), + /*completed_at_ms*/ 42 + ) ); Ok(()) } @@ -3511,8 +3477,8 @@ mod tests { delivery: None, questions: None, }), - started_at_ms: Some(0), - completed_at_ms: 0, + started_at_ms: Some(42_123), + completed_at_ms: 42_200, }), ); state.track_current_turn_event( @@ -3530,8 +3496,8 @@ mod tests { delivery: None, questions: None, }), - started_at_ms: Some(0), - completed_at_ms: 0, + started_at_ms: Some(42_321), + completed_at_ms: 42_400, }), ); state.track_current_turn_event(&event_turn_id, &EventMsg::TurnComplete(event.clone())); @@ -3554,7 +3520,7 @@ mod tests { assert_eq!(n.turn.items_view, TurnItemsView::Summary); assert!(matches!( &n.turn.items[..], - [ThreadItem::AgentMessage { id, text, .. }] + [ThreadItem { payload: ThreadItemPayload::AgentMessage { id, text, .. }, started_at_ms: Some(42_123), completed_at_ms: Some(42_200) }] if id == "msg-1" && text == "complete response" )); assert_eq!(n.turn.error, None); diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index c9576732b0..ffdc5bc29e 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -80,7 +80,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; @@ -120,8 +120,11 @@ fn server_notification_requires_delivery(notification: &ServerNotification) -> b | ServerNotification::ThreadAttachmentUpdated(_) | ServerNotification::ExternalAgentConfigImportCompleted(_) | ServerNotification::ItemCompleted(ItemCompletedNotification { - item: ThreadItem::AgentMessage { - delivery: Some(AgentMessageDelivery::Async), + item: codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::AgentMessage { + delivery: Some(AgentMessageDelivery::Async), + .. + }, .. }, .. @@ -1084,8 +1087,10 @@ mod tests { }) )); assert!(server_notification_requires_delivery( - &ServerNotification::ItemCompleted(ItemCompletedNotification { - item: ThreadItem::AgentMessage { + &ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "item-1".to_string(), text: "Still working".to_string(), phase: None, @@ -1093,10 +1098,9 @@ mod tests { delivery: Some(AgentMessageDelivery::Async), questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }) + /*started_at_ms*/ None, + /*completed_at_ms*/ 0 + )) )); } diff --git a/codex-rs/app-server/src/notification_media.rs b/codex-rs/app-server/src/notification_media.rs index 2391a0bc22..99e4d1e1cb 100644 --- a/codex-rs/app-server/src/notification_media.rs +++ b/codex-rs/app-server/src/notification_media.rs @@ -1,7 +1,7 @@ use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::GeneratedImageRef; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::UserInput; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; @@ -11,11 +11,11 @@ use codex_protocol::models::ResponseItem; pub(crate) fn without_notification_media(notification: ServerNotification) -> ServerNotification { match notification { ServerNotification::ItemStarted(mut notification) => { - notification.item = without_thread_item_media(notification.item); + notification.item.payload = without_thread_item_media(notification.item.payload); ServerNotification::ItemStarted(notification) } ServerNotification::ItemCompleted(mut notification) => { - notification.item = without_thread_item_media(notification.item); + notification.item.payload = without_thread_item_media(notification.item.payload); ServerNotification::ItemCompleted(notification) } ServerNotification::RawResponseItemCompleted(mut notification) => { @@ -142,9 +142,9 @@ pub(crate) fn without_notification_media(notification: ServerNotification) -> Se } } -fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem { +fn without_thread_item_media(mut item: ThreadItemPayload) -> ThreadItemPayload { match &mut item { - ThreadItem::UserMessage { content, .. } => { + ThreadItemPayload::UserMessage { content, .. } => { content.retain(|item| match item { UserInput::Image { .. } | UserInput::Audio { .. } => false, UserInput::Text { .. } @@ -154,7 +154,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem { | UserInput::Mention { .. } => true, }); } - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { content_items: Some(content_items), .. } => { @@ -164,7 +164,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem { DynamicToolCallOutputContentItem::InputText { .. } => true, }); } - ThreadItem::FunctionCallOutput { + ThreadItemPayload::FunctionCallOutput { output: FunctionCallOutputBody::ContentItems(items), .. } => { @@ -175,7 +175,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem { | FunctionCallOutputContentItem::EncryptedContent { .. } => true, }); } - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { result: Some(result), .. } => { @@ -191,34 +191,34 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem { .is_none() }); } - ThreadItem::ImageGeneration(item) => { + ThreadItemPayload::ImageGeneration(item) => { item.image = GeneratedImageRef::Inline { result: String::new(), }; } - ThreadItem::HookPrompt { .. } - | ThreadItem::AgentMessage { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::FunctionCallOutput { + ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::AgentMessage { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::FunctionCallOutput { output: FunctionCallOutputBody::Text(_), .. } - | ThreadItem::CommandExecution { .. } - | ThreadItem::FileChange { .. } - | ThreadItem::McpToolCall { result: None, .. } - | ThreadItem::DynamicToolCall { + | ThreadItemPayload::CommandExecution { .. } + | ThreadItemPayload::FileChange { .. } + | ThreadItemPayload::McpToolCall { result: None, .. } + | ThreadItemPayload::DynamicToolCall { content_items: None, .. } - | ThreadItem::CollabAgentToolCall { .. } - | ThreadItem::SubAgentActivity { .. } - | ThreadItem::WebSearch(_) - | ThreadItem::ImageView { .. } - | ThreadItem::Sleep(_) - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => {} + | ThreadItemPayload::CollabAgentToolCall { .. } + | ThreadItemPayload::SubAgentActivity { .. } + | ThreadItemPayload::WebSearch(_) + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::Sleep(_) + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => {} } item } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 21c8d2327c..3ebcd1d7ee 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -229,8 +229,8 @@ use codex_app_server_protocol::ThreadIncrementElicitationParams; use codex_app_server_protocol::ThreadIncrementElicitationResponse; use codex_app_server_protocol::ThreadInjectItemsParams; use codex_app_server_protocol::ThreadInjectItemsResponse; -use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadItemEntry; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListParams; use codex_app_server_protocol::ThreadItemsListResponse; use codex_app_server_protocol::ThreadItemsReadParams; diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index f864f0f341..fe7602cf2c 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -271,6 +271,7 @@ pub(super) async fn ensure_listener_task_running( &conversation, watch_registration, thread_settings_baseline, + config_snapshot.history_mode, ); let Some(listener_command_tx) = thread_state.listener_command_tx() else { tracing::warn!( diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 941e32e5c0..b8907648ba 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -19,6 +19,7 @@ use crate::thread_sample::SampleThread; use crate::thread_sample::generate_sample; use codex_app_server_protocol::SelectedCapabilityRoot; use codex_app_server_protocol::ThreadHistoryMode as ApiThreadHistoryMode; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadItemsListAnchor; use codex_app_server_protocol::ThreadItemsListCursor; use codex_app_server_protocol::ThreadRevertParams; @@ -3515,8 +3516,8 @@ impl ThreadRequestProcessor { }) .await .map_err(paginated_history_list_error)?; - for item in page.items { - items.push(deserialize_stored_thread_item(item)?); + for item in &page.items { + items.push(stored_thread_item_to_api_item(item)?); } let Some(next_cursor) = page.next_cursor else { return Ok(items); @@ -6366,29 +6367,31 @@ fn paginated_history_list_error(err: ThreadStoreError) -> JSONRPCErrorError { } } -fn deserialize_stored_thread_item( - item: codex_thread_store::StoredThreadItem, +fn stored_thread_item_to_api_item( + stored_item: &codex_thread_store::StoredThreadItem, ) -> Result { - serde_json::from_slice::(&item.item_json).map_err(|err| { + let payload = serde_json::from_slice(&stored_item.item_json).map_err(|err| { internal_error(format!( "failed to deserialize stored thread item {}: {err}", - item.item_id + stored_item.item_id )) + })?; + Ok(ThreadItem { + payload, + started_at_ms: stored_item.started_at_ms, + completed_at_ms: stored_item.completed_at_ms, }) } fn stored_thread_item_to_entry( stored_item: codex_thread_store::StoredThreadItem, ) -> Result { - let turn_id = stored_item.turn_id.clone(); - let started_at_ms = stored_item.started_at_ms; - let completed_at_ms = stored_item.completed_at_ms; - let item = deserialize_stored_thread_item(stored_item)?; + let item = stored_thread_item_to_api_item(&stored_item)?; Ok(ThreadItemEntry { - turn_id, + turn_id: stored_item.turn_id, + started_at_ms: item.started_at_ms, + completed_at_ms: item.completed_at_ms, item, - started_at_ms, - completed_at_ms, }) } @@ -6410,8 +6413,8 @@ fn stored_turn_to_api_turn( }); let items = turn .items - .into_iter() - .map(deserialize_stored_thread_item) + .iter() + .map(stored_thread_item_to_api_item) .collect::, _>>()?; Ok(Turn { id: turn.turn_id, diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 147c1e084d..e4549bb1d3 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -153,6 +153,7 @@ mod thread_processor_behavior_tests { use chrono::Utc; use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::ThreadItem; + use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; @@ -333,14 +334,14 @@ mod thread_processor_behavior_tests { let active_turn = Turn { id: "live-turn".to_string(), root_turn_id: None, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "live-user-message".to_string(), client_id: None, content: vec![V2UserInput::Text { text: "live".to_string(), text_elements: Vec::new(), }], - }], + })], items_view: TurnItemsView::Full, error: None, status: TurnStatus::InProgress, diff --git a/codex-rs/app-server/src/request_processors/thread_read_state.rs b/codex-rs/app-server/src/request_processors/thread_read_state.rs index a64c31b55e..e329369001 100644 --- a/codex-rs/app-server/src/request_processors/thread_read_state.rs +++ b/codex-rs/app-server/src/request_processors/thread_read_state.rs @@ -112,8 +112,12 @@ pub(super) async fn publish( return None; } let has_error = summary.last_error.is_some(); - let final_text = match &summary.last_agent_message { - Some(ThreadItem::AgentMessage { text, .. }) => Some(text.as_str()), + let final_text = match summary + .last_agent_message + .as_ref() + .map(|message| &message.payload) + { + Some(ThreadItemPayload::AgentMessage { text, .. }) => Some(text.as_str()), _ => None, }; let visible = match &event.msg { diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index 9238d91f67..5d4cd7a6fa 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -1,7 +1,7 @@ #[cfg(test)] use codex_app_server_protocol::GeneratedImageRef; use codex_app_server_protocol::McpToolCallResult; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use serde_json::Value as JsonValue; @@ -18,8 +18,8 @@ pub(super) fn should_redact_thread_resume_payloads(client_name: Option<&str>) -> pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) { for turn in turns { - turn.items.retain_mut(|item| match item { - ThreadItem::McpToolCall { + turn.items.retain_mut(|entry| match &mut entry.payload { + ThreadItemPayload::McpToolCall { arguments, result, error, @@ -34,7 +34,7 @@ pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) { } true } - ThreadItem::ImageGeneration(_) => false, + ThreadItemPayload::ImageGeneration(_) => false, _ => true, }); } @@ -60,6 +60,7 @@ mod tests { use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::SessionSource; use codex_app_server_protocol::Thread; + use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; @@ -70,7 +71,7 @@ mod tests { #[test] fn redacts_mcp_success_result_and_removes_image_generation() { let mut thread = test_thread(vec![ - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id: "agent-1".to_string(), text: "kept".to_string(), phase: None, @@ -78,7 +79,7 @@ mod tests { delivery: None, questions: None, }, - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "docs".to_string(), tool: "lookup".to_string(), @@ -106,7 +107,7 @@ mod tests { error: None, duration_ms: Some(8), }, - ThreadItem::ImageGeneration(ImageGenerationItem { + ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: "ig-1".to_string(), status: "completed".to_string(), revised_prompt: Some("revised".to_string()), @@ -121,13 +122,16 @@ mod tests { generation_id: None, }), ]); + thread.turns[0].items[1].started_at_ms = Some(123); + thread.turns[0].items[2].started_at_ms = Some(456); redact_thread_resume_payloads(&mut thread.turns); assert_eq!(thread.turns[0].items.len(), 2); + assert_eq!(thread.turns[0].items[1].started_at_ms, Some(123)); assert_eq!( - thread.turns[0].items[0], - ThreadItem::AgentMessage { + thread.turns[0].items[0].payload, + ThreadItemPayload::AgentMessage { id: "agent-1".to_string(), text: "kept".to_string(), phase: None, @@ -137,8 +141,8 @@ mod tests { } ); assert_eq!( - thread.turns[0].items[1], - ThreadItem::McpToolCall { + thread.turns[0].items[1].payload, + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "docs".to_string(), tool: "lookup".to_string(), @@ -164,7 +168,7 @@ mod tests { #[test] fn redacts_mcp_error_message() { - let mut thread = test_thread(vec![ThreadItem::McpToolCall { + let mut thread = test_thread(vec![ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "docs".to_string(), tool: "lookup".to_string(), @@ -185,8 +189,8 @@ mod tests { redact_thread_resume_payloads(&mut thread.turns); assert_eq!( - thread.turns[0].items[0], - ThreadItem::McpToolCall { + thread.turns[0].items[0].payload, + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "docs".to_string(), tool: "lookup".to_string(), @@ -206,7 +210,7 @@ mod tests { ); } - fn test_thread(items: Vec) -> Thread { + fn test_thread(items: Vec) -> Thread { Thread { originator: None, environments: None, @@ -242,7 +246,7 @@ mod tests { turns: vec![Turn { id: "turn-1".to_string(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 1ce59c4cf4..bc2f029dfa 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -4,6 +4,7 @@ use codex_agent_extension::AgentInvocation; use codex_agent_extension::AgentRun; use codex_agent_extension::AgentRunner; use codex_app_server_protocol::ImageReference as V2ImageReference; +use codex_app_server_protocol::ThreadItem; use codex_protocol::error::CodexErrorDetails; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; @@ -1412,7 +1413,7 @@ impl TurnRequestProcessor { let items = if display_text.is_empty() { Vec::new() } else { - vec![ThreadItem::UserMessage { + vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: turn_id.clone(), client_id: None, content: vec![V2UserInput::Text { @@ -1420,7 +1421,7 @@ impl TurnRequestProcessor { // Review prompt display text is synthesized; no UI element ranges to preserve. text_elements: Vec::new(), }], - }] + })] }; Turn { diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 6dfe969ab3..5f0bbe5ab8 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -19,6 +19,7 @@ use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::models::MessagePhase; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::ThreadHistoryMode; use codex_rollout::RolloutItem; use codex_rollout::state_db::StateDbHandle; use codex_utils_path_uri::LegacyAppPathString; @@ -95,11 +96,24 @@ pub(crate) enum ThreadListenerCommand { #[derive(Default, Clone)] pub(crate) struct TurnSummary { pub(crate) started_at: Option, - pub(crate) command_execution_started: HashSet, + pub(crate) command_start_times: HashMap, pub(crate) last_error: Option, pub(crate) last_agent_message: Option, } +impl TurnSummary { + pub(crate) fn record_command_start(&mut self, item_id: String, started_at_ms: i64) -> bool { + if let std::collections::hash_map::Entry::Vacant(entry) = + self.command_start_times.entry(item_id) + { + entry.insert(started_at_ms); + true + } else { + false + } + } +} + #[derive(Default)] pub(crate) struct ThreadState { goal_resume_lock: Arc>, @@ -115,6 +129,7 @@ pub(crate) struct ThreadState { last_thread_settings: Option, listener_command_tx: Option>, current_turn_history: ThreadHistoryBuilder, + history_mode: ThreadHistoryMode, listener_thread: Option>, watch_registration: WatchRegistration, } @@ -133,12 +148,14 @@ impl ThreadState { conversation: &Arc, watch_registration: WatchRegistration, thread_settings_baseline: ThreadSettings, + history_mode: ThreadHistoryMode, ) -> (mpsc::UnboundedReceiver, u64) { if let Some(previous) = self.cancel_tx.replace(cancel_tx) { let _ = previous.send(()); } self.listener_generation = self.listener_generation.wrapping_add(1); self.last_thread_settings = Some(thread_settings_baseline); + self.history_mode = history_mode; let (listener_command_tx, listener_command_rx) = mpsc::unbounded_channel(); self.listener_command_tx = Some(listener_command_tx); self.listener_thread = Some(Arc::downgrade(conversation)); @@ -209,10 +226,10 @@ impl ThreadState { matches!(content, CoreAgentMessageContent::Text { text } if !text.trim().is_empty()) }) { - self.turn_summary.last_agent_message = - Some(ThreadItem::from(CoreTurnItem::AgentMessage(item.clone()))); + self.turn_summary.last_agent_message = Some(ThreadItem::from(payload)); } - self.current_turn_history.handle_event(event); + self.current_turn_history + .handle_event_for_history_mode(event, self.history_mode); if matches!(event, EventMsg::TurnAborted(_) | EventMsg::TurnComplete(_)) { self.last_terminal_turn_id = Some(event_turn_id.to_string()); if !self.current_turn_history.has_active_turn() { diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index 29b2b4be8b..f72a5beeff 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -15,7 +15,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxPolicy; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadStartParams; @@ -439,8 +439,8 @@ async fn multi_agent_v2_tools_emit_collaborator_analytics() -> Result<()> { .items; let child_thread_id = items .iter() - .find_map(|item| match item { - ThreadItem::SubAgentActivity { + .find_map(|item| match &item.payload { + ThreadItemPayload::SubAgentActivity { id, agent_thread_id, .. @@ -451,12 +451,14 @@ async fn multi_agent_v2_tools_emit_collaborator_analytics() -> Result<()> { assert!( !items .iter() - .any(|item| matches!(item, ThreadItem::CollabAgentToolCall { .. })) + .any(|item| matches!(&item.payload, ThreadItemPayload::CollabAgentToolCall { .. })) ); let activity_ids = items .iter() - .filter_map(|item| match item { - ThreadItem::SubAgentActivity { id, .. } if id.starts_with("call-") => Some(id.as_str()), + .filter_map(|item| match &item.payload { + ThreadItemPayload::SubAgentActivity { id, .. } if id.starts_with("call-") => { + Some(id.as_str()) + } _ => None, }) .collect::>(); diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 47082d837f..784e6f9291 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -22,7 +22,7 @@ use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; @@ -427,10 +427,10 @@ async fn auto_compaction_emits_started_and_completed_items(route: CompactionRout let started = wait_for_context_compaction_started(&mut mcp).await?; let completed = wait_for_context_compaction_completed(&mut mcp).await?; - let ThreadItem::ContextCompaction { id: started_id } = started.item else { + let ThreadItemPayload::ContextCompaction { id: started_id } = started.item.payload else { unreachable!("started item should be context compaction"); }; - let ThreadItem::ContextCompaction { id: completed_id } = completed.item else { + let ThreadItemPayload::ContextCompaction { id: completed_id } = completed.item.payload else { unreachable!("completed item should be context compaction"); }; @@ -548,10 +548,10 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() - let completed = wait_for_context_compaction_completed(&mut mcp).await?; wait_for_turn_completed(&mut mcp, &started.turn_id).await?; - let ThreadItem::ContextCompaction { id: started_id } = started.item else { + let ThreadItemPayload::ContextCompaction { id: started_id } = started.item.payload else { unreachable!("started item should be context compaction"); }; - let ThreadItem::ContextCompaction { id: completed_id } = completed.item else { + let ThreadItemPayload::ContextCompaction { id: completed_id } = completed.item.payload else { unreachable!("completed item should be context compaction"); }; @@ -723,7 +723,7 @@ async fn wait_for_context_compaction_started( loop { let started: ItemStartedNotification = timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??; - if let ThreadItem::ContextCompaction { .. } = started.item { + if let ThreadItemPayload::ContextCompaction { .. } = started.item.payload { return Ok(started); } } @@ -738,7 +738,7 @@ async fn wait_for_context_compaction_completed( mcp.read_notification("item/completed"), ) .await??; - if let ThreadItem::ContextCompaction { .. } = completed.item { + if let ThreadItemPayload::ContextCompaction { .. } = completed.item.payload { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/config_requirements_exec_tests.rs b/codex-rs/app-server/tests/suite/v2/config_requirements_exec_tests.rs index 8a0c8fcf2b..8db91aad33 100644 --- a/codex-rs/app-server/tests/suite/v2/config_requirements_exec_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/config_requirements_exec_tests.rs @@ -9,7 +9,7 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; @@ -125,12 +125,12 @@ decision = "prompt" loop { let completed: ItemCompletedNotification = app.read_notification("item/completed").await?; - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { id, status, exit_code, .. - } = completed.item + } = completed.item.payload && id == call_id { assert_eq!( diff --git a/codex-rs/app-server/tests/suite/v2/daybreak_access.rs b/codex-rs/app-server/tests/suite/v2/daybreak_access.rs index 923212b4c5..c61b973854 100644 --- a/codex-rs/app-server/tests/suite/v2/daybreak_access.rs +++ b/codex-rs/app-server/tests/suite/v2/daybreak_access.rs @@ -9,7 +9,7 @@ use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::SelectedCapabilityRoot; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -185,9 +185,9 @@ async fn daybreak_access_respects_plugin_provenance(case: AccessCall) -> Result< .await?; loop { let event: ItemCompletedNotification = app.read_notification("item/completed").await?; - if let ThreadItem::McpToolCall { + if let ThreadItemPayload::McpToolCall { result, plugin_id, .. - } = event.item + } = event.item.payload { assert_eq!(plugin_id.as_deref(), Some("selected-daybreak")); results.push(( diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index 38f34330ba..0875bedc17 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -20,7 +20,7 @@ use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -448,7 +448,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?; assert_eq!(started.thread_id, thread_id); assert_eq!(started.turn_id, turn_id.clone()); - let ThreadItem::DynamicToolCall { + let ThreadItemPayload::DynamicToolCall { id, namespace, tool, @@ -457,7 +457,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res content_items, success, duration_ms, - } = started.item + } = started.item.payload else { panic!("expected dynamic tool call item"); }; @@ -504,7 +504,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; assert_eq!(completed.thread_id, thread_id); assert_eq!(completed.turn_id, turn_id); - let ThreadItem::DynamicToolCall { + let ThreadItemPayload::DynamicToolCall { id, namespace, tool, @@ -513,7 +513,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res content_items, success, duration_ms, - } = completed.item + } = completed.item.payload else { panic!("expected dynamic tool call item"); }; @@ -735,12 +735,12 @@ async fn dynamic_tool_call_round_trip_handles_content_items() -> Result<()> { let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; assert_eq!(completed.thread_id, params.thread_id); assert_eq!(completed.turn_id, params.turn_id); - let ThreadItem::DynamicToolCall { + let ThreadItemPayload::DynamicToolCall { status, content_items: completed_content_items, success, .. - } = completed.item + } = completed.item.payload else { panic!("expected dynamic tool call item"); }; @@ -830,12 +830,12 @@ async fn dynamic_tool_remote_image_response_becomes_model_visible_error() -> Res let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; assert_eq!(completed.thread_id, params.thread_id); assert_eq!(completed.turn_id, params.turn_id); - let ThreadItem::DynamicToolCall { + let ThreadItemPayload::DynamicToolCall { status, content_items, success, .. - } = completed.item + } = completed.item.payload else { panic!("expected dynamic tool call item"); }; @@ -886,12 +886,12 @@ async fn dynamic_tool_remote_audio_response_becomes_model_visible_error() -> Res let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; assert_eq!(completed.thread_id, params.thread_id); assert_eq!(completed.turn_id, params.turn_id); - let ThreadItem::DynamicToolCall { + let ThreadItemPayload::DynamicToolCall { status, content_items, success, .. - } = completed.item + } = completed.item.payload else { panic!("expected dynamic tool call item"); }; @@ -978,7 +978,8 @@ async fn wait_for_dynamic_tool_started( continue; }; let started: ItemStartedNotification = serde_json::from_value(params)?; - if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + if matches!(&started.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id) + { return Ok(started); } } @@ -998,7 +999,8 @@ async fn wait_for_dynamic_tool_completed( continue; }; let completed: ItemCompletedNotification = serde_json::from_value(params)?; - if matches!(&completed.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + if matches!(&completed.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id) + { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index 555ef3036b..e6a8d863b3 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -34,7 +34,7 @@ use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::PluginListParams; use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -645,10 +645,10 @@ source = {:?} let response: ThreadReadResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; assert_eq!(response.thread.turns.len(), 1); - let imported_items = &response.thread.turns[0].items; + let mut imported_items = response.thread.turns[0].item_payloads(); assert_eq!(imported_items.len(), 3); - match &imported_items[0] { - ThreadItem::UserMessage { content, .. } => assert_eq!( + match imported_items.next() { + Some(ThreadItemPayload::UserMessage { content, .. }) => assert_eq!( content, &vec![UserInput::Text { text: "first request".to_string(), @@ -657,8 +657,8 @@ source = {:?} ), other => panic!("expected user message item, got {other:?}"), } - match &imported_items[1] { - ThreadItem::AgentMessage { text, .. } => assert_eq!(text, "first answer"), + match imported_items.next() { + Some(ThreadItemPayload::AgentMessage { text, .. }) => assert_eq!(text, "first answer"), other => panic!("expected agent message item, got {other:?}"), } @@ -2108,10 +2108,10 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { let response: ThreadReadResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; assert_eq!(response.thread.turns.len(), 2); - let control_items = &response.thread.turns[0].items; + let mut control_items = response.thread.turns[0].item_payloads(); assert_eq!(control_items.len(), 1); - match &control_items[0] { - ThreadItem::UserMessage { content, .. } => { + match control_items.next() { + Some(ThreadItemPayload::UserMessage { content, .. }) => { assert_eq!( content, &vec![UserInput::Text { @@ -2122,10 +2122,10 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { } other => panic!("expected user message item, got {other:?}"), } - let imported_items = &response.thread.turns[1].items; + let mut imported_items = response.thread.turns[1].item_payloads(); assert_eq!(imported_items.len(), 3); - match &imported_items[0] { - ThreadItem::UserMessage { content, .. } => { + match imported_items.next() { + Some(ThreadItemPayload::UserMessage { content, .. }) => { assert_eq!( content, &vec![UserInput::Text { @@ -2137,8 +2137,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { other => panic!("expected user message item, got {other:?}"), } assert_eq!( - imported_items.last(), - Some(&ThreadItem::AgentMessage { + imported_items.next_back(), + Some(&ThreadItemPayload::AgentMessage { id: "item-4".into(), text: "".into(), phase: None, @@ -2183,8 +2183,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { let response: ThreadReadResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; assert_eq!(response.thread.turns.len(), 3); - match &response.thread.turns[2].items[1] { - ThreadItem::AgentMessage { text, .. } => assert_eq!(text, "follow-up answer"), + match response.thread.turns[2].item_payloads().nth(/*n*/ 1) { + Some(ThreadItemPayload::AgentMessage { text, .. }) => assert_eq!(text, "follow-up answer"), other => panic!("expected agent message item, got {other:?}"), } diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs b/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs index a527e45d41..f04718b1ec 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs @@ -14,9 +14,10 @@ use codex_app_server_protocol::ExternalAgentConfigDetectResponse; use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification; use codex_app_server_protocol::ExternalAgentConfigImportResponse; use codex_app_server_protocol::ExternalAgentConfigMigrationItemType; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::UserInput; use pretty_assertions::assert_eq; @@ -229,16 +230,18 @@ async fn start_app_server(codex_home: &Path) -> Result { fn assert_history(response: &ThreadReadResponse, expected: &[(&str, &str)]) { let mut actual = Vec::new(); let mut markers = 0; - for item in response.thread.turns.iter().flat_map(|turn| &turn.items) { + for item in response.thread.turns.iter().flat_map(Turn::item_payloads) { match item { - ThreadItem::UserMessage { content, .. } => { + ThreadItemPayload::UserMessage { content, .. } => { actual.extend(content.iter().filter_map(|input| match input { UserInput::Text { text, .. } => Some(("user", text.as_str())), _ => None, })); } - ThreadItem::AgentMessage { text, .. } if text == IMPORT_MARKER => markers += 1, - ThreadItem::AgentMessage { text, .. } => actual.push(("assistant", text.as_str())), + ThreadItemPayload::AgentMessage { text, .. } if text == IMPORT_MARKER => markers += 1, + ThreadItemPayload::AgentMessage { text, .. } => { + actual.push(("assistant", text.as_str())) + } _ => {} } } diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs index eee2b00f97..9e1bd0a63c 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs @@ -37,7 +37,7 @@ use codex_app_server_protocol::ThreadCompactStartParams; use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; @@ -2650,12 +2650,12 @@ async fn user_approval_skips_async_guardian_without_changing_other_modes() -> Re while tool_results.len() < 3 { let completed: ItemCompletedNotification = timeout(TIMEOUT, app_server.read_notification("item/completed")).await??; - if let ThreadItem::McpToolCall { + if let ThreadItemPayload::McpToolCall { status, result, error, .. - } = completed.item + } = completed.item.payload { tool_results.push((status, result.map(|result| result.content), error)); } diff --git a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs index 4d531a7776..e81f9e7be3 100644 --- a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs @@ -13,13 +13,14 @@ use codex_app_server_protocol::ImageGenerationInputImage; use codex_app_server_protocol::ImageGenerationItem; use codex_app_server_protocol::ImageReference as V2ImageReference; use codex_app_server_protocol::ItemCompletedNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; @@ -161,14 +162,14 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model( ) .await??; - let ThreadItem::ImageGeneration(ImageGenerationItem { + let ThreadItemPayload::ImageGeneration(ImageGenerationItem { status, revised_prompt, image, transparent_background, saved_path: Some(saved_path), .. - }) = completed.item + }) = completed.item.payload else { panic!("expected completed image generation item with saved path"); }; @@ -353,13 +354,13 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history( ) .await??; let thread_id = completed.thread_id.clone(); - let ThreadItem::ImageGeneration(ImageGenerationItem { + let ThreadItemPayload::ImageGeneration(ImageGenerationItem { status, image, transparent_background, input_images, .. - }) = completed.item + }) = completed.item.payload else { panic!("expected completed image-generation item"); }; @@ -390,9 +391,9 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history( let persisted_image = thread .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .find_map(|item| match item { - ThreadItem::ImageGeneration(item) => Some(item), + ThreadItemPayload::ImageGeneration(item) => Some(item), _ => None, }) .context("persisted legacy history should contain the generated image")?; @@ -410,9 +411,9 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history( let resumed_image = thread .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .find_map(|item| match item { - ThreadItem::ImageGeneration(item) => Some(item), + ThreadItemPayload::ImageGeneration(item) => Some(item), _ => None, }) .context("resumed legacy history should contain the generated image")?; @@ -471,7 +472,7 @@ async fn automatic_image_background_preserves_unknown_transparency() -> Result<( mcp.read_stream_until_notification_message("turn/completed"), ) .await??; - let ThreadItem::ImageGeneration(image) = completed.item else { + let ThreadItemPayload::ImageGeneration(image) = completed.item.payload else { panic!("expected completed image-generation item"); }; assert_eq!(image.transparent_background, None); @@ -630,8 +631,8 @@ async fn standalone_image_generation_failure_emits_terminal_item( ) .await??; assert_eq!( - completed.item, - ThreadItem::ImageGeneration(ImageGenerationItem { + completed.item.payload, + ThreadItemPayload::ImageGeneration(ImageGenerationItem { id: call_id.to_string(), status: "failed".to_string(), revised_prompt: Some("paint a blue whale".to_string()), @@ -744,7 +745,7 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() -> ) .await??; let thread_id = completed.thread_id.clone(); - let ThreadItem::ImageGeneration(image) = completed.item else { + let ThreadItemPayload::ImageGeneration(image) = completed.item.payload else { panic!("expected failed image-generation item"); }; assert_eq!(image.status, "failed"); @@ -772,9 +773,9 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() -> let persisted_failure = thread .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .find_map(|item| match item { - ThreadItem::ImageGeneration(item) => item.failure.as_ref(), + ThreadItemPayload::ImageGeneration(item) => item.failure.as_ref(), _ => None, }); assert_eq!( @@ -796,9 +797,9 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() -> let resumed_failure = thread .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .find_map(|item| match item { - ThreadItem::ImageGeneration(item) => item.failure.as_ref(), + ThreadItemPayload::ImageGeneration(item) => item.failure.as_ref(), _ => None, }); assert_eq!( @@ -1157,7 +1158,7 @@ async fn run_image_edit_test( wait_for_image_generation_completed(&mut mcp), ) .await??; - let ThreadItem::ImageGeneration(completed_image) = completed.item else { + let ThreadItemPayload::ImageGeneration(completed_image) = completed.item.payload else { panic!("expected completed image-generation item"); }; timeout( @@ -1240,7 +1241,10 @@ async fn wait_for_image_generation_completed( ) -> Result { loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if matches!(&completed.item, ThreadItem::ImageGeneration(_)) { + if matches!( + &completed.item.payload, + ThreadItemPayload::ImageGeneration(_) + ) { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index f95a709820..847bfbe8d3 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -29,13 +29,14 @@ use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SelectedCapabilityRoot; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnEnvironmentParams; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; @@ -388,10 +389,10 @@ async fn model_mcp_tool_call_uses_session_client_extensions() -> Result<()> { .await?; let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?; - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { result: Some(result), .. - } = completed.item + } = completed.item.payload else { panic!("expected completed MCP tool call item"); }; @@ -1053,7 +1054,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result() let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?; assert_eq!(completed.turn_id, turn.id); - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { id, server, tool, @@ -1061,7 +1062,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result() result: Some(result), error, .. - } = completed.item + } = completed.item.payload else { panic!("expected completed MCP tool call item"); }; @@ -1081,7 +1082,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result() assert!(text.contains("truncated")); assert!(text.len() < DEFAULT_OUTPUT_BYTES_CAP + 1024); - let serialized_item = serde_json::to_string(&ThreadItem::McpToolCall { + let serialized_item = serde_json::to_string(&ThreadItemPayload::McpToolCall { id, server, tool, @@ -1193,7 +1194,7 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result .. } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; - let expected_item = ThreadItem::McpToolCall { + let expected_item = ThreadItemPayload::McpToolCall { id: call_id.to_string(), server: TEST_SERVER_NAME.to_string(), tool: TEST_TOOL_NAME.to_string(), @@ -1211,8 +1212,8 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result let resumed_item = resumed_thread .turns .iter() - .flat_map(|turn| &turn.items) - .find(|item| matches!(item, ThreadItem::McpToolCall { id, .. } if id == call_id)) + .flat_map(Turn::item_payloads) + .find(|item| matches!(item, ThreadItemPayload::McpToolCall { id, .. } if id == call_id)) .expect("resumed thread should include the in-progress MCP tool call"); assert_eq!(resumed_item, &expected_item); @@ -1228,11 +1229,11 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?; assert_eq!(completed.turn_id, turn.id); - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { status, read_only_hint, .. - } = &completed.item + } = &completed.item.payload else { panic!("expected the completed MCP tool call item"); }; @@ -1258,10 +1259,10 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result let persisted_item = completed_read .turns .iter() - .flat_map(|turn| &turn.items) - .find(|item| matches!(item, ThreadItem::McpToolCall { id, .. } if id == call_id)) + .flat_map(Turn::item_payloads) + .find(|item| matches!(item, ThreadItemPayload::McpToolCall { id, .. } if id == call_id)) .expect("completed thread history should include the persisted MCP tool call"); - assert_eq!(persisted_item, &completed.item); + assert_eq!(persisted_item, &completed.item.payload); mcp_server_handle.abort(); let _ = mcp_server_handle.await; @@ -1608,7 +1609,8 @@ async fn wait_for_mcp_tool_call_completed( mcp.read_notification("item/completed"), ) .await??; - if matches!(&completed.item, ThreadItem::McpToolCall { id, .. } if id == call_id) { + if matches!(&completed.item.payload, ThreadItemPayload::McpToolCall { id, .. } if id == call_id) + { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/plan_item.rs b/codex-rs/app-server/tests/suite/v2/plan_item.rs index 9709f2cb00..bb2cb65780 100644 --- a/codex-rs/app-server/tests/suite/v2/plan_item.rs +++ b/codex-rs/app-server/tests/suite/v2/plan_item.rs @@ -9,7 +9,7 @@ use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::PlanDeltaNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; @@ -63,7 +63,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> { assert_eq!(turn_completed.turn.id, turn.id); assert_eq!(turn_completed.turn.status, TurnStatus::Completed); - let expected_plan = ThreadItem::Plan { + let expected_plan = ThreadItemPayload::Plan { id: format!("{}-plan", turn.id), text: "# Final plan\n- first\n- second\n".to_string(), }; @@ -81,7 +81,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> { let plan_items = completed_items .iter() .filter_map(|item| match item { - ThreadItem::Plan { .. } => Some(item.clone()), + ThreadItemPayload::Plan { .. } => Some(item.clone()), _ => None, }) .collect::>(); @@ -89,7 +89,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> { assert!( completed_items .iter() - .any(|item| matches!(item, ThreadItem::AgentMessage { .. })), + .any(|item| matches!(item, ThreadItemPayload::AgentMessage { .. })), "agent message items should still be emitted alongside the plan item" ); @@ -123,7 +123,7 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()> let has_plan_item = completed_items .iter() - .any(|item| matches!(item, ThreadItem::Plan { .. })); + .any(|item| matches!(item, ThreadItemPayload::Plan { .. })); assert!(!has_plan_item); assert!(plan_deltas.is_empty()); @@ -168,8 +168,8 @@ async fn start_plan_mode_turn(mcp: &mut TestAppServer) -> Result Result<( - Vec, - Vec, + Vec, + Vec, Vec, TurnCompletedNotification, )> { @@ -188,14 +188,14 @@ async fn collect_turn_notifications( .params .ok_or_else(|| anyhow!("item/started notifications must include params"))?; let payload: ItemStartedNotification = serde_json::from_value(params)?; - started_items.push(payload.item); + started_items.push(payload.item.payload); } "item/completed" => { let params = notification .params .ok_or_else(|| anyhow!("item/completed notifications must include params"))?; let payload: ItemCompletedNotification = serde_json::from_value(params)?; - completed_items.push(payload.item); + completed_items.push(payload.item.payload); } "item/plan/delta" => { let params = notification diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index 8c3927d1e2..6c33d9e11a 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -14,7 +14,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListParams; use codex_app_server_protocol::ThreadItemsListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -56,6 +56,7 @@ use codex_app_server_protocol::ThreadTimelineListParams; use codex_app_server_protocol::ThreadTimelineListResponse; use codex_app_server_protocol::ThreadTurnsListParams; use codex_app_server_protocol::ThreadTurnsListResponse; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; @@ -1285,8 +1286,8 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa .read_notification::("item/completed") .await?; assert!(matches!( - initial_message.item, - ThreadItem::UserMessage { .. } + initial_message.item.payload, + ThreadItemPayload::UserMessage { .. } )); harness @@ -1397,8 +1398,8 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa item, .. } if matches!( - item.as_ref(), - ThreadItem::UserMessage { client_id, .. } + &item.payload, + ThreadItemPayload::UserMessage { client_id, .. } if client_id.as_deref() == Some("accepted-steer") ) ) @@ -1411,7 +1412,7 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa .iter() .filter_map(|entry| match entry { ThreadTimelineEntry::Item { item, .. } - if matches!(item.as_ref(), ThreadItem::AgentMessage { id, .. } if id == "promoted-message") => Some("artifact"), + if matches!(&item.payload, ThreadItemPayload::AgentMessage { id, .. } if id == "promoted-message") => Some("artifact"), ThreadTimelineEntry::Realtime { item: ThreadRealtimeItem { content: ThreadRealtimeItemContent::BemItemPromoted { item_id, .. }, @@ -1883,7 +1884,7 @@ async fn realtime_conversation_stop_emits_closed_notification( let items: ThreadItemsListResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request)).await??; let tails = items.data.iter().filter(|entry| matches!( - &entry.item, ThreadItem::UserMessage { content, .. } + &entry.item.payload, ThreadItemPayload::UserMessage { content, .. } if content.iter().any(|part| matches!(part, V2UserInput::Text { text, .. } if text.contains("transcript_tail_flush") && text.contains("user: final words"))) )).collect::>(); @@ -1913,8 +1914,8 @@ async fn realtime_conversation_stop_emits_closed_notification( .await?; let history: ThreadReadResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request)).await??; - assert_eq!(history.thread.turns.iter().flat_map(|turn| &turn.items).filter(|item| matches!( - item, ThreadItem::UserMessage { content, .. } + assert_eq!(history.thread.turns.iter().flat_map(Turn::item_payloads).filter(|item| matches!( + item, ThreadItemPayload::UserMessage { content, .. } if content.iter().any(|part| matches!(part, V2UserInput::Text { text, .. } if text.contains("transcript_tail_flush") && text.contains("user: final words"))) )).count(), 1); @@ -3963,7 +3964,8 @@ async fn websocket_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Resul // Phase 2: observe the delegated background agent turn executing the requested shell command. let started_command = wait_for_started_command_execution(&mut harness.mcp).await?; - let ThreadItem::CommandExecution { id, status, .. } = started_command.item else { + let ThreadItemPayload::CommandExecution { id, status, .. } = started_command.item.payload + else { unreachable!("helper returns command execution items"); }; assert_eq!( @@ -3972,12 +3974,12 @@ async fn websocket_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Resul ); let completed_command = wait_for_completed_command_execution(&mut harness.mcp).await?; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, status, aggregated_output, .. - } = completed_command.item + } = completed_command.item.payload else { unreachable!("helper returns command execution items"); }; @@ -4298,7 +4300,7 @@ async fn wait_for_started_command_execution( ) -> Result { loop { let started = read_notification::(mcp, "item/started").await?; - if let ThreadItem::CommandExecution { .. } = &started.item { + if let ThreadItemPayload::CommandExecution { .. } = &started.item.payload { return Ok(started); } } @@ -4310,7 +4312,7 @@ async fn wait_for_completed_command_execution( loop { let completed = read_notification::(mcp, "item/completed").await?; - if let ThreadItem::CommandExecution { .. } = &completed.item { + if let ThreadItemPayload::CommandExecution { .. } = &completed.item.payload { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs index 077590aa9c..07fe60c2ec 100644 --- a/codex-rs/app-server/tests/suite/v2/review.rs +++ b/codex-rs/app-server/tests/suite/v2/review.rs @@ -19,6 +19,7 @@ use codex_app_server_protocol::ReviewTarget; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; @@ -148,14 +149,14 @@ async fn review_start_runs_review_turn_and_emits_code_review_item( assert_eq!(turn.items_view, TurnItemsView::NotLoaded); assert_eq!( turn.items, - vec![ThreadItem::UserMessage { + [ThreadItem::untimed(ThreadItemPayload::UserMessage { id: turn_id.clone(), client_id: None, content: vec![V2UserInput::Text { text: "commit 1234567: Tidy UI colors".to_string(), text_elements: Vec::new(), }], - }] + })] ); // Confirm we see the EnteredReviewMode marker on the main thread. @@ -163,8 +164,8 @@ async fn review_start_runs_review_turn_and_emits_code_review_item( for _ in 0..10 { let started: ItemStartedNotification = timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??; - match started.item { - ThreadItem::EnteredReviewMode { review, .. } => { + match started.item.payload { + ThreadItemPayload::EnteredReviewMode { review, .. } => { assert_eq!(started.turn_id, turn_id); assert_eq!(review, "commit 1234567: Tidy UI colors"); saw_entered_review_mode = true; @@ -187,8 +188,8 @@ async fn review_start_runs_review_turn_and_emits_code_review_item( mcp.read_notification("item/completed"), ) .await??; - match completed.item { - ThreadItem::ExitedReviewMode { review, .. } => { + match completed.item.payload { + ThreadItemPayload::ExitedReviewMode { review, .. } => { assert_eq!(completed.turn_id, turn_id); review_body = Some(review); break; @@ -257,14 +258,14 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() -> assert_eq!(turn.items_view, TurnItemsView::NotLoaded); assert_eq!( turn.items, - vec![ThreadItem::UserMessage { + [ThreadItem::untimed(ThreadItemPayload::UserMessage { id: turn_id.clone(), client_id: None, content: vec![V2UserInput::Text { text: "commit 1234567: Check review approvals".to_string(), text_elements: Vec::new(), }], - }] + })] ); let server_req = timeout( @@ -282,7 +283,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() -> for _ in 0..10 { let started: ItemStartedNotification = timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??; - if let ThreadItem::CommandExecution { id, .. } = started.item { + if let ThreadItemPayload::CommandExecution { id, .. } = started.item.payload { command_item_id = Some(id); break; } @@ -437,14 +438,14 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<( assert_eq!(turn.items_view, TurnItemsView::NotLoaded); assert_eq!( turn.items, - vec![ThreadItem::UserMessage { + [ThreadItem::untimed(ThreadItemPayload::UserMessage { id: turn.id.clone(), client_id: None, content: vec![V2UserInput::Text { text: expected_prompt.clone(), text_elements: Vec::new(), }], - }] + })] ); assert_ne!( review_thread_id, thread_id, diff --git a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs index 0ac5cdb2b7..b1f1d1955a 100644 --- a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs @@ -11,7 +11,7 @@ use codex_app_server_protocol::ModelRerouteReason; use codex_app_server_protocol::ModelReroutedNotification; use codex_app_server_protocol::ModelVerification; use codex_app_server_protocol::ModelVerificationNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnModerationMetadataNotification; @@ -365,14 +365,14 @@ async fn collect_turn_notifications_and_validate_no_warning_item( anyhow::anyhow!("item/started notifications must include params") })?; let payload: ItemStartedNotification = serde_json::from_value(params)?; - assert!(!is_warning_user_message_item(&payload.item)); + assert!(!is_warning_user_message_item(&payload.item.payload)); } "item/completed" => { let params = notification.params.ok_or_else(|| { anyhow::anyhow!("item/completed notifications must include params") })?; let payload: ItemCompletedNotification = serde_json::from_value(params)?; - assert!(!is_warning_user_message_item(&payload.item)); + assert!(!is_warning_user_message_item(&payload.item.payload)); } "turn/completed" => { return rerouted.ok_or_else(|| { @@ -413,14 +413,14 @@ async fn collect_model_verification_notifications_and_validate_no_warning_item( anyhow::anyhow!("item/started notifications must include params") })?; let payload: ItemStartedNotification = serde_json::from_value(params)?; - assert!(!is_warning_user_message_item(&payload.item)); + assert!(!is_warning_user_message_item(&payload.item.payload)); } "item/completed" => { let params = notification.params.ok_or_else(|| { anyhow::anyhow!("item/completed notifications must include params") })?; let payload: ItemCompletedNotification = serde_json::from_value(params)?; - assert!(!is_warning_user_message_item(&payload.item)); + assert!(!is_warning_user_message_item(&payload.item.payload)); } "turn/completed" => { let verification = verification.ok_or_else(|| { @@ -468,8 +468,8 @@ async fn collect_cyber_policy_error_and_validate_no_reroute( } } -fn warning_text_from_item(item: &ThreadItem) -> Option<&str> { - let ThreadItem::UserMessage { content, .. } = item else { +fn warning_text_from_item(item: &ThreadItemPayload) -> Option<&str> { + let ThreadItemPayload::UserMessage { content, .. } = item else { return None; }; @@ -479,7 +479,7 @@ fn warning_text_from_item(item: &ThreadItem) -> Option<&str> { }) } -fn is_warning_user_message_item(item: &ThreadItem) -> bool { +fn is_warning_user_message_item(item: &ThreadItemPayload) -> bool { warning_text_from_item(item).is_some() } diff --git a/codex-rs/app-server/tests/suite/v2/sleep.rs b/codex-rs/app-server/tests/suite/v2/sleep.rs index 2b60c6a783..b515dbce08 100644 --- a/codex-rs/app-server/tests/suite/v2/sleep.rs +++ b/codex-rs/app-server/tests/suite/v2/sleep.rs @@ -8,7 +8,7 @@ use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SleepItem; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnCompletedNotification; @@ -286,28 +286,29 @@ clock_source = "external" ); } - let expected_item = ThreadItem::Sleep(SleepItem { + let expected_item = ThreadItemPayload::Sleep(SleepItem { id: CALL_ID.to_string(), duration_ms: DURATION_MS, }); assert!(completed.completed_at_ms >= started.started_at_ms); assert_eq!( started, - ItemStartedNotification { - item: expected_item.clone(), - thread_id: thread.id.clone(), - turn_id: turn.id.clone(), - started_at_ms: started.started_at_ms, - } + ItemStartedNotification::new( + thread.id.clone(), + turn.id.clone(), + expected_item.clone(), + started.started_at_ms + ) ); assert_eq!( completed, - ItemCompletedNotification { - item: expected_item, - thread_id: thread.id, - turn_id: turn.id, - completed_at_ms: completed.completed_at_ms, - } + ItemCompletedNotification::new( + thread.id, + turn.id, + expected_item, + Some(started.started_at_ms), + completed.completed_at_ms + ) ); Ok(()) @@ -320,7 +321,7 @@ async fn wait_for_sleep_started( loop { let started: ItemStartedNotification = timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??; - if matches!(&started.item, ThreadItem::Sleep(item) if item.id == call_id) { + if matches!(&started.item.payload, ThreadItemPayload::Sleep(item) if item.id == call_id) { return Ok(started); } } @@ -336,7 +337,7 @@ async fn wait_for_sleep_completed( mcp.read_notification("item/completed"), ) .await??; - if matches!(&completed.item, ThreadItem::Sleep(item) if item.id == call_id) { + if matches!(&completed.item.payload, ThreadItemPayload::Sleep(item) if item.id == call_id) { return Ok(completed); } } diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index c7eabe111f..11678f0961 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -37,7 +37,7 @@ use codex_app_server_protocol::ThreadClosedNotification; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -250,8 +250,8 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { let turn = &thread.turns[0]; assert_eq!(turn.status, TurnStatus::Interrupted); assert_eq!(turn.items.len(), 1, "expected user message item"); - match &turn.items[0] { - ThreadItem::UserMessage { content, .. } => { + match &turn.items[0].payload { + ThreadItemPayload::UserMessage { content, .. } => { assert_eq!( content, &vec![UserInput::Text { @@ -2102,8 +2102,8 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted( assert_eq!(turns[0].status, TurnStatus::Interrupted); assert_eq!(turns[0].items.len(), 1); assert!(matches!( - &turns[0].items[0], - ThreadItem::UserMessage { id, .. } if id == "before-fork" + &turns[0].items[0].payload, + ThreadItemPayload::UserMessage { id, .. } if id == "before-fork" )); let search: ThreadSearchOccurrencesResponse = mcp @@ -2486,8 +2486,8 @@ async fn assert_thread_fork_ephemeral_remains_pathless_and_omits_listing( let turn = &thread.turns[0]; assert_eq!(turn.status, TurnStatus::Completed); assert_eq!(turn.items.len(), 1, "expected user message item"); - match &turn.items[0] { - ThreadItem::UserMessage { content, .. } => { + match &turn.items[0].payload { + ThreadItemPayload::UserMessage { content, .. } => { assert_eq!( content, &vec![UserInput::Text { diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index f2720fedf1..70aa592be2 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -11,6 +11,7 @@ use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadInjectItemsParams; use codex_app_server_protocol::ThreadInjectItemsResponse; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; @@ -330,10 +331,16 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( assert_eq!(turn.id, turn_started.turn.id); assert_eq!(turn_started.turn.status, TurnStatus::InProgress); assert_eq!(item_completed.turn_id, turn_started.turn.id); - assert_eq!(item_started.item, item_completed.item); + assert_eq!( + item_completed.item, + ThreadItem { + completed_at_ms: Some(item_completed.completed_at_ms), + ..item_started.item + } + ); assert!(matches!( - &item_completed.item, - ThreadItem::FunctionCallOutput { + &item_completed.item.payload, + ThreadItemPayload::FunctionCallOutput { name, namespace, output: FunctionCallOutputBody::Text(output), diff --git a/codex-rs/app-server/tests/suite/v2/thread_queue.rs b/codex-rs/app-server/tests/suite/v2/thread_queue.rs index a27c3b5a1f..58ec3a90da 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_queue.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_queue.rs @@ -18,7 +18,7 @@ use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::QueuedSubmission; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; @@ -386,9 +386,9 @@ async fn idle_queue_dispatch_preserves_client_id() -> Result<()> { ); let started: ItemStartedNotification = timeout(READ_TIMEOUT, app.read_notification("item/started")).await??; - let ThreadItem::UserMessage { + let ThreadItemPayload::UserMessage { client_id, content, .. - } = started.item + } = started.item.payload else { anyhow::bail!("queued turn did not begin with its user message"); }; @@ -538,9 +538,9 @@ async fn cold_thread_resume_dispatches_a_persisted_queued_submission() -> Result let started: ItemStartedNotification = timeout(READ_TIMEOUT, resumed.read_notification("item/started")).await??; - let ThreadItem::UserMessage { + let ThreadItemPayload::UserMessage { client_id, content, .. - } = started.item + } = started.item.payload else { anyhow::bail!("cold resume did not start the persisted queued user message"); }; @@ -747,14 +747,14 @@ async fn queue_start_while_active_returns_busy_and_preserves_the_queue() -> Resu let started_item: ItemStartedNotification = timeout(READ_TIMEOUT, app.read_notification("item/started")).await??; if matches!( - &started_item.item, - ThreadItem::UserMessage { client_id, .. } + &started_item.item.payload, + ThreadItemPayload::UserMessage { client_id, .. } if client_id.as_deref() == Some("active-queued-client-id") ) { break started_item; } }; - let ThreadItem::UserMessage { client_id, .. } = started_item.item else { + let ThreadItemPayload::UserMessage { client_id, .. } = started_item.item.payload else { anyhow::bail!("queued message did not start after the active turn completed"); }; assert_eq!(client_id.as_deref(), Some("active-queued-client-id")); @@ -823,8 +823,8 @@ async fn queue_start_without_id_starts_the_head_when_idle() -> Result<()> { let started_item: ItemStartedNotification = timeout(READ_TIMEOUT, app.read_notification("item/started")).await??; if matches!( - &started_item.item, - ThreadItem::UserMessage { client_id, .. } + &started_item.item.payload, + ThreadItemPayload::UserMessage { client_id, .. } if client_id.as_deref() == Some(first.client_user_message_id.as_str()) ) { break started_item; diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index f1c704e905..85acd3b319 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -25,8 +25,8 @@ use codex_app_server_protocol::SortDirection; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadItemEntry; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListAnchor; use codex_app_server_protocol::ThreadItemsListCursor; use codex_app_server_protocol::ThreadItemsListParams; @@ -222,8 +222,8 @@ async fn thread_read_can_include_turns() -> Result<()> { assert_eq!(turn.root_turn_id, None); assert_eq!(turn.items_view, TurnItemsView::Full); assert_eq!(turn.items.len(), 1, "expected user message item"); - match &turn.items[0] { - ThreadItem::UserMessage { content, .. } => { + match &turn.items[0].payload { + ThreadItemPayload::UserMessage { content, .. } => { assert_eq!( content, &vec![UserInput::Text { @@ -297,7 +297,8 @@ async fn thread_read_preserves_file_id_and_root_from_completed_turn( thread: loaded_thread, .. } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??; - let ThreadItem::UserMessage { content, .. } = &loaded_thread.turns[0].items[0] else { + let ThreadItemPayload::UserMessage { content, .. } = &loaded_thread.turns[0].items[0].payload + else { panic!("expected completed turn to start with a user message"); }; assert_eq!(content, &vec![image.clone()]); @@ -323,7 +324,9 @@ async fn thread_read_preserves_file_id_and_root_from_completed_turn( thread: restarted_thread, .. } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??; - let ThreadItem::UserMessage { content, .. } = &restarted_thread.turns[0].items[0] else { + let ThreadItemPayload::UserMessage { content, .. } = + &restarted_thread.turns[0].items[0].payload + else { panic!("expected persisted turn to start with a user message"); }; assert_eq!(content, &vec![image]); @@ -500,6 +503,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> { data, next_cursor, backwards_cursor, + .. } = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??; assert_eq!(turn_user_texts(&data), vec!["third", "second"]); assert!( @@ -1062,7 +1066,13 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa let ThreadReadResponse { thread, .. } = serde_json::from_value(result)?; assert_eq!(turn_user_texts(&thread.turns), vec!["history from store"]); - let [ThreadItem::UserMessage { content, .. }] = thread.turns[0].items.as_slice() else { + let [ + codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::UserMessage { content, .. }, + .. + }, + ] = thread.turns[0].items.as_slice() + else { panic!("expected one user message item"); }; assert_eq!( @@ -1834,17 +1844,17 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( id: "turn-1".to_string(), root_turn_id: None, items: vec![ - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: Vec::new(), }, - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { id: "steer-1".to_string(), client_id: Some("updated-steer".to_string()), content: Vec::new(), }, - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id: "agent-1".to_string(), text: "first".to_string(), phase: None, @@ -1852,7 +1862,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( delivery: None, questions: None, }, - ], + ] + .into_iter() + .map(|payload| codex_app_server_protocol::ThreadItem { + payload, + started_at_ms: Some(0), + completed_at_ms: Some(1), + }) + .collect(), items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, @@ -1863,11 +1880,18 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( let expected_turn_2_full = Turn { id: "turn-2".to_string(), root_turn_id: None, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItemPayload::UserMessage { id: "user-2".to_string(), client_id: None, content: Vec::new(), - }], + }] + .into_iter() + .map(|payload| codex_app_server_protocol::ThreadItem { + payload, + started_at_ms: Some(0), + completed_at_ms: Some(1), + }) + .collect(), items_view: TurnItemsView::Full, status: TurnStatus::Interrupted, error: None, @@ -2029,12 +2053,12 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( id: "turn-1".to_string(), root_turn_id: None, items: vec![ - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: Vec::new(), }, - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id: "agent-1".to_string(), text: "first".to_string(), phase: None, @@ -2042,7 +2066,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( delivery: None, questions: None, }, - ], + ] + .into_iter() + .map(|payload| codex_app_server_protocol::ThreadItem { + payload, + started_at_ms: Some(0), + completed_at_ms: Some(1) + }) + .collect(), items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -2051,6 +2082,24 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( duration_ms: Some(10_000), }] ); + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread_id.to_string(), + exclude_turns: true, + initial_turns_page: Some(ThreadResumeInitialTurnsPageParams { + limit: Some(1), + sort_direction: Some(SortDirection::Asc), + items_view: Some(TurnItemsView::Summary), + }), + ..Default::default() + }) + .await?; + let resumed: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; + assert_eq!( + resumed.initial_turns_page.expect("summary page").data, + first_page.data + ); let next_cursor = first_page.next_cursor.expect("next turn cursor"); let second_page = read_turns_page( &mut mcp, @@ -2100,10 +2149,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( first_items_page.data, vec![ThreadItemEntry { turn_id: "turn-1".to_string(), - item: ThreadItem::UserMessage { - id: "user-1".to_string(), - client_id: None, - content: Vec::new(), + item: codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::UserMessage { + id: "user-1".to_string(), + client_id: None, + content: Vec::new(), + }, + started_at_ms: Some(0), + completed_at_ms: Some(1) }, started_at_ms: Some(0), completed_at_ms: Some(1), @@ -2232,23 +2285,31 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( vec![ ThreadItemEntry { turn_id: "turn-1".to_string(), - item: ThreadItem::UserMessage { - id: "steer-1".to_string(), - client_id: Some("updated-steer".to_string()), - content: Vec::new(), + item: codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::UserMessage { + id: "steer-1".to_string(), + client_id: Some("updated-steer".to_string()), + content: Vec::new(), + }, + started_at_ms: Some(0), + completed_at_ms: Some(1) }, started_at_ms: Some(0), completed_at_ms: Some(1), }, ThreadItemEntry { turn_id: "turn-1".to_string(), - item: ThreadItem::AgentMessage { - id: "agent-1".to_string(), - text: "first".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, + item: codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::AgentMessage { + id: "agent-1".to_string(), + text: "first".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + started_at_ms: Some(0), + completed_at_ms: Some(1) }, started_at_ms: Some(0), completed_at_ms: Some(1), @@ -2607,8 +2668,8 @@ fn paginated_completed_item( fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> { turns .iter() - .filter_map(|turn| match turn.items.first()? { - ThreadItem::UserMessage { content, .. } => match content.first()? { + .filter_map(|turn| match &turn.items.first()?.payload { + ThreadItemPayload::UserMessage { content, .. } => match content.first()? { UserInput::Text { text, .. } => Some(text.as_str()), UserInput::Image { .. } | UserInput::LocalImage { .. } @@ -2625,9 +2686,9 @@ fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> { fn turn_agent_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> { turns .iter() - .flat_map(|turn| &turn.items) + .flat_map(Turn::item_payloads) .filter_map(|item| match item { - ThreadItem::AgentMessage { text, .. } => Some(text.as_str()), + ThreadItemPayload::AgentMessage { text, .. } => Some(text.as_str()), _ => None, }) .collect() 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 19cd962b1b..9179528428 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -52,7 +52,7 @@ use codex_app_server_protocol::ThreadGoalGetResponse; use codex_app_server_protocol::ThreadGoalSetResponse; use codex_app_server_protocol::ThreadGoalStatus; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; @@ -133,6 +133,7 @@ use core_test_support::streaming_sse::StreamingSseChunk; use core_test_support::streaming_sse::start_streaming_sse_server; use pretty_assertions::assert_eq; use serde_json::json; +use std::collections::HashMap; use std::fs::FileTimes; use std::io::Write; use std::path::Path; @@ -2736,8 +2737,8 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { let turn = &thread.turns[0]; assert_eq!(turn.status, TurnStatus::Completed); assert_eq!(turn.items.len(), 1, "expected user message item"); - match &turn.items[0] { - ThreadItem::UserMessage { content, .. } => { + match &turn.items[0].payload { + ThreadItemPayload::UserMessage { content, .. } => { assert_eq!( content, &vec![UserInput::Text { @@ -2772,16 +2773,16 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( let remote_mcp_item = remote_turn .items .iter() - .find(|item| matches!(item, ThreadItem::McpToolCall { .. })) + .find(|item| matches!(&item.payload, ThreadItemPayload::McpToolCall { .. })) .expect("remote resume should include redacted MCP item"); - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { arguments, app_context, read_only_hint, result, error, .. - } = remote_mcp_item + } = &remote_mcp_item.payload else { unreachable!("matched MCP item"); }; @@ -2812,7 +2813,7 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( !remote_turn .items .iter() - .any(|item| matches!(item, ThreadItem::ImageGeneration(_))), + .any(|item| matches!(&item.payload, ThreadItemPayload::ImageGeneration(_))), "remote resume should drop image generation items for {client_name}" ); } @@ -2827,15 +2828,15 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( let normal_mcp_item = normal_turn .items .iter() - .find(|item| matches!(item, ThreadItem::McpToolCall { .. })) + .find(|item| matches!(&item.payload, ThreadItemPayload::McpToolCall { .. })) .expect("normal resume should include MCP item"); - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { arguments, app_context, read_only_hint, result, .. - } = normal_mcp_item + } = &normal_mcp_item.payload else { unreachable!("matched MCP item"); }; @@ -2866,8 +2867,8 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( assert_eq!(result.meta, Some(json!({"secret":"meta"}))); assert!( normal_turn.items.iter().any(|item| matches!( - item, - ThreadItem::ImageGeneration(item) + &item.payload, + ThreadItemPayload::ImageGeneration(item) if matches!( &item.image, GeneratedImageRef::Inline { result } @@ -5364,6 +5365,8 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R gate: None, body: responses::sse(vec![ responses::ev_response_created("resp-2"), + responses::ev_message_item_added("progress", ""), + responses::ev_assistant_message("progress", "Checking"), responses::ev_message_item_added("msg-2", ""), ]), }, @@ -5438,17 +5441,17 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R primary.read_stream_until_notification_message("turn/started"), ) .await??; - // An assistant item starts after the user message has reached live history. - // The gated remainder of the response keeps the turn running during resume. + // Starting the gated reply means the prompt and the progress message have completed. + let mut message_starts = HashMap::new(); timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = primary.read_notification("item/started").await?; - if started.turn_id == running_turn.id - && matches!(started.item, ThreadItem::AgentMessage { .. }) - { + assert_eq!(started.turn_id, running_turn.id); + if started.item.id() == "msg-2" { return Ok::<(), anyhow::Error>(()); } + message_starts.insert(started.item.id().to_owned(), started.started_at_ms); } }) .await??; @@ -5550,7 +5553,18 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R page.data[0].items_view, items_view.unwrap_or(TurnItemsView::Summary) ); - assert!(!page.data[0].items.is_empty()); + assert!(matches!( + page.data[0].items.as_slice(), + [codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::UserMessage { .. }, .. }, codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::AgentMessage { text, .. }, .. }] if text == "Checking" + )); + assert_eq!( + page.data[0] + .items + .iter() + .filter_map(|item| Some((item.id().to_owned(), item.started_at_ms?))) + .collect::>(), + message_starts + ); if !exclude_turns { let full_turn = resumed.thread.turns.last().expect("full active turn"); assert_eq!(full_turn.id, running_turn.id); @@ -5899,14 +5913,14 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result< .await?; let started: ItemStartedNotification = serde_json::from_value(notification.params.clone().expect("item/started params"))?; - if let ThreadItem::FileChange { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::FileChange { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; let expected_readme_path = workspace.join("README.md"); - let expected_file_change = ThreadItem::FileChange { + let expected_file_change = ThreadItemPayload::FileChange { id: "patch-call".to_string(), changes: vec![codex_app_server_protocol::FileUpdateChange { path: expected_readme_path.to_string_lossy().into_owned(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs index a9c2dd19bf..561b890d18 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs @@ -20,7 +20,7 @@ use codex_app_server_protocol::SortDirection; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadShellCommandParams; @@ -86,9 +86,9 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut .await?; let started = wait_for_command_execution_started(&mut mcp, /*expected_id*/ None).await?; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, source, status, .. - } = &started.item + } = &started.item.payload else { unreachable!("helper returns command execution item"); }; @@ -103,14 +103,14 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut ); let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, source, status, aggregated_output, exit_code, .. - } = &completed.item + } = &completed.item.payload else { unreachable!("helper returns command execution item"); }; @@ -279,9 +279,9 @@ async fn check_thread_shell_command_in_active_turn(timeout_ms: Option) -> R let started: TurnStartedNotification = mcp.read_notification("turn/started").await?; assert_eq!(started.turn.id, turn.id); let agent_started = wait_for_command_execution_started(&mut mcp, Some("call-approve")).await?; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { command, source, .. - } = &agent_started.item + } = &agent_started.item.payload else { unreachable!("helper returns command execution item"); }; @@ -315,19 +315,19 @@ async fn check_thread_shell_command_in_active_turn(timeout_ms: Option) -> R wait_for_command_execution_started_by_source(&mut mcp, CommandExecutionSource::UserShell) .await?; assert_eq!(started.turn_id, turn.id); - let command_id = match &started.item { - ThreadItem::CommandExecution { id, .. } => id.clone(), + let command_id = match &started.item.payload { + ThreadItemPayload::CommandExecution { id, .. } => id.clone(), _ => unreachable!("helper returns command execution item"), }; let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?; assert_eq!(completed.turn_id, turn.id); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { source, aggregated_output, status, exit_code, .. - } = &completed.item + } = &completed.item.payload else { unreachable!("helper returns command execution item"); }; @@ -443,12 +443,12 @@ async fn thread_shell_command_honors_optional_timeout() -> Result<()> { let completed = wait_for_command_execution_completed(&mut mcp, /*expected_id*/ None).await?; assert_eq!(completed.turn_id, started.turn.id); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { status, exit_code, aggregated_output, .. - } = completed.item + } = completed.item.payload else { unreachable!("helper returns command execution item"); }; @@ -544,11 +544,11 @@ fn current_shell_slow_command() -> Result<(String, String)> { Ok((format!("{command}{suffix}"), expected_output)) } -fn assert_no_command_executions(items: &[ThreadItem], context: &str) { +fn assert_no_command_executions(items: &[codex_app_server_protocol::ThreadItem], context: &str) { assert!( items .iter() - .all(|item| !matches!(item, ThreadItem::CommandExecution { .. })), + .all(|entry| !matches!(&entry.payload, ThreadItemPayload::CommandExecution { .. })), "{context} should always exclude command executions from returned turns" ); } @@ -577,7 +577,7 @@ async fn wait_for_command_execution_started( ) -> Result { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - let ThreadItem::CommandExecution { id, .. } = &started.item else { + let ThreadItemPayload::CommandExecution { id, .. } = &started.item.payload else { continue; }; if expected_id.is_none() || expected_id == Some(id.as_str()) { @@ -592,7 +592,7 @@ async fn wait_for_command_execution_started_by_source( ) -> Result { loop { let started = wait_for_command_execution_started(mcp, /*expected_id*/ None).await?; - let ThreadItem::CommandExecution { source, .. } = &started.item else { + let ThreadItemPayload::CommandExecution { source, .. } = &started.item.payload else { continue; }; if source == &expected_source { @@ -607,7 +607,7 @@ async fn wait_for_command_execution_completed( ) -> Result { loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - let ThreadItem::CommandExecution { id, .. } = &completed.item else { + let ThreadItemPayload::CommandExecution { id, .. } = &completed.item.payload else { continue; }; if expected_id.is_none() || expected_id == Some(id.as_str()) { diff --git a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs index 46a2a0b1bd..732f5460c8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs @@ -4,7 +4,7 @@ use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::SortDirection; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListParams; use codex_app_server_protocol::ThreadItemsListResponse; use codex_app_server_protocol::ThreadRealtimeItemContent; @@ -164,7 +164,9 @@ async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() -> &page.data[0], ThreadTimelineEntry::Item { turn_id, item, .. } if turn_id == "turn-1" - && matches!(item.as_ref(), ThreadItem::UserMessage { .. }) + && matches!(&item.payload, ThreadItemPayload::UserMessage { .. }) + && item.started_at_ms == Some(0) + && item.completed_at_ms == Some(1) )); assert!(matches!( &page.data[1], diff --git a/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs index 67e5d1bdda..16273cf31e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs @@ -12,7 +12,7 @@ use codex_app_server_protocol::DynamicToolSpec; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ThreadClosedNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -611,7 +611,8 @@ async fn wait_for_dynamic_tool_started( continue; }; let started: ItemStartedNotification = serde_json::from_value(params)?; - if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + if matches!(&started.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id) + { return Ok(started); } } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 822643bc7e..0f8b25c9e8 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -52,7 +52,7 @@ use codex_app_server_protocol::ThreadDeleteResponse; use codex_app_server_protocol::ThreadDeletedNotification; use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadInjectItemsParams; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; use codex_app_server_protocol::ThreadMetadataUpdateParams; @@ -607,7 +607,7 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> { assert_eq!(completed.turn.items_view, TurnItemsView::Summary); assert!(matches!( &completed.turn.items[..], - [ThreadItem::AgentMessage { text, .. }] if text == "Done" + [codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::AgentMessage { text, .. }, .. }] if text == "Done" )); let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_event").await?; @@ -972,15 +972,15 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { loop { let item_started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::UserMessage { .. } = item_started.item { - return Ok::(item_started.item); + if let ThreadItemPayload::UserMessage { .. } = item_started.item.payload { + return Ok::(item_started.item.payload); } } }) .await??; match user_message_item { - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { client_id, content, .. } => { assert_eq!(client_id, Some("client-message-1".to_string())); @@ -2249,12 +2249,12 @@ allow_upstream_proxy = false loop { let completed: ItemCompletedNotification = app_server.read_notification("item/completed").await?; - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { id, status, exit_code, .. - } = completed.item + } = completed.item.payload && id == call_id { assert_eq!( @@ -3030,6 +3030,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { params.command.as_deref(), Some(expected_approval_command.as_str()) ); + let approval_started_at_ms = params.started_at_ms; let resolved_request_id = request_id.clone(); // Approve and wait for task completion @@ -3051,14 +3052,15 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { "item/completed" => { let completed: ItemCompletedNotification = serde_json::from_value(notification.params.expect("item/completed params"))?; - match completed.item { - ThreadItem::CommandExecution { + match completed.item.payload { + ThreadItemPayload::CommandExecution { id, command, exit_code, aggregated_output, .. } if id == "call1" => { + assert_eq!(completed.item.started_at_ms, Some(approval_started_at_ms)); assert_eq!(command, expected_display_command); assert_eq!(exit_code, Some(0)); assert!(aggregated_output.is_some_and(|output| output.contains("True"))); @@ -3236,13 +3238,13 @@ async fn run_turn_start_exec_approval_rejection_v2( let started_command_execution = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::CommandExecution { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::CommandExecution { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, status, command, @@ -3283,13 +3285,13 @@ async fn run_turn_start_exec_approval_rejection_v2( loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if let ThreadItem::CommandExecution { .. } = completed.item { - return Ok::(completed.item); + if let ThreadItemPayload::CommandExecution { .. } = completed.item.payload { + return Ok::(completed.item.payload); } } }) .await??; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, status, command, @@ -3494,13 +3496,16 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns( loop { let item_started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if matches!(item_started.item, ThreadItem::CommandExecution { .. }) { - return Ok::(item_started.item); + if matches!( + item_started.item.payload, + ThreadItemPayload::CommandExecution { .. } + ) { + return Ok::(item_started.item.payload); } } }) .await??; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { cwd, command, status, @@ -3868,13 +3873,13 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { let started_file_change = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::FileChange { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::FileChange { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::FileChange { + let ThreadItemPayload::FileChange { ref id, status, ref changes, @@ -3917,7 +3922,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { ) .await?; let mut saw_resolved = false; - let mut completed_file_change: Option = None; + let mut completed_file_change: Option = None; while completed_file_change.is_none() { let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??; let JSONRPCMessage::Notification(notification) = message else { @@ -3939,9 +3944,9 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { let completed: ItemCompletedNotification = serde_json::from_value( notification.params.clone().expect("item/completed params"), )?; - if let ThreadItem::FileChange { .. } = completed.item { + if let ThreadItemPayload::FileChange { .. } = completed.item.payload { assert!(saw_resolved, "serverRequest/resolved should arrive first"); - completed_file_change = Some(completed.item); + completed_file_change = Some(completed.item.payload); } } _ => {} @@ -3949,7 +3954,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { } let completed_file_change = completed_file_change.expect("file change completion should be observed"); - let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else { + let ThreadItemPayload::FileChange { ref id, status, .. } = completed_file_change else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); @@ -4333,17 +4338,17 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<() let spawn_started = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::CollabAgentToolCall { id, .. } = &started.item + if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &started.item.payload && id == SPAWN_CALL_ID { - return Ok::(started.item); + return Ok::(started.item.payload); } } }) .await??; assert_eq!( spawn_started, - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id: SPAWN_CALL_ID.to_string(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::InProgress, @@ -4360,15 +4365,15 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<() loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if let ThreadItem::CollabAgentToolCall { id, .. } = &completed.item + if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &completed.item.payload && id == SPAWN_CALL_ID { - return Ok::(completed.item); + return Ok::(completed.item.payload); } } }) .await??; - let ThreadItem::CollabAgentToolCall { + let ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -4577,15 +4582,15 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected( let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; assert!(!matches!( - &completed.item, - ThreadItem::CollabAgentToolCall { .. } + &completed.item.payload, + ThreadItemPayload::CollabAgentToolCall { .. } )); - if let ThreadItem::SubAgentActivity { + if let ThreadItemPayload::SubAgentActivity { id, kind: SubAgentActivityKind::Started, agent_thread_id, .. - } = completed.item + } = completed.item.payload && id == SPAWN_CALL_ID { return Ok::(agent_thread_id); @@ -4993,15 +4998,15 @@ config_file = "./custom-role.toml" loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if let ThreadItem::CollabAgentToolCall { id, .. } = &completed.item + if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &completed.item.payload && id == SPAWN_CALL_ID { - return Ok::(completed.item); + return Ok::(completed.item.payload); } } }) .await??; - let ThreadItem::CollabAgentToolCall { + let ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -5128,13 +5133,13 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res let started_file_change_1 = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::FileChange { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::FileChange { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::FileChange { id, status, .. } = started_file_change_1 else { + let ThreadItemPayload::FileChange { id, status, .. } = started_file_change_1 else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call-1"); @@ -5181,7 +5186,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res "item/completed" => { let completed: ItemCompletedNotification = serde_json::from_value(notification.params.expect("item/completed params"))?; - if matches!(completed.item, ThreadItem::FileChange { ref id, .. } if id == "patch-call-1") + if matches!(completed.item.payload, ThreadItemPayload::FileChange { ref id, .. } if id == "patch-call-1") { assert_eq!(completed.thread_id, thread.id); assert_eq!(completed.turn_id, turn_1.id); @@ -5220,13 +5225,13 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res let started_file_change_2 = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::FileChange { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::FileChange { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::FileChange { id, status, .. } = started_file_change_2 else { + let ThreadItemPayload::FileChange { id, status, .. } = started_file_change_2 else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call-2"); @@ -5244,7 +5249,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res let completed: ItemCompletedNotification = serde_json::from_value( notification.params.expect("item/completed params"), )?; - if matches!(completed.item, ThreadItem::FileChange { ref id, .. } if id == "patch-call-2") + if matches!(completed.item.payload, ThreadItemPayload::FileChange { ref id, .. } if id == "patch-call-2") { return Ok::(completed); } @@ -5356,13 +5361,13 @@ async fn run_turn_start_file_change_approval_rejection_v2( let started_file_change = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::FileChange { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::FileChange { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::FileChange { + let ThreadItemPayload::FileChange { ref id, status, ref changes, @@ -5402,13 +5407,13 @@ async fn run_turn_start_file_change_approval_rejection_v2( loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if let ThreadItem::FileChange { .. } = completed.item { - return Ok::(completed.item); + if let ThreadItemPayload::FileChange { .. } = completed.item.payload { + return Ok::(completed.item.payload); } } }) .await??; - let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else { + let ThreadItemPayload::FileChange { ref id, status, .. } = completed_file_change else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); @@ -5506,13 +5511,13 @@ async fn command_execution_notifications_include_process_id( let started_command = timeout(DEFAULT_READ_TIMEOUT, async { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if let ThreadItem::CommandExecution { .. } = started.item { - return Ok::(started.item); + if let ThreadItemPayload::CommandExecution { .. } = started.item.payload { + return Ok::(started.item.payload); } } }) .await??; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, process_id: started_process_id, status, @@ -5529,13 +5534,13 @@ async fn command_execution_notifications_include_process_id( loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if let ThreadItem::CommandExecution { .. } = completed.item { - return Ok::(completed.item); + if let ThreadItemPayload::CommandExecution { .. } = completed.item.payload { + return Ok::(completed.item.payload); } } }) .await??; - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id: completed_id, process_id: completed_process_id, status: completed_status, @@ -5691,8 +5696,8 @@ async fn command_execution_notifications_include_trusted_plugin_id( let notification = mcp.read_stream_until_notification_message(method).await?; let params = notification.params.expect("item notification params"); let item_json = params.get("item").expect("item notification item").clone(); - let item = serde_json::from_value::(item_json.clone())?; - if let ThreadItem::CommandExecution { status, .. } = item { + let item = serde_json::from_value::(item_json.clone())?; + if let ThreadItemPayload::CommandExecution { status, .. } = item { let emitted_script_path = item_json .get("scriptPath") .and_then(serde_json::Value::as_str) diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index 374b537c3c..9d4966a44a 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -16,7 +16,7 @@ use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -318,9 +318,9 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let params = notification.params.expect("item/started params"); let item_started: ItemStartedNotification = serde_json::from_value(params).expect("deserialize item/started notification"); - let ThreadItem::UserMessage { + let ThreadItemPayload::UserMessage { client_id, content, .. - } = item_started.item + } = item_started.item.payload else { continue; }; diff --git a/codex-rs/app-server/tests/suite/v2/view_image.rs b/codex-rs/app-server/tests/suite/v2/view_image.rs index 8ea35efabd..7844903415 100644 --- a/codex-rs/app-server/tests/suite/v2/view_image.rs +++ b/codex-rs/app-server/tests/suite/v2/view_image.rs @@ -8,7 +8,7 @@ use app_test_support::create_final_assistant_message_sse_response; use app_test_support::write_models_cache; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ItemCompletedNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; @@ -143,7 +143,8 @@ async fn fresh_context_subagent_inherits_disabled_view_image_and_mcp_tools() -> mcp.read_notification("item/completed"), ) .await??; - if matches!(&completed.item, ThreadItem::McpToolCall { id, .. } if id == MCP_CALL_ID) { + if matches!(&completed.item.payload, ThreadItemPayload::McpToolCall { id, .. } if id == MCP_CALL_ID) + { assert_ne!(completed.thread_id, thread.id); break completed.thread_id; } diff --git a/codex-rs/app-server/tests/suite/v2/web_search.rs b/codex-rs/app-server/tests/suite/v2/web_search.rs index e282c4d781..45254ac3f4 100644 --- a/codex-rs/app-server/tests/suite/v2/web_search.rs +++ b/codex-rs/app-server/tests/suite/v2/web_search.rs @@ -9,11 +9,12 @@ use app_test_support::TestAppServer; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; @@ -304,15 +305,15 @@ async fn assert_standalone_web_search_round_trips_output( }) ); assert_eq!( - started.item, - ThreadItem::WebSearch(WebSearchItem { + started.item.payload, + ThreadItemPayload::WebSearch(WebSearchItem { id: call_id.to_string(), query: String::new(), action: None, results: None, }) ); - let expected_completed_item = ThreadItem::WebSearch(WebSearchItem { + let expected_completed_item = ThreadItemPayload::WebSearch(WebSearchItem { id: call_id.to_string(), query: "standalone web search".to_string(), action: Some(WebSearchAction::Search { @@ -328,7 +329,7 @@ async fn assert_standalone_web_search_round_trips_output( "future_field": {"preserved": true}, })]), }); - assert_eq!(completed.item, expected_completed_item); + assert_eq!(completed.item.payload, expected_completed_item); drop(mcp); let mut reloaded_mcp = TestAppServer::builder() @@ -344,11 +345,11 @@ async fn assert_standalone_web_search_round_trips_output( .await?; let ThreadReadResponse { thread, .. } = timeout(DEFAULT_READ_TIMEOUT, reloaded_mcp.read_response(read_req)).await??; - let persisted_web_searches: Vec<&ThreadItem> = thread + let persisted_web_searches: Vec<&ThreadItemPayload> = thread .turns .iter() - .flat_map(|turn| &turn.items) - .filter(|item| matches!(item, ThreadItem::WebSearch(_))) + .flat_map(Turn::item_payloads) + .filter(|item| matches!(item, ThreadItemPayload::WebSearch(_))) .collect(); assert_eq!(persisted_web_searches, vec![&expected_completed_item]); @@ -358,7 +359,7 @@ async fn assert_standalone_web_search_round_trips_output( async fn wait_for_web_search_started(mcp: &mut TestAppServer) -> Result { loop { let started: ItemStartedNotification = mcp.read_notification("item/started").await?; - if matches!(&started.item, ThreadItem::WebSearch(_)) { + if matches!(&started.item.payload, ThreadItemPayload::WebSearch(_)) { return Ok(started); } } @@ -369,7 +370,7 @@ async fn wait_for_web_search_completed( ) -> Result { loop { let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?; - if matches!(&completed.item, ThreadItem::WebSearch(_)) { + if matches!(&completed.item.payload, ThreadItemPayload::WebSearch(_)) { return Ok(completed); } } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 073e7a19ee..8e5f7cf0fa 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2500,6 +2500,14 @@ impl Session { /// Persist the event to rollout and send it to clients. pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) { + if let Some((item_id, started_at_ms)) = msg.command_approval_start() { + // The client can see the command before execution. Keep its first start + // for the canonical lifecycle events that will be saved in history. + turn_context + .turn_timing_state + .record_item_started(item_id.to_owned(), started_at_ms) + .await; + } let legacy_source = msg.clone(); if let EventMsg::Error(error) = &legacy_source && error diff --git a/codex-rs/core/tests/suite/guardian_retry.rs b/codex-rs/core/tests/suite/guardian_retry.rs index e6eeac038b..30fdbc6e90 100644 --- a/codex-rs/core/tests/suite/guardian_retry.rs +++ b/codex-rs/core/tests/suite/guardian_retry.rs @@ -308,9 +308,13 @@ async fn guardian_retry_executes_only_after_a_completed_approval() -> Result<()> .await?; let mut reviews = Vec::new(); let mut warnings = Vec::new(); + let mut command_started_at_ms = None; loop { match test.codex.next_event().await?.msg { - EventMsg::GuardianAssessment(review) => reviews.push(review.status), + EventMsg::GuardianAssessment(review) => reviews.push(review), + EventMsg::ItemCompleted(event) if event.item.id() == "write-marker" => { + command_started_at_ms = event.started_at_ms; + } EventMsg::GuardianWarning(warning) => warnings.push(warning.message), EventMsg::TurnComplete(_) => break, _ => {} @@ -338,12 +342,16 @@ async fn guardian_retry_executes_only_after_a_completed_approval() -> Result<()> assert_eq!(request.inputs_of_type("agent_message"), expected_messages); } assert_eq!( - reviews, + reviews + .iter() + .map(|review| review.status) + .collect::>(), vec![ GuardianAssessmentStatus::InProgress, GuardianAssessmentStatus::Approved, ] ); + assert_eq!(command_started_at_ms, Some(reviews[0].started_at_ms)); assert_eq!( warnings.len(), 1, diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index c4b773aae5..3d0dc16401 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -3937,7 +3937,7 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime(ephemeral: bool) -> ThreadTimelineEntry::Realtime { item, .. } if matches!(&item.content, ThreadRealtimeItemContent::TranscriptSegment { text, .. } if text == "spoken before typed input") => Some("speech"), ThreadTimelineEntry::Item { item, .. } - if matches!(item.as_ref(), codex_app_server_protocol::ThreadItem::UserMessage { .. }) => Some("typed input"), + if matches!(&item.payload, codex_app_server_protocol::ThreadItemPayload::UserMessage { .. }) => Some("typed input"), _ => None, }).collect::>(), vec!["speech", "typed input"] diff --git a/codex-rs/core/tests/suite/unified_exec_launch_failure_tests.rs b/codex-rs/core/tests/suite/unified_exec_launch_failure_tests.rs index 501c35f8db..120cdcbefb 100644 --- a/codex-rs/core/tests/suite/unified_exec_launch_failure_tests.rs +++ b/codex-rs/core/tests/suite/unified_exec_launch_failure_tests.rs @@ -6,7 +6,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; use codex_app_server_protocol::ThreadHistoryBuilder; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnStatus; use codex_config::Constrained; use codex_config::test_support::CloudConfigBundleFixture; @@ -203,14 +203,15 @@ async fn interrupted_one_shot_launch_failure_completes_and_persists() -> Result< .context("interrupted turn in persisted history")?; assert_eq!(turn.status, TurnStatus::Interrupted); let commands = turn - .items - .into_iter() + .item_payloads() .filter(|item| item.id() == CALL_ID) .collect::>(); // History reconstructs persisted fields; model_context is only present on live items. assert_eq!( commands, - vec![ThreadItem::from(persisted_completions[0].item.clone())] + vec![&ThreadItemPayload::from( + persisted_completions[0].item.clone() + )] ); assert!( matches!(aborted, EventMsg::TurnAborted(event) if event.turn_id.as_deref() == Some(completed[0].turn_id.as_str())) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 37f1a04795..0a6ceeabce 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -5,7 +5,7 @@ use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::TurnStatus; use codex_core::config::Config; @@ -67,16 +67,16 @@ impl EventProcessorWithHumanOutput { } } - fn render_item_started(&self, item: &ThreadItem) { + fn render_item_started(&self, item: &ThreadItemPayload) { match item { - ThreadItem::CommandExecution { command, cwd, .. } => { + ThreadItemPayload::CommandExecution { command, cwd, .. } => { eprintln!( "{}\n{} in {cwd}", "exec".style(self.italic).style(self.magenta), command.style(self.bold), ); } - ThreadItem::McpToolCall { server, tool, .. } => { + ThreadItemPayload::McpToolCall { server, tool, .. } => { eprintln!( "{} {} {}", "mcp:".style(self.bold), @@ -84,22 +84,22 @@ impl EventProcessorWithHumanOutput { "started".style(self.dimmed) ); } - ThreadItem::WebSearch(item) => { + ThreadItemPayload::WebSearch(item) => { eprintln!("{} {}", "web search:".style(self.bold), item.query); } - ThreadItem::FileChange { .. } => { + ThreadItemPayload::FileChange { .. } => { eprintln!("{}", "apply patch".style(self.bold)); } - ThreadItem::CollabAgentToolCall { tool, .. } => { + ThreadItemPayload::CollabAgentToolCall { tool, .. } => { eprintln!("{} {:?}", "collab:".style(self.bold), tool); } _ => {} } } - fn render_item_completed(&mut self, item: ThreadItem) { + fn render_item_completed(&mut self, item: ThreadItemPayload) { match item { - ThreadItem::AgentMessage { text, .. } => { + ThreadItemPayload::AgentMessage { text, .. } => { eprintln!( "{}\n{}", "codex".style(self.italic).style(self.magenta), @@ -108,7 +108,7 @@ impl EventProcessorWithHumanOutput { self.final_message = Some(text); self.final_message_rendered = true; } - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { summary, content, .. } => { if self.show_agent_reasoning @@ -119,7 +119,7 @@ impl EventProcessorWithHumanOutput { eprintln!("{}", text.style(self.dimmed)); } } - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { command: _, aggregated_output, exit_code, @@ -163,7 +163,7 @@ impl EventProcessorWithHumanOutput { eprintln!("{output}"); } } - ThreadItem::FileChange { + ThreadItemPayload::FileChange { changes, status, .. } => { let status_text = match status { @@ -177,7 +177,7 @@ impl EventProcessorWithHumanOutput { eprintln!("{}", change.path.style(self.dimmed)); } } - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { server, tool, status, @@ -199,10 +199,10 @@ impl EventProcessorWithHumanOutput { eprintln!("{}", error.message.style(self.red)); } } - ThreadItem::WebSearch(item) => { + ThreadItemPayload::WebSearch(item) => { eprintln!("{} {}", "web search:".style(self.bold), item.query); } - ThreadItem::ContextCompaction { .. } => { + ThreadItemPayload::ContextCompaction { .. } => { eprintln!("{}", "context compacted".style(self.dimmed)); } _ => {} @@ -287,11 +287,11 @@ impl EventProcessor for EventProcessorWithHumanOutput { CodexStatus::Running } ServerNotification::ItemStarted(notification) => { - self.render_item_started(¬ification.item); + self.render_item_started(¬ification.item.payload); CodexStatus::Running } ServerNotification::ItemCompleted(notification) => { - self.render_item_completed(notification.item); + self.render_item_completed(notification.item.payload); CodexStatus::Running } ServerNotification::ModelRerouted(notification) => { @@ -499,17 +499,19 @@ fn reasoning_text( } } -fn final_message_from_turn_items(items: &[ThreadItem]) -> Option { +fn final_message_from_turn_items( + items: &[impl std::borrow::Borrow], +) -> Option { items .iter() .rev() - .find_map(|item| match item { - ThreadItem::AgentMessage { text, .. } => Some(text.clone()), + .find_map(|item| match item.borrow() { + ThreadItemPayload::AgentMessage { text, .. } => Some(text.clone()), _ => None, }) .or_else(|| { - items.iter().rev().find_map(|item| match item { - ThreadItem::Plan { text, .. } => Some(text.clone()), + items.iter().rev().find_map(|item| match item.borrow() { + ThreadItemPayload::Plan { text, .. } => Some(text.clone()), _ => None, }) }) diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index c16e505ce0..179e99bddb 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -1,5 +1,6 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; use codex_core::config::ConfigBuilder; @@ -352,7 +353,7 @@ async fn config_summary_entries_include_runtime_workspace_roots() { #[test] fn final_message_from_turn_items_uses_latest_agent_message() { let message = final_message_from_turn_items(&[ - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "first".to_string(), phase: None, @@ -360,11 +361,11 @@ fn final_message_from_turn_items_uses_latest_agent_message() { delivery: None, questions: None, }, - ThreadItem::Plan { + ThreadItemPayload::Plan { id: "plan-1".to_string(), text: "plan".to_string(), }, - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id: "msg-2".to_string(), text: "second".to_string(), phase: None, @@ -380,16 +381,16 @@ fn final_message_from_turn_items_uses_latest_agent_message() { #[test] fn final_message_from_turn_items_falls_back_to_latest_plan() { let message = final_message_from_turn_items(&[ - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec!["inspect".to_string()], content: Vec::new(), }, - ThreadItem::Plan { + ThreadItemPayload::Plan { id: "plan-1".to_string(), text: "first plan".to_string(), }, - ThreadItem::Plan { + ThreadItemPayload::Plan { id: "plan-2".to_string(), text: "final plan".to_string(), }, @@ -426,14 +427,14 @@ fn turn_completed_recovers_final_message_from_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::AgentMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }], + })], status: TurnStatus::Completed, error: None, started_at: None, @@ -478,14 +479,14 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::AgentMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }], + })], status: TurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 73e0c9d911..a92f762096 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -10,7 +10,7 @@ use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction; @@ -141,15 +141,15 @@ impl EventProcessorWithJsonOutput { } fn map_item_with_id( - item: ThreadItem, + item: ThreadItemPayload, make_id: impl FnOnce() -> String, ) -> Option { match item { - ThreadItem::AgentMessage { text, .. } => Some(ExecThreadItem { + ThreadItemPayload::AgentMessage { text, .. } => Some(ExecThreadItem { id: make_id(), details: ThreadItemDetails::AgentMessage(AgentMessageItem { text }), }), - ThreadItem::Reasoning { summary, .. } => { + ThreadItemPayload::Reasoning { summary, .. } => { let text = summary.join("\n"); if text.trim().is_empty() { return None; @@ -159,7 +159,7 @@ impl EventProcessorWithJsonOutput { details: ThreadItemDetails::Reasoning(ReasoningItem { text }), }) } - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { command, aggregated_output, exit_code, @@ -179,7 +179,7 @@ impl EventProcessorWithJsonOutput { }, }), }), - ThreadItem::FileChange { + ThreadItemPayload::FileChange { changes, status, .. } => Some(ExecThreadItem { id: make_id(), @@ -204,7 +204,7 @@ impl EventProcessorWithJsonOutput { }, }), }), - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { server, tool, status, @@ -233,7 +233,7 @@ impl EventProcessorWithJsonOutput { }), }), }), - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { tool, sender_thread_id, receiver_thread_ids, @@ -300,7 +300,7 @@ impl EventProcessorWithJsonOutput { }, }), }), - ThreadItem::WebSearch(item) => Some(ExecThreadItem { + ThreadItemPayload::WebSearch(item) => Some(ExecThreadItem { id: make_id(), details: ThreadItemDetails::WebSearch(WebSearchItem { id: item.id, @@ -340,9 +340,9 @@ impl EventProcessorWithJsonOutput { .unwrap_or_else(|| self.next_item_id()) } - fn map_started_item(&mut self, item: ThreadItem) -> Option { + fn map_started_item(&mut self, item: ThreadItemPayload) -> Option { match item { - ThreadItem::AgentMessage { .. } | ThreadItem::Reasoning { .. } => None, + ThreadItemPayload::AgentMessage { .. } | ThreadItemPayload::Reasoning { .. } => None, other => { let raw_id = other.id().to_string(); Self::map_item_with_id(other, || self.started_item_id(&raw_id)) @@ -350,14 +350,14 @@ impl EventProcessorWithJsonOutput { } } - fn map_completed_item_mut(&mut self, item: ThreadItem) -> Option { - if let ThreadItem::Reasoning { summary, .. } = &item + fn map_completed_item_mut(&mut self, item: ThreadItemPayload) -> Option { + if let ThreadItemPayload::Reasoning { summary, .. } = &item && summary.join("\n").trim().is_empty() { return None; } match &item { - ThreadItem::AgentMessage { .. } | ThreadItem::Reasoning { .. } => { + ThreadItemPayload::AgentMessage { .. } | ThreadItemPayload::Reasoning { .. } => { Self::map_item_with_id(item, || self.next_item_id()) } other => { @@ -369,7 +369,7 @@ impl EventProcessorWithJsonOutput { fn reconcile_unfinished_started_items( &mut self, - turn_items: &[ThreadItem], + turn_items: &[codex_app_server_protocol::ThreadItem], ) -> Vec { turn_items .iter() @@ -378,23 +378,25 @@ impl EventProcessorWithJsonOutput { if !self.raw_to_exec_item_id.contains_key(&raw_id) { return None; } - self.map_completed_item_mut(item.clone()) + self.map_completed_item_mut(item.payload.clone()) .map(|item| ThreadEvent::ItemCompleted(ItemCompletedEvent { item })) }) .collect() } - fn final_message_from_turn_items(items: &[ThreadItem]) -> Option { + fn final_message_from_turn_items( + items: &[impl std::borrow::Borrow], + ) -> Option { items .iter() .rev() - .find_map(|item| match item { - ThreadItem::AgentMessage { text, .. } => Some(text.clone()), + .find_map(|item| match item.borrow() { + ThreadItemPayload::AgentMessage { text, .. } => Some(text.clone()), _ => None, }) .or_else(|| { - items.iter().rev().find_map(|item| match item { - ThreadItem::Plan { text, .. } => Some(text.clone()), + items.iter().rev().find_map(|item| match item.borrow() { + ThreadItemPayload::Plan { text, .. } => Some(text.clone()), _ => None, }) }) @@ -475,13 +477,13 @@ impl EventProcessorWithJsonOutput { CodexStatus::Running } ServerNotification::ItemStarted(notification) => { - if let Some(item) = self.map_started_item(notification.item) { + if let Some(item) = self.map_started_item(notification.item.payload) { events.push(ThreadEvent::ItemStarted(ItemStartedEvent { item })); } CodexStatus::Running } ServerNotification::ItemCompleted(notification) => { - if let Some(item) = self.map_completed_item_mut(notification.item) { + if let Some(item) = self.map_completed_item_mut(notification.item.payload) { if let ThreadItemDetails::AgentMessage(AgentMessageItem { text }) = &item.details { 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 9082f912ce..7205c8ec47 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 @@ -12,8 +12,10 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { let mut processor = EventProcessorWithJsonOutput::new(Some(output_path.clone())); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - item: ThreadItem::AgentMessage { + codex_app_server_protocol::ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "partial answer".to_string(), phase: None, @@ -21,10 +23,9 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!(collected.status, CodexStatus::Running); @@ -95,8 +96,10 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - item: ThreadItem::McpToolCall { + codex_app_server_protocol::ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "search service".to_string(), tool: "web_run".to_string(), @@ -115,10 +118,9 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() { error: None, duration_ms: Some(42), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!(collected.status, CodexStatus::Running); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a6e9abe9bd..a8761fdfaf 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -39,7 +39,6 @@ use codex_app_server_protocol::Thread as AppServerThread; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem as AppServerThreadItem; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -1793,7 +1792,7 @@ fn should_backfill_turn_completed_items( fn turn_items_for_thread( thread: &AppServerThread, turn_id: &str, -) -> Option> { +) -> Option> { thread .turns .iter() diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index d2e20e5f39..6079fb5a7d 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -1,5 +1,7 @@ use super::*; use codex_app_server_protocol::AuthRecoveryNotification; +use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload as AppServerThreadItemPayload; use codex_otel::set_parent_from_w3c_trace_context; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::ActivePermissionProfile; @@ -387,14 +389,16 @@ fn turn_items_for_thread_returns_matching_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![AppServerThreadItem::AgentMessage { - id: "msg-1".to_string(), - text: "hello".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }], + items: vec![ThreadItem::untimed( + AppServerThreadItemPayload::AgentMessage { + id: "msg-1".to_string(), + text: "hello".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + )], status: codex_app_server_protocol::TurnStatus::Completed, error: None, started_at: None, @@ -405,10 +409,10 @@ fn turn_items_for_thread_returns_matching_turn_items() { id: "turn-2".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![AppServerThreadItem::Plan { + items: vec![ThreadItem::untimed(AppServerThreadItemPayload::Plan { id: "plan-1".to_string(), text: "ship it".to_string(), - }], + })], status: codex_app_server_protocol::TurnStatus::Completed, error: None, started_at: None, @@ -420,14 +424,16 @@ fn turn_items_for_thread_returns_matching_turn_items() { assert_eq!( turn_items_for_thread(&thread, "turn-1"), - Some(vec![AppServerThreadItem::AgentMessage { - id: "msg-1".to_string(), - text: "hello".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }]) + Some(vec![ThreadItem::untimed( + AppServerThreadItemPayload::AgentMessage { + id: "msg-1".to_string(), + text: "hello".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + } + )]) ); assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None); } 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 c1cc894749..4771e96c3e 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -16,6 +16,7 @@ use codex_app_server_protocol::PatchApplyStatus as ApiPatchApplyStatus; use codex_app_server_protocol::PatchChangeKind as ApiPatchChangeKind; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::TokenUsageBreakdown; use codex_app_server_protocol::Turn; @@ -168,7 +169,7 @@ fn turn_started_emits_turn_started_event() { #[test] fn command_execution_started_and_completed_translate_to_thread_events() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let command_item = ThreadItem::CommandExecution { + let command_item = ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -185,13 +186,14 @@ fn command_execution_started_and_completed_translate_to_thread_events() { duration_ms: None, }; - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: command_item, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + command_item, + /*started_at_ms*/ 0, + ), + )); assert_eq!( started, CollectedThreadEvents { @@ -211,8 +213,10 @@ fn command_execution_started_and_completed_translate_to_thread_events() { ); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::CommandExecution { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -228,10 +232,9 @@ fn command_execution_started_and_completed_translate_to_thread_events() { exit_code: Some(0), duration_ms: Some(3), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( completed, @@ -257,16 +260,17 @@ fn empty_reasoning_items_are_ignored() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: Vec::new(), content: vec!["raw reasoning".to_string()], }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -283,15 +287,16 @@ fn unsupported_items_do_not_consume_synthetic_ids() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let ignored = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Plan { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Plan { id: "plan-1".to_string(), text: "ignored plan".to_string(), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -303,8 +308,10 @@ fn unsupported_items_do_not_consume_synthetic_ids() { ); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "message-1".to_string(), text: "hello".to_string(), phase: None, @@ -312,10 +319,9 @@ fn unsupported_items_do_not_consume_synthetic_ids() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -339,16 +345,17 @@ fn reasoning_items_emit_summary_not_raw_content() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec!["safe summary".to_string()], content: vec!["raw reasoning".to_string()], }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -372,8 +379,10 @@ fn web_search_completion_preserves_query_and_action() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::WebSearch(ApiWebSearchItem { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::WebSearch(ApiWebSearchItem { id: "search-1".to_string(), query: "rust async await".to_string(), action: Some(ApiWebSearchAction::Search { @@ -382,10 +391,9 @@ fn web_search_completion_preserves_query_and_action() { }), results: None, }), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -436,17 +444,18 @@ fn web_search_page_actions_and_results_survive_json_output() { ] { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::WebSearch(ApiWebSearchItem { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::WebSearch(ApiWebSearchItem { id: "search-1".to_string(), query: url.to_string(), action: Some(action.clone()), results: results.clone(), }), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); let mut expected_item = json!({ @@ -470,22 +479,25 @@ fn web_search_page_actions_and_results_survive_json_output() { fn web_search_start_and_completion_reuse_item_id() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::WebSearch(ApiWebSearchItem { + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::WebSearch(ApiWebSearchItem { id: "search-1".to_string(), query: String::new(), action: None, results: None, }), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::WebSearch(ApiWebSearchItem { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::WebSearch(ApiWebSearchItem { id: "search-1".to_string(), query: "rust async await".to_string(), action: Some(ApiWebSearchAction::Search { @@ -494,10 +506,9 @@ fn web_search_start_and_completion_reuse_item_id() { }), results: None, }), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -543,9 +554,11 @@ fn web_search_start_and_completion_reuse_item_id() { fn mcp_tool_call_begin_and_end_emit_item_events() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::McpToolCall { + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "server_a".to_string(), tool: "tool_x".to_string(), @@ -560,13 +573,14 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { error: None, duration_ms: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::McpToolCall { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "server_a".to_string(), tool: "tool_x".to_string(), @@ -585,10 +599,9 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { error: None, duration_ms: Some(1_000), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -640,8 +653,10 @@ fn mcp_tool_call_failure_sets_failed_status() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::McpToolCall { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-2".to_string(), server: "server_b".to_string(), tool: "tool_y".to_string(), @@ -658,10 +673,9 @@ fn mcp_tool_call_failure_sets_failed_status() { }), duration_ms: Some(5), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -691,9 +705,11 @@ fn mcp_tool_call_failure_sets_failed_status() { fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::McpToolCall { + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-3".to_string(), server: "server_c".to_string(), tool: "tool_z".to_string(), @@ -708,13 +724,14 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { error: None, duration_ms: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::McpToolCall { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::McpToolCall { id: "mcp-3".to_string(), server: "server_c".to_string(), tool: "tool_z".to_string(), @@ -736,10 +753,9 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { error: None, duration_ms: Some(10), }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -793,9 +809,11 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { fn collab_spawn_begin_and_end_emit_item_events() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::CollabAgentToolCall { + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-parent".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: "collab-1".to_string(), tool: CollabAgentTool::SpawnAgent, status: ApiCollabAgentToolCallStatus::InProgress, @@ -806,13 +824,14 @@ fn collab_spawn_begin_and_end_emit_item_events() { reasoning_effort: None, agents_states: std::collections::HashMap::new(), }, - thread_id: "thread-parent".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); let completed = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::CollabAgentToolCall { + ItemCompletedNotification::new( + "thread-parent".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: "collab-1".to_string(), tool: CollabAgentTool::SpawnAgent, status: ApiCollabAgentToolCallStatus::Completed, @@ -829,10 +848,9 @@ 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, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -886,8 +904,10 @@ fn file_change_completion_maps_change_kinds() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::FileChange { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::FileChange { id: "patch-1".to_string(), changes: vec![ ApiFileUpdateChange { @@ -908,10 +928,9 @@ fn file_change_completion_maps_change_kinds() { ], status: ApiPatchApplyStatus::Completed, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -949,8 +968,10 @@ fn file_change_declined_maps_to_failed_status() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::FileChange { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::FileChange { id: "patch-2".to_string(), changes: vec![ApiFileUpdateChange { path: "file.txt".to_string(), @@ -959,10 +980,9 @@ fn file_change_declined_maps_to_failed_status() { }], status: ApiPatchApplyStatus::Declined, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -990,8 +1010,10 @@ fn agent_message_item_updates_final_message() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "hello".to_string(), phase: None, @@ -999,10 +1021,9 @@ fn agent_message_item_updates_final_message() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -1026,9 +1047,11 @@ fn agent_message_item_updates_final_message() { fn agent_message_item_started_is_ignored() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let collected = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::AgentMessage { + let collected = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "hello".to_string(), phase: None, @@ -1036,10 +1059,9 @@ fn agent_message_item_started_is_ignored() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); assert_eq!( collected, @@ -1055,16 +1077,17 @@ fn reasoning_item_completed_uses_synthetic_id() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Reasoning { id: "rs-1".to_string(), summary: vec!["thinking...".to_string()], content: vec!["raw".to_string()], }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!( @@ -1384,14 +1407,14 @@ fn turn_completion_recovers_final_message_from_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::AgentMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }], + })], status: TurnStatus::Completed, error: None, started_at: None, @@ -1417,9 +1440,11 @@ fn turn_completion_recovers_final_message_from_turn_items() { fn turn_completion_reconciles_started_items_from_turn_items() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); - let started = - processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification { - item: ThreadItem::CommandExecution { + let started = processor.collect_thread_events(ServerNotification::ItemStarted( + ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -1435,10 +1460,9 @@ fn turn_completion_reconciles_started_items_from_turn_items() { exit_code: None, duration_ms: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - })); + /*started_at_ms*/ 0, + ), + )); assert_eq!( started, CollectedThreadEvents { @@ -1464,7 +1488,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::CommandExecution { + items: vec![ThreadItem::untimed(ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -1479,7 +1503,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() { aggregated_output: Some("a.txt\n".to_string()), exit_code: Some(0), duration_ms: Some(3), - }], + })], status: TurnStatus::Completed, error: None, started_at: None, @@ -1517,8 +1541,10 @@ fn turn_completion_reconciles_started_items_from_turn_items() { fn turn_completion_overwrites_stale_final_message_from_turn_items() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let _ = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-stale".to_string(), text: "stale answer".to_string(), phase: None, @@ -1526,10 +1552,9 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); let completed = processor.collect_thread_events(ServerNotification::TurnCompleted( @@ -1539,14 +1564,14 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::AgentMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }], + })], status: TurnStatus::Completed, error: None, started_at: None, @@ -1572,8 +1597,10 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let _ = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-streamed".to_string(), text: "streamed answer".to_string(), phase: None, @@ -1581,10 +1608,9 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); let completed = processor.collect_thread_events(ServerNotification::TurnCompleted( @@ -1621,8 +1647,10 @@ fn failed_turn_clears_stale_final_message() { let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None); let collected = processor.collect_thread_events(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "partial answer".to_string(), phase: None, @@ -1630,10 +1658,9 @@ fn failed_turn_clears_stale_final_message() { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); assert_eq!(collected.status, CodexStatus::Running); @@ -1676,10 +1703,10 @@ fn turn_completion_falls_back_to_final_plan_text() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::Plan { + items: vec![ThreadItem::untimed(ThreadItemPayload::Plan { id: "plan-1".to_string(), text: "ship the typed adapter".to_string(), - }], + })], status: TurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/external-agent-migration/src/sessions/export.rs b/codex-rs/external-agent-migration/src/sessions/export.rs index 3900ea1293..d9067e2e4a 100644 --- a/codex-rs/external-agent-migration/src/sessions/export.rs +++ b/codex-rs/external-agent-migration/src/sessions/export.rs @@ -227,7 +227,7 @@ fn turn_complete_item( #[cfg(test)] mod tests { use super::*; - use codex_app_server_protocol::ThreadItem; + use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::build_turns_from_rollout_items; use serde_json::Value as JsonValue; use std::path::Path; @@ -258,15 +258,15 @@ mod tests { assert_eq!(turns[0].items.len(), 2); assert_eq!(turns[1].items.len(), 2); assert_eq!( - turns[1].items[1], - ThreadItem::AgentMessage { + turns[1].item_payloads().next_back(), + Some(&ThreadItemPayload::AgentMessage { id: "item-4".into(), text: EXTERNAL_SESSION_IMPORTED_MARKER.into(), phase: None, memory_citation: None, delivery: None, questions: None, - } + }) ); } @@ -292,8 +292,8 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( - turns[0].items.last(), - Some(&ThreadItem::AgentMessage { + turns[0].item_payloads().next_back(), + Some(&ThreadItemPayload::AgentMessage { id: "item-3".into(), text: EXTERNAL_SESSION_IMPORTED_MARKER.into(), phase: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 727422c795..586dd6f0fe 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1695,6 +1695,34 @@ pub enum EventMsg { SubAgentActivity(SubAgentActivityEvent), } +impl EventMsg { + /// Returns the ID and start time of the command this approval opens. + /// Stdin and network reviews have their own prompts for an existing operation. + pub fn command_approval_start(&self) -> Option<(&str, i64)> { + match self { + Self::ExecApprovalRequest(request) + if request.approval_id.is_none() && request.network_approval_context.is_none() => + { + Some((&request.call_id, request.started_at_ms)) + } + Self::GuardianAssessment(assessment) + if assessment.status == GuardianAssessmentStatus::InProgress + && matches!( + assessment.action, + GuardianAssessmentAction::Command { .. } + | GuardianAssessmentAction::Execve { .. } + ) => + { + Some(( + assessment.target_item_id.as_deref()?, + assessment.started_at_ms, + )) + } + _ => None, + } + } +} + #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, EnumIter)] #[serde(rename_all = "snake_case")] pub enum HookEventName { diff --git a/codex-rs/thread-store/src/local/thread_history.rs b/codex-rs/thread-store/src/local/thread_history.rs index 7be33d6720..1685cb3e28 100644 --- a/codex-rs/thread-store/src/local/thread_history.rs +++ b/codex-rs/thread-store/src/local/thread_history.rs @@ -1,5 +1,5 @@ use codex_app_server_protocol::ThreadHistoryChangeSet; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnStatus; use codex_protocol::ThreadId; use codex_protocol::models::MessagePhase; @@ -428,7 +428,8 @@ WHERE thread_id = ? .map_err(thread_history_error)?; } - for item in changes.changed_items { + for change in changes.changed_items { + let item = change.item; let created_at_ms = item.started_at_ms .or(fallback_created_at_ms) @@ -437,8 +438,8 @@ WHERE thread_id = ? "thread history projection for {thread_id} is missing an item creation timestamp" ), })?; - let item_id = item.item.id().to_string(); - let item_json = serde_json::to_string(&item.item).map_err(thread_history_error)?; + let item_id = item.id().to_string(); + let item_json = serde_json::to_string(&item.payload).map_err(thread_history_error)?; // Completed items are immutable: local producers emit ItemCompleted exactly once per // item. Tolerate an unexpected duplicate defensively so it cannot poison materialization, // preserving the original creation ordinal and timestamp while updating its snapshot. @@ -465,7 +466,7 @@ ON CONFLICT(thread_id, turn_id, item_id) DO UPDATE SET "#, ) .bind(thread_id) - .bind(item.turn_id.as_str()) + .bind(change.turn_id.as_str()) .bind(item_id.as_str()) .bind(rollout_ordinal) .bind(rollout_ordinal) @@ -480,8 +481,8 @@ ON CONFLICT(thread_id, turn_id, item_id) DO UPDATE SET // Keep summary item IDs on the turn row so reads do not need to scan every item in the // turn. - match item.item { - ThreadItem::UserMessage { .. } => { + match item.payload { + ThreadItemPayload::UserMessage { .. } => { sqlx::query( r#" UPDATE thread_turns @@ -494,12 +495,12 @@ WHERE thread_id = ? ) .bind(item_id.as_str()) .bind(thread_id) - .bind(item.turn_id.as_str()) + .bind(change.turn_id.as_str()) .execute(&mut **transaction) .await .map_err(thread_history_error)?; } - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { phase: Some(MessagePhase::FinalAnswer), .. } => { @@ -515,32 +516,32 @@ WHERE thread_id = ? ) .bind(item_id.as_str()) .bind(thread_id) - .bind(item.turn_id.as_str()) + .bind(change.turn_id.as_str()) .execute(&mut **transaction) .await .map_err(thread_history_error)?; } - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { phase: Some(MessagePhase::Commentary | MessagePhase::PartialAnswer) | None, .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::CommandExecution { .. } - | ThreadItem::FileChange { .. } - | ThreadItem::McpToolCall { .. } - | ThreadItem::DynamicToolCall { .. } - | ThreadItem::CollabAgentToolCall { .. } - | ThreadItem::SubAgentActivity { .. } - | ThreadItem::WebSearch(_) - | ThreadItem::ImageView { .. } - | ThreadItem::Sleep(_) - | ThreadItem::ImageGeneration(_) - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => {} + | ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::CommandExecution { .. } + | ThreadItemPayload::FileChange { .. } + | ThreadItemPayload::McpToolCall { .. } + | ThreadItemPayload::DynamicToolCall { .. } + | ThreadItemPayload::CollabAgentToolCall { .. } + | ThreadItemPayload::SubAgentActivity { .. } + | ThreadItemPayload::WebSearch(_) + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::Sleep(_) + | ThreadItemPayload::ImageGeneration(_) + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => {} } } Ok(()) diff --git a/codex-rs/thread-store/src/local/thread_history/realtime.rs b/codex-rs/thread-store/src/local/thread_history/realtime.rs index cfb897caf3..af0209fd47 100644 --- a/codex-rs/thread-store/src/local/thread_history/realtime.rs +++ b/codex-rs/thread-store/src/local/thread_history/realtime.rs @@ -97,7 +97,18 @@ pub(in crate::local) async fn list_timeline( .map(|value| sqlite_integer(value, "rollout ordinal")) .transpose()? .unwrap_or(i64::MAX); - let segment_rows = sqlx::query_as::<_, (i64, i64, String, Option, String)>( + let segment_rows = sqlx::query_as::< + _, + ( + i64, + i64, + String, + Option, + String, + Option, + Option, + ), + >( r#" WITH starts AS ( SELECT rollout_ordinal, 0 AS kind, turn_id AS id, turn_id, @@ -109,7 +120,7 @@ WHERE thread_id = ?1 AND rollout_ordinal >= ?2 AND (rollout_ordinal, 0, turn_id) < (?4, ?5, ?6) ORDER BY rollout_ordinal DESC, turn_id DESC LIMIT ?7 ), items AS ( -SELECT rollout_ordinal, 1 AS kind, item_id AS id, turn_id, item_json +SELECT rollout_ordinal, 1 AS kind, item_id AS id, turn_id, item_json, started_at_ms, completed_at_ms FROM thread_items WHERE thread_id = ?1 AND rollout_ordinal >= ?2 AND rollout_ordinal < ?3 AND rollout_ordinal <= ?4 @@ -134,10 +145,10 @@ WHERE thread_id = ?1 AND rollout_end_ordinal >= ?2 AND (rollout_end_ordinal, 3, turn_id) < (?4, ?5, ?6) ORDER BY rollout_end_ordinal DESC, turn_id DESC LIMIT ?7 ) -SELECT * FROM starts +SELECT *, NULL, NULL FROM starts UNION ALL SELECT * FROM items -UNION ALL SELECT * FROM realtime -UNION ALL SELECT * FROM ends +UNION ALL SELECT *, NULL, NULL FROM realtime +UNION ALL SELECT *, NULL, NULL FROM ends ORDER BY rollout_ordinal DESC, kind DESC, id DESC LIMIT ?7 "#, @@ -153,7 +164,9 @@ LIMIT ?7 .await .map_err(thread_history_error)?; - for (position, kind, _id, turn_id, item_json) in segment_rows { + for (position, kind, _id, turn_id, item_json, started_at_ms, completed_at_ms) in + segment_rows + { let position = u64::try_from(position).map_err(thread_history_error)?; rows.push(match kind { 0 | 3 => serde_json::from_str::(&item_json) @@ -161,10 +174,11 @@ LIMIT ?7 1 => ThreadTimelineEntry::Item { position, turn_id: turn_id.ok_or_else(|| thread_history_error("missing turn ID"))?, - item: Box::new( - serde_json::from_str::(&item_json) - .map_err(thread_history_error)?, - ), + item: Box::new(ThreadItem { + payload: serde_json::from_str(&item_json).map_err(thread_history_error)?, + started_at_ms, + completed_at_ms, + }), }, 2 => ThreadTimelineEntry::Realtime { position, diff --git a/codex-rs/thread-store/src/local/thread_history/search.rs b/codex-rs/thread-store/src/local/thread_history/search.rs index 0fe9f4de4f..6aeb6d576a 100644 --- a/codex-rs/thread-store/src/local/thread_history/search.rs +++ b/codex-rs/thread-store/src/local/thread_history/search.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::collections::HashMap; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::UserInput; use codex_protocol::ThreadId; use codex_protocol::protocol::strip_user_message_prefix; @@ -176,12 +176,11 @@ ORDER BY rollout_ordinal ASC let mut matching_occurrences = 0_usize; while let Some(row) = rows.try_next().await.map_err(thread_history_error)? { let row = candidate_row(row)?; - let item = - serde_json::from_str::(row.item_json.as_str()).map_err(|err| { - ThreadStoreError::Internal { - message: format!("failed to deserialize stored thread item: {err}"), - } - })?; + let item = serde_json::from_str::(row.item_json.as_str()).map_err( + |err| ThreadStoreError::Internal { + message: format!("failed to deserialize stored thread item: {err}"), + }, + )?; let Some(text) = searchable_text(&item) else { continue; }; @@ -309,9 +308,9 @@ fn invalid_cursor(cursor: &str) -> ThreadStoreError { } } -fn searchable_text(item: &ThreadItem) -> Option> { +fn searchable_text(item: &ThreadItemPayload) -> Option> { match item { - ThreadItem::UserMessage { content, .. } => { + ThreadItemPayload::UserMessage { content, .. } => { let mut text_parts = content .iter() .filter_map(|input| match input { @@ -335,27 +334,27 @@ fn searchable_text(item: &ThreadItem) -> Option> { } } } - ThreadItem::AgentMessage { text, .. } => { + ThreadItemPayload::AgentMessage { text, .. } => { let text = markdown_to_search_text(text); (!text.is_empty()).then_some(Cow::Owned(text)) } - ThreadItem::HookPrompt { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::CommandExecution { .. } - | ThreadItem::FileChange { .. } - | ThreadItem::McpToolCall { .. } - | ThreadItem::DynamicToolCall { .. } - | ThreadItem::CollabAgentToolCall { .. } - | ThreadItem::SubAgentActivity { .. } - | ThreadItem::WebSearch(_) - | ThreadItem::ImageView { .. } - | ThreadItem::Sleep(_) - | ThreadItem::ImageGeneration(_) - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => None, + ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::CommandExecution { .. } + | ThreadItemPayload::FileChange { .. } + | ThreadItemPayload::McpToolCall { .. } + | ThreadItemPayload::DynamicToolCall { .. } + | ThreadItemPayload::CollabAgentToolCall { .. } + | ThreadItemPayload::SubAgentActivity { .. } + | ThreadItemPayload::WebSearch(_) + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::Sleep(_) + | ThreadItemPayload::ImageGeneration(_) + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => None, } } diff --git a/codex-rs/thread-store/src/local/thread_history_materialization.rs b/codex-rs/thread-store/src/local/thread_history_materialization.rs index e28fff1152..4d408c3bee 100644 --- a/codex-rs/thread-store/src/local/thread_history_materialization.rs +++ b/codex-rs/thread-store/src/local/thread_history_materialization.rs @@ -236,7 +236,7 @@ fn read_projection_batches( let fallback_created_at_ms = if changes .changed_items .iter() - .any(|item| item.started_at_ms.is_none()) + .any(|change| change.item.started_at_ms.is_none()) || (!is_inherited_subagent_history && matches!(&line.item, RolloutItem::RealtimeItem(_))) { diff --git a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs index c9e2fe2859..ce951add86 100644 --- a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs +++ b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs @@ -7,7 +7,7 @@ use std::time::Duration; use chrono::Utc; use codex_app_server_protocol::McpToolCallResult; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_protocol::ThreadId; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; @@ -499,9 +499,9 @@ async fn paginated_command_history_caps_aggregated_output() { .fetch_one(&pool) .await .expect("read projected command"); - let projected: ThreadItem = + let projected: ThreadItemPayload = serde_json::from_str(&item_json).expect("deserialize projected command"); - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { aggregated_output: Some(output), .. } = projected @@ -573,9 +573,9 @@ async fn paginated_mcp_history_caps_large_results() { .fetch_one(&pool) .await .expect("read projected MCP tool call"); - let projected: ThreadItem = + let projected: ThreadItemPayload = serde_json::from_str(&item_json).expect("deserialize projected MCP tool call"); - let ThreadItem::McpToolCall { + let ThreadItemPayload::McpToolCall { arguments: projected_arguments, result: Some(result), .. @@ -1678,8 +1678,8 @@ WHERE thread_id = ? AND turn_id = ? AND item_id = ? .expect("read projected item"); assert_eq!((item.0, item.1, item.2), (2, 3, first_created_at_ms)); assert_eq!( - serde_json::from_str::(item.3.as_str()).expect("parse projected item"), - ThreadItem::UserMessage { + serde_json::from_str::(item.3.as_str()).expect("parse projected item"), + ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: Some("updated".to_string()), content: Vec::new(), diff --git a/codex-rs/thread-store/src/types.rs b/codex-rs/thread-store/src/types.rs index 4a55334b10..79497861ca 100644 --- a/codex-rs/thread-store/src/types.rs +++ b/codex-rs/thread-store/src/types.rs @@ -543,7 +543,7 @@ pub struct StoredThreadItem { pub started_at_ms: Option, /// Exact lifecycle completion time in Unix milliseconds, when recorded by the producer. pub completed_at_ms: Option, - /// Serialized app-server ThreadItem snapshot. + /// Serialized app-server item payload. Its lifecycle times are stored in the columns above. pub item_json: Vec, } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1b3b21e07f..f3326b44bb 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -130,7 +130,7 @@ use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SkillErrorInfo; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadMemoryMode; use codex_app_server_protocol::ThreadSettingsUpdateParams; @@ -322,15 +322,15 @@ enum ThreadInteractiveRequest { /// receiver thread ids. All other notification variants return `None`. fn collab_receiver_thread_ids(notification: &ServerNotification) -> Option<&[String]> { match notification { - ServerNotification::ItemStarted(notification) => match ¬ification.item { - ThreadItem::CollabAgentToolCall { + ServerNotification::ItemStarted(notification) => match ¬ification.item.payload { + ThreadItemPayload::CollabAgentToolCall { receiver_thread_ids, .. } => Some(receiver_thread_ids), _ => None, }, - ServerNotification::ItemCompleted(notification) => match ¬ification.item { - ThreadItem::CollabAgentToolCall { + ServerNotification::ItemCompleted(notification) => match ¬ification.item.payload { + ThreadItemPayload::CollabAgentToolCall { receiver_thread_ids, .. } => Some(receiver_thread_ids), @@ -340,14 +340,14 @@ fn collab_receiver_thread_ids(notification: &ServerNotification) -> Option<&[Str } } -fn sub_agent_activity_item(notification: &ServerNotification) -> Option<&ThreadItem> { +fn sub_agent_activity_item(notification: &ServerNotification) -> Option<&ThreadItemPayload> { match notification { - ServerNotification::ItemStarted(notification) => match ¬ification.item { - ThreadItem::SubAgentActivity { .. } => Some(¬ification.item), + ServerNotification::ItemStarted(notification) => match ¬ification.item.payload { + ThreadItemPayload::SubAgentActivity { .. } => Some(¬ification.item.payload), _ => None, }, - ServerNotification::ItemCompleted(notification) => match ¬ification.item { - ThreadItem::SubAgentActivity { .. } => Some(¬ification.item), + ServerNotification::ItemCompleted(notification) => match ¬ification.item.payload { + ThreadItemPayload::SubAgentActivity { .. } => Some(¬ification.item.payload), _ => None, }, _ => None, @@ -359,8 +359,8 @@ fn collab_receiver_is_not_found( receiver_thread_id: &str, ) -> bool { match notification { - ServerNotification::ItemCompleted(notification) => match ¬ification.item { - ThreadItem::CollabAgentToolCall { agents_states, .. } => { + ServerNotification::ItemCompleted(notification) => match ¬ification.item.payload { + ThreadItemPayload::CollabAgentToolCall { agents_states, .. } => { agents_states.get(receiver_thread_id).is_some_and(|state| { matches!( &state.status, @@ -627,7 +627,7 @@ pub(crate) struct App { windows_sandbox: WindowsSandboxState, thread_event_channels: HashMap, - pending_realtime_speech_replay: HashMap>, + pending_realtime_speech_replay: HashMap>, pending_realtime_transcript_replay: HashMap>, realtime_replay_order: VecDeque, diff --git a/codex-rs/tui/src/app/activity_groups_tests.rs b/codex-rs/tui/src/app/activity_groups_tests.rs index 4e5ae718ce..3a70965e89 100644 --- a/codex-rs/tui/src/app/activity_groups_tests.rs +++ b/codex-rs/tui/src/app/activity_groups_tests.rs @@ -4,6 +4,7 @@ use super::*; use crate::app::test_support::make_test_app; use crate::thread_transcript::RawReasoningVisibility; use crate::thread_transcript::thread_items_to_transcript_cells; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::TurnItemsView; use pretty_assertions::assert_eq; @@ -13,7 +14,7 @@ enum ActivityKind { Exploration, } -fn call(id: &str, kind: ActivityKind, status: &str) -> ThreadItem { +fn call(id: &str, kind: ActivityKind, status: &str) -> ThreadItemPayload { let value = match kind { ActivityKind::Computer => serde_json::json!({ "type": "mcpToolCall", "id": id, "server": "cua_repl", "tool": "js", @@ -31,8 +32,8 @@ fn call(id: &str, kind: ActivityKind, status: &str) -> ThreadItem { serde_json::from_value(value).expect("activity fixture") } -fn reasoning(id: &str) -> ThreadItem { - ThreadItem::Reasoning { +fn reasoning(id: &str) -> ThreadItemPayload { + ThreadItemPayload::Reasoning { id: id.to_owned(), summary: vec![format!( "**Considering {id}**\n\nRetained reasoning for {id}." @@ -41,11 +42,11 @@ fn reasoning(id: &str) -> ThreadItem { } } -fn turn(items: Vec) -> Turn { +fn turn(items: Vec) -> Turn { Turn { id: "turn".to_owned(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, @@ -55,11 +56,14 @@ fn turn(items: Vec) -> Turn { } } -fn project(app: &App, items: &[ThreadItem]) -> Vec> { +fn project( + app: &App, + items: &[impl std::borrow::Borrow], +) -> Vec> { thread_items_to_transcript_cells( /*thread_id*/ None, &app.config.cwd, - items.iter().cloned(), + items.iter().map(|entry| entry.borrow().clone()), RawReasoningVisibility::Hidden, Some(&app.config), ) @@ -127,7 +131,7 @@ async fn older_page_hydration_keeps_pending_computer_and_exploration_completion( .chat_widget .handle_command_execution_completed_now(call("pending", kind, "completed")), } - current.items[3] = call("pending", kind, "completed"); + current.items[3] = ThreadItem::untimed(call("pending", kind, "completed")); let expected = project(&app, ¤t.items); assert_eq!( app.chat_widget @@ -146,7 +150,7 @@ async fn trailing_reasoning_identity_rejects_identical_text_from_another_turn() let mut same_turn = reasoning("same-turn-detail"); let mut next_turn = reasoning("next-turn-detail"); for item in [&mut same_turn, &mut next_turn] { - if let ThreadItem::Reasoning { summary, .. } = item { + if let ThreadItemPayload::Reasoning { summary, .. } = item { *summary = vec!["Exactly the same reasoning text".to_owned()]; } } @@ -191,7 +195,7 @@ async fn every_page_split_folds_reasoning_before_answer_and_completion_boundarie let mut app = make_test_app().await; let mut items = vec![call("call", kind, "completed"), reasoning("detail")]; if let Some(text) = answer { - items.push(ThreadItem::AgentMessage { + items.push(ThreadItemPayload::AgentMessage { id: "answer".to_owned(), text: text.to_owned(), phase: None, @@ -247,7 +251,7 @@ async fn every_page_split_folds_reasoning_before_answer_and_completion_boundarie async fn failed_exploration_retains_following_reasoning_at_every_page_split() { let mut app = make_test_app().await; let mut failed = call("failed", ActivityKind::Exploration, "failed"); - if let ThreadItem::CommandExecution { exit_code, .. } = &mut failed { + if let ThreadItemPayload::CommandExecution { exit_code, .. } = &mut failed { *exit_code = Some(1); } let current = turn(vec![ diff --git a/codex-rs/tui/src/app/agent_status_feed.rs b/codex-rs/tui/src/app/agent_status_feed.rs index bab6e56737..1dee7b6dfc 100644 --- a/codex-rs/tui/src/app/agent_status_feed.rs +++ b/codex-rs/tui/src/app/agent_status_feed.rs @@ -9,7 +9,7 @@ use crate::text_formatting::truncate_text; use codex_app_server_protocol::CollabAgentTool; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::SubAgentActivityKind; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use ratatui::style::Stylize; use ratatui::text::Line; use std::collections::HashSet; @@ -98,7 +98,7 @@ impl AgentStatusThreadPreview { if !seen_item_ids.insert(item.id().to_string()) { continue; } - if let Some(summary) = activity_summary(item) { + if let Some(summary) = activity_summary(&item.payload) { activity.push(summary); if activity.len() == AGENT_STATUS_PREVIEW_ITEMS { break; @@ -135,24 +135,24 @@ impl AgentStatusThreadPreview { } } -fn activity_summary(item: &ThreadItem) -> Option { +fn activity_summary(item: &ThreadItemPayload) -> Option { let summary = match item { - ThreadItem::AgentMessage { text, .. } | ThreadItem::Plan { text, .. } => text, - ThreadItem::Reasoning { summary, .. } => summary.last()?, - ThreadItem::CommandExecution { command, .. } => { + ThreadItemPayload::AgentMessage { text, .. } | ThreadItemPayload::Plan { text, .. } => text, + ThreadItemPayload::Reasoning { summary, .. } => summary.last()?, + ThreadItemPayload::CommandExecution { command, .. } => { let command = truncate_text( command, AGENT_STATUS_PREVIEW_GRAPHEMES.saturating_sub("$ ".len()), ); return bounded_summary(&format!("$ {command}")); } - ThreadItem::FileChange { changes, .. } => { + ThreadItemPayload::FileChange { changes, .. } => { return bounded_summary(&format!("Updated {} file(s)", changes.len())); } - ThreadItem::McpToolCall { server, tool, .. } => { + ThreadItemPayload::McpToolCall { server, tool, .. } => { return bounded_summary(&format!("MCP {server}/{tool}")); } - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { namespace, tool, .. } => { let tool = namespace @@ -161,7 +161,7 @@ fn activity_summary(item: &ThreadItem) -> Option { .unwrap_or_else(|| tool.clone()); return bounded_summary(&format!("Tool {tool}")); } - ThreadItem::CollabAgentToolCall { tool, .. } => { + ThreadItemPayload::CollabAgentToolCall { tool, .. } => { let action = match tool { CollabAgentTool::SendMessage | CollabAgentTool::FollowupTask @@ -175,7 +175,7 @@ fn activity_summary(item: &ThreadItem) -> Option { }; return Some(action.to_string()); } - ThreadItem::SubAgentActivity { + ThreadItemPayload::SubAgentActivity { kind, agent_path, .. } => { let action = match kind { @@ -186,21 +186,27 @@ fn activity_summary(item: &ThreadItem) -> Option { }; return bounded_summary(&format!("{action} {agent_path}")); } - ThreadItem::WebSearch(item) => { + ThreadItemPayload::WebSearch(item) => { return bounded_summary(&format!("Web search: {}", item.query)); } - ThreadItem::ImageView { path, .. } => { + ThreadItemPayload::ImageView { path, .. } => { let path = path.render_for_ui(); return bounded_summary(&format!("Viewed {path}")); } - ThreadItem::ImageGeneration(_) => return Some("Generated an image".to_string()), - ThreadItem::EnteredReviewMode { .. } => return Some("Entered review mode".to_string()), - ThreadItem::ExitedReviewMode { .. } => return Some("Exited review mode".to_string()), - ThreadItem::ContextCompaction { .. } => return Some("Compacted context".to_string()), - ThreadItem::UserMessage { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Sleep(_) => { + ThreadItemPayload::ImageGeneration(_) => return Some("Generated an image".to_string()), + ThreadItemPayload::EnteredReviewMode { .. } => { + return Some("Entered review mode".to_string()); + } + ThreadItemPayload::ExitedReviewMode { .. } => { + return Some("Exited review mode".to_string()); + } + ThreadItemPayload::ContextCompaction { .. } => { + return Some("Compacted context".to_string()); + } + ThreadItemPayload::UserMessage { .. } + | ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Sleep(_) => { return None; } }; diff --git a/codex-rs/tui/src/app/agent_status_feed_tests.rs b/codex-rs/tui/src/app/agent_status_feed_tests.rs index 395e5fb103..6ea67c2c97 100644 --- a/codex-rs/tui/src/app/agent_status_feed_tests.rs +++ b/codex-rs/tui/src/app/agent_status_feed_tests.rs @@ -8,8 +8,10 @@ use codex_utils_absolute_path::AbsolutePathBuf; fn agent_status_uses_bounded_buffered_activity() { let mut store = ThreadEventStore::new(/*capacity*/ 8); store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::CommandExecution { + ItemCompletedNotification::new( + "thread-child".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "command-1".to_string(), @@ -27,14 +29,15 @@ fn agent_status_uses_bounded_buffered_activity() { exit_code: Some(0), duration_ms: Some(42), }, - thread_id: "thread-child".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 1, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1, + ), )); store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ItemCompletedNotification::new( + "thread-child".to_string(), + "turn-1".to_string(), + ThreadItemPayload::AgentMessage { id: "message-1".to_string(), text: "Finished checking the focused TUI tests.".to_string(), phase: None, @@ -42,10 +45,9 @@ fn agent_status_uses_bounded_buffered_activity() { delivery: None, questions: None, }, - thread_id: "thread-child".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 2, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 2, + ), )); let preview = AgentStatusThreadPreview::from_store("/root/reviewer".to_string(), &store); @@ -72,28 +74,30 @@ fn agent_status_uses_bounded_buffered_activity() { fn agent_status_uses_reasoning_summaries_only() { let mut store = ThreadEventStore::new(/*capacity*/ 8); store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + "thread-child".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Reasoning { id: "reasoning-with-summary".to_string(), summary: vec!["safe summary".to_string()], content: vec!["hidden raw reasoning".to_string()], }, - thread_id: "thread-child".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 1, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 1, + ), )); store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + "thread-child".to_string(), + "turn-1".to_string(), + ThreadItemPayload::Reasoning { id: "reasoning-without-summary".to_string(), summary: Vec::new(), content: vec!["raw-only reasoning".to_string()], }, - thread_id: "thread-child".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 2, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 2, + ), )); let preview = AgentStatusThreadPreview::from_store("/root/reviewer".to_string(), &store); diff --git a/codex-rs/tui/src/app/agents_overview_details.rs b/codex-rs/tui/src/app/agents_overview_details.rs index 1a86a59098..0214604643 100644 --- a/codex-rs/tui/src/app/agents_overview_details.rs +++ b/codex-rs/tui/src/app/agents_overview_details.rs @@ -12,7 +12,7 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadActiveFlag; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStatus; use codex_protocol::ThreadId; use codex_utils_absolute_path::AbsolutePathBuf; @@ -104,7 +104,11 @@ impl App { had_header || reasoning.header.is_some() } ServerNotification::ItemCompleted(ItemCompletedNotification { - item: ThreadItem::AgentMessage { text, .. }, + item: + codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::AgentMessage { text, .. }, + .. + }, .. }) => { activity.last_message = Some(preview_agent_message(text)); diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index ad1c7b95f2..57e9b69159 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -929,19 +929,22 @@ async fn agents_overview_details_render_markdown() { app.agents_overview.last_messages.clear(); app.track_agents_overview_activity( thread_id, - &ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: thread.id.clone(), - turn_id: "turn".into(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { - id: "answer".into(), - text: message.into(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, - }), + &ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + thread.id.clone(), + "turn".into(), + ThreadItemPayload::AgentMessage { + id: "answer".into(), + text: message.into(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), ); let view = app.agents_overview_view(vec![thread.clone()], Some(thread_id)); terminal @@ -1080,11 +1083,10 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm .threads .insert(parent_id, Some(parent.clone())); app.track_agents_overview_notification(&ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - thread_id: parent.id.clone(), - turn_id: "previous-turn".into(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { + codex_app_server_protocol::ItemCompletedNotification::new( + parent.id.clone(), + "previous-turn".into(), + ThreadItemPayload::AgentMessage { id: "answer".into(), text: "Previous answer".into(), phase: None, @@ -1092,7 +1094,9 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm delivery: None, questions: None, }, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); let channel = app.thread_event_channels.remove(&thread_id).unwrap(); let details = app.agents_overview_details( diff --git a/codex-rs/tui/src/app/agents_overview_threads.rs b/codex-rs/tui/src/app/agents_overview_threads.rs index ab3e56f242..b4e2ca1836 100644 --- a/codex-rs/tui/src/app/agents_overview_threads.rs +++ b/codex-rs/tui/src/app/agents_overview_threads.rs @@ -18,7 +18,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::SessionSource; use codex_app_server_protocol::Thread; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; use codex_app_server_protocol::ThreadReadParams; @@ -400,16 +400,17 @@ impl App { .await && let Some(turn) = turns.data.first() { - if let Some(ThreadItem::UserMessage { content, .. }) = - turn.items.first() + if let Some(ThreadItemPayload::UserMessage { + content, .. + }) = turn.item_payloads().next() { response.thread.preview = ChatWidget::user_message_display_from_inputs(content) .message; } last_message = - turn.items.iter().rev().find_map(|item| match item { - ThreadItem::AgentMessage { text, .. } => { + turn.item_payloads().rev().find_map(|item| match item { + ThreadItemPayload::AgentMessage { text, .. } => { Some(preview_agent_message(text)) } _ => None, diff --git a/codex-rs/tui/src/app/app_server_events.rs b/codex-rs/tui/src/app/app_server_events.rs index cd8f1678d6..aa1564a54d 100644 --- a/codex-rs/tui/src/app/app_server_events.rs +++ b/codex-rs/tui/src/app/app_server_events.rs @@ -25,7 +25,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SessionSource; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadSource; @@ -469,10 +469,10 @@ impl App { ServerNotificationThreadTarget::Thread(thread_id) => { if self.current_displayed_thread_id() != Some(thread_id) && let ServerNotification::ItemCompleted(item) = ¬ification - && let ThreadItem::UserMessage { + && let ThreadItemPayload::UserMessage { client_id: Some(client_id), .. - } = &item.item + } = &item.item.payload { // Acknowledge by ID before routing can discard the receipt. ID-less receipts // cannot safely distinguish identical pending submissions. diff --git a/codex-rs/tui/src/app/history_completion.rs b/codex-rs/tui/src/app/history_completion.rs index 6b597bebc9..0081bbf1bf 100644 --- a/codex-rs/tui/src/app/history_completion.rs +++ b/codex-rs/tui/src/app/history_completion.rs @@ -1,15 +1,15 @@ //! Separate older history at every turn boundary; only successful turns gain completion metadata. //! A page splitting a turn only adds completion metadata when it contains that turn's last item. -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; use std::collections::HashMap; pub(super) fn group_completed_turn_items( - items: Vec, + items: Vec, turns: &[Turn], -) -> Vec<(Vec, Option<&Turn>)> { +) -> Vec<(Vec, Option<&Turn>)> { let boundaries: HashMap<_, _> = turns .iter() .filter_map(|turn| turn.items.last().map(|item| (item.id(), turn))) diff --git a/codex-rs/tui/src/app/history_completion_tests.rs b/codex-rs/tui/src/app/history_completion_tests.rs index d82b71bb71..a25ffb80fb 100644 --- a/codex-rs/tui/src/app/history_completion_tests.rs +++ b/codex-rs/tui/src/app/history_completion_tests.rs @@ -1,20 +1,26 @@ //! Pagination boundaries preserve grouped item rendering and attach completion metadata once. use super::*; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::TurnItemsView; use pretty_assertions::assert_eq; +fn payloads(items: &[codex_app_server_protocol::ThreadItem]) -> Vec { + items.iter().map(|entry| entry.payload.clone()).collect() +} + fn turn(id: &str, status: TurnStatus, item_ids: &[&str]) -> Turn { Turn { id: id.to_string(), root_turn_id: None, items: item_ids .iter() - .map(|id| ThreadItem::UserMessage { + .map(|id| ThreadItemPayload::UserMessage { id: id.to_string(), client_id: None, content: Vec::new(), }) + .map(ThreadItem::untimed) .collect(), items_view: TurnItemsView::Summary, status, @@ -33,12 +39,12 @@ fn split_turn_only_completes_on_the_page_with_its_last_item() { &["first", "second", "last"], )]; assert_eq!( - group_completed_turn_items(turns[0].items[1..].to_vec(), &turns), - vec![(turns[0].items[1..].to_vec(), Some(&turns[0]))], + group_completed_turn_items(payloads(&turns[0].items[1..]), &turns), + vec![(payloads(&turns[0].items[1..]), Some(&turns[0]))], ); assert_eq!( - group_completed_turn_items(turns[0].items[..1].to_vec(), &turns), - vec![(turns[0].items[..1].to_vec(), None)], + group_completed_turn_items(payloads(&turns[0].items[..1]), &turns), + vec![(payloads(&turns[0].items[..1]), None)], ); } @@ -48,12 +54,16 @@ fn multiple_turns_keep_item_groups_and_completion_order() { turn("first", TurnStatus::Completed, &["a", "b"]), turn("second", TurnStatus::Completed, &["c", "d"]), ]; - let items = turns.iter().flat_map(|turn| turn.items.clone()).collect(); + let items = turns + .iter() + .flat_map(Turn::item_payloads) + .cloned() + .collect(); assert_eq!( group_completed_turn_items(items, &turns), vec![ - (turns[0].items.clone(), Some(&turns[0])), - (turns[1].items.clone(), Some(&turns[1])), + (payloads(&turns[0].items), Some(&turns[0])), + (payloads(&turns[1].items), Some(&turns[1])), ], ); } @@ -67,13 +77,14 @@ fn unsuccessful_and_running_turns_separate_groups_without_completion_metadata() ]; let items = turns .iter() - .flat_map(|turn| turn.items.clone()) + .flat_map(Turn::item_payloads) + .cloned() .collect::>(); assert_eq!( group_completed_turn_items(items, &turns), turns .iter() - .map(|turn| (turn.items.clone(), None)) + .map(|turn| (payloads(&turn.items), None)) .collect::>(), ); } diff --git a/codex-rs/tui/src/app/history_pagination.rs b/codex-rs/tui/src/app/history_pagination.rs index bd04368c10..0c9f0053fb 100644 --- a/codex-rs/tui/src/app/history_pagination.rs +++ b/codex-rs/tui/src/app/history_pagination.rs @@ -155,8 +155,8 @@ impl App { } let user_items = turns .iter() - .flat_map(|turn| turn.items.iter()) - .filter(|item| matches!(item, ThreadItem::UserMessage { .. })) + .flat_map(Turn::item_payloads) + .filter(|item| matches!(item, ThreadItemPayload::UserMessage { .. })) .map(|item| (item.id().to_string(), item.clone())) .collect::>(); let projected_user_cells = thread_items_to_transcript_cells( @@ -217,7 +217,7 @@ impl App { /// Project successful turn completion metadata after the corresponding page's final item. fn project_older_history_cells( &mut self, - items: Vec, + items: Vec, turns: &[Turn], hidden_item_ids: &HashSet<&str>, thread_id: ThreadId, @@ -363,10 +363,12 @@ fn hidden_review_item_ids(turns: &[Turn]) -> HashSet<&str> { crate::app_backtrack::is_hidden_nested_review_turn(previous, turn) }); for item in &turn.items { - match item { - ThreadItem::EnteredReviewMode { .. } => review_mode = true, - ThreadItem::ExitedReviewMode { .. } => review_mode = false, - ThreadItem::UserMessage { .. } if review_mode || hidden_nested_review_turn => { + match &item.payload { + ThreadItemPayload::EnteredReviewMode { .. } => review_mode = true, + ThreadItemPayload::ExitedReviewMode { .. } => review_mode = false, + ThreadItemPayload::UserMessage { .. } + if review_mode || hidden_nested_review_turn => + { hidden.insert(item.id()); } _ => {} diff --git a/codex-rs/tui/src/app/history_pagination_tests.rs b/codex-rs/tui/src/app/history_pagination_tests.rs index f33a9347e5..69e996d6de 100644 --- a/codex-rs/tui/src/app/history_pagination_tests.rs +++ b/codex-rs/tui/src/app/history_pagination_tests.rs @@ -5,6 +5,7 @@ use crate::app::test_support::make_test_app; use crate::history_cell::ComputerActivityCell; use crate::history_cell::FinalMessageSeparator; use crate::history_cell::ForkedThreadHistoryCell; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::UserInput; use pretty_assertions::assert_eq; @@ -15,11 +16,12 @@ fn turn(id: &str, status: TurnStatus, item_ids: &[&str]) -> Turn { root_turn_id: None, items: item_ids .iter() - .map(|id| ThreadItem::UserMessage { + .map(|id| ThreadItemPayload::UserMessage { id: id.to_string(), client_id: None, content: Vec::new(), }) + .map(ThreadItem::untimed) .collect(), items_view: TurnItemsView::Full, status, @@ -138,19 +140,19 @@ async fn hidden_last_item_keeps_turn_groups_and_completion_boundaries() { let expected_completions = usize::from(status == TurnStatus::Completed); let mut first = turn("first", status, &[]); first.items = vec![ - ThreadItem::EnteredReviewMode { + ThreadItem::untimed(ThreadItemPayload::EnteredReviewMode { id: "enter-review".to_string(), review: "current changes".to_string(), - }, + }), computer("older-call"), - ThreadItem::UserMessage { + ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "hidden".to_string(), client_id: None, content: vec![UserInput::Text { text: "internal review prompt".to_string(), text_elements: Vec::new(), }], - }, + }), ]; first.completed_at = Some(1_700_000_000); first.duration_ms = Some(125_000); @@ -161,7 +163,8 @@ async fn hidden_last_item_keeps_turn_groups_and_completion_boundaries() { assert_eq!(hidden, HashSet::from(["hidden"])); let items = turns .iter() - .flat_map(|turn| turn.items.clone()) + .flat_map(Turn::item_payloads) + .cloned() .collect::>(); let cells = app.project_older_history_cells( @@ -261,7 +264,7 @@ async fn browsing_waits_for_a_prompt_outside_the_initial_history_window() -> Res &["older prompt", "latest prompt"], ); for item in &mut review_turn.items { - if let ThreadItem::UserMessage { id, content, .. } = item { + if let ThreadItemPayload::UserMessage { id, content, .. } = &mut item.payload { *content = vec![UserInput::Text { text: id.clone(), text_elements: Vec::new(), @@ -270,17 +273,17 @@ async fn browsing_waits_for_a_prompt_outside_the_initial_history_window() -> Res } review_turn.items.insert( /*index*/ 0, - ThreadItem::EnteredReviewMode { + ThreadItem::untimed(ThreadItemPayload::EnteredReviewMode { id: "enter".to_string(), review: "review".to_string(), - }, + }), ); review_turn.items.insert( /*index*/ 2, - ThreadItem::ExitedReviewMode { + ThreadItem::untimed(ThreadItemPayload::ExitedReviewMode { id: "exit".to_string(), review: "review".to_string(), - }, + }), ); let turns = [review_turn]; app.remove_hidden_review_cells( diff --git a/codex-rs/tui/src/app/native_history_tests.rs b/codex-rs/tui/src/app/native_history_tests.rs index 6e340c424b..3521543cd5 100644 --- a/codex-rs/tui/src/app/native_history_tests.rs +++ b/codex-rs/tui/src/app/native_history_tests.rs @@ -49,7 +49,7 @@ async fn quota_warnings_are_emitted_once_and_survive_native_replay() -> Result<( async fn settled_tool_precedes_queued_output_and_is_not_emitted_twice() -> Result<()> { let mut app = crate::app::test_support::make_test_app().await; let mut tui = crate::tui::test_support::make_test_tui()?; - let item = |status| ThreadItem::DynamicToolCall { + let item = |status| ThreadItemPayload::DynamicToolCall { id: "lookup".into(), namespace: None, tool: "lookup".into(), diff --git a/codex-rs/tui/src/app/owned_transcript_input_tests.rs b/codex-rs/tui/src/app/owned_transcript_input_tests.rs index 15cf0bf8c9..075a625662 100644 --- a/codex-rs/tui/src/app/owned_transcript_input_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_input_tests.rs @@ -200,17 +200,18 @@ fn complete_plan_turn(app: &mut App) { thread_id: thread_id.clone(), turn: turn.clone(), }), - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.clone(), - turn_id: turn.id.clone(), - completed_at_ms: 0, - item: ThreadItem::Plan { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.clone(), + turn.id.clone(), + ThreadItemPayload::Plan { id: "plan".into(), text: (1..=40) .map(|row| format!("- Plan step {row:02}\n")) .collect(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), ServerNotification::TurnCompleted(TurnCompletedNotification { thread_id, turn: Turn { diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index cdbc8512fa..ee932e5c26 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -2,7 +2,7 @@ use crate::app_command::AppCommand; use codex_app_server_protocol::RequestId as AppServerRequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use std::collections::HashMap; use std::collections::HashSet; @@ -255,15 +255,15 @@ impl PendingInteractiveReplayState { pub(super) fn note_server_notification(&mut self, notification: &ServerNotification) { match notification { - ServerNotification::ItemStarted(notification) => match ¬ification.item { - ThreadItem::CommandExecution { id, .. } => { + ServerNotification::ItemStarted(notification) => match ¬ification.item.payload { + ThreadItemPayload::CommandExecution { id, .. } => { self.exec_approval_call_ids.remove(id); Self::remove_call_id_from_turn_map( &mut self.exec_approval_call_ids_by_turn_id, id, ); } - ThreadItem::FileChange { id, .. } => { + ThreadItemPayload::FileChange { id, .. } => { self.patch_approval_call_ids.remove(id); Self::remove_call_id_from_turn_map( &mut self.patch_approval_call_ids_by_turn_id, diff --git a/codex-rs/tui/src/app/realtime_delivery.rs b/codex-rs/tui/src/app/realtime_delivery.rs index 723ea7e9ba..16e1b867cb 100644 --- a/codex-rs/tui/src/app/realtime_delivery.rs +++ b/codex-rs/tui/src/app/realtime_delivery.rs @@ -17,13 +17,13 @@ fn normalized_replay_text(text: &str) -> String { } fn record_voice_item( - item: &ThreadItem, + item: &ThreadItemPayload, turn_id: &str, delegated: &mut HashSet, items: &mut HashMap<(String, String, &'static str), String>, ) { match item { - ThreadItem::UserMessage { id, content, .. } => { + ThreadItemPayload::UserMessage { id, content, .. } => { if let Some(input) = crate::chatwidget::realtime_delegation_input(content) { delegated.insert(turn_id.to_string()); let display = crate::chatwidget::realtime_delegation_display_text(input); @@ -33,7 +33,7 @@ fn record_voice_item( ); } } - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id, text, phase: Some(MessagePhase::FinalAnswer) | None, @@ -55,7 +55,7 @@ pub(super) fn replayed_voice_texts_from_turns(turns: &[Turn]) -> ReplayedVoiceTe let mut delegated = HashSet::new(); let mut items = HashMap::new(); for turn in turns { - for item in &turn.items { + for item in turn.item_payloads() { record_voice_item(item, &turn.id, &mut delegated, &mut items); } } @@ -82,7 +82,7 @@ pub(super) fn replayed_voice_texts(snapshot: &ThreadEventSnapshot) -> ReplayedVo .collect::>(); let mut items = HashMap::new(); for turn in &snapshot.turns { - for item in &turn.items { + for item in turn.item_payloads() { record_voice_item(item, &turn.id, &mut delegated, &mut items); } } @@ -92,20 +92,20 @@ pub(super) fn replayed_voice_texts(snapshot: &ThreadEventSnapshot) -> ReplayedVo }; match notification.as_ref() { ServerNotification::ItemStarted(n) => { - if let ThreadItem::UserMessage { content, .. } = &n.item + if let ThreadItemPayload::UserMessage { content, .. } = &n.item.payload && crate::chatwidget::realtime_delegation_input(content).is_some() { delegated.insert(n.turn_id.clone()); } } ServerNotification::ItemCompleted(n) => { - record_voice_item(&n.item, &n.turn_id, &mut delegated, &mut items); + record_voice_item(&n.item.payload, &n.turn_id, &mut delegated, &mut items); } ServerNotification::TurnCompleted(n) if n.turn.status == TurnStatus::Completed => { - if let Some(item) = n.turn.items.iter().rev().find(|item| { + if let Some(item) = n.turn.item_payloads().rev().find(|item| { matches!( item, - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { phase: Some(MessagePhase::FinalAnswer) | None, .. } @@ -349,7 +349,7 @@ impl App { return; }; for (turn_id, item) in pending { - let ThreadItem::AgentMessage { id, text, .. } = &item else { + let ThreadItemPayload::AgentMessage { id, text, .. } = &item else { continue; }; // A matching ID with empty text did not render the saved answer. @@ -386,7 +386,8 @@ pub(super) fn completed_agent_items( }; match notification.as_ref() { ServerNotification::ItemCompleted(completed_item) => { - if let ThreadItem::AgentMessage { id, text, .. } = &completed_item.item + if let ThreadItemPayload::AgentMessage { id, text, .. } = + &completed_item.item.payload && !text.trim().is_empty() { completed.insert((completed_item.turn_id.clone(), id.clone()), text.clone()); @@ -395,8 +396,8 @@ pub(super) fn completed_agent_items( ServerNotification::TurnCompleted(completed_turn) if completed_turn.turn.status == TurnStatus::Completed => { - for item in &completed_turn.turn.items { - if let ThreadItem::AgentMessage { id, text, .. } = item + for item in completed_turn.turn.item_payloads() { + if let ThreadItemPayload::AgentMessage { id, text, .. } = item && !text.trim().is_empty() { completed @@ -415,8 +416,8 @@ pub(super) fn completed_agent_items_from_turns( ) -> HashMap<(String, String), String> { let mut completed = HashMap::new(); for turn in turns { - for item in &turn.items { - if let ThreadItem::AgentMessage { id, text, .. } = item + for item in turn.item_payloads() { + if let ThreadItemPayload::AgentMessage { id, text, .. } = item && !text.trim().is_empty() { completed.insert((turn.id.clone(), id.clone()), text.clone()); diff --git a/codex-rs/tui/src/app/reasoning_replay.rs b/codex-rs/tui/src/app/reasoning_replay.rs index 84ae583873..07e02d87ca 100644 --- a/codex-rs/tui/src/app/reasoning_replay.rs +++ b/codex-rs/tui/src/app/reasoning_replay.rs @@ -8,7 +8,7 @@ use super::ThreadEventSnapshot; use crate::chatwidget::ChatWidget; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnStatus; pub(super) struct ReasoningReplay { @@ -25,10 +25,10 @@ impl ReasoningReplay { .iter_mut() .find(|turn| turn.id == started.turn_id && turn.status == TurnStatus::InProgress)?; let index = turn.items.iter().position( - |item| matches!(item, ThreadItem::Reasoning { id, .. } if id == started.item.id()), + |item| matches!(&item.payload, ThreadItemPayload::Reasoning { id, .. } if id == started.item.id()), )?; - match turn.items.remove(index) { - ThreadItem::Reasoning { + match turn.items.remove(index).payload { + ThreadItemPayload::Reasoning { summary, content, .. } => Some((summary, content)), _ => unreachable!("matched a reasoning item"), diff --git a/codex-rs/tui/src/app/replay_filter.rs b/codex-rs/tui/src/app/replay_filter.rs index 01b0c6a415..0058e808cc 100644 --- a/codex-rs/tui/src/app/replay_filter.rs +++ b/codex-rs/tui/src/app/replay_filter.rs @@ -3,7 +3,7 @@ use codex_app_server_protocol::CodexErrorInfo; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use std::collections::HashSet; use super::ThreadBufferedEvent; @@ -67,7 +67,7 @@ pub(super) fn omit_completed_agent_deltas(events: &mut Vec) if let ThreadBufferedEvent::Notification(notification) = event { match notification.as_ref() { ServerNotification::ItemCompleted(notification) => { - if let ThreadItem::AgentMessage { id, .. } = ¬ification.item { + if let ThreadItemPayload::AgentMessage { id, .. } = ¬ification.item.payload { completed.insert(( notification.thread_id.clone(), notification.turn_id.clone(), diff --git a/codex-rs/tui/src/app/safety_buffering.rs b/codex-rs/tui/src/app/safety_buffering.rs index 9c14dde984..72da56bfcd 100644 --- a/codex-rs/tui/src/app/safety_buffering.rs +++ b/codex-rs/tui/src/app/safety_buffering.rs @@ -10,6 +10,7 @@ use crate::chatwidget::ThreadInputState; use crate::chatwidget::ThreadInputStateRestoreMode; use crate::chatwidget::UserMessage; use codex_app_server_protocol::ThreadHistoryMode; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::UserInput; @@ -153,7 +154,7 @@ impl App { .data .into_iter() .rev() - .map(|entry| entry.item) + .map(ThreadItem::from) .collect(); turn.items_view = TurnItemsView::Full; } @@ -178,9 +179,9 @@ impl App { .iter() .find(|turn| turn.id == turn_id) .into_iter() - .flat_map(|turn| &turn.items) + .flat_map(Turn::item_payloads) .filter_map(|item| match item { - ThreadItem::UserMessage { content, .. } => Some(content), + ThreadItemPayload::UserMessage { content, .. } => Some(content), _ => None, }) .skip(/*n*/ 1) diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index c0c378b16d..9e5b8315da 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -126,6 +126,7 @@ use crate::app_backtrack::nth_user_position; use crate::app_backtrack::user_count; use crate::app_event::HistoryBatchEntryResponse; use crate::test_support::boxed_future; +use codex_app_server_protocol::ThreadItem; async fn drain_managed_worktree_start(app: &mut App, server: &mut AppServerSession) { if let Some((mode, name)) = app.pending_start_managed_worktree.take() { @@ -190,7 +191,7 @@ use codex_app_server_protocol::ThreadArchivedNotification; use codex_app_server_protocol::ThreadAttachmentOperation; use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; use codex_app_server_protocol::ThreadClosedNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSettings; use codex_app_server_protocol::ThreadSettingsUpdatedNotification; use codex_app_server_protocol::ThreadStartedNotification; @@ -916,7 +917,7 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm vec![test_turn( "turn-1", TurnStatus::Completed, - vec![ThreadItem::UserMessage { + vec![ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -1847,11 +1848,10 @@ async fn replay_thread_snapshot_does_not_submit_queue_before_replay_catches_up() turns: Vec::new(), events: vec![ ThreadBufferedEvent::Notification(Box::new(ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-1".into(), - completed_at_ms: 0, - item: ThreadItem::UserMessage { + codex_app_server_protocol::ItemCompletedNotification::new( + thread_id.to_string(), + "turn-1".into(), + ThreadItemPayload::UserMessage { id: "accepted".into(), client_id: Some(client_id), content: vec![UserInput::Text { @@ -1859,7 +1859,9 @@ async fn replay_thread_snapshot_does_not_submit_queue_before_replay_catches_up() text_elements: Vec::new(), }], }, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), ))), ThreadBufferedEvent::Notification(Box::new(turn_completed_notification( thread_id, @@ -2202,11 +2204,10 @@ async fn collab_receiver_notification_caches_thread_without_app_server_read() { ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id"); app.handle_thread_event_now(ThreadBufferedEvent::Notification(Box::new( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: ThreadId::new().to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: ThreadItem::CollabAgentToolCall { + ServerNotification::ItemStarted(ItemStartedNotification::new( + ThreadId::new().to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: "wait-1".to_string(), tool: codex_app_server_protocol::CollabAgentTool::Wait, status: codex_app_server_protocol::CollabAgentToolCallStatus::InProgress, @@ -2217,7 +2218,8 @@ async fn collab_receiver_notification_caches_thread_without_app_server_read() { reasoning_effort: None, agents_states: HashMap::new(), }, - }), + /*started_at_ms*/ 0, + )), ))); assert_eq!( @@ -2239,28 +2241,31 @@ async fn collab_receiver_notification_does_not_cache_not_found_thread() { ThreadId::from_string("00000000-0000-0000-0000-000000000124").expect("valid thread id"); app.handle_thread_event_now(ThreadBufferedEvent::Notification(Box::new( - ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: ThreadId::new().to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: ThreadItem::CollabAgentToolCall { - id: "send-1".to_string(), - tool: codex_app_server_protocol::CollabAgentTool::SendInput, - status: codex_app_server_protocol::CollabAgentToolCallStatus::Failed, - sender_thread_id: ThreadId::new().to_string(), - receiver_thread_ids: vec![receiver_thread_id.to_string()], - prompt: Some("hello".to_string()), - model: None, - reasoning_effort: None, - agents_states: HashMap::from([( - receiver_thread_id.to_string(), - codex_app_server_protocol::CollabAgentState { - status: codex_app_server_protocol::CollabAgentStatus::NotFound, - message: None, - }, - )]), - }, - }), + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + ThreadId::new().to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { + id: "send-1".to_string(), + tool: codex_app_server_protocol::CollabAgentTool::SendInput, + status: codex_app_server_protocol::CollabAgentToolCallStatus::Failed, + sender_thread_id: ThreadId::new().to_string(), + receiver_thread_ids: vec![receiver_thread_id.to_string()], + prompt: Some("hello".to_string()), + model: None, + reasoning_effort: None, + agents_states: HashMap::from([( + receiver_thread_id.to_string(), + codex_app_server_protocol::CollabAgentState { + status: codex_app_server_protocol::CollabAgentStatus::NotFound, + message: None, + }, + )]), + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), ))); assert_eq!(app.agent_navigation.get(&receiver_thread_id), None); @@ -4498,11 +4503,10 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() { let thread_id = ThreadId::new(); app.enqueue_thread_notification( thread_id, - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-approval".to_string(), - started_at_ms: 0, - item: ThreadItem::FileChange { + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + "turn-approval".to_string(), + ThreadItemPayload::FileChange { id: "patch-approval".to_string(), changes: vec![FileUpdateChange { path: "README.md".to_string(), @@ -4511,7 +4515,8 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() { }], status: codex_app_server_protocol::PatchApplyStatus::InProgress, }, - }), + /*started_at_ms*/ 0, + )), ) .await .expect("enqueue file change item"); @@ -4576,11 +4581,10 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() { app.startup_protected_input_boundary = true; app.enqueue_thread_notification( thread_id, - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-active-approval".to_string(), - started_at_ms: 0, - item: ThreadItem::FileChange { + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + "turn-active-approval".to_string(), + ThreadItemPayload::FileChange { id: "patch-active-approval".to_string(), changes: vec![FileUpdateChange { path: "visible-target.md".to_string(), @@ -4589,7 +4593,8 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() { }], status: codex_app_server_protocol::PatchApplyStatus::InProgress, }, - }), + /*started_at_ms*/ 0, + )), ) .await .expect("enqueue file change item"); @@ -4626,19 +4631,22 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() { assert!(!app.chat_widget.has_active_view()); assert!(app.startup_pending_protected_request); app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn-active-approval".to_string(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { - id: "agent-1".to_string(), - text: "streaming".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, - }), + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + thread_id.to_string(), + "turn-active-approval".to_string(), + ThreadItemPayload::AgentMessage { + id: "agent-1".to_string(), + text: "streaming".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), /*replay_kind*/ None, ); @@ -4661,7 +4669,7 @@ async fn replayed_file_change_approval_recovers_snapshot_changes() { turns: vec![test_turn( "turn-replayed-approval", TurnStatus::InProgress, - vec![ThreadItem::FileChange { + vec![ThreadItemPayload::FileChange { id: "patch-replayed-approval".to_string(), changes: vec![FileUpdateChange { path: "visible-target.md".to_string(), @@ -5591,7 +5599,7 @@ async fn side_thread_snapshot_hides_forked_parent_transcript() { let parent_turn = test_turn( "parent-turn", TurnStatus::Completed, - vec![ThreadItem::UserMessage { + vec![ThreadItemPayload::UserMessage { id: "parent-user".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -6913,7 +6921,7 @@ async fn app_server_thread_replacement_clears_previous_transcript_before_replay( turns: vec![test_turn( "next-turn", TurnStatus::Completed, - vec![ThreadItem::UserMessage { + vec![ThreadItemPayload::UserMessage { id: "next-user".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -6997,7 +7005,7 @@ async fn snapshot_thread_switch_discards_queued_previous_history() -> Result<()> turns: vec![test_turn( "next-turn", TurnStatus::Completed, - vec![ThreadItem::UserMessage { + vec![ThreadItemPayload::UserMessage { id: "next-user".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -7157,17 +7165,20 @@ async fn copy_picker_opening_preserves_terminal_scrollback_without_reflow() { crate::transcript_mode::TranscriptMode::Terminal; let response = "Existing response\n\n```rust\nkeep_scrollback();\n```"; app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: String::new(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: serde_json::from_value(serde_json::json!({ - "type": "agentMessage", - "id": "message-1", - "text": response, - })) - .expect("valid completed agent message"), - }), + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + String::new(), + "turn-1".to_string(), + serde_json::from_value(serde_json::json!({ + "type": "agentMessage", + "id": "message-1", + "text": response, + })) + .expect("valid completed agent message"), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), /*replay_kind*/ None, ); while app_event_rx.try_recv().is_ok() {} @@ -7498,12 +7509,12 @@ async fn resizing_empty_transcript_schedules_settled_size_recheck() { ); } -fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { +fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { Turn { id: turn_id.to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), status, error: None, started_at: None, @@ -8774,12 +8785,13 @@ async fn prompt_edit_reverts_earlier_and_first_visible_prompts_in_place_scenario )); for item in selected_turn.items { store.push_notification(ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - thread_id: source_thread_id.to_string(), - turn_id: selected_turn.id.clone(), - completed_at_ms: 0, - item, - }, + codex_app_server_protocol::ItemCompletedNotification::new( + source_thread_id.to_string(), + selected_turn.id.clone(), + item.payload, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); } store.push_notification(turn_completed_notification( @@ -9112,14 +9124,14 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::UserMessage { + items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: vec![AppServerUserInput::Text { text: "first prompt".to_string(), text_elements: Vec::new(), }], - }], + })], status: TurnStatus::Completed, error: None, started_at: None, @@ -9131,22 +9143,22 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() { root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ - ThreadItem::UserMessage { + ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "user-2".to_string(), client_id: None, content: vec![AppServerUserInput::Text { text: "third prompt".to_string(), text_elements: Vec::new(), }], - }, - ThreadItem::AgentMessage { + }), + ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "assistant-2".to_string(), text: "done".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }, + }), ], status: TurnStatus::Completed, error: None, @@ -9227,11 +9239,10 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() { turns: Vec::new(), events: vec![ThreadBufferedEvent::Notification(Box::new( ServerNotification::ItemStarted( - codex_app_server_protocol::ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: ThreadItem::CollabAgentToolCall { + codex_app_server_protocol::ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + ThreadItemPayload::CollabAgentToolCall { id: "wait-1".to_string(), tool: codex_app_server_protocol::CollabAgentTool::Wait, status: @@ -9243,7 +9254,8 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() { reasoning_effort: None, agents_states: HashMap::new(), }, - }, + /*started_at_ms*/ 0, + ), ), ))], active_reasoning_item: None, @@ -9283,7 +9295,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() { let resumed_turns = vec![test_turn( "turn-1", TurnStatus::Completed, - vec![ThreadItem::UserMessage { + vec![ThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: vec![AppServerUserInput::Text { diff --git a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs index f102e25b47..e294f284f2 100644 --- a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs @@ -128,7 +128,7 @@ async fn reconnect_restores_history_permissions_and_resumes_unsent_input() -> Re "id": id, "sessionId": id, "preview": "only once", "ephemeral": false, "modelProvider": "test-provider", "createdAt": 1, "updatedAt": 2, "status": {"type": "idle"}, "cwd": cwd, "cliVersion": "0.0.0", "source": "cli", - "turns": [test_turn("accepted", TurnStatus::Completed, vec![ThreadItem::UserMessage { + "turns": [test_turn("accepted", TurnStatus::Completed, vec![ThreadItemPayload::UserMessage { id: "input-1".into(), client_id: None, content: vec![UserInput::Text { text: "only once".into(), text_elements: Vec::new() }], }])] }); diff --git a/codex-rs/tui/src/app/tests/browsing_pagination_tests.rs b/codex-rs/tui/src/app/tests/browsing_pagination_tests.rs index a52491902b..2a3f602117 100644 --- a/codex-rs/tui/src/app/tests/browsing_pagination_tests.rs +++ b/codex-rs/tui/src/app/tests/browsing_pagination_tests.rs @@ -38,7 +38,7 @@ async fn browsing_loads_before_the_oldest_prompt_after_a_partial_answer() -> Res .extend(crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), )); diff --git a/codex-rs/tui/src/app/tests/buffered_replay.rs b/codex-rs/tui/src/app/tests/buffered_replay.rs index 47ea84e91d..2c122f8042 100644 --- a/codex-rs/tui/src/app/tests/buffered_replay.rs +++ b/codex-rs/tui/src/app/tests/buffered_replay.rs @@ -6,6 +6,7 @@ use crate::chatwidget::tests::helpers::render_bottom_popup; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; fn delta(thread: &str, turn: &str, item: &str) -> ServerNotification { @@ -18,11 +19,10 @@ fn delta(thread: &str, turn: &str, item: &str) -> ServerNotification { } fn completed(thread: &str) -> ServerNotification { - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread.into(), - turn_id: "turn".into(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread.into(), + "turn".into(), + ThreadItemPayload::AgentMessage { id: "answer".into(), text: "Final **answer**".into(), phase: None, @@ -30,7 +30,9 @@ fn completed(thread: &str) -> ServerNotification { delivery: None, questions: None, }, - }) + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )) } #[tokio::test] @@ -50,16 +52,18 @@ async fn refreshed_active_reasoning_accepts_later_deltas_and_complete_summary() ThreadEventChannel::new_with_session(capacity, session.clone(), vec![turn.clone()]); { let mut store = channel.store.lock().await; - store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: turn.id.clone(), - item: ThreadItem::Reasoning { - id: "reasoning".into(), - summary: Vec::new(), - content: Vec::new(), - }, - started_at_ms: 0, - })); + store.push_notification(ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.to_string(), + turn.id.clone(), + ThreadItemPayload::Reasoning { + id: "reasoning".into(), + summary: Vec::new(), + content: Vec::new(), + }, + /*started_at_ms*/ 0, + ), + )); store.push_notification(ServerNotification::ReasoningSummaryTextDelta( ReasoningSummaryTextDeltaNotification { thread_id: thread_id.to_string(), @@ -71,29 +75,25 @@ async fn refreshed_active_reasoning_accepts_later_deltas_and_complete_summary() )); assert_eq!(store.buffer.len(), capacity.min(/*other*/ 2)); if voice_handoff { - store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: turn.id.clone(), - item: ThreadItem::UserMessage { + store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification::new(thread_id.to_string(), turn.id.clone(), ThreadItemPayload::UserMessage { id: "voice-handoff".into(), client_id: None, content: vec![AppServerUserInput::Text { text: "spoken follow-up".into(), text_elements: Vec::new(), }], - }, - started_at_ms: 0, - })); + }, /*started_at_ms*/ 0))); } } let mut snapshot = channel.store.lock().await.snapshot(); app.thread_event_channels.insert(thread_id, channel); if refreshed_item_is_partial { - turn.items.push(ThreadItem::Reasoning { - id: "reasoning".into(), - summary: vec!["**First heading**\nOriginal analysis paragraph".into()], - content: Vec::new(), - }); + turn.items + .push(ThreadItem::untimed(ThreadItemPayload::Reasoning { + id: "reasoning".into(), + summary: vec!["**First heading**\nOriginal analysis paragraph".into()], + content: Vec::new(), + })); } if refresh { app.apply_refreshed_snapshot_thread( @@ -122,19 +122,14 @@ async fn refreshed_active_reasoning_accepts_later_deltas_and_complete_summary() /*replay_kind*/ None, ); assert!(render_bottom_popup(&app.chat_widget, /*width*/ 80).contains("Final paragraph")); - let completed = ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "reasoning-turn".into(), - item: ThreadItem::Reasoning { + let completed = ServerNotification::ItemCompleted(ItemCompletedNotification::new(thread_id.to_string(), "reasoning-turn".into(), ThreadItemPayload::Reasoning { id: "reasoning".into(), summary: vec![ "**First heading**\nOriginal analysis paragraph\n**Running checks**\nFinal paragraph" .into(), ], content: Vec::new(), - }, - completed_at_ms: 0, - }); + }, /*started_at_ms*/ None, /*completed_at_ms*/ 0)); let store = &app.thread_event_channels[&thread_id].store; store.lock().await.push_notification_ref(&completed); assert!( @@ -224,7 +219,7 @@ fn buffered_replay_keeps_unfinished_and_unrelated_deltas_in_order() { #[test] fn evicted_voice_delegation_marker_still_suppresses_private_replay() { let mut store = ThreadEventStore::new(/*capacity*/ 2); - let voice_request = ThreadItem::UserMessage { + let voice_request = ThreadItemPayload::UserMessage { id: "user".into(), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { @@ -233,19 +228,18 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() { }], }; store.push_notification(ServerNotification::ItemStarted( - codex_app_server_protocol::ItemStartedNotification { - thread_id: "thread".into(), - turn_id: "voice-turn".into(), - started_at_ms: 0, - item: voice_request.clone(), - }, + codex_app_server_protocol::ItemStartedNotification::new( + "thread".into(), + "voice-turn".into(), + voice_request.clone(), + /*started_at_ms*/ 0, + ), )); store.push_notification(delta("thread", "voice-turn", "reasoning")); - let private = ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread".into(), - turn_id: "voice-turn".into(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { + let private = ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".into(), + "voice-turn".into(), + ThreadItemPayload::AgentMessage { id: "reasoning".into(), text: "[ANALYSIS] private".into(), phase: None, @@ -253,7 +247,9 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() { delivery: None, questions: None, }, - }); + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )); store.push_notification(private.clone()); assert!(store.snapshot().events.is_empty()); store.push_notification(delta("thread", "voice-turn", "unfinished")); @@ -286,16 +282,17 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() { content_index: 0, }, ), - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread".into(), - turn_id: "voice-turn".into(), - completed_at_ms: 0, - item: ThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".into(), + "voice-turn".into(), + ThreadItemPayload::Reasoning { id: "reasoning".into(), summary: vec!["private summary".into()], content: vec!["private raw reasoning".into()], }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), ] { store.push_notification(notification); assert!(store.snapshot().events.is_empty()); @@ -305,7 +302,7 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() { resumed.set_turns(vec![Turn { id: "voice-turn".into(), root_turn_id: None, - items: vec![voice_request], + items: vec![ThreadItem::untimed(voice_request)], items_view: codex_app_server_protocol::TurnItemsView::Summary, status: TurnStatus::InProgress, error: None, @@ -331,12 +328,12 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() { #[test] fn saved_voice_turn_suppresses_reasoning_without_hiding_typed_reasoning() { - let reasoning = ThreadItem::Reasoning { + let reasoning = ThreadItemPayload::Reasoning { id: "reasoning".into(), summary: vec!["private summary".into()], content: vec!["private raw reasoning".into()], }; - let voice_request = ThreadItem::UserMessage { + let voice_request = ThreadItemPayload::UserMessage { id: "voice-request".into(), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { @@ -357,21 +354,27 @@ fn saved_voice_turn_suppresses_reasoning_without_hiding_typed_reasoning() { }; let mut store = ThreadEventStore::new(/*capacity*/ 4); store.set_turns(vec![ - turn("voice", vec![voice_request.clone(), reasoning.clone()]), - turn("typed", vec![reasoning.clone()]), + turn( + "voice", + vec![ + ThreadItem::untimed(voice_request.clone()), + ThreadItem::untimed(reasoning.clone()), + ], + ), + turn("typed", vec![ThreadItem::untimed(reasoning.clone())]), ]); assert_eq!( store.snapshot().turns, vec![ - turn("voice", vec![voice_request]), - turn("typed", vec![reasoning]) + turn("voice", vec![ThreadItem::untimed(voice_request)]), + turn("typed", vec![ThreadItem::untimed(reasoning)]) ] ); } #[test] fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() { - let commentary = |id: &str, text: &str| ThreadItem::AgentMessage { + let commentary = |id: &str, text: &str| ThreadItemPayload::AgentMessage { id: id.into(), text: text.into(), phase: Some(codex_protocol::models::MessagePhase::Commentary), @@ -379,7 +382,7 @@ fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() { memory_citation: None, delivery: None, }; - let marker = ThreadItem::UserMessage { + let marker = ThreadItemPayload::UserMessage { id: "voice".into(), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { @@ -393,7 +396,11 @@ fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() { store.set_turns(vec![Turn { id: "shared".into(), root_turn_id: None, - items: vec![typed.clone(), marker.clone(), private.clone()], + items: vec![ + ThreadItem::untimed(typed.clone()), + ThreadItem::untimed(marker.clone()), + ThreadItem::untimed(private.clone()), + ], items_view: codex_app_server_protocol::TurnItemsView::Full, status: TurnStatus::InProgress, error: None, @@ -403,33 +410,44 @@ fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() { }]); for item in [typed.clone(), marker.clone(), private] { store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread".into(), - turn_id: "shared".into(), - completed_at_ms: 0, + ItemCompletedNotification::new( + "thread".into(), + "shared".into(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); } let snapshot = store.snapshot(); - assert_eq!(snapshot.turns[0].items, vec![typed.clone(), marker]); + assert_eq!( + snapshot.turns[0] + .items + .iter() + .map(|entry| &entry.payload) + .collect::>(), + [&typed, &marker] + ); let completed = snapshot .events .into_iter() .filter_map(|event| match event { ThreadBufferedEvent::Notification(notification) => match *notification { - ServerNotification::ItemCompleted(item) => Some(item.item), + ServerNotification::ItemCompleted(item) => Some(item.item.payload), _ => None, }, _ => None, }) .collect::>(); - assert_eq!(completed, vec![typed, snapshot.turns[0].items[1].clone()]); + assert_eq!( + completed, + vec![typed, snapshot.turns[0].items[1].payload.clone()] + ); } #[test] fn snapshot_keeps_typed_item_completed_after_voice_handoff() { - let typed = ThreadItem::AgentMessage { + let typed = ThreadItemPayload::AgentMessage { id: "typed".into(), text: "Typed commentary completed late".into(), phase: Some(codex_protocol::models::MessagePhase::Commentary), @@ -437,7 +455,7 @@ fn snapshot_keeps_typed_item_completed_after_voice_handoff() { memory_citation: None, delivery: None, }; - let private = ThreadItem::AgentMessage { + let private = ThreadItemPayload::AgentMessage { id: "private".into(), text: "Voice-private commentary".into(), phase: Some(codex_protocol::models::MessagePhase::Commentary), @@ -445,7 +463,7 @@ fn snapshot_keeps_typed_item_completed_after_voice_handoff() { memory_citation: None, delivery: None, }; - let marker = ThreadItem::UserMessage { + let marker = ThreadItemPayload::UserMessage { id: "voice".into(), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { @@ -456,22 +474,23 @@ fn snapshot_keeps_typed_item_completed_after_voice_handoff() { let mut store = ThreadEventStore::new(/*capacity*/ 8); for item in [typed.clone(), marker, private.clone()] { store.push_notification(ServerNotification::ItemStarted( - codex_app_server_protocol::ItemStartedNotification { - thread_id: "thread".into(), - turn_id: "shared".into(), - started_at_ms: 0, + codex_app_server_protocol::ItemStartedNotification::new( + "thread".into(), + "shared".into(), item, - }, + /*started_at_ms*/ 0, + ), )); } for item in [typed.clone(), private] { store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: "thread".into(), - turn_id: "shared".into(), - completed_at_ms: 0, + ItemCompletedNotification::new( + "thread".into(), + "shared".into(), item, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); } let completed = store @@ -480,7 +499,7 @@ fn snapshot_keeps_typed_item_completed_after_voice_handoff() { .into_iter() .filter_map(|event| match event { ThreadBufferedEvent::Notification(notification) => match *notification { - ServerNotification::ItemCompleted(n) => Some(n.item), + ServerNotification::ItemCompleted(n) => Some(n.item.payload), _ => None, }, _ => None, @@ -494,11 +513,10 @@ async fn evicted_voice_marker_survives_widget_snapshot_for_late_reasoning() { let thread_id = ThreadId::new(); let mut store = ThreadEventStore::new(/*capacity*/ 1); store.push_notification(ServerNotification::ItemStarted( - codex_app_server_protocol::ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "voice-turn".into(), - started_at_ms: 0, - item: ThreadItem::UserMessage { + codex_app_server_protocol::ItemStartedNotification::new( + thread_id.to_string(), + "voice-turn".into(), + ThreadItemPayload::UserMessage { id: "user".into(), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { @@ -507,7 +525,8 @@ async fn evicted_voice_marker_survives_widget_snapshot_for_late_reasoning() { text_elements: Vec::new(), }], }, - }, + /*started_at_ms*/ 0, + ), )); store.push_notification(delta(&thread_id.to_string(), "voice-turn", "private")); let snapshot = store.snapshot(); @@ -534,16 +553,17 @@ async fn evicted_voice_marker_survives_widget_snapshot_for_late_reasoning() { /*replay_kind*/ None, ); app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "voice-turn".into(), - completed_at_ms: 0, - item: ThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.to_string(), + "voice-turn".into(), + ThreadItemPayload::Reasoning { id: "late-reasoning".into(), summary: vec!["private after switch".into()], content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); app.chat_widget.handle_server_notification( @@ -551,11 +571,10 @@ async fn evicted_voice_marker_survives_widget_snapshot_for_late_reasoning() { /*replay_kind*/ None, ); app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "voice-turn".into(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.to_string(), + "voice-turn".into(), + ThreadItemPayload::AgentMessage { id: "private-commentary".into(), text: "[COMMENTARY] private after switch".into(), phase: None, @@ -563,7 +582,9 @@ async fn evicted_voice_marker_survives_widget_snapshot_for_late_reasoning() { delivery: None, questions: None, }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); while let Ok(event) = events.try_recv() { @@ -649,7 +670,7 @@ async fn misalignment_buffered_replay_preserves_input_after_continuation() { }; let failed_turn = Turn { error: Some(error.clone()), - items: vec![ThreadItem::McpToolCall { + items: vec![ThreadItem::untimed(ThreadItemPayload::McpToolCall { id: "tool-call".into(), server: "sample".into(), tool: "inspect".into(), @@ -663,7 +684,7 @@ async fn misalignment_buffered_replay_preserves_input_after_continuation() { result: None, error: None, duration_ms: None, - }], + })], ..test_turn("failed-turn", TurnStatus::Failed, Vec::new()) }; let continued_turn = test_turn("continued-turn", TurnStatus::Completed, Vec::new()); diff --git a/codex-rs/tui/src/app/tests/history_hydration_tests.rs b/codex-rs/tui/src/app/tests/history_hydration_tests.rs index 9fd420d285..ecc4ba35e4 100644 --- a/codex-rs/tui/src/app/tests/history_hydration_tests.rs +++ b/codex-rs/tui/src/app/tests/history_hydration_tests.rs @@ -156,7 +156,7 @@ async fn history_hydration_metadata_tracks_missing_turns_and_stale_completions_k .turns .iter() .flat_map(|turn| &turn.items) - .map(ThreadItem::id) + .map(codex_app_server_protocol::ThreadItem::id) .collect::>(), vec!["item-12-0"] ); @@ -175,7 +175,7 @@ async fn history_hydration_metadata_tracks_missing_turns_and_stale_completions_k .apply_older_history_page(target.thread_id, &old_cursor, page, &mut thread.turns) .await?; assert_eq!( - items.iter().map(ThreadItem::id).collect::>(), + items.iter().map(ThreadItemPayload::id).collect::>(), vec!["item-3-0", "item-5-0"] ); assert_eq!( @@ -183,7 +183,7 @@ async fn history_hydration_metadata_tracks_missing_turns_and_stale_completions_k .turns .iter() .flat_map(|turn| &turn.items) - .map(ThreadItem::id) + .map(codex_app_server_protocol::ThreadItem::id) .collect::>(), vec!["item-3-0", "item-5-0", "item-12-0"] ); diff --git a/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs b/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs index 5f8a8e5381..6125e94028 100644 --- a/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs @@ -60,7 +60,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv vec![test_turn( "cached", TurnStatus::Completed, - vec![ThreadItem::Plan { + vec![ThreadItemPayload::Plan { id: "cached-plan".into(), text: "Cached previous conversation".into(), }], diff --git a/codex-rs/tui/src/app/tests/pagination_completion_tests.rs b/codex-rs/tui/src/app/tests/pagination_completion_tests.rs index 7cfc88a950..ec7d8b433e 100644 --- a/codex-rs/tui/src/app/tests/pagination_completion_tests.rs +++ b/codex-rs/tui/src/app/tests/pagination_completion_tests.rs @@ -211,7 +211,7 @@ async fn beginning_navigation_holds_the_view_until_the_last_page_arrives() -> Re app.transcript_cells = crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), ); @@ -331,7 +331,7 @@ async fn returning_to_latest_retains_pending_pages_without_continuing_to_the_beg app.transcript_cells = crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), ); diff --git a/codex-rs/tui/src/app/tests/patch_approval_tests.rs b/codex-rs/tui/src/app/tests/patch_approval_tests.rs index 8f5e36e101..c5dee23417 100644 --- a/codex-rs/tui/src/app/tests/patch_approval_tests.rs +++ b/codex-rs/tui/src/app/tests/patch_approval_tests.rs @@ -55,8 +55,8 @@ fn expected_changes() -> HashMap { ]) } -fn patch_item() -> ThreadItem { - ThreadItem::FileChange { +fn patch_item() -> ThreadItemPayload { + ThreadItemPayload::FileChange { id: ITEM_ID.to_string(), changes: changes(), status: PatchApplyStatus::InProgress, @@ -83,12 +83,12 @@ async fn enqueue_pending_patch(app: &mut App, thread_id: ThreadId) -> Result<()> .await?; app.enqueue_thread_notification( thread_id, - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: TURN_ID.to_string(), - started_at_ms: 0, - item: patch_item(), - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + TURN_ID.to_string(), + patch_item(), + /*started_at_ms*/ 0, + )), ) .await?; let request = request(thread_id); @@ -261,19 +261,22 @@ async fn active_patch_approval_preserves_deferred_startup_protection() -> Result assert!(app.startup_pending_protected_request); app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: TURN_ID.to_string(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { - id: "agent-1".to_string(), - text: "streaming".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, - }), + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + thread_id.to_string(), + TURN_ID.to_string(), + ThreadItemPayload::AgentMessage { + id: "agent-1".to_string(), + text: "streaming".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), /*replay_kind*/ None, ); assert_eq!(open_patch(&mut app, &mut rx).changes, expected_changes()); diff --git a/codex-rs/tui/src/app/tests/realtime_requests.rs b/codex-rs/tui/src/app/tests/realtime_requests.rs index 2bda4f08bf..6a02e1d720 100644 --- a/codex-rs/tui/src/app/tests/realtime_requests.rs +++ b/codex-rs/tui/src/app/tests/realtime_requests.rs @@ -12,6 +12,7 @@ use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnItemsView; @@ -45,8 +46,8 @@ fn voice_snapshot_directory_keeps_header_width_for_windows_paths() { ); } -fn test_agent_message(id: &str, text: &str) -> ThreadItem { - ThreadItem::AgentMessage { +fn test_agent_message(id: &str, text: &str) -> ThreadItemPayload { + ThreadItemPayload::AgentMessage { id: id.into(), text: text.into(), phase: Some(MessagePhase::FinalAnswer), @@ -56,8 +57,8 @@ fn test_agent_message(id: &str, text: &str) -> ThreadItem { } } -fn test_user_message(id: &str, text: &str) -> ThreadItem { - ThreadItem::UserMessage { +fn test_user_message(id: &str, text: &str) -> ThreadItemPayload { + ThreadItemPayload::UserMessage { id: id.into(), client_id: None, content: vec![UserInput::Text { @@ -300,7 +301,7 @@ async fn switching_agent_threads_preserves_backend_voice_and_routes_pending_spee turn: Turn { id: turn_id.into(), root_turn_id: None, - items: vec![answer.clone()], + items: vec![ThreadItem::untimed(answer.clone())], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -668,11 +669,11 @@ async fn replay_reconciles_only_matching_voice_captions_one_for_one() { id: "voice-turn".into(), root_turn_id: None, items: vec![ - test_user_message( + ThreadItem::untimed(test_user_message( "voice-user", "show <code> & tests", - ), - test_agent_message("voice-answer", "spoken answer"), + )), + ThreadItem::untimed(test_agent_message("voice-answer", "spoken answer")), ], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, @@ -683,7 +684,10 @@ async fn replay_reconciles_only_matching_voice_captions_one_for_one() { }; let typed = Turn { id: "typed-turn".into(), - items: vec![test_user_message("typed-user", "typed words")], + items: vec![ThreadItem::untimed(test_user_message( + "typed-user", + "typed words", + ))], ..voice.clone() }; app.replay_thread_snapshot( @@ -738,7 +742,7 @@ async fn retained_caption_consumes_only_one_matching_answer_fallback_on_reattach }] .into(), ); - let answer = |id: &str, text: &str| ThreadItem::AgentMessage { + let answer = |id: &str, text: &str| ThreadItemPayload::AgentMessage { id: id.into(), text: text.into(), phase: Some(MessagePhase::FinalAnswer), @@ -838,7 +842,7 @@ async fn buffered_voice_items_reconcile_captions_after_thread_switch() { .collect(), ); let items = [ - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { id: "user-item".into(), client_id: None, content: vec![UserInput::Text { @@ -853,12 +857,13 @@ async fn buffered_voice_items_reconcile_captions_after_thread_switch() { .into_iter() .map(|item| { ThreadBufferedEvent::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { + ItemCompletedNotification::new( + source.to_string(), + "buffered-turn".into(), item, - thread_id: source.to_string(), - turn_id: "buffered-turn".into(), - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), ))) }) .collect(); @@ -917,7 +922,7 @@ async fn unrendered_buffered_items_do_not_consume_retained_captions() { }) .collect(), ); - let user_item = |id: &str, text: &str| ThreadItem::UserMessage { + let user_item = |id: &str, text: &str| ThreadItemPayload::UserMessage { id: id.into(), client_id: None, content: vec![UserInput::Text { @@ -925,14 +930,14 @@ async fn unrendered_buffered_items_do_not_consume_retained_captions() { text_elements: Vec::new(), }], }; - let completion = |turn_id: &str, status: TurnStatus, items: Vec| { + let completion = |turn_id: &str, status: TurnStatus, items: Vec| { ThreadBufferedEvent::Notification(Box::new(ServerNotification::TurnCompleted( TurnCompletedNotification { thread_id: source.to_string(), turn: Turn { id: turn_id.into(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Summary, status, error: None, @@ -950,12 +955,12 @@ async fn unrendered_buffered_items_do_not_consume_retained_captions() { turns: Vec::new(), events: vec![ ThreadBufferedEvent::Notification(Box::new(ServerNotification::ItemStarted( - ItemStartedNotification { - item: user_item("started-user", "started only"), - thread_id: source.to_string(), - turn_id: "started".into(), - started_at_ms: 0, - }, + ItemStartedNotification::new( + source.to_string(), + "started".into(), + user_item("started-user", "started only"), + /*started_at_ms*/ 0, + ), ))), completion( "evicted", @@ -1064,7 +1069,10 @@ async fn completed_voice_caption_survives_repeated_thread_replacement() { let mut snapshot = empty_thread_snapshot(&app, source); snapshot.turns.push(Turn { id: "earlier-typed-turn".into(), - items: vec![test_agent_message("earlier-answer", "Earlier answer")], + items: vec![ThreadItem::untimed(test_agent_message( + "earlier-answer", + "Earlier answer", + ))], ..typed.clone() }); snapshot.turns.push(typed.clone()); @@ -1216,7 +1224,7 @@ async fn rejected_realtime_speech_restores_the_delegated_final_answer() -> Resul turn: Turn { id: "rejected-turn".to_string(), root_turn_id: None, - items: vec![answer], + items: vec![ThreadItem::untimed(answer)], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -1316,7 +1324,7 @@ async fn switching_threads_retains_undelivered_voice_answer_after_replay_evictio turn: Turn { id: "switched-turn".to_string(), root_turn_id: None, - items: vec![answer], + items: vec![ThreadItem::untimed(answer)], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -1385,7 +1393,7 @@ async fn switching_threads_retains_undelivered_voice_answer_after_replay_evictio assert_eq!(restored, 1); let replayed_item = test_agent_message("replayed-final", "Already in the thread snapshot."); let mut older_item = replayed_item.clone(); - if let ThreadItem::AgentMessage { text, .. } = &mut older_item { + if let ThreadItemPayload::AgentMessage { text, .. } = &mut older_item { *text = "Older pending text.".to_string(); } app.pending_realtime_speech_replay @@ -1397,7 +1405,7 @@ async fn switching_threads_retains_undelivered_voice_answer_after_replay_evictio turns: vec![Turn { id: "replayed-turn".to_string(), root_turn_id: None, - items: vec![replayed_item], + items: vec![ThreadItem::untimed(replayed_item)], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -1518,22 +1526,25 @@ fn send_item( app: &mut App, thread_id: ThreadId, turn_id: &str, - item: ThreadItem, + item: ThreadItemPayload, kind: ItemEventKind, ) { let notification = match kind { - ItemEventKind::Started => ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: turn_id.to_string(), + ItemEventKind::Started => ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + turn_id.to_string(), item, - started_at_ms: 0, - }), - ItemEventKind::Completed => ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: turn_id.to_string(), - item, - completed_at_ms: 0, - }), + /*started_at_ms*/ 0, + )), + ItemEventKind::Completed => { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.to_string(), + turn_id.to_string(), + item, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )) + } }; app.handle_thread_event_now(ThreadBufferedEvent::Notification(Box::new(notification))); } @@ -1600,7 +1611,7 @@ async fn delegated_final_speech_reaches_app_server_once_and_stale_speech_is_reje turn: Turn { id: turn_id.to_string(), root_turn_id: None, - items: vec![answer], + items: vec![ThreadItem::untimed(answer)], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, @@ -1678,7 +1689,7 @@ async fn delegated_final_speech_reaches_app_server_once_and_stale_speech_is_reje turn: Turn { id: second_turn.to_string(), root_turn_id: None, - items: vec![second_answer], + items: vec![ThreadItem::untimed(second_answer)], items_view: TurnItemsView::Summary, status: TurnStatus::Completed, error: None, diff --git a/codex-rs/tui/src/app/tests/reasoning_resume_tests.rs b/codex-rs/tui/src/app/tests/reasoning_resume_tests.rs index 526a0ee8e0..54da8ce235 100644 --- a/codex-rs/tui/src/app/tests/reasoning_resume_tests.rs +++ b/codex-rs/tui/src/app/tests/reasoning_resume_tests.rs @@ -15,7 +15,7 @@ async fn resumed_reasoning_without_start_accepts_deltas_and_completion() { let session = test_thread_session(thread_id, app.config.cwd.to_path_buf()); let items = snapshot_item_id .into_iter() - .map(|id| ThreadItem::Reasoning { + .map(|id| ThreadItemPayload::Reasoning { id: id.into(), summary: vec![ if id == "previous" { @@ -72,18 +72,19 @@ async fn resumed_reasoning_without_start_accepts_deltas_and_completion() { ); } app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn".into(), - completed_at_ms: 0, - item: ThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.to_string(), + "turn".into(), + ThreadItemPayload::Reasoning { id: "reasoning".into(), summary: vec![ "**Inspecting**\nSnapshot paragraph.\nCompleted paragraph.".into(), ], content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); app.chat_widget.handle_server_notification( @@ -126,7 +127,7 @@ async fn resumed_trailing_reasoning_is_kept_when_no_more_reasoning_arrives() { vec![test_turn( "turn", TurnStatus::InProgress, - vec![ThreadItem::Reasoning { + vec![ThreadItemPayload::Reasoning { id: "finished".into(), summary: vec!["**Inspecting**\nSaved paragraph.".into()], content: Vec::new(), @@ -137,16 +138,16 @@ async fn resumed_trailing_reasoning_is_kept_when_no_more_reasoning_arrives() { if next_item { app.chat_widget.handle_server_notification( ServerNotification::ItemStarted( - codex_app_server_protocol::ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "turn".into(), - started_at_ms: 0, - item: ThreadItem::Reasoning { + codex_app_server_protocol::ItemStartedNotification::new( + thread_id.to_string(), + "turn".into(), + ThreadItemPayload::Reasoning { id: "next".into(), summary: Vec::new(), content: Vec::new(), }, - }, + /*started_at_ms*/ 0, + ), ), /*replay_kind*/ None, ); diff --git a/codex-rs/tui/src/app/tests/safety_buffering.rs b/codex-rs/tui/src/app/tests/safety_buffering.rs index 1dcfae9370..fcd201a8eb 100644 --- a/codex-rs/tui/src/app/tests/safety_buffering.rs +++ b/codex-rs/tui/src/app/tests/safety_buffering.rs @@ -317,9 +317,9 @@ stream_max_retries = 0 .expect("app-server remains open"); let committed_id = if let AppServerEvent::ServerNotification(notification) = &event.event && let ServerNotification::ItemCompleted(item) = notification.as_ref() - && let ThreadItem::UserMessage { + && let ThreadItemPayload::UserMessage { content, client_id, .. - } = &item.item + } = &item.item.payload && ChatWidget::user_message_display_from_inputs(content).message == COMMITTED_STEER { Some(client_id.clone().expect("receipt echoes the submission ID")) @@ -457,9 +457,9 @@ fn user_message_count(thread: &Thread, prompt: &str) -> usize { thread .turns .iter() - .flat_map(|turn| &turn.items) + .flat_map(Turn::item_payloads) .filter_map(|item| match item { - ThreadItem::UserMessage { content, .. } => Some(content), + ThreadItemPayload::UserMessage { content, .. } => Some(content), _ => None, }) .filter(|content| { diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index 787c7e086a..98f48e201b 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -2589,7 +2589,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() -> let initial_cells = crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), ); @@ -2763,7 +2763,7 @@ async fn transcript_alt_beginning_loads_every_older_history_page() -> Result<()> let initial_cells = crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), ); @@ -3179,7 +3179,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri let mut initial_cells = crate::thread_transcript::thread_items_to_transcript_cells( Some(thread_id), &app.config.cwd, - started.turns.iter().flat_map(|turn| turn.items.clone()), + started.turns.iter().flat_map(Turn::item_payloads).cloned(), crate::thread_transcript::RawReasoningVisibility::Hidden, Some(&app.config), ); @@ -3709,7 +3709,7 @@ async fn cold_paginated_subagent_transcript_excludes_inherited_parent_history() let [child_entry] = child_item_page.data.as_slice() else { panic!("paginated subagent should expose exactly one child message"); }; - let ThreadItem::UserMessage { content, .. } = &child_entry.item else { + let ThreadItemPayload::UserMessage { content, .. } = &child_entry.item.payload else { panic!("paginated subagent should expose its child user message"); }; assert_eq!(resumed.session.thread_id, child_thread_id); @@ -3725,9 +3725,9 @@ async fn cold_paginated_subagent_transcript_excludes_inherited_parent_history() resumed .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .collect::>(), - vec![&child_entry.item], + vec![&child_entry.item.payload], ); let cells = crate::thread_transcript::load_session_transcript( diff --git a/codex-rs/tui/src/app/tests/startup_frame_tests.rs b/codex-rs/tui/src/app/tests/startup_frame_tests.rs index 2c6adf43c1..9fe4a7663b 100644 --- a/codex-rs/tui/src/app/tests/startup_frame_tests.rs +++ b/codex-rs/tui/src/app/tests/startup_frame_tests.rs @@ -94,7 +94,7 @@ async fn owned_startup_hides_tip_in_transcript() -> Result<()> { async fn owned_startup_preserves_loading_until_resume_replay_is_applied() -> Result<()> { for items in [ Vec::new(), - vec![ThreadItem::AgentMessage { + vec![ThreadItemPayload::AgentMessage { id: "answer".into(), text: "Retained **answer** after resume.".into(), phase: None, diff --git a/codex-rs/tui/src/app/tests/streaming_interleave_e2e.rs b/codex-rs/tui/src/app/tests/streaming_interleave_e2e.rs index a1db23cfa9..63d9de8dd3 100644 --- a/codex-rs/tui/src/app/tests/streaming_interleave_e2e.rs +++ b/codex-rs/tui/src/app/tests/streaming_interleave_e2e.rs @@ -126,23 +126,23 @@ async fn command_start_before_message_completion_does_not_duplicate_preamble() - observed_order.push("agent delta"); } ServerNotification::ItemStarted(started) - if matches!(&started.item, ThreadItem::CommandExecution { .. }) => + if matches!(&started.item.payload, ThreadItemPayload::CommandExecution { .. }) => { observed_order.push("command started"); release_completion.take().expect("first command start").send(()).expect("completion gate should remain open"); } ServerNotification::ItemCompleted(completed) - if matches!(&completed.item, ThreadItem::AgentMessage { text, .. } if text == PREAMBLE) => + if matches!(&completed.item.payload, ThreadItemPayload::AgentMessage { text, .. } if text == PREAMBLE) => { observed_order.push("agent completed"); } ServerNotification::ItemCompleted(completed) => { - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { status, aggregated_output, exit_code, .. - } = &completed.item + } = &completed.item.payload { completed_command = Some(( status.clone(), diff --git a/codex-rs/tui/src/app/tests/thread_usage.rs b/codex-rs/tui/src/app/tests/thread_usage.rs index 8bd1900150..8d6d408de6 100644 --- a/codex-rs/tui/src/app/tests/thread_usage.rs +++ b/codex-rs/tui/src/app/tests/thread_usage.rs @@ -280,7 +280,7 @@ async fn terminal_reflow_rebases_pending_status_update_to_new_width() -> Result< app.insert_history_cell( &mut tui, Box::new( - history_cell::DynamicToolCallCell::from_item(ThreadItem::DynamicToolCall { + history_cell::DynamicToolCallCell::from_item(ThreadItemPayload::DynamicToolCall { id: "pending-tool".into(), namespace: None, tool: "lookup".into(), diff --git a/codex-rs/tui/src/app/tests/unavailable_commands_tests.rs b/codex-rs/tui/src/app/tests/unavailable_commands_tests.rs index b762305b4a..8ff73ff2b0 100644 --- a/codex-rs/tui/src/app/tests/unavailable_commands_tests.rs +++ b/codex-rs/tui/src/app/tests/unavailable_commands_tests.rs @@ -154,7 +154,7 @@ async fn unavailable_thread_new_and_clear_start_a_writable_session() -> Result<( vec![test_turn( "review", TurnStatus::InProgress, - vec![ThreadItem::EnteredReviewMode { + vec![ThreadItemPayload::EnteredReviewMode { id: "review-start".into(), review: "changes against main".into(), }], diff --git a/codex-rs/tui/src/app/tests/user_verification_routes_tests.rs b/codex-rs/tui/src/app/tests/user_verification_routes_tests.rs index d3d07eded4..ca06b5196d 100644 --- a/codex-rs/tui/src/app/tests/user_verification_routes_tests.rs +++ b/codex-rs/tui/src/app/tests/user_verification_routes_tests.rs @@ -239,19 +239,22 @@ async fn inactive_thread_user_verification_preserves_foreground_stream() -> Resu /*replay_kind*/ None, ); app.chat_widget.handle_server_notification( - ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification { - thread_id: foreground_thread_id.to_string(), - turn_id: "turn-foreground".to_string(), - completed_at_ms: 0, - item: ThreadItem::AgentMessage { - id: "message-foreground".to_string(), - text: "The foreground answer continues.".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, - }), + ServerNotification::ItemCompleted( + codex_app_server_protocol::ItemCompletedNotification::new( + foreground_thread_id.to_string(), + "turn-foreground".to_string(), + ThreadItemPayload::AgentMessage { + id: "message-foreground".to_string(), + text: "The foreground answer continues.".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), + ), /*replay_kind*/ None, ); diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index a6ca1a22b6..f810d38745 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -26,17 +26,23 @@ pub(super) enum ThreadBufferedEvent { FeedbackSubmission(FeedbackThreadEvent), } -fn is_voice_handoff_item(item: &ThreadItem) -> bool { - matches!(item, ThreadItem::UserMessage { content, .. } +fn is_voice_handoff_item(item: &ThreadItemPayload) -> bool { + matches!(item, ThreadItemPayload::UserMessage { content, .. } if crate::chatwidget::realtime_delegation_input(content).is_some()) } -fn hide_private_items_after_voice_handoff(items: &mut Vec, delegated: bool) { - let has_marker = items.iter().any(is_voice_handoff_item); +fn hide_private_items_after_voice_handoff( + items: &mut Vec, + delegated: bool, +) { + let has_marker = items + .iter() + .any(|entry| is_voice_handoff_item(&entry.payload)); // An evicted marker leaves no reliable order, so hide private items // conservatively. A present marker lets earlier typed output survive. let mut voice_started = delegated && !has_marker; - items.retain(|item| { + items.retain(|entry| { + let item = &entry.payload; if is_voice_handoff_item(item) { voice_started = true; } @@ -83,11 +89,11 @@ impl ThreadEventStore { pub(super) fn event_survives_session_refresh(event: &mut ThreadBufferedEvent) -> bool { if let ThreadBufferedEvent::Notification(notification) = event && let ServerNotification::ItemCompleted(notification) = notification.as_mut() - && let ThreadItem::AgentMessage { + && let ThreadItemPayload::AgentMessage { questions: Some(_), text, .. - } = &mut notification.item + } = &mut notification.item.payload { // Refreshed turns contain the text; retain only the live question state. text.clear(); @@ -182,11 +188,11 @@ impl ThreadEventStore { fn push_notification_inner(&mut self, notification: Cow<'_, ServerNotification>) { let user_item = match notification.as_ref() { - ServerNotification::ItemStarted(n) => Some((&n.turn_id, &n.item)), - ServerNotification::ItemCompleted(n) => Some((&n.turn_id, &n.item)), + ServerNotification::ItemStarted(n) => Some((&n.turn_id, &n.item.payload)), + ServerNotification::ItemCompleted(n) => Some((&n.turn_id, &n.item.payload)), _ => None, }; - if let Some((turn_id, ThreadItem::UserMessage { content, .. })) = user_item + if let Some((turn_id, ThreadItemPayload::UserMessage { content, .. })) = user_item && turn_id.len() <= 512 && crate::chatwidget::realtime_delegation_input(content).is_some() && !self.delegated_turns.contains(turn_id) @@ -242,28 +248,36 @@ impl ThreadEventStore { } ServerNotification::ItemStarted( started @ codex_app_server_protocol::ItemStartedNotification { - item: ThreadItem::Reasoning { id, .. }, + item: + codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::Reasoning { id, .. }, + .. + }, .. }, ) if !self.delegated_turns.contains(&started.turn_id) && !self.turns.iter().any(|turn| { - turn.id == started.turn_id && turn.items.iter().any(is_voice_handoff_item) + turn.id == started.turn_id + && turn + .items + .iter() + .any(|entry| is_voice_handoff_item(&entry.payload)) }) => { self.active_reasoning_item = - Some(codex_app_server_protocol::ItemStartedNotification { - thread_id: started.thread_id.clone(), - turn_id: started.turn_id.clone(), - item: ThreadItem::Reasoning { + Some(codex_app_server_protocol::ItemStartedNotification::new( + started.thread_id.clone(), + started.turn_id.clone(), + ThreadItemPayload::Reasoning { id: id.clone(), summary: Vec::new(), content: Vec::new(), }, - started_at_ms: started.started_at_ms, - }); + started.started_at_ms, + )); } ServerNotification::ItemCompleted(completed) - if matches!(&completed.item, ThreadItem::Reasoning { .. }) + if matches!(&completed.item.payload, ThreadItemPayload::Reasoning { .. }) && self.active_reasoning_item.as_ref().is_some_and(|started| { started.turn_id == completed.turn_id && started.item.id() == completed.item.id() @@ -384,7 +398,7 @@ impl ThreadEventStore { .iter() .filter(|turn| { turn.items.iter().any(|item| { - matches!(item, ThreadItem::UserMessage { content, .. } + matches!(&item.payload, ThreadItemPayload::UserMessage { content, .. } if crate::chatwidget::realtime_delegation_input(content).is_some()) }) }) @@ -399,10 +413,14 @@ impl ThreadEventStore { .iter() .filter_map(|event| match event { ThreadBufferedEvent::Notification(notification) => match notification.as_ref() { - ServerNotification::ItemStarted(n) if is_voice_handoff_item(&n.item) => { + ServerNotification::ItemStarted(n) + if is_voice_handoff_item(&n.item.payload) => + { Some(n.turn_id.clone()) } - ServerNotification::ItemCompleted(n) if is_voice_handoff_item(&n.item) => { + ServerNotification::ItemCompleted(n) + if is_voice_handoff_item(&n.item.payload) => + { Some(n.turn_id.clone()) } _ => None, @@ -424,15 +442,15 @@ impl ThreadEventStore { return true; }; match notification.as_ref() { - ServerNotification::ItemStarted(n) if is_voice_handoff_item(&n.item) => { + ServerNotification::ItemStarted(n) if is_voice_handoff_item(&n.item.payload) => { voice_started.insert(n.turn_id.clone()); } - ServerNotification::ItemCompleted(n) if is_voice_handoff_item(&n.item) => { + ServerNotification::ItemCompleted(n) if is_voice_handoff_item(&n.item.payload) => { voice_started.insert(n.turn_id.clone()); } ServerNotification::ItemStarted(n) => { - if let ThreadItem::AgentMessage { id, .. } | ThreadItem::Reasoning { id, .. } = - &n.item + if let ThreadItemPayload::AgentMessage { id, .. } | ThreadItemPayload::Reasoning { id, .. } = + &n.item.payload { let key = (n.turn_id.clone(), id.clone()); if voice_started.contains(&n.turn_id) { @@ -462,9 +480,9 @@ impl ThreadEventStore { || typed_items.contains(&(n.turn_id.clone(), n.item_id.clone())) } ServerNotification::ItemCompleted(n) if voice_started.contains(&n.turn_id) => { - matches!(&n.item, ThreadItem::AgentMessage { id, .. } | ThreadItem::Reasoning { id, .. } + matches!(&n.item.payload, ThreadItemPayload::AgentMessage { id, .. } | ThreadItemPayload::Reasoning { id, .. } if typed_items.contains(&(n.turn_id.clone(), id.clone()))) - || !crate::chatwidget::is_private_realtime_agent_item(&n.item) + || !crate::chatwidget::is_private_realtime_agent_item(&n.item.payload) } ServerNotification::TurnCompleted(n) if delegated.contains(&n.turn.id) => { hide_private_items_after_voice_handoff( @@ -547,10 +565,10 @@ pub(super) fn file_change_changes<'a>( }; let (candidate_turn_id, item) = match notification.as_ref() { ServerNotification::ItemStarted(notification) => { - (¬ification.turn_id, ¬ification.item) + (¬ification.turn_id, ¬ification.item.payload) } ServerNotification::ItemCompleted(notification) => { - (¬ification.turn_id, ¬ification.item) + (¬ification.turn_id, ¬ification.item.payload) } _ => return None, }; @@ -562,17 +580,17 @@ pub(super) fn file_change_changes<'a>( .iter() .rev() .filter(|turn| turn_id_matches(turn_id, &turn.id)) - .flat_map(|turn| turn.items.iter().rev()), + .flat_map(|turn| turn.item_payloads().rev()), ) .find_map(|item| file_change_item_changes(item, item_id)) } fn file_change_item_changes( - item: &ThreadItem, + item: &ThreadItemPayload, item_id: &str, ) -> Option> { match item { - ThreadItem::FileChange { id, changes, .. } if id == item_id => Some(changes.clone()), + ThreadItemPayload::FileChange { id, changes, .. } if id == item_id => Some(changes.clone()), _ => None, } } @@ -650,6 +668,7 @@ mod tests { use codex_app_server_protocol::RequestId as AppServerRequestId; use codex_app_server_protocol::ThreadAttachmentOperation; use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; + use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadRealtimeAudioChunk; use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification; use codex_app_server_protocol::TurnCompletedNotification; @@ -686,12 +705,12 @@ mod tests { } } - fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { + fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { Turn { id: turn_id.to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), status, error: None, started_at: None, @@ -972,9 +991,10 @@ mod tests { fn refresh_retains_live_questions_without_replaying_their_text() { let mut expected = serde_json::json!({ "method": "item/completed", "params": { - "threadId": "thread", "turnId": "turn", "completedAtMs": 0, + "threadId": "thread", "turnId": "turn", "completedAtMs": 20, "item": { "type": "agentMessage", "id": "question", "text": "already in the snapshot", + "startedAtMs": 10, "completedAtMs": 20, "phase": null, "memoryCitation": null, "delivery": null, "questions": [{"title": "Which way?", "options": null}] } @@ -1054,47 +1074,53 @@ mod tests { let thread_id = ThreadId::new().to_string(); let turn_id = "mixed-reasoning-turn".to_string(); let mut store = ThreadEventStore::new(/*capacity*/ 16); - let typed = ThreadItem::Reasoning { + let typed = ThreadItemPayload::Reasoning { id: "typed-reasoning".into(), summary: Vec::new(), content: Vec::new(), }; - store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.clone(), - turn_id: turn_id.clone(), - item: typed, - started_at_ms: 0, - })); - store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.clone(), - turn_id: turn_id.clone(), - item: ThreadItem::UserMessage { - id: "voice-marker".into(), - client_id: None, - content: vec![UserInput::Text { + store.push_notification(ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.clone(), + turn_id.clone(), + typed, + /*started_at_ms*/ 0, + ), + )); + store.push_notification(ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.clone(), + turn_id.clone(), + ThreadItemPayload::UserMessage { + id: "voice-marker".into(), + client_id: None, + content: vec![UserInput::Text { text: "spoken follow-up" .into(), text_elements: Vec::new(), }], - }, - started_at_ms: 0, - })); + }, + /*started_at_ms*/ 0, + ), + )); for (id, text) in [ ("typed-reasoning", "Typed tail"), ("private-reasoning", "Private voice reasoning"), ] { if id == "private-reasoning" { - store.push_notification(ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.clone(), - turn_id: turn_id.clone(), - item: ThreadItem::Reasoning { - id: id.into(), - summary: Vec::new(), - content: Vec::new(), - }, - started_at_ms: 0, - })); + store.push_notification(ServerNotification::ItemStarted( + ItemStartedNotification::new( + thread_id.clone(), + turn_id.clone(), + ThreadItemPayload::Reasoning { + id: id.into(), + summary: Vec::new(), + content: Vec::new(), + }, + /*started_at_ms*/ 0, + ), + )); } store.push_notification(ServerNotification::ReasoningSummaryTextDelta( ReasoningSummaryTextDeltaNotification { @@ -1106,16 +1132,17 @@ mod tests { }, )); store.push_notification(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: thread_id.clone(), - turn_id: turn_id.clone(), - item: ThreadItem::Reasoning { + ItemCompletedNotification::new( + thread_id.clone(), + turn_id.clone(), + ThreadItemPayload::Reasoning { id: id.into(), summary: vec![text.into()], content: Vec::new(), }, - completed_at_ms: 0, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), )); } @@ -1126,8 +1153,8 @@ mod tests { .filter_map(|event| match event { ThreadBufferedEvent::Notification(notification) => match *notification { ServerNotification::ReasoningSummaryTextDelta(n) => Some(n.delta), - ServerNotification::ItemCompleted(n) => match n.item { - ThreadItem::Reasoning { summary, .. } => Some(summary.join("")), + ServerNotification::ItemCompleted(n) => match n.item.payload { + ThreadItemPayload::Reasoning { summary, .. } => Some(summary.join("")), _ => None, }, _ => None, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 07a986931b..55042b535e 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -1731,23 +1731,23 @@ impl App { let confirmed_message_ids = snapshot .turns .iter() - .flat_map(|turn| &turn.items) + .flat_map(Turn::item_payloads) .chain(snapshot.events.iter().filter_map(|event| { let ThreadBufferedEvent::Notification(notification) = event else { return None; }; if let ServerNotification::ItemStarted(notification) = notification.as_ref() { - Some(¬ification.item) + Some(¬ification.item.payload) } else if let ServerNotification::ItemCompleted(notification) = notification.as_ref() { - Some(¬ification.item) + Some(¬ification.item.payload) } else { None } })) .filter_map(|item| { - if let ThreadItem::UserMessage { client_id, .. } = item { + if let ThreadItemPayload::UserMessage { client_id, .. } = item { client_id.clone() } else { None @@ -2046,7 +2046,7 @@ impl App { let automatic_title_user_message = if self.chat_widget.thread_name().is_none() && let ThreadBufferedEvent::Notification(notification) = &event && let ServerNotification::ItemCompleted(notification) = notification.as_ref() - && let ThreadItem::UserMessage { content, .. } = ¬ification.item + && let ThreadItemPayload::UserMessage { content, .. } = ¬ification.item.payload { Some( content diff --git a/codex-rs/tui/src/app/thread_title.rs b/codex-rs/tui/src/app/thread_title.rs index e248dde09b..3b09a741d0 100644 --- a/codex-rs/tui/src/app/thread_title.rs +++ b/codex-rs/tui/src/app/thread_title.rs @@ -14,8 +14,9 @@ use crate::temporary_structured_request::run_temporary_structured_turn; use crate::temporary_structured_request::start_temporary_thread; use crate::temporary_structured_request::unsubscribe_temporary_thread; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSource; +use codex_app_server_protocol::Turn; use codex_app_server_protocol::UserInput; use codex_protocol::ThreadId; use codex_protocol::models::MessagePhase; @@ -215,7 +216,7 @@ impl App { store .turns .iter() - .flat_map(|turn| turn.items.iter()) + .flat_map(Turn::item_payloads) .chain(store.buffer.iter().filter_map(|event| { let ThreadBufferedEvent::Notification(notification) = event else { return None; @@ -226,7 +227,7 @@ impl App { return None; }; - Some(¬ification.item) + Some(¬ification.item.payload) })) .filter(|item| seen.insert(item.id().to_string())), ) @@ -293,14 +294,14 @@ pub(super) fn thread_title_prompt(user_message: &str) -> String { /// Format recent substantive messages chronologically without trusting their markup. pub(super) fn recent_conversation_messages<'a, I>(items: I) -> Option where - I: IntoIterator, + I: IntoIterator, I::IntoIter: DoubleEndedIterator, { let mut messages = items .into_iter() .rev() .filter_map(|item| match item { - ThreadItem::UserMessage { content, .. } => { + ThreadItemPayload::UserMessage { content, .. } => { let text = content .iter() .filter_map(|input| match input { @@ -314,7 +315,7 @@ where (!text.trim().is_empty()).then_some(("user", text)) } - ThreadItem::AgentMessage { text, phase, .. } + ThreadItemPayload::AgentMessage { text, phase, .. } if !matches!(phase, Some(MessagePhase::Commentary)) && !text.trim().is_empty() => { Some(("assistant", text.clone())) diff --git a/codex-rs/tui/src/app/thread_title_tests.rs b/codex-rs/tui/src/app/thread_title_tests.rs index 68a0644d2b..2f7eb1d06f 100644 --- a/codex-rs/tui/src/app/thread_title_tests.rs +++ b/codex-rs/tui/src/app/thread_title_tests.rs @@ -20,6 +20,7 @@ use codex_app_server_client::AppServerEvent; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; @@ -270,7 +271,10 @@ async fn check_thread_title_generation(scenario: TitleScenario) -> color_eyre::R .push(Turn { id: "existing-turn".to_string(), root_turn_id: None, - items: vec![title_user_message("user-message", "Fix the login timeout")], + items: vec![ThreadItem::untimed(title_user_message( + "user-message", + "Fix the login timeout", + ))], items_view: Default::default(), status: TurnStatus::Completed, error: None, @@ -286,12 +290,13 @@ async fn check_thread_title_generation(scenario: TitleScenario) -> color_eyre::R &mut tui, &mut app_server, ThreadBufferedEvent::Notification(Box::new(ServerNotification::ItemCompleted( - ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: "existing-turn".to_string(), - item: title_user_message("user-message", "Fix the login timeout"), - completed_at_ms: 0, - }, + ItemCompletedNotification::new( + thread_id.to_string(), + "existing-turn".to_string(), + title_user_message("user-message", "Fix the login timeout"), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), ))), ) .await?; @@ -742,8 +747,8 @@ fn truncates_generated_titles_without_splitting_unicode() { assert_eq!(parse_thread_title(&response), Some(expected)); } -fn title_user_message(id: &str, text: &str) -> ThreadItem { - ThreadItem::UserMessage { +fn title_user_message(id: &str, text: &str) -> ThreadItemPayload { + ThreadItemPayload::UserMessage { id: id.to_string(), client_id: None, content: vec![UserInput::Text { @@ -753,8 +758,8 @@ fn title_user_message(id: &str, text: &str) -> ThreadItem { } } -fn title_agent_message(id: &str, text: &str, phase: Option) -> ThreadItem { - ThreadItem::AgentMessage { +fn title_agent_message(id: &str, text: &str, phase: Option) -> ThreadItemPayload { + ThreadItemPayload::AgentMessage { id: id.to_string(), text: text.to_string(), phase, diff --git a/codex-rs/tui/src/app/transcript_export.rs b/codex-rs/tui/src/app/transcript_export.rs index 273ecee194..00775bba50 100644 --- a/codex-rs/tui/src/app/transcript_export.rs +++ b/codex-rs/tui/src/app/transcript_export.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::sync::Arc; use codex_app_server_client::TypedRequestError; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_protocol::ThreadId; use codex_protocol::models::local_image_label_text; @@ -136,9 +136,9 @@ pub(super) async fn load_export_transcript( Ok(cells) } -fn export_activity_cell(item: &ThreadItem) -> Option { +fn export_activity_cell(item: &ThreadItemPayload) -> Option { let lines = match item { - ThreadItem::FileChange { + ThreadItemPayload::FileChange { changes, status, .. } => { let mut lines = @@ -149,7 +149,7 @@ fn export_activity_cell(item: &ThreadItem) -> Option { } lines } - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { server, tool, status, @@ -199,7 +199,7 @@ fn export_activity_cell(item: &ThreadItem) -> Option { Some(PlainHistoryCell::new(lines)) } -fn visible_export_items(turns: Vec) -> Vec { +fn visible_export_items(turns: Vec) -> Vec { let mut visible = Vec::new(); let mut review_mode = false; let mut previous_turn = None; @@ -208,13 +208,15 @@ fn visible_export_items(turns: Vec) -> Vec { let hidden_nested_review_turn = previous_turn.as_ref().is_some_and(|previous| { crate::app_backtrack::is_hidden_nested_review_turn(previous, &turn) }); - for item in turn.items.iter().cloned() { + for item in turn.item_payloads().cloned() { match item { - ThreadItem::EnteredReviewMode { .. } | ThreadItem::ExitedReviewMode { .. } => { - review_mode = matches!(item, ThreadItem::EnteredReviewMode { .. }); + ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } => { + review_mode = matches!(item, ThreadItemPayload::EnteredReviewMode { .. }); visible.push(item); } - ThreadItem::UserMessage { .. } if review_mode || hidden_nested_review_turn => {} + ThreadItemPayload::UserMessage { .. } + if review_mode || hidden_nested_review_turn => {} _ => visible.push(item), } } diff --git a/codex-rs/tui/src/app/transcript_export_tests.rs b/codex-rs/tui/src/app/transcript_export_tests.rs index 21ee6e4c76..15d1555b15 100644 --- a/codex-rs/tui/src/app/transcript_export_tests.rs +++ b/codex-rs/tui/src/app/transcript_export_tests.rs @@ -1,3 +1,4 @@ +use codex_app_server_protocol::ThreadItem; use std::sync::Arc; use pretty_assertions::assert_eq; @@ -10,7 +11,7 @@ use crate::history_cell::HistoryCell; use crate::history_cell::PlainHistoryCell; use crate::history_cell::UserHistoryCell; use crate::history_cell::new_proposed_plan; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; @@ -63,7 +64,7 @@ fn markdown_transcript_preserves_messages_and_formats_activity() { #[test] fn transcript_export_excludes_hidden_review_prompts_and_nested_duplicates() { - let user = |id: &str, text: &str| ThreadItem::UserMessage { + let user = |id: &str, text: &str| ThreadItemPayload::UserMessage { id: id.to_string(), client_id: None, content: vec![UserInput::Text { @@ -71,20 +72,20 @@ fn transcript_export_excludes_hidden_review_prompts_and_nested_duplicates() { text_elements: Vec::new(), }], }; - let entered_review = ThreadItem::EnteredReviewMode { + let entered_review = ThreadItemPayload::EnteredReviewMode { id: "review-enter".to_string(), review: "review".to_string(), }; - let exited_review = ThreadItem::ExitedReviewMode { + let exited_review = ThreadItemPayload::ExitedReviewMode { id: "review-exit".to_string(), review: "review".to_string(), }; let duplicate_one = user("duplicate-one", "duplicate review prompt"); let duplicate_two = user("duplicate-two", "duplicate review prompt"); - let turn = |id: &str, items: Vec, status| Turn { + let turn = |id: &str, items: Vec, status| Turn { id: id.to_string(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Full, status, error: None, @@ -149,7 +150,7 @@ fn persisted_transcript_includes_file_and_mcp_details() { }), ] .into_iter() - .map(|item| serde_json::from_value::(item).expect("valid thread item")) + .map(|item| serde_json::from_value::(item).expect("valid thread item")) .filter_map(|item| { super::export_activity_cell(&item).map(|cell| Arc::new(cell) as Arc) }) @@ -220,7 +221,7 @@ fn persisted_web_and_image_activity_preserves_full_details() { serde_json::json!({ "type": "imageView", "id": "image", "path": "C:\\workspace\\assets\\screenshot.png" }), - ].into_iter().map(|item| serde_json::from_value::(item).expect("valid thread item")); + ].into_iter().map(|item| serde_json::from_value::(item).expect("valid thread item")); let cells = crate::thread_transcript::thread_items_to_transcript_cells( /*thread_id*/ None, &codex_utils_absolute_path::AbsolutePathBuf::current_dir().expect("cwd"), diff --git a/codex-rs/tui/src/app/turn_tips.rs b/codex-rs/tui/src/app/turn_tips.rs index f3f481b755..caf751a65e 100644 --- a/codex-rs/tui/src/app/turn_tips.rs +++ b/codex-rs/tui/src/app/turn_tips.rs @@ -44,8 +44,8 @@ pub(super) enum TipSurface { Completion, } -fn is_final_answer(item: &ThreadItem) -> bool { - matches!(item, ThreadItem::AgentMessage { text, phase: Some(MessagePhase::FinalAnswer) | None, .. } +fn is_final_answer(item: &ThreadItemPayload) -> bool { + matches!(item, ThreadItemPayload::AgentMessage { text, phase: Some(MessagePhase::FinalAnswer) | None, .. } if !text.trim().is_empty()) } @@ -84,14 +84,18 @@ impl TurnTips { if item.thread_id == current.thread_id.to_string() && item.turn_id == current.turn_id => { - current.has_final_answer |= is_final_answer(&item.item); + current.has_final_answer |= is_final_answer(&item.item.payload); } ServerNotification::TurnCompleted(completed) if completed.thread_id == current.thread_id.to_string() && completed.turn.id == current.turn_id && matches!(current.phase, Phase::Working) => { - current.has_final_answer |= completed.turn.items.iter().any(is_final_answer); + current.has_final_answer |= completed + .turn + .items + .iter() + .any(|entry| is_final_answer(&entry.payload)); if completed.turn.status == TurnStatus::Completed && current.has_final_answer && !current.shown diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index fb8babd3d1..f3e5d5a844 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -44,7 +44,7 @@ use crate::pager_overlay::Overlay; use crate::pager_overlay::TranscriptHistoryState; use crate::tui; use crate::tui::TuiEvent; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; use codex_protocol::ThreadId; @@ -539,12 +539,12 @@ pub(crate) fn backtrack_revert_before_turn_id( &previous.items }; is_hidden_nested_review_turn(previous, turn) - && previous_items - .iter() - .any(|item| matches!(item, ThreadItem::EnteredReviewMode { .. })) - && previous_items - .iter() - .any(|item| matches!(item, ThreadItem::ExitedReviewMode { .. })) + && previous_items.iter().any(|item| { + matches!(&item.payload, ThreadItemPayload::EnteredReviewMode { .. }) + }) + && previous_items.iter().any(|item| { + matches!(&item.payload, ThreadItemPayload::ExitedReviewMode { .. }) + }) }); let first_item = if turn_index == start_turn { start_item @@ -553,21 +553,22 @@ pub(crate) fn backtrack_revert_before_turn_id( }; // Clipped items affect whether an input is a steer, but were never displayed and // therefore must not change the transcript's review visibility or prompt ordinals. - let mut user_messages_in_turn = turn.items[..first_item] - .iter() - .filter(|item| matches!(item, ThreadItem::UserMessage { .. })) + let mut user_messages_in_turn = turn + .item_payloads() + .take(first_item) + .filter(|item| matches!(item, ThreadItemPayload::UserMessage { .. })) .count(); - for item in &turn.items[first_item..] { + for item in turn.item_payloads().skip(first_item) { let content = match item { - ThreadItem::EnteredReviewMode { .. } => { + ThreadItemPayload::EnteredReviewMode { .. } => { review_mode = true; continue; } - ThreadItem::ExitedReviewMode { .. } => { + ThreadItemPayload::ExitedReviewMode { .. } => { review_mode = false; continue; } - ThreadItem::UserMessage { content, .. } => content, + ThreadItemPayload::UserMessage { content, .. } => content, _ => continue, }; let is_steer = user_messages_in_turn > 0; @@ -625,19 +626,17 @@ pub(crate) fn is_hidden_nested_review_turn(previous: &Turn, turn: &Turn) -> bool || turn.status != TurnStatus::Interrupted || turn.completed_at.is_some() || !previous - .items - .iter() - .any(|item| matches!(item, ThreadItem::EnteredReviewMode { .. })) + .item_payloads() + .any(|item| matches!(item, ThreadItemPayload::EnteredReviewMode { .. })) || !previous - .items - .iter() - .any(|item| matches!(item, ThreadItem::ExitedReviewMode { .. })) + .item_payloads() + .any(|item| matches!(item, ThreadItemPayload::ExitedReviewMode { .. })) { return false; } - let mut user_messages = turn.items.iter().filter_map(|item| match item { - ThreadItem::UserMessage { content, .. } => Some(content), + let mut user_messages = turn.item_payloads().filter_map(|item| match item { + ThreadItemPayload::UserMessage { content, .. } => Some(content), _ => None, }); matches!( @@ -722,6 +721,7 @@ mod tests { use crate::bottom_pane::MentionBinding; use crate::history_cell::AgentMessageCell; use crate::history_cell::HistoryCell; + use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::UserInput; use pretty_assertions::assert_eq; use ratatui::prelude::Line; @@ -745,7 +745,7 @@ mod tests { id: turn_id.to_string(), root_turn_id: None, items: (0..user_messages) - .map(|index| ThreadItem::UserMessage { + .map(|index| ThreadItemPayload::UserMessage { id: format!("user-{index}"), client_id: None, content: vec![UserInput::Text { @@ -753,6 +753,7 @@ mod tests { text_elements: Vec::new(), }], }) + .map(ThreadItem::untimed) .collect(), items_view: codex_app_server_protocol::TurnItemsView::Full, status, @@ -776,7 +777,7 @@ mod tests { #[test] fn backtrack_revert_before_turn_id_resolves_first_and_later_prompts() { let mut hidden = turn("hidden", TurnStatus::Completed, /*user_messages*/ 1); - let ThreadItem::UserMessage { content, .. } = &mut hidden.items[0] else { + let ThreadItemPayload::UserMessage { content, .. } = &mut hidden.items[0].payload else { unreachable!() }; *content = vec![UserInput::Text { @@ -917,15 +918,17 @@ mod tests { ); review_turn.items.insert( /*index*/ 0, - ThreadItem::EnteredReviewMode { + ThreadItem::untimed(ThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: "changes against main".to_string(), - }, + }), ); - review_turn.items.push(ThreadItem::ExitedReviewMode { - id: "review-end".to_string(), - review: "review complete".to_string(), - }); + review_turn + .items + .push(ThreadItem::untimed(ThreadItemPayload::ExitedReviewMode { + id: "review-end".to_string(), + review: "review complete".to_string(), + })); let turns = vec![ turn("turn-1", TurnStatus::Completed, /*user_messages*/ 1), review_turn, @@ -963,14 +966,14 @@ mod tests { "Review the current code changes (staged, unstaged, and untracked files)."; let review_turn = Turn { items: vec![ - ThreadItem::EnteredReviewMode { + ThreadItem::untimed(ThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: review_hint.to_string(), - }, - ThreadItem::ExitedReviewMode { + }), + ThreadItem::untimed(ThreadItemPayload::ExitedReviewMode { id: "review-end".to_string(), review: "review complete".to_string(), - }, + }), ], ..turn( "turn-review", @@ -980,7 +983,7 @@ mod tests { }; let review_child_turn = Turn { items: (0..2) - .map(|index| ThreadItem::UserMessage { + .map(|index| ThreadItemPayload::UserMessage { id: format!("review-prompt-{index}"), client_id: None, content: vec![UserInput::Text { @@ -988,6 +991,7 @@ mod tests { text_elements: Vec::new(), }], }) + .map(ThreadItem::untimed) .collect(), ..turn( "turn-review-child", @@ -1029,7 +1033,7 @@ mod tests { #[test] fn backtrack_revert_before_turn_id_restores_canonical_mention_bindings() { let mut selected_turn = turn("turn-2", TurnStatus::Completed, /*user_messages*/ 1); - selected_turn.items = vec![ThreadItem::UserMessage { + selected_turn.items = vec![ThreadItem::untimed(ThreadItemPayload::UserMessage { id: "selected-prompt".to_string(), client_id: None, content: vec![ @@ -1050,7 +1054,7 @@ mod tests { path: "app://google_calendar".to_string(), }, ], - }]; + })]; let turns = vec![ turn("turn-1", TurnStatus::Completed, /*user_messages*/ 1), selected_turn, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 2bc1ef9a1a..9d0166a7e7 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -2684,6 +2684,7 @@ mod tests { use crate::legacy_core::config::ConfigOverrides; use app_test_support::create_fake_paginated_rollout; use app_test_support::create_fake_rollout; + use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; @@ -4127,7 +4128,7 @@ mod tests { assert_eq!(regular.turns.len(), 1); assert!(matches!( regular.turns[0].items.as_slice(), - [codex_app_server_protocol::ThreadItem::UserMessage { content, .. }] + [codex_app_server_protocol::ThreadItem { payload: codex_app_server_protocol::ThreadItemPayload::UserMessage { content, .. }, .. }] if content == &[UserInput::Text { text: "Saved user message".to_string(), text_elements: Vec::new(), @@ -4261,22 +4262,26 @@ mod tests { root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ - codex_app_server_protocol::ThreadItem::UserMessage { - id: "user-1".to_string(), - client_id: None, - content: vec![codex_app_server_protocol::UserInput::Text { - text: "hello from history".to_string(), - text_elements: Vec::new(), - }], - }, - codex_app_server_protocol::ThreadItem::AgentMessage { - id: "assistant-1".to_string(), - text: "assistant reply".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - }, + ThreadItem::untimed( + codex_app_server_protocol::ThreadItemPayload::UserMessage { + id: "user-1".to_string(), + client_id: None, + content: vec![codex_app_server_protocol::UserInput::Text { + text: "hello from history".to_string(), + text_elements: Vec::new(), + }], + }, + ), + ThreadItem::untimed( + codex_app_server_protocol::ThreadItemPayload::AgentMessage { + id: "assistant-1".to_string(), + text: "assistant reply".to_string(), + phase: None, + memory_citation: None, + delivery: None, + questions: None, + }, + ), ], status: TurnStatus::Completed, error: None, diff --git a/codex-rs/tui/src/app_server_session/history.rs b/codex-rs/tui/src/app_server_session/history.rs index 55ce6daf35..956bb96372 100644 --- a/codex-rs/tui/src/app_server_session/history.rs +++ b/codex-rs/tui/src/app_server_session/history.rs @@ -1,5 +1,6 @@ //! Bounded app-server transcript loading for resume, fork, and transcript views. +use codex_app_server_protocol::ThreadItem; use std::collections::HashSet; use super::AppServerSession; @@ -13,7 +14,7 @@ use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::SortDirection; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListCursor; use codex_app_server_protocol::ThreadItemsListParams; use codex_app_server_protocol::ThreadItemsListResponse; @@ -208,7 +209,7 @@ impl AppServerSession { cursor: &str, page: ThreadItemsListResponse, turns: &mut Vec, - ) -> Result> { + ) -> Result> { if !self.is_older_history_page_pending(thread_id, cursor) { return Ok(Vec::new()); } @@ -268,7 +269,7 @@ impl AppServerSession { page: ThreadItemsListResponse, state: &mut ThreadHistoryPagination, turns: &mut Vec, - ) -> Result> { + ) -> Result> { state.next_item_cursor = advancing_cursor( state.next_item_cursor.as_deref(), page.next_cursor, @@ -305,8 +306,8 @@ impl AppServerSession { if let Some(turn) = turns.iter_mut().find(|turn| turn.id == entry.turn_id) && !turn.items.iter().any(|item| item.id() == entry.item.id()) { - items.push(entry.item.clone()); - turn.items.insert(/*index*/ 0, entry.item); + items.push(entry.item.payload.clone()); + turn.items.insert(/*index*/ 0, ThreadItem::from(entry)); turn.items_view = TurnItemsView::Summary; } } @@ -408,7 +409,7 @@ impl AppServerSession { fn rendered_history_rows( thread_id: ThreadId, thread: &Thread, - items: Vec, + items: Vec, config: &Config, local_settings: &crate::local_settings::LocalSettings, width: u16, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4b9d285d29..5cf8b6ac3d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -106,7 +106,7 @@ use codex_app_server_protocol::SkillMetadata; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::ThreadGoal as AppThreadGoal; use codex_app_server_protocol::ThreadGoalStatus as AppThreadGoalStatus; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSettings; use codex_app_server_protocol::ThreadSettingsUpdatedNotification; use codex_app_server_protocol::ThreadTokenUsage; diff --git a/codex-rs/tui/src/chatwidget/command_lifecycle.rs b/codex-rs/tui/src/chatwidget/command_lifecycle.rs index a0533b315e..c65cae1fec 100644 --- a/codex-rs/tui/src/chatwidget/command_lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/command_lifecycle.rs @@ -18,8 +18,8 @@ impl ChatWidget { self.restore_reasoning_status_header(); } - pub(super) fn on_command_execution_started(&mut self, item: ThreadItem) { - let ThreadItem::CommandExecution { + pub(super) fn on_command_execution_started(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::CommandExecution { id, command, process_id, @@ -132,8 +132,8 @@ impl ChatWidget { } } - pub(super) fn on_command_execution_completed(&mut self, item: ThreadItem) { - let ThreadItem::CommandExecution { + pub(super) fn on_command_execution_completed(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::CommandExecution { id, process_id, source, @@ -240,8 +240,8 @@ impl ChatWidget { } } - pub(crate) fn handle_command_execution_started_now(&mut self, item: ThreadItem) { - let ThreadItem::CommandExecution { + pub(crate) fn handle_command_execution_started_now(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::CommandExecution { id, command, source, @@ -319,7 +319,7 @@ impl ChatWidget { /// standalone history entry instead of replacing or flushing the unrelated active exploring /// cell. If this method treated every unknown end as "complete the active cell", the UI could /// merge unrelated commands and hide still-running exploring work. - pub(crate) fn handle_command_execution_completed_now(&mut self, item: ThreadItem) { + pub(crate) fn handle_command_execution_completed_now(&mut self, item: ThreadItemPayload) { enum ExecEndTarget { // Normal case: the active exec cell already tracks this call id. ActiveTracked, @@ -330,7 +330,7 @@ impl ChatWidget { NewCell, } - let ThreadItem::CommandExecution { + let ThreadItemPayload::CommandExecution { id, command, process_id: _, diff --git a/codex-rs/tui/src/chatwidget/dynamic_activity.rs b/codex-rs/tui/src/chatwidget/dynamic_activity.rs index 348031a326..ec2551258a 100644 --- a/codex-rs/tui/src/chatwidget/dynamic_activity.rs +++ b/codex-rs/tui/src/chatwidget/dynamic_activity.rs @@ -3,10 +3,10 @@ use super::*; impl ChatWidget { - pub(super) fn on_dynamic_tool_item(&mut self, item: ThreadItem) { + pub(super) fn on_dynamic_tool_item(&mut self, item: ThreadItemPayload) { let started = matches!( &item, - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { status: codex_app_server_protocol::DynamicToolCallStatus::InProgress, .. } @@ -24,8 +24,8 @@ impl ChatWidget { ); } - pub(crate) fn handle_dynamic_tool_item_now(&mut self, item: ThreadItem) { - let ThreadItem::DynamicToolCall { id, .. } = &item else { + pub(crate) fn handle_dynamic_tool_item_now(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::DynamicToolCall { id, .. } = &item else { return; }; let id = id.clone(); diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 2adf454870..7b1d1b6802 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -9,7 +9,7 @@ use crate::approval_events::ApplyPatchApprovalRequestEvent; use crate::approval_events::ExecApprovalRequestEvent; use codex_app_server_protocol::McpServerElicitationRequestParams; use codex_app_server_protocol::RequestId as AppServerRequestId; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_protocol::request_permissions::RequestPermissionsEvent; @@ -25,8 +25,8 @@ pub(crate) enum QueuedInterrupt { }, RequestPermissions(RequestPermissionsEvent), RequestUserInput(ToolRequestUserInputParams), - ItemStarted(ThreadItem), - ItemCompleted(ThreadItem), + ItemStarted(ThreadItemPayload), + ItemCompleted(ThreadItemPayload), } #[derive(Default)] @@ -87,11 +87,11 @@ impl InterruptManager { self.queue.push_back(QueuedInterrupt::RequestUserInput(ev)); } - pub(crate) fn push_item_started(&mut self, item: ThreadItem) { + pub(crate) fn push_item_started(&mut self, item: ThreadItemPayload) { self.queue.push_back(QueuedInterrupt::ItemStarted(item)); } - pub(crate) fn push_item_completed(&mut self, item: ThreadItem) { + pub(crate) fn push_item_completed(&mut self, item: ThreadItemPayload) { self.queue.push_back(QueuedInterrupt::ItemCompleted(item)); } @@ -174,7 +174,7 @@ mod tests { use crate::approval_events::ExecApprovalRequestEvent; use codex_app_server_protocol::CommandExecutionSource; use codex_app_server_protocol::CommandExecutionStatus; - use codex_app_server_protocol::ThreadItem; + use codex_app_server_protocol::ThreadItemPayload; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -209,8 +209,8 @@ mod tests { } } - fn command_execution(call_id: &str) -> ThreadItem { - ThreadItem::CommandExecution { + fn command_execution(call_id: &str) -> ThreadItemPayload { + ThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: call_id.to_string(), diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index 5bd2254d86..0cb7a7a984 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -437,11 +437,10 @@ impl ChatWidget { let last_agent_message = notification .turn - .items - .iter() + .item_payloads() .rev() .find_map(|item| match item { - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id, text, phase: Some(MessagePhase::FinalAnswer) | None, @@ -557,19 +556,19 @@ impl ChatWidget { replay_kind: Option, ) { self.restore_realtime_transcripts_before_turn(¬ification.turn_id); - match notification.item { - ThreadItem::UserMessage { content, .. } if replay_kind.is_none() => { + match notification.item.payload { + ThreadItemPayload::UserMessage { content, .. } if replay_kind.is_none() => { self.note_realtime_user_item_started(¬ification.turn_id, &content); } - ThreadItem::UserMessage { content, .. } + ThreadItemPayload::UserMessage { content, .. } if realtime::realtime_delegation_input(&content).is_some() => { self.remember_realtime_delegated_reasoning_turn(¬ification.turn_id); } - ThreadItem::AgentMessage { id, .. } if replay_kind.is_none() => { + ThreadItemPayload::AgentMessage { id, .. } if replay_kind.is_none() => { self.is_realtime_delegated_agent_item(¬ification.turn_id, &id); } - ThreadItem::Reasoning { id, .. } => { + ThreadItemPayload::Reasoning { id, .. } => { if replay_kind.is_none() && !self.is_realtime_delegated_reasoning_turn(¬ification.turn_id) { @@ -586,7 +585,7 @@ impl ChatWidget { self.on_reasoning_item_started(id); } } - ThreadItem::ContextCompaction { id } + ThreadItemPayload::ContextCompaction { id } if !matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) => { // Buffered starts reconstruct an in-flight compaction when switching tasks. @@ -601,19 +600,21 @@ impl ChatWidget { }; self.on_context_compaction_started(id, elapsed); } - item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_started(item), - ThreadItem::FileChange { id: _, changes, .. } => { + item @ ThreadItemPayload::CommandExecution { .. } => { + self.on_command_execution_started(item) + } + ThreadItemPayload::FileChange { id: _, changes, .. } => { self.on_patch_apply_begin(file_update_changes_to_display(changes)); } - item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_started(item), - item @ ThreadItem::DynamicToolCall { .. } => self.on_dynamic_tool_item(item), - ThreadItem::WebSearch(item) => { + item @ ThreadItemPayload::McpToolCall { .. } => self.on_mcp_tool_call_started(item), + item @ ThreadItemPayload::DynamicToolCall { .. } => self.on_dynamic_tool_item(item), + ThreadItemPayload::WebSearch(item) => { self.on_web_search_begin(item.id); } - ThreadItem::ImageGeneration(_) => { + ThreadItemPayload::ImageGeneration(_) => { self.on_image_generation_begin(); } - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -623,7 +624,7 @@ impl ChatWidget { model, reasoning_effort, agents_states, - } => self.on_collab_agent_tool_call(ThreadItem::CollabAgentToolCall { + } => self.on_collab_agent_tool_call(ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -634,7 +635,7 @@ impl ChatWidget { reasoning_effort, agents_states, }), - ThreadItem::EnteredReviewMode { review, .. } if replay_kind.is_none() => { + ThreadItemPayload::EnteredReviewMode { review, .. } if replay_kind.is_none() => { self.enter_review_mode_with_hint(review, /*from_replay*/ false); } _ => {} @@ -649,8 +650,8 @@ impl ChatWidget { self.restore_realtime_transcripts_before_turn(¬ification.turn_id); if replay_kind.is_none() && self.is_realtime_delegated_reasoning_turn(¬ification.turn_id) - && realtime::is_private_realtime_agent_item(¬ification.item) - && !matches!(¬ification.item, ThreadItem::AgentMessage { id, .. } | ThreadItem::Reasoning { id, .. } + && realtime::is_private_realtime_agent_item(¬ification.item.payload) + && !matches!(¬ification.item.payload, ThreadItemPayload::AgentMessage { id, .. } | ThreadItemPayload::Reasoning { id, .. } if matches!( self.realtime_conversation.agent_items.get(&(notification.turn_id.clone(), id.clone())), Some(realtime::RealtimeAgentItemOrigin::Typed) @@ -660,22 +661,24 @@ impl ChatWidget { } // Buffered live notifications can introduce questions; historical turn replay cannot. if replay_kind == Some(ReplayKind::ThreadSnapshot) - && let ThreadItem::AgentMessage { + && let ThreadItemPayload::AgentMessage { id, questions: Some(questions), .. - } = ¬ification.item + } = ¬ification.item.payload { self.add_async_questions(id, questions); } if replay_kind.is_none() - && let ThreadItem::Reasoning { id, .. } = ¬ification.item + && let ThreadItemPayload::Reasoning { id, .. } = ¬ification.item.payload && self.status_state.reasoning_item_id.as_ref() != Some(id) { return; } - match notification.item { - item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_completed(item), + match notification.item.payload { + item @ ThreadItemPayload::CommandExecution { .. } => { + self.on_command_execution_completed(item) + } item => self.handle_thread_item( item, notification.turn_id, diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 513f40276d..fc794186a3 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -21,7 +21,7 @@ use crate::bottom_pane::VoiceStripPhase; use crate::bottom_pane::VoiceStripState; use crate::history_cell; use crate::motion::MotionMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::UserInput; use codex_protocol::ThreadId; use codex_protocol::models::MessagePhase; @@ -72,7 +72,7 @@ struct PendingRealtimeSpeech { input_generation: u64, thread_id: ThreadId, turn_id: String, - item: ThreadItem, + item: ThreadItemPayload, } #[derive(Clone)] @@ -90,7 +90,7 @@ enum PendingSpeechState { Accepted, } -fn can_retain_realtime_speech(turn_id: &str, item: &ThreadItem) -> bool { +fn can_retain_realtime_speech(turn_id: &str, item: &ThreadItemPayload) -> bool { turn_id.len() <= MAX_PENDING_SPEECH_TURN_ID_BYTES && serde_json::to_vec(item) .is_ok_and(|encoded| encoded.len() <= MAX_PENDING_SPEECH_ITEM_BYTES) @@ -222,11 +222,11 @@ pub(crate) fn realtime_delegation_input(items: &[UserInput]) -> Option<&str> { input.split_once("").map(|(input, _)| input) } -pub(crate) fn is_private_realtime_agent_item(item: &ThreadItem) -> bool { - if matches!(item, ThreadItem::Reasoning { .. }) { +pub(crate) fn is_private_realtime_agent_item(item: &ThreadItemPayload) -> bool { + if matches!(item, ThreadItemPayload::Reasoning { .. }) { return true; } - let ThreadItem::AgentMessage { text, phase, .. } = item else { + let ThreadItemPayload::AgentMessage { text, phase, .. } = item else { return false; }; let text = text.trim_start(); @@ -836,10 +836,10 @@ impl ChatWidget { pub(super) fn complete_realtime_delegated_agent_item( &mut self, turn_id: &str, - item: &ThreadItem, + item: &ThreadItemPayload, from_replay: bool, ) -> bool { - let ThreadItem::AgentMessage { + let ThreadItemPayload::AgentMessage { id: item_id, text, phase, @@ -929,8 +929,12 @@ impl ChatWidget { true } - pub(super) fn speak_completed_realtime_delegation(&mut self, turn_id: &str, item: &ThreadItem) { - let ThreadItem::AgentMessage { + pub(super) fn speak_completed_realtime_delegation( + &mut self, + turn_id: &str, + item: &ThreadItemPayload, + ) { + let ThreadItemPayload::AgentMessage { id: item_id, text, .. } = item else { @@ -1004,7 +1008,7 @@ impl ChatWidget { .iter_mut() .position(|pending| { pending.turn_id == turn_id - && matches!(&pending.item, ThreadItem::AgentMessage { id, .. } if id == item_id) + && matches!(&pending.item, ThreadItemPayload::AgentMessage { id, .. } if id == item_id) && pending.state == PendingSpeechState::AwaitingTurn }); let index = match index { @@ -1060,7 +1064,7 @@ impl ChatWidget { pub(crate) fn take_undelivered_realtime_speech_for_replay( &mut self, - ) -> Vec<(ThreadId, String, ThreadItem)> { + ) -> Vec<(ThreadId, String, ThreadItemPayload)> { let input_generation = self.realtime_conversation.input_generation; self.realtime_conversation .pending_speech @@ -1161,7 +1165,7 @@ impl ChatWidget { &mut self, thread_id: ThreadId, turn_id: String, - item: ThreadItem, + item: ThreadItemPayload, ) { if self.thread_id() == Some(thread_id) { self.handle_thread_item(item, turn_id, super::ThreadItemRenderSource::Live); @@ -1251,7 +1255,7 @@ impl ChatWidget { self.realtime_conversation.pending_speech.retain(|pending| { pending.turn_id != turn_id || pending.state != PendingSpeechState::AwaitingTurn - || !matches!(&pending.item, ThreadItem::AgentMessage { id, .. } if id == item_id) + || !matches!(&pending.item, ThreadItemPayload::AgentMessage { id, .. } if id == item_id) }); } @@ -1523,7 +1527,7 @@ impl ChatWidget { !delivery.captioned && delivery.input_generation == caption_generation && delivery.state != PendingSpeechState::AwaitingTurn - && matches!(&delivery.item, ThreadItem::AgentMessage { text: answer, .. } + && matches!(&delivery.item, ThreadItemPayload::AgentMessage { text: answer, .. } if answer.trim().strip_prefix("[FINAL]").unwrap_or(answer.trim()) .split_whitespace().eq(text.split_whitespace())) }) diff --git a/codex-rs/tui/src/chatwidget/realtime_navigation.rs b/codex-rs/tui/src/chatwidget/realtime_navigation.rs index 5a87af15f8..6dbaaa137b 100644 --- a/codex-rs/tui/src/chatwidget/realtime_navigation.rs +++ b/codex-rs/tui/src/chatwidget/realtime_navigation.rs @@ -37,7 +37,7 @@ impl ChatWidget { .pending_speech .iter() .filter_map(|pending| match &pending.item { - codex_app_server_protocol::ThreadItem::AgentMessage { text, .. } => { + codex_app_server_protocol::ThreadItemPayload::AgentMessage { text, .. } => { Some(text.as_str()) } _ => None, diff --git a/codex-rs/tui/src/chatwidget/realtime_tests.rs b/codex-rs/tui/src/chatwidget/realtime_tests.rs index 0ada7c233c..b77f114f0d 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests.rs @@ -23,6 +23,7 @@ use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::ReasoningTextDeltaNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnItemsView; @@ -69,8 +70,8 @@ pub(crate) fn activate_voice_for_thread(chat: &mut ChatWidget, thread_id: Thread chat.realtime_conversation.latest_input_was_voice = true; } -fn user_item(text: &str) -> ThreadItem { - ThreadItem::UserMessage { +fn user_item(text: &str) -> ThreadItemPayload { + ThreadItemPayload::UserMessage { id: format!("user-{text}"), client_id: None, content: vec![UserInput::Text { @@ -80,8 +81,8 @@ fn user_item(text: &str) -> ThreadItem { } } -fn agent_item(item_id: &str, text: &str, phase: Option) -> ThreadItem { - ThreadItem::AgentMessage { +fn agent_item(item_id: &str, text: &str, phase: Option) -> ThreadItemPayload { + ThreadItemPayload::AgentMessage { id: item_id.to_string(), text: text.to_string(), phase, @@ -91,26 +92,32 @@ fn agent_item(item_id: &str, text: &str, phase: Option) -> ThreadI } } -fn start_item(chat: &mut ChatWidget, thread_id: ThreadId, turn_id: &str, item: ThreadItem) { +fn start_item(chat: &mut ChatWidget, thread_id: ThreadId, turn_id: &str, item: ThreadItemPayload) { chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: turn_id.to_string(), + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + turn_id.to_string(), item, - started_at_ms: 0, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); } -fn complete_item(chat: &mut ChatWidget, thread_id: ThreadId, turn_id: &str, item: ThreadItem) { +fn complete_item( + chat: &mut ChatWidget, + thread_id: ThreadId, + turn_id: &str, + item: ThreadItemPayload, +) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id.to_string(), - turn_id: turn_id.to_string(), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id.to_string(), + turn_id.to_string(), item, - completed_at_ms: 0, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -119,7 +126,7 @@ fn finish_turn( chat: &mut ChatWidget, thread_id: ThreadId, turn_id: &str, - items: Vec, + items: Vec, status: TurnStatus, ) { chat.handle_server_notification( @@ -128,7 +135,7 @@ fn finish_turn( turn: Turn { id: turn_id.to_string(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Summary, status, error: None, diff --git a/codex-rs/tui/src/chatwidget/realtime_tests/caption_replay.rs b/codex-rs/tui/src/chatwidget/realtime_tests/caption_replay.rs index c5b234de6b..3cc6910076 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests/caption_replay.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests/caption_replay.rs @@ -1,6 +1,7 @@ //! Replay and captions reconcile once across thread and session changes. use super::*; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; #[tokio::test] @@ -12,30 +13,30 @@ async fn replay_preserves_typed_updates_before_voice_steers_the_turn() { id: "typed-then-voice".into(), root_turn_id: None, items: vec![ - user_item("Typed question"), - agent_item( + ThreadItem::untimed(user_item("Typed question")), + ThreadItem::untimed(agent_item( "typed-update", "Checking the typed request", Some(MessagePhase::Commentary), - ), - ThreadItem::Reasoning { + )), + ThreadItem::untimed(ThreadItemPayload::Reasoning { id: "typed-reasoning".into(), summary: vec!["Typed reasoning summary".into()], content: Vec::new(), - }, - user_item( + }), + ThreadItem::untimed(user_item( "spoken correction", - ), - agent_item( + )), + ThreadItem::untimed(agent_item( "private-update", "Private voice commentary", Some(MessagePhase::Commentary), - ), - ThreadItem::Reasoning { + )), + ThreadItem::untimed(ThreadItemPayload::Reasoning { id: "private-reasoning".into(), summary: vec!["Private voice reasoning".into()], content: Vec::new(), - }, + }), ], items_view: TurnItemsView::Full, status: TurnStatus::Completed, @@ -81,7 +82,7 @@ async fn in_progress_voice_replay_restores_the_late_reasoning_guard() { vec![Turn { id: "saved-voice-turn".into(), root_turn_id: None, - items: vec![voice_request.clone()], + items: vec![ThreadItem::untimed(voice_request.clone())], items_view: TurnItemsView::Full, status: TurnStatus::InProgress, error: None, @@ -92,12 +93,12 @@ async fn in_progress_voice_replay_restores_the_late_reasoning_guard() { ReplayKind::ThreadSnapshot, ); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id.to_string(), - turn_id: "buffered-voice-turn".into(), - item: voice_request, - started_at_ms: 0, - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id.to_string(), + "buffered-voice-turn".into(), + voice_request, + /*started_at_ms*/ 0, + )), Some(ReplayKind::ThreadSnapshot), ); while events.try_recv().is_ok() {} @@ -116,7 +117,7 @@ async fn in_progress_voice_replay_restores_the_late_reasoning_guard() { &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "late-reasoning".into(), summary: vec!["private after switch".into()], content: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/realtime_tests/handoff_privacy.rs b/codex-rs/tui/src/chatwidget/realtime_tests/handoff_privacy.rs index 3afea6c54b..7a3b2a04ec 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests/handoff_privacy.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests/handoff_privacy.rs @@ -178,7 +178,7 @@ async fn voice_handoff_preserves_started_typed_reasoning_but_hides_new_reasoning &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "typed-reasoning".into(), summary: Vec::new(), content: Vec::new(), @@ -212,7 +212,7 @@ async fn voice_handoff_preserves_started_typed_reasoning_but_hides_new_reasoning &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "typed-reasoning".into(), summary: vec!["Typed beginning and typed tail".into()], content: Vec::new(), @@ -222,7 +222,7 @@ async fn voice_handoff_preserves_started_typed_reasoning_but_hides_new_reasoning &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "private-reasoning".into(), summary: Vec::new(), content: Vec::new(), @@ -242,7 +242,7 @@ async fn voice_handoff_preserves_started_typed_reasoning_but_hides_new_reasoning &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "private-reasoning".into(), summary: vec!["private after handoff".into()], content: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/realtime_tests/handoffs.rs b/codex-rs/tui/src/chatwidget/realtime_tests/handoffs.rs index cefc45dc85..94bd4484b3 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests/handoffs.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests/handoffs.rs @@ -180,7 +180,7 @@ async fn delegated_async_question_stays_local_and_expires_when_its_turn_ends() { turn_id, user_item("choose"), ); - let answer = ThreadItem::AgentMessage { + let answer = ThreadItemPayload::AgentMessage { id: "question-answer".into(), text: "Which option?".into(), phase: Some(MessagePhase::FinalAnswer), diff --git a/codex-rs/tui/src/chatwidget/realtime_tests/speech_recovery.rs b/codex-rs/tui/src/chatwidget/realtime_tests/speech_recovery.rs index d3c8c012f8..01b0a9b0ba 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests/speech_recovery.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests/speech_recovery.rs @@ -411,7 +411,7 @@ async fn delegated_reasoning_never_enters_live_history() { &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "reasoning".into(), summary: vec!["private summary".into()], content: vec!["private raw reasoning".into()], @@ -439,7 +439,7 @@ async fn delegated_reasoning_never_enters_live_history() { &mut chat, thread_id, turn_id, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "late-reasoning".into(), summary: vec!["private late summary".into()], content: Vec::new(), @@ -459,7 +459,7 @@ async fn delegated_reasoning_never_enters_live_history() { &mut chat, thread_id, "late-start-turn", - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "late-start-reasoning".into(), summary: vec!["private delayed start".into()], content: Vec::new(), @@ -508,7 +508,7 @@ async fn delegated_reasoning_never_enters_live_history() { } } assert!(!chat.is_realtime_delegated_reasoning_turn(turn_id)); - let typed_reasoning = ThreadItem::Reasoning { + let typed_reasoning = ThreadItemPayload::Reasoning { id: "typed-reasoning".into(), summary: vec!["ordinary summary".into()], content: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/replay.rs b/codex-rs/tui/src/chatwidget/replay.rs index 6c8a7fb7fb..fbe40068bc 100644 --- a/codex-rs/tui/src/chatwidget/replay.rs +++ b/codex-rs/tui/src/chatwidget/replay.rs @@ -54,13 +54,17 @@ impl ChatWidget { ServerNotification::ItemCompleted( codex_app_server_protocol::ItemCompletedNotification { turn_id, - item: ThreadItem::Reasoning { id, .. }, + item: + codex_app_server_protocol::ThreadItem { + payload: ThreadItemPayload::Reasoning { id, .. }, + .. + }, .. }, ) => (turn_id, id), ServerNotification::ItemStarted(item) if resumed_turn_id == item.turn_id - && !matches!(item.item, ThreadItem::UserMessage { .. }) + && !matches!(item.item.payload, ThreadItemPayload::UserMessage { .. }) && self.status_state.reasoning_item_id.as_deref() != Some(item.item.id()) => { // A new item closes a trailing snapshot that was already complete at resume. @@ -75,16 +79,16 @@ impl ChatWidget { return false; } self.restore_active_reasoning_item( - codex_app_server_protocol::ItemStartedNotification { - thread_id: self.thread_id.map(|id| id.to_string()).unwrap_or_default(), - turn_id: turn_id.clone(), - item: ThreadItem::Reasoning { + codex_app_server_protocol::ItemStartedNotification::new( + self.thread_id.map(|id| id.to_string()).unwrap_or_default(), + turn_id.clone(), + ThreadItemPayload::Reasoning { id: item_id.clone(), summary: Vec::new(), content: Vec::new(), }, - started_at_ms: 0, - }, + /*started_at_ms*/ 0, + ), /*parts*/ None, ); true @@ -127,16 +131,16 @@ impl ChatWidget { let Turn { id: turn_id, root_turn_id, - items_view: _, items, status, mut error, started_at, completed_at, duration_ms, + .. } = turn; let delegated = items.iter().any(|item| { - matches!(item, ThreadItem::UserMessage { content, .. } + matches!(&item.payload, ThreadItemPayload::UserMessage { content, .. } if realtime::realtime_delegation_input(content).is_some()) }); if matches!(status, TurnStatus::InProgress) { @@ -151,13 +155,13 @@ impl ChatWidget { let trailing_reasoning_id = (status == TurnStatus::InProgress) .then(|| items.last()) .flatten() - .and_then(|item| match item { - ThreadItem::Reasoning { id, .. } => Some(id.clone()), + .and_then(|item| match &item.payload { + ThreadItemPayload::Reasoning { id, .. } => Some(id.clone()), _ => None, }); let mut replaying_delegation = false; - for item in items { - if matches!(&item, ThreadItem::UserMessage { content, .. } + for item in items.into_iter().map(|entry| entry.payload) { + if matches!(&item, ThreadItemPayload::UserMessage { content, .. } if realtime::realtime_delegation_input(content).is_some()) { replaying_delegation = true; @@ -167,27 +171,29 @@ impl ChatWidget { if replaying_delegation && realtime::is_private_realtime_agent_item(&item) { continue; } - if hidden_nested_review_turn && matches!(item, ThreadItem::UserMessage { .. }) { + if hidden_nested_review_turn + && matches!(item, ThreadItemPayload::UserMessage { .. }) + { continue; } if trailing_reasoning_id.as_deref() == Some(item.id()) - && let ThreadItem::Reasoning { + && let ThreadItemPayload::Reasoning { id, summary, content, } = item { self.restore_active_reasoning_item( - codex_app_server_protocol::ItemStartedNotification { - thread_id: self.thread_id.map(|id| id.to_string()).unwrap_or_default(), - turn_id: turn_id.clone(), - item: ThreadItem::Reasoning { + codex_app_server_protocol::ItemStartedNotification::new( + self.thread_id.map(|id| id.to_string()).unwrap_or_default(), + turn_id.clone(), + ThreadItemPayload::Reasoning { id, summary: Vec::new(), content: Vec::new(), }, - started_at_ms: 0, - }, + /*started_at_ms*/ 0, + ), Some((summary, content)), ); } else { @@ -243,13 +249,13 @@ impl ChatWidget { pub(crate) fn replay_thread_item( &mut self, - item: ThreadItem, + item: ThreadItemPayload, turn_id: String, replay_kind: ReplayKind, ) { match item { // Snapshots contain the completed item, without the live start that renders its diff. - ThreadItem::FileChange { + ThreadItemPayload::FileChange { changes, status: codex_app_server_protocol::PatchApplyStatus::Completed, .. @@ -266,14 +272,14 @@ impl ChatWidget { pub(super) fn handle_thread_item( &mut self, - item: ThreadItem, + item: ThreadItemPayload, turn_id: String, render_source: ThreadItemRenderSource, ) { let from_replay = render_source.is_replay(); let replay_kind = render_source.replay_kind(); match item { - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { content, client_id, .. } => { if let Some(replies) = crate::async_question_reply::parse_input(&content) { @@ -296,7 +302,7 @@ impl ChatWidget { &turn_id, ); } - ThreadItem::AgentMessage { + ThreadItemPayload::AgentMessage { id, text, phase, @@ -307,7 +313,7 @@ impl ChatWidget { } => { if self.complete_realtime_delegated_agent_item( &turn_id, - &ThreadItem::AgentMessage { + &ThreadItemPayload::AgentMessage { id: id.clone(), text: text.clone(), phase: phase.clone(), @@ -348,8 +354,8 @@ impl ChatWidget { from_replay, ); } - ThreadItem::Plan { text, .. } => self.on_plan_item_completed(text), - ThreadItem::Reasoning { + ThreadItemPayload::Plan { text, .. } => self.on_plan_item_completed(text), + ThreadItemPayload::Reasoning { id, summary, content, @@ -382,29 +388,31 @@ impl ChatWidget { } self.on_agent_reasoning_final(); } - item @ ThreadItem::CommandExecution { + item @ ThreadItemPayload::CommandExecution { status: codex_app_server_protocol::CommandExecutionStatus::InProgress, .. } => self.on_command_execution_started(item), - item @ ThreadItem::CommandExecution { + item @ ThreadItemPayload::CommandExecution { source: ExecCommandSource::Agent | ExecCommandSource::UnifiedExecStartup, status: codex_app_server_protocol::CommandExecutionStatus::Completed | codex_app_server_protocol::CommandExecutionStatus::Failed, .. } if from_replay => self.handle_command_execution_completed_now(item), - item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_completed(item), - ThreadItem::FileChange { + item @ ThreadItemPayload::CommandExecution { .. } => { + self.on_command_execution_completed(item) + } + ThreadItemPayload::FileChange { status: codex_app_server_protocol::PatchApplyStatus::InProgress, .. } => {} - item @ ThreadItem::FileChange { .. } => self.on_file_change_completed(item), - item @ ThreadItem::McpToolCall { + item @ ThreadItemPayload::FileChange { .. } => self.on_file_change_completed(item), + item @ ThreadItemPayload::McpToolCall { status: codex_app_server_protocol::McpToolCallStatus::InProgress, .. } => self.on_mcp_tool_call_started(item), - item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_completed(item), - ThreadItem::WebSearch(item) => { + item @ ThreadItemPayload::McpToolCall { .. } => self.on_mcp_tool_call_completed(item), + ThreadItemPayload::WebSearch(item) => { self.on_web_search_begin(item.id.clone()); self.on_web_search_end( item.id, @@ -413,10 +421,10 @@ impl ChatWidget { .unwrap_or(codex_app_server_protocol::WebSearchAction::Other), ); } - ThreadItem::ImageView { id, path } => { + ThreadItemPayload::ImageView { id, path } => { self.on_view_image_tool_call(id, path); } - ThreadItem::ImageGeneration(item) => { + ThreadItemPayload::ImageGeneration(item) => { self.on_image_generation_end( item.id, item.status, @@ -424,18 +432,18 @@ impl ChatWidget { item.saved_path, ); } - ThreadItem::EnteredReviewMode { review, .. } => { + ThreadItemPayload::EnteredReviewMode { review, .. } => { if from_replay { self.enter_review_mode_with_hint(review, /*from_replay*/ true); } } - ThreadItem::ExitedReviewMode { .. } => { + ThreadItemPayload::ExitedReviewMode { .. } => { self.exit_review_mode_after_item(); } - ThreadItem::ContextCompaction { id } => { + ThreadItemPayload::ContextCompaction { id } => { self.on_context_compaction_completed(&id, from_replay); } - ThreadItem::FunctionCallOutput { + ThreadItemPayload::FunctionCallOutput { name, namespace, output, @@ -455,8 +463,8 @@ impl ChatWidget { )); } } - ThreadItem::HookPrompt { .. } => {} - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::HookPrompt { .. } => {} + ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -466,7 +474,7 @@ impl ChatWidget { model, reasoning_effort, agents_states, - } => self.on_collab_agent_tool_call(ThreadItem::CollabAgentToolCall { + } => self.on_collab_agent_tool_call(ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -477,9 +485,9 @@ impl ChatWidget { reasoning_effort, agents_states, }), - item @ ThreadItem::SubAgentActivity { .. } => self.on_sub_agent_activity(item), - item @ ThreadItem::DynamicToolCall { .. } => self.on_dynamic_tool_item(item), - ThreadItem::Sleep(_) => {} + item @ ThreadItemPayload::SubAgentActivity { .. } => self.on_sub_agent_activity(item), + item @ ThreadItemPayload::DynamicToolCall { .. } => self.on_dynamic_tool_item(item), + ThreadItemPayload::Sleep(_) => {} } if matches!(replay_kind, Some(ReplayKind::ThreadSnapshot)) && turn_id.is_empty() { diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index 3e78f3f4bd..90c06a91d2 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -355,8 +355,8 @@ impl ChatWidget { self.transcript.reset_turn_flags(); for turn in retained_turns { let mut replaying_delegation = false; - for item in &turn.items { - if matches!(item, ThreadItem::UserMessage { content, .. } + for item in turn.item_payloads() { + if matches!(item, ThreadItemPayload::UserMessage { content, .. } if realtime::realtime_delegation_input(content).is_some()) { replaying_delegation = true; @@ -365,11 +365,11 @@ impl ChatWidget { continue; } let (markdown, source) = match item { - ThreadItem::AgentMessage { text, .. } => ( + ThreadItemPayload::AgentMessage { text, .. } => ( parse_assistant_markdown(text, self.config.cwd.as_path()).visible_markdown, text, ), - ThreadItem::Plan { text, .. } => (text.clone(), text), + ThreadItemPayload::Plan { text, .. } => (text.clone(), text), _ => continue, }; if !markdown.trim().is_empty() { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 1cc51a1646..aae8906ab9 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -102,7 +102,7 @@ pub(super) use codex_app_server_protocol::ServerNotification; pub(super) use codex_app_server_protocol::SkillMetadata; pub(super) use codex_app_server_protocol::SkillSummary; pub(super) use codex_app_server_protocol::ThreadClosedNotification; -pub(super) use codex_app_server_protocol::ThreadItem as AppServerThreadItem; +pub(super) use codex_app_server_protocol::ThreadItemPayload as AppServerThreadItemPayload; pub(super) use codex_app_server_protocol::ToolRequestUserInputOption; pub(super) use codex_app_server_protocol::ToolRequestUserInputParams; pub(super) use codex_app_server_protocol::ToolRequestUserInputQuestion; diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 7017aca6c1..70f70c3cf3 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -1,6 +1,7 @@ use super::helpers::drain_insert_history_transcript; use super::*; use codex_app_server_protocol::AuthRecoveryNotification; +use codex_app_server_protocol::ThreadItem; use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErrorDetails; use pretty_assertions::assert_eq; @@ -734,11 +735,10 @@ async fn collab_spawn_end_shows_requested_model_and_effort() { ); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "call-spawn".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, status: AppServerCollabAgentToolCallStatus::InProgress, @@ -749,15 +749,15 @@ async fn collab_spawn_end_shows_requested_model_and_effort() { reasoning_effort: Some(ReasoningEffortConfig::High), agents_states: HashMap::new(), }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "call-spawn".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, status: AppServerCollabAgentToolCallStatus::Completed, @@ -774,7 +774,9 @@ async fn collab_spawn_end_shows_requested_model_and_effort() { }, )]), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -810,11 +812,10 @@ async fn live_app_server_user_message_item_completed_does_not_duplicate_rendered assert!(lines_to_single_string(&inserted[0]).contains("Hi, are you there?")); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::UserMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -822,7 +823,9 @@ async fn live_app_server_user_message_item_completed_does_not_duplicate_rendered text_elements: Vec::new(), }], }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -834,11 +837,10 @@ async fn live_app_server_user_message_omits_unsupported_media() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::UserMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::UserMessage { id: "user-1".to_string(), client_id: None, content: vec![ @@ -854,7 +856,9 @@ async fn live_app_server_user_message_omits_unsupported_media() { }, ], }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -895,7 +899,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( .expect("status indicator should be visible"); assert_eq!(status.header(), "Working"); - let item = AppServerThreadItem::AgentMessage { + let item = AppServerThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: "Yes. What do you need?".to_string(), phase: Some(MessagePhase::FinalAnswer), @@ -904,12 +908,13 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( questions: None, }; chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: item.clone(), - }), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + item.clone(), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -925,7 +930,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Summary, - items: vec![item], + items: vec![ThreadItem::untimed(item)], status: AppServerTurnStatus::Completed, error: None, started_at: None, @@ -1221,7 +1226,7 @@ async fn startup_config_warning_is_not_repeated_by_thread() { async fn live_app_server_file_change_item_started_preserves_changes() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - let item = AppServerThreadItem::FileChange { + let item = AppServerThreadItemPayload::FileChange { id: "patch-1".to_string(), changes: vec![FileUpdateChange { path: "foo.txt".to_string(), @@ -1231,23 +1236,23 @@ async fn live_app_server_file_change_item_started_preserves_changes() { status: AppServerPatchApplyStatus::InProgress, }; chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: item.clone(), - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + item.clone(), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected patch history to be rendered"); let transcript = lines_to_single_string(cells.last().expect("patch cell")); - let AppServerThreadItem::FileChange { id, changes, .. } = item else { + let AppServerThreadItemPayload::FileChange { id, changes, .. } = item else { unreachable!() }; chat.replay_thread_item( - AppServerThreadItem::FileChange { + AppServerThreadItemPayload::FileChange { id, changes, status: AppServerPatchApplyStatus::Completed, @@ -1272,11 +1277,10 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { shlex::try_join(["/bin/zsh", "-lc", script]).expect("round-trippable shell wrapper"); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: AppServerThreadItem::CommandExecution { + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -1294,15 +1298,15 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { exit_code: None, duration_ms: None, }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::CommandExecution { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "cmd-1".to_string(), @@ -1320,7 +1324,9 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { exit_code: Some(0), duration_ms: Some(5), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -1391,7 +1397,7 @@ async fn live_app_server_command_output_delta_transcript_snapshot() { #[tokio::test] async fn live_app_server_sub_agent_activity_renders_once() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - let activity = AppServerThreadItem::SubAgentActivity { + let activity = AppServerThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "activity-1".to_string(), @@ -1401,21 +1407,22 @@ async fn live_app_server_sub_agent_activity_renders_once() { }; chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: activity.clone(), - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + activity.clone(), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: activity, - }), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + activity, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -1446,11 +1453,10 @@ async fn live_app_server_collab_wait_items_render_history() { ); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "wait-1".to_string(), tool: AppServerCollabAgentTool::Wait, status: AppServerCollabAgentToolCallStatus::InProgress, @@ -1464,16 +1470,16 @@ async fn live_app_server_collab_wait_items_render_history() { reasoning_effort: None, agents_states: HashMap::new(), }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "wait-1".to_string(), tool: AppServerCollabAgentTool::Wait, status: AppServerCollabAgentToolCallStatus::Completed, @@ -1502,7 +1508,9 @@ async fn live_app_server_collab_wait_items_render_history() { ), ]), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -1523,11 +1531,10 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo ThreadId::from_string("019cff70-2599-75e2-af72-b91781b41a8e").expect("valid thread id"); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "spawn-1".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, status: AppServerCollabAgentToolCallStatus::InProgress, @@ -1538,16 +1545,16 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo reasoning_effort: Some(ReasoningEffortConfig::High), agents_states: HashMap::new(), }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::CollabAgentToolCall { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::CollabAgentToolCall { id: "spawn-1".to_string(), tool: AppServerCollabAgentTool::SpawnAgent, status: AppServerCollabAgentToolCallStatus::Completed, @@ -1564,7 +1571,9 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo }, )]), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -1697,7 +1706,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() { /*error*/ None, ); completed_turn.items_view = codex_app_server_protocol::TurnItemsView::Summary; - completed_turn.items = vec![AppServerThreadItem::AgentMessage { + completed_turn.items = vec![ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage { id: "msg-1".to_string(), text: concat!( "The transport kept this.\nAnd dropped this.\n\n", @@ -1711,7 +1720,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() { title: "Which way?".into(), options: None, }]), - }]; + })]; chat.handle_server_notification( ServerNotification::TurnCompleted(TurnCompletedNotification { thread_id: "thread-1".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/compaction_tests.rs b/codex-rs/tui/src/chatwidget/tests/compaction_tests.rs index 9143e7a539..291dc844f4 100644 --- a/codex-rs/tui/src/chatwidget/tests/compaction_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/compaction_tests.rs @@ -9,21 +9,22 @@ fn normalize_compaction_snapshot(text: String) -> String { } fn compaction_started(id: &str) -> ServerNotification { - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: chrono::Utc::now().timestamp_millis(), - item: AppServerThreadItem::ContextCompaction { id: id.to_string() }, - }) + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::ContextCompaction { id: id.to_string() }, + chrono::Utc::now().timestamp_millis(), + )) } fn compaction_completed(id: &str) -> ServerNotification { - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::ContextCompaction { id: id.to_string() }, - }) + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::ContextCompaction { id: id.to_string() }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )) } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs b/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs index 6838e3a6a4..44e5099d1f 100644 --- a/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs @@ -4,6 +4,7 @@ use super::*; use crate::clock_format::ClockFormat; use chrono::Local; use chrono::TimeZone; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; const COMPLETED_AT: i64 = 1_700_000_000; @@ -11,14 +12,16 @@ const COMPLETED_AT: i64 = 1_700_000_000; fn completed_turn(duration_ms: Option, completed_at: Option) -> AppServerTurn { AppServerTurn { completed_at, - items: vec![AppServerThreadItem::AgentMessage { - id: "answer-1".to_string(), - text: "The change is ready.".to_string(), - phase: Some(MessagePhase::FinalAnswer), - memory_citation: None, - delivery: None, - questions: None, - }], + items: vec![ThreadItem::untimed( + AppServerThreadItemPayload::AgentMessage { + id: "answer-1".to_string(), + text: "The change is ready.".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + delivery: None, + questions: None, + }, + )], ..app_server_turn( "turn-1", AppServerTurnStatus::Completed, diff --git a/codex-rs/tui/src/chatwidget/tests/computer_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/computer_activity_tests.rs index 4e7dca4ac2..e291b4206c 100644 --- a/codex-rs/tui/src/chatwidget/tests/computer_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/computer_activity_tests.rs @@ -4,8 +4,8 @@ use pretty_assertions::assert_eq; fn computer_item( id: &str, status: codex_app_server_protocol::McpToolCallStatus, -) -> AppServerThreadItem { - AppServerThreadItem::McpToolCall { +) -> AppServerThreadItemPayload { + AppServerThreadItemPayload::McpToolCall { id: id.to_string(), server: "cua_repl".to_string(), tool: "js".to_string(), @@ -83,7 +83,7 @@ async fn computer_activity_keeps_reasoning_in_order_live_and_replayed() { let summary = format!("{part} action {id}"); if replay { chat.replay_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: summary.clone(), summary: vec![summary], content: Vec::new(), @@ -133,7 +133,7 @@ async fn computer_activity_preserves_boundaries_and_expanded_output() { assert!(lines_to_single_string(&cells[0]).contains("Used computer ยท 1 action")); chat.on_mcp_tool_call_completed(computer_item("2", McpToolCallStatus::Completed)); let mut other = computer_item("other", McpToolCallStatus::Completed); - if let AppServerThreadItem::McpToolCall { server, .. } = &mut other { + if let AppServerThreadItemPayload::McpToolCall { server, .. } = &mut other { *server = "another_server".to_string(); } chat.on_mcp_tool_call_completed(other); diff --git a/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs index 72edc6cac4..b98edb77a4 100644 --- a/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs @@ -12,9 +12,9 @@ use ratatui::layout::Rect; use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; -fn dynamic_item(id: &str, status: DynamicToolCallStatus) -> AppServerThreadItem { +fn dynamic_item(id: &str, status: DynamicToolCallStatus) -> AppServerThreadItemPayload { let completed = status != DynamicToolCallStatus::InProgress; - AppServerThreadItem::DynamicToolCall { + AppServerThreadItemPayload::DynamicToolCall { id: id.to_string(), namespace: Some("test".to_string()), tool: "lookup".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 4e1127ba46..5d2f4693dc 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; #[tokio::test] @@ -52,17 +53,18 @@ async fn replayed_command_completion_preserves_tracking_without_duplicate_starts chat.on_task_started(); let mut item = begin_unified_exec_startup(&mut chat, "call-replay", "process-replay", "cat replay"); - if let AppServerThreadItem::CommandExecution { status, .. } = &mut item { + if let AppServerThreadItemPayload::CommandExecution { status, .. } = &mut item { *status = AppServerCommandExecutionStatus::Completed; } chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: String::new(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + String::new(), + "turn-1".to_string(), item, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), Some(ReplayKind::ThreadSnapshot), ); @@ -95,7 +97,7 @@ async fn replayed_completion_preserves_unrelated_running_command() { /*animations_enabled*/ false, ))); } - if let AppServerThreadItem::CommandExecution { + if let AppServerThreadItemPayload::CommandExecution { id, command, status, @@ -238,7 +240,7 @@ async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() { ("after-message", "cat final.txt", 0), ] { let command = vec!["bash".to_string(), "-lc".to_string(), script.to_string()]; - let mut item = AppServerThreadItem::CommandExecution { + let mut item = AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: id.to_string(), @@ -262,7 +264,7 @@ async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() { if !replay { handle_exec_begin(&mut chat, item.clone()); } - if let AppServerThreadItem::CommandExecution { status, .. } = &mut item { + if let AppServerThreadItemPayload::CommandExecution { status, .. } = &mut item { *status = if exit_code == 0 { AppServerCommandExecutionStatus::Completed } else { @@ -278,7 +280,7 @@ async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() { for summary in ["Inspecting the page", "Checking its contents"] { if replay { chat.replay_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: summary.to_string(), summary: vec![summary.to_string()], content: Vec::new(), @@ -332,8 +334,8 @@ async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() { async fn replayed_commands_preserve_individual_output_and_failure_status() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let cwd = chat.config.cwd.clone(); - let replayed_command = - |id: &str, output: &str, source: ExecCommandSource| AppServerThreadItem::CommandExecution { + let replayed_command = |id: &str, output: &str, source: ExecCommandSource| { + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: id.to_string(), @@ -348,21 +350,22 @@ async fn replayed_commands_preserve_individual_output_and_failure_status() { aggregated_output: Some(format!("{output}\n")), exit_code: Some(0), duration_ms: Some(5), - }; + } + }; let mut declined = replayed_command("call-declined", "declined", ExecCommandSource::Agent); let mut failed = replayed_command( "call-failed", "failure", ExecCommandSource::UnifiedExecStartup, ); - if let AppServerThreadItem::CommandExecution { + if let AppServerThreadItemPayload::CommandExecution { status, exit_code, .. } = &mut failed { *status = AppServerCommandExecutionStatus::Failed; *exit_code = Some(7); } - if let AppServerThreadItem::CommandExecution { + if let AppServerThreadItemPayload::CommandExecution { status, exit_code, .. } = &mut declined { @@ -371,14 +374,18 @@ async fn replayed_commands_preserve_individual_output_and_failure_status() { } let turn = AppServerTurn { items: vec![ - replayed_command("call-first", "first", ExecCommandSource::Agent), - replayed_command( + ThreadItem::untimed(replayed_command( + "call-first", + "first", + ExecCommandSource::Agent, + )), + ThreadItem::untimed(replayed_command( "call-second", "second", ExecCommandSource::UnifiedExecStartup, - ), - failed, - declined, + )), + ThreadItem::untimed(failed), + ThreadItem::untimed(declined), ], ..app_server_turn( "turn-1", @@ -530,7 +537,7 @@ async fn exec_end_without_begin_uses_event_command() { let cwd = chat.config.cwd.clone(); handle_exec_end( &mut chat, - AppServerThreadItem::CommandExecution { + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "call-orphan".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 75f63ce1ea..3ac0a5b811 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -632,20 +632,19 @@ pub(super) fn handle_agent_message_delta(chat: &mut ChatWidget, delta: impl Into pub(super) fn handle_agent_reasoning_started(chat: &mut ChatWidget, id: impl Into) { chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - started_at_ms: 0, - item: AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: id.into(), summary: Vec::new(), content: Vec::new(), }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -675,46 +674,45 @@ pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) { handle_agent_reasoning_started(chat, "reasoning-1"); } chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - completed_at_ms: 0, - item: AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: Vec::new(), content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Into) { chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - started_at_ms: 0, - item: AppServerThreadItem::EnteredReviewMode { + AppServerThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: review.into(), }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); } pub(super) fn replay_entered_review_mode(chat: &mut ChatWidget, review: impl Into) { chat.replay_thread_item( - AppServerThreadItem::EnteredReviewMode { + AppServerThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: review.into(), }, @@ -725,19 +723,19 @@ pub(super) fn replay_entered_review_mode(chat: &mut ChatWidget, review: impl Int pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - completed_at_ms: 0, - item: AppServerThreadItem::ExitedReviewMode { + AppServerThreadItemPayload::ExitedReviewMode { id: "review-end".to_string(), review: String::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -786,16 +784,16 @@ pub(super) fn handle_patch_apply_begin( changes: HashMap, ) { chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id(chat), - turn_id: turn_id.into(), - started_at_ms: 0, - item: AppServerThreadItem::FileChange { + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id(chat), + turn_id.into(), + AppServerThreadItemPayload::FileChange { id: call_id.into(), changes: file_update_changes_from_tui(changes), status: AppServerPatchApplyStatus::InProgress, }, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -808,16 +806,17 @@ pub(super) fn handle_patch_apply_end( status: AppServerPatchApplyStatus, ) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: turn_id.into(), - completed_at_ms: 0, - item: AppServerThreadItem::FileChange { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + turn_id.into(), + AppServerThreadItemPayload::FileChange { id: call_id.into(), changes: file_update_changes_from_tui(changes), status, }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -828,15 +827,16 @@ pub(super) fn handle_view_image_tool_call( path: impl Into, ) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::ImageView { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + "turn-1".to_string(), + AppServerThreadItemPayload::ImageView { id: call_id.into(), path: path.into(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -849,11 +849,10 @@ pub(super) fn handle_image_generation_end( saved_path: Option, ) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::ImageGeneration(ImageGenerationItem { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + "turn-1".to_string(), + AppServerThreadItemPayload::ImageGeneration(ImageGenerationItem { id: call_id.into(), status: status.into(), revised_prompt, @@ -867,7 +866,9 @@ pub(super) fn handle_image_generation_end( imagegen_request_id: None, generation_id: None, }), - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -879,7 +880,7 @@ pub(super) fn replay_user_message_inputs( replay_kind: ReplayKind, ) { chat.replay_thread_item( - AppServerThreadItem::UserMessage { + AppServerThreadItemPayload::UserMessage { id: item_id.to_string(), client_id: None, content, @@ -913,7 +914,7 @@ pub(super) fn replay_agent_message( replay_kind: ReplayKind, ) { chat.replay_thread_item( - AppServerThreadItem::AgentMessage { + AppServerThreadItemPayload::AgentMessage { id: item_id.to_string(), text: text.into(), phase: Some(MessagePhase::FinalAnswer), @@ -965,7 +966,7 @@ pub(super) fn begin_exec_with_source( call_id: &str, raw_cmd: &str, source: ExecCommandSource, -) -> AppServerThreadItem { +) -> AppServerThreadItemPayload { // Build the full command vec and parse it using core's parser, // then convert to protocol variants for the event payload. let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()]; @@ -973,7 +974,7 @@ pub(super) fn begin_exec_with_source( .into_iter() .map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd)) .collect(); - let item = AppServerThreadItem::CommandExecution { + let item = AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: call_id.to_string(), @@ -998,9 +999,9 @@ pub(super) fn begin_unified_exec_startup( call_id: &str, process_id: &str, raw_cmd: &str, -) -> AppServerThreadItem { +) -> AppServerThreadItemPayload { let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()]; - let item = AppServerThreadItem::CommandExecution { + let item = AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: call_id.to_string(), @@ -1020,18 +1021,17 @@ pub(super) fn begin_unified_exec_startup( item } -pub(super) fn handle_exec_begin(chat: &mut ChatWidget, item: AppServerThreadItem) { +pub(super) fn handle_exec_begin(chat: &mut ChatWidget, item: AppServerThreadItemPayload) { chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemStarted(ItemStartedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - started_at_ms: 0, item, - }), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -1067,11 +1067,10 @@ pub(super) fn complete_assistant_message( phase: Option, ) { chat.handle_server_notification( - 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 { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), + "turn-1".to_string(), + AppServerThreadItemPayload::AgentMessage { id: item_id.to_string(), text: text.to_string(), phase, @@ -1079,7 +1078,9 @@ pub(super) fn complete_assistant_message( delivery: None, questions: None, }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -1114,16 +1115,17 @@ pub(super) fn complete_user_message_for_inputs( content: Vec, ) { chat.handle_server_notification( - 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 { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), + "turn-1".to_string(), + AppServerThreadItemPayload::UserMessage { id: item_id.to_string(), client_id: None, content, }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -1205,13 +1207,13 @@ pub(super) fn begin_exec( chat: &mut ChatWidget, call_id: &str, raw_cmd: &str, -) -> AppServerThreadItem { +) -> AppServerThreadItemPayload { begin_exec_with_source(chat, call_id, raw_cmd, ExecCommandSource::Agent) } pub(super) fn end_exec( chat: &mut ChatWidget, - begin_item: AppServerThreadItem, + begin_item: AppServerThreadItemPayload, stdout: &str, stderr: &str, exit_code: i32, @@ -1221,7 +1223,7 @@ pub(super) fn end_exec( } else { format!("{stdout}{stderr}") }; - let AppServerThreadItem::CommandExecution { + let AppServerThreadItemPayload::CommandExecution { id, command, cwd, @@ -1237,7 +1239,7 @@ pub(super) fn end_exec( }; handle_exec_end( chat, - AppServerThreadItem::CommandExecution { + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id, @@ -1260,18 +1262,18 @@ pub(super) fn end_exec( ); } -pub(super) fn handle_exec_end(chat: &mut ChatWidget, item: AppServerThreadItem) { +pub(super) fn handle_exec_end(chat: &mut ChatWidget, item: AppServerThreadItemPayload) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: thread_id(chat), - turn_id: chat - .turn_lifecycle + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + thread_id(chat), + chat.turn_lifecycle .last_turn_id .clone() .unwrap_or_else(|| "turn-1".to_string()), - completed_at_ms: 0, item, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } diff --git a/codex-rs/tui/src/chatwidget/tests/history_projection.rs b/codex-rs/tui/src/chatwidget/tests/history_projection.rs index 30674a7167..edd451f65c 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_projection.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_projection.rs @@ -5,6 +5,7 @@ use crate::thread_transcript::RawReasoningVisibility; use crate::thread_transcript::thread_items_to_transcript_cells; use codex_app_server_protocol::McpToolCallResult; use codex_app_server_protocol::McpToolCallStatus; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; #[tokio::test] @@ -12,7 +13,7 @@ async fn older_tool_projection_matches_initial_replay() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; drain_insert_history(&mut rx); let command = "cargo check"; - let mut items = vec![AppServerThreadItem::CommandExecution { + let mut items = vec![AppServerThreadItemPayload::CommandExecution { id: "command".to_string(), plugin_id: None, script_path: None, @@ -38,7 +39,7 @@ async fn older_tool_projection_matches_initial_replay() { // Completion-only replay must group consecutive exploration just like older-page projection. for name in ["one.rs", "two.rs"] { let command = format!("cat {name}"); - items.push(AppServerThreadItem::CommandExecution { + items.push(AppServerThreadItemPayload::CommandExecution { id: name.to_string(), plugin_id: None, script_path: None, @@ -69,13 +70,13 @@ async fn older_tool_projection_matches_initial_replay() { ("cua_repl", "5"), ] { if id == "2" { - items.push(AppServerThreadItem::Reasoning { + items.push(AppServerThreadItemPayload::Reasoning { id: "computer-reasoning".to_string(), summary: vec!["**Inspecting the page**".to_string()], content: Vec::new(), }); } - items.push(AppServerThreadItem::McpToolCall { + items.push(AppServerThreadItemPayload::McpToolCall { id: id.to_string(), server: server.to_string(), tool: "js".to_string(), @@ -99,14 +100,14 @@ async fn older_tool_projection_matches_initial_replay() { // inside the active exploration group until the completed turn commits it. for (index, id) in [(1, "trailing-read-one"), (2, "trailing-read-two")] { if index == 2 { - items.push(AppServerThreadItem::Reasoning { + items.push(AppServerThreadItemPayload::Reasoning { id: "exploration-reasoning".to_string(), summary: vec!["**Checking the implementation**".to_string()], content: Vec::new(), }); } let mut read = items[index].clone(); - if let AppServerThreadItem::CommandExecution { id: read_id, .. } = &mut read { + if let AppServerThreadItemPayload::CommandExecution { id: read_id, .. } = &mut read { *read_id = id.to_string(); } items.push(read); @@ -120,7 +121,7 @@ async fn older_tool_projection_matches_initial_replay() { ); chat.replay_thread_turns( vec![AppServerTurn { - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), ..app_server_turn( "turn", AppServerTurnStatus::Completed, @@ -185,7 +186,7 @@ async fn snapshot_formatter_reasoning_matches_compact_and_detailed_replay() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; drain_insert_history(&mut rx); chat.config.show_raw_agent_reasoning = visibility == RawReasoningVisibility::Visible; - let item = AppServerThreadItem::Reasoning { + let item = AppServerThreadItemPayload::Reasoning { id: "reasoning".to_string(), summary: vec![ "**Plan**\n\nRead [src/main.rs](src/main.rs:3).".to_string(), @@ -222,7 +223,7 @@ async fn snapshot_formatter_reasoning_matches_compact_and_detailed_replay() { )); chat.replay_thread_turns( vec![AppServerTurn { - items: vec![item], + items: vec![ThreadItem::untimed(item)], ..app_server_turn( "turn", AppServerTurnStatus::Completed, @@ -250,7 +251,7 @@ fn raw_reasoning_keeps_its_own_heading() { let projected = thread_items_to_transcript_cells( /*thread_id*/ None, &codex_utils_absolute_path::AbsolutePathBuf::current_dir().unwrap(), - [AppServerThreadItem::Reasoning { + [AppServerThreadItemPayload::Reasoning { id: "raw".into(), summary: Vec::new(), content: vec!["**Raw investigation**\nKeep this heading and its details.".into()], @@ -312,21 +313,22 @@ async fn snapshot_formatter_live_and_buffered_patch_notifications_render_once() let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; drain_insert_history(&mut rx); chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread".to_string(), - turn_id: "turn".to_string(), - item: patch_item(AppServerPatchApplyStatus::InProgress), - started_at_ms: 0, - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread".to_string(), + "turn".to_string(), + patch_item(AppServerPatchApplyStatus::InProgress), + /*started_at_ms*/ 0, + )), replay_kind, ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread".to_string(), - turn_id: "turn".to_string(), - item: patch_item(AppServerPatchApplyStatus::Completed), - completed_at_ms: 0, - }), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".to_string(), + "turn".to_string(), + patch_item(AppServerPatchApplyStatus::Completed), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), replay_kind, ); let replayed = take_history_cells(&mut rx); @@ -343,7 +345,7 @@ async fn snapshot_formatter_empty_success_and_failed_patch_keep_their_outcomes() let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; drain_insert_history(&mut rx); chat.replay_thread_item( - AppServerThreadItem::FileChange { + AppServerThreadItemPayload::FileChange { id: "empty".to_string(), changes: Vec::new(), status: AppServerPatchApplyStatus::Completed, @@ -363,8 +365,8 @@ async fn snapshot_formatter_empty_success_and_failed_patch_keep_their_outcomes() "); } -fn patch_item(status: AppServerPatchApplyStatus) -> AppServerThreadItem { - AppServerThreadItem::FileChange { +fn patch_item(status: AppServerPatchApplyStatus) -> AppServerThreadItemPayload { + AppServerThreadItemPayload::FileChange { id: "patch".to_string(), changes: vec![FileUpdateChange { path: "sample.txt".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 8e1e0a525b..434aaa19cf 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -4,6 +4,7 @@ use crate::app_event::HistoryLookupResponse; use codex_app_server_protocol::ImageReference; use codex_app_server_protocol::NetworkAccess; use codex_app_server_protocol::SandboxPolicy; +use codex_app_server_protocol::ThreadItem; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::permissions::FileSystemAccessMode; @@ -131,14 +132,16 @@ async fn replayed_failed_turns_preserve_overload_warnings_between_retries() { let prompt = "The workspace also looks super confusing with its separator."; let error_message = "Selected model is at capacity. Please try a different model."; let failed_turn = |turn_id: &str, item_id: &str| AppServerTurn { - items: vec![AppServerThreadItem::UserMessage { - id: item_id.to_string(), - client_id: None, - content: vec![AppServerUserInput::Text { - text: prompt.to_string(), - text_elements: Vec::new(), - }], - }], + items: vec![ThreadItem::untimed( + AppServerThreadItemPayload::UserMessage { + id: item_id.to_string(), + client_id: None, + content: vec![AppServerUserInput::Text { + text: prompt.to_string(), + text_elements: Vec::new(), + }], + }, + )], ..app_server_turn( turn_id, AppServerTurnStatus::Failed, @@ -307,7 +310,7 @@ async fn replayed_review_prompt_does_not_seed_composer_history() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; chat.replay_thread_item( - AppServerThreadItem::EnteredReviewMode { + AppServerThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: "changes against main".to_string(), }, @@ -330,7 +333,7 @@ async fn replayed_review_prompt_does_not_seed_composer_history() { async fn replayed_delegated_tool_output_is_attributed_without_seeding_composer_history() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - let item = AppServerThreadItem::FunctionCallOutput { + let item = AppServerThreadItemPayload::FunctionCallOutput { id: "delegation-1".to_string(), name: "send_message_to_thread".to_string(), namespace: Some("codex_tui".to_string()), @@ -374,7 +377,7 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; let review_hint = "current changes"; let review_prompt = "Review the current code changes (staged, unstaged, and untracked files)."; - let user_message = |id: &str, text: &str| AppServerThreadItem::UserMessage { + let user_message = |id: &str, text: &str| AppServerThreadItemPayload::UserMessage { id: id.to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -384,14 +387,14 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() }; let review_marker = |turn_id: &str| AppServerTurn { items: vec![ - AppServerThreadItem::EnteredReviewMode { + ThreadItem::untimed(AppServerThreadItemPayload::EnteredReviewMode { id: format!("{turn_id}-start"), review: review_hint.to_string(), - }, - AppServerThreadItem::ExitedReviewMode { + }), + ThreadItem::untimed(AppServerThreadItemPayload::ExitedReviewMode { id: format!("{turn_id}-end"), review: "review complete".to_string(), - }, + }), ], ..app_server_turn( turn_id, @@ -406,8 +409,8 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() review_marker("turn-review-before-steer"), AppServerTurn { items: vec![ - user_message("interrupted-prompt", review_hint), - user_message("interrupted-steer", review_hint), + ThreadItem::untimed(user_message("interrupted-prompt", review_hint)), + ThreadItem::untimed(user_message("interrupted-steer", review_hint)), ], completed_at: Some(1), ..app_server_turn( @@ -420,16 +423,16 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() review_marker("turn-review"), AppServerTurn { items: vec![ - user_message("review-prompt-1", review_prompt), - user_message("review-prompt-2", review_prompt), - AppServerThreadItem::AgentMessage { + ThreadItem::untimed(user_message("review-prompt-1", review_prompt)), + ThreadItem::untimed(user_message("review-prompt-2", review_prompt)), + ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage { id: "review-result".to_string(), text: "review result is retained".to_string(), phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, questions: None, - }, + }), ], ..app_server_turn( "turn-review-child", @@ -1000,15 +1003,15 @@ async fn prompt_edit_stops_streaming_without_submitting_queued_input() { let retained_turn = AppServerTurn { items: vec![ - AppServerThreadItem::AgentMessage { + ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage { id: "retained-response".to_string(), text: "retained response".to_string(), phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, questions: None, - }, - AppServerThreadItem::UserMessage { + }), + ThreadItem::untimed(AppServerThreadItemPayload::UserMessage { id: "voice-steer".to_string(), client_id: None, content: vec![UserInput::Text { @@ -1016,15 +1019,15 @@ async fn prompt_edit_stops_streaming_without_submitting_queued_input() { .to_string(), text_elements: Vec::new(), }], - }, - AppServerThreadItem::AgentMessage { + }), + ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage { id: "private-commentary".to_string(), text: "hidden voice commentary".to_string(), phase: Some(MessagePhase::Commentary), memory_citation: None, delivery: None, questions: None, - }, + }), ], ..app_server_turn( "retained-turn", @@ -1262,7 +1265,7 @@ async fn replayed_reasoning_item_preserves_summary_parts_and_hides_raw_reasoning let _ = drain_insert_history(&mut rx); chat.replay_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec![ "**Plan**\n\ndone".to_string(), @@ -1314,7 +1317,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { let _ = drain_insert_history(&mut rx); chat.replay_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec!["Summary only".to_string()], content: vec!["Raw reasoning".to_string()], @@ -1337,7 +1340,7 @@ async fn replayed_mcp_tool_call_stays_active_until_completion() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let _ = drain_insert_history(&mut rx); - let mut item = AppServerThreadItem::McpToolCall { + let mut item = AppServerThreadItemPayload::McpToolCall { id: "mcp-1".to_string(), server: "copilot-bridge".to_string(), tool: "copilot".to_string(), @@ -1363,7 +1366,7 @@ async fn replayed_mcp_tool_call_stays_active_until_completion() { assert!(active.contains("Calling")); assert!(!active.contains("MCP tool call completed without a result")); - if let AppServerThreadItem::McpToolCall { status, .. } = &mut item { + if let AppServerThreadItemPayload::McpToolCall { status, .. } = &mut item { *status = codex_app_server_protocol::McpToolCallStatus::Completed; } chat.on_mcp_tool_call_completed(item); @@ -1382,11 +1385,7 @@ async fn failed_repl_mcp_tool_call_preserves_status_and_result() { let _ = drain_insert_history(&mut rx); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::McpToolCall { + ServerNotification::ItemCompleted(ItemCompletedNotification::new("thread-1".to_string(), "turn-1".to_string(), AppServerThreadItemPayload::McpToolCall { id: "mcp-failed".to_string(), server: server.to_string(), tool: "js".to_string(), @@ -1408,8 +1407,7 @@ async fn failed_repl_mcp_tool_call_preserves_status_and_result() { })), error: None, duration_ms: Some(5), - }, - }), + }, /*started_at_ms*/ None, /*completed_at_ms*/ 0)), /*replay_kind*/ None, ); @@ -1453,7 +1451,7 @@ async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() { chat.history_render_mode(), )); - chat.on_mcp_tool_call_started(AppServerThreadItem::McpToolCall { + chat.on_mcp_tool_call_started(AppServerThreadItemPayload::McpToolCall { id: "mcp-deferred".to_string(), server: "copilot-bridge".to_string(), tool: "copilot".to_string(), @@ -1471,7 +1469,7 @@ async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() { assert!(!chat.interrupts.is_empty()); chat.stream_controller = None; - chat.on_mcp_tool_call_completed(AppServerThreadItem::McpToolCall { + chat.on_mcp_tool_call_completed(AppServerThreadItemPayload::McpToolCall { id: "mcp-deferred".to_string(), server: "copilot-bridge".to_string(), tool: "copilot".to_string(), @@ -1543,16 +1541,17 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() { ); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec!["Summary only".to_string()], content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -1619,11 +1618,10 @@ async fn live_reasoning_summary_drops_empty_parts_without_losing_content() { } chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::Reasoning { id: "reasoning-1".to_string(), summary: vec![ "**Plan**\n\ndone".to_string(), @@ -1631,7 +1629,9 @@ async fn live_reasoning_summary_drops_empty_parts_without_losing_content() { ], content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 2b3257073c..2b325ba2db 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1,4 +1,5 @@ use super::*; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; fn paste_hidden_plan_shell_payload(chat: &mut ChatWidget) -> String { @@ -812,14 +813,16 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![AppServerThreadItem::AgentMessage { - id: "msg-plan".to_string(), - text: "Plan details".to_string(), - phase: Some(MessagePhase::FinalAnswer), - memory_citation: None, - delivery: None, - questions: None, - }], + items: vec![ThreadItem::untimed( + AppServerThreadItemPayload::AgentMessage { + id: "msg-plan".to_string(), + text: "Plan details".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + delivery: None, + questions: None, + }, + )], status: AppServerTurnStatus::Completed, error: None, started_at: None, @@ -853,14 +856,16 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com id: "turn-1".to_string(), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![AppServerThreadItem::AgentMessage { - id: "msg-plan-replay".to_string(), - text: "Plan details".to_string(), - phase: Some(MessagePhase::FinalAnswer), - memory_citation: None, - delivery: None, - questions: None, - }], + items: vec![ThreadItem::untimed( + AppServerThreadItemPayload::AgentMessage { + id: "msg-plan-replay".to_string(), + text: "Plan details".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + delivery: None, + questions: None, + }, + )], status: AppServerTurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/tui/src/chatwidget/tests/question_notifications_tests.rs b/codex-rs/tui/src/chatwidget/tests/question_notifications_tests.rs index 7f88c36863..8907c40f2f 100644 --- a/codex-rs/tui/src/chatwidget/tests/question_notifications_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/question_notifications_tests.rs @@ -7,7 +7,7 @@ use pretty_assertions::assert_eq; #[tokio::test] async fn live_async_question_notifies_once_and_takes_priority_over_turn_completion() { let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - let item = AppServerThreadItem::AgentMessage { + let item = AppServerThreadItemPayload::AgentMessage { id: "question".into(), text: String::new(), phase: None, @@ -26,12 +26,13 @@ async fn live_async_question_notifies_once_and_takes_priority_over_turn_completi assert!(chat.pending_notification.is_none()); } - let notification = ServerNotification::ItemCompleted(ItemCompletedNotification { + let notification = ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".into(), + "turn".into(), item, - thread_id: "thread".into(), - turn_id: "turn".into(), - completed_at_ms: 0, - }); + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )); chat.handle_server_notification(notification.clone(), /*replay_kind*/ None); chat.notify(Notification::AgentTurnComplete { response: "Done".into(), diff --git a/codex-rs/tui/src/chatwidget/tests/questions_tests.rs b/codex-rs/tui/src/chatwidget/tests/questions_tests.rs index 1bd8d1c25f..6357208c95 100644 --- a/codex-rs/tui/src/chatwidget/tests/questions_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/questions_tests.rs @@ -779,7 +779,7 @@ async fn question_drafts_survive_navigation_and_snapshot_replay() { chat.bottom_pane.handle_paste("second draft".into()); let saved = chat.capture_thread_input_state(); let (mut chat, _rx, mut ops) = make_chatwidget_manual(/*model_override*/ None).await; - let item = AppServerThreadItem::AgentMessage { + let item = AppServerThreadItemPayload::AgentMessage { id: "buffered".into(), text: String::new(), phase: None, @@ -795,12 +795,13 @@ async fn question_drafts_survive_navigation_and_snapshot_replay() { assert!(chat.bottom_pane.questions.is_none()); } chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".into(), + "turn".into(), item, - thread_id: "thread".into(), - turn_id: "turn".into(), - completed_at_ms: 0, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), Some(ReplayKind::ThreadSnapshot), ); assert_eq!(question_count(&chat), 1); @@ -980,7 +981,7 @@ async fn desktop_async_answer_dismisses_only_its_question_and_preserves_the_othe let reply = format!( "# Context from my IDE setup:\n\n## Open tabs:\n- lib.rs: src/lib.rs\n\n## My request for Codex:\n{reply}" ); - let item = AppServerThreadItem::UserMessage { + let item = AppServerThreadItemPayload::UserMessage { id: "answer".into(), client_id: None, content: vec![UserInput::Text { @@ -989,12 +990,13 @@ async fn desktop_async_answer_dismisses_only_its_question_and_preserves_the_othe }], }; chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread".into(), - turn_id: "turn".into(), - completed_at_ms: 0, - item: item.clone(), - }), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread".into(), + "turn".into(), + item.clone(), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), replay_kind, ); assert_eq!(question_count(&chat), 1); diff --git a/codex-rs/tui/src/chatwidget/tests/reasoning_status_tests.rs b/codex-rs/tui/src/chatwidget/tests/reasoning_status_tests.rs index 904cb25827..67bdacdca4 100644 --- a/codex-rs/tui/src/chatwidget/tests/reasoning_status_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/reasoning_status_tests.rs @@ -18,16 +18,17 @@ fn delta(chat: &mut ChatWidget, id: &str, text: &str) { fn complete(chat: &mut ChatWidget, id: &str) { chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::Reasoning { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::Reasoning { id: id.to_string(), summary: Vec::new(), content: Vec::new(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); } @@ -216,7 +217,7 @@ async fn reasoning_status_replay_retains_last_usable_heading() { ("third", Vec::new(), "Second heading"), ] { chat.handle_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: id.to_string(), summary, content: Vec::new(), @@ -272,7 +273,7 @@ async fn completed_reasoning_stays_in_expanded_transcript_for_live_and_replay() ] { if replay { chat.handle_thread_item( - AppServerThreadItem::Reasoning { + AppServerThreadItemPayload::Reasoning { id: id.to_string(), summary: vec![summary.to_string()], content: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index b72283bee2..640019ba6c 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -149,17 +149,17 @@ async fn live_review_prompt_item_is_not_rendered() { async fn live_app_server_review_prompt_item_is_not_rendered() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - let review_mode_item = AppServerThreadItem::EnteredReviewMode { + let review_mode_item = AppServerThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: "changes against 'main'".to_string(), }; chat.handle_server_notification( - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: review_mode_item.clone(), - }), + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + review_mode_item.clone(), + /*started_at_ms*/ 0, + )), /*replay_kind*/ None, ); let cells = drain_insert_history(&mut rx); @@ -167,22 +167,22 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { assert!(lines_to_single_string(&cells[0]).contains("Code review started")); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: review_mode_item, - }), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + review_mode_item, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); assert!(drain_insert_history(&mut rx).is_empty()); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::UserMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + AppServerThreadItemPayload::UserMessage { id: "review-prompt".to_string(), client_id: None, content: vec![AppServerUserInput::Text { @@ -190,7 +190,9 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { text_elements: Vec::new(), }], }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); @@ -467,7 +469,7 @@ async fn identical_steer_receipts_only_acknowledge_the_matching_submission() { (pending.client_id.as_str(), VecDeque::new()), ] { chat.handle_thread_item( - AppServerThreadItem::UserMessage { + AppServerThreadItemPayload::UserMessage { id: client_id.to_string(), client_id: Some(client_id.to_string()), content: vec![UserInput::Text { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 900ad18062..41ff4c3fbd 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1626,11 +1626,10 @@ async fn slash_copy_picker_uses_completed_commentary_during_active_turn() { let commentary = "Here is the command:\n\n```sh\necho current\n```"; let current_thread_id = chat.thread_id.map(|id| id.to_string()).unwrap_or_default(); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: current_thread_id, - turn_id: "active".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::AgentMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + current_thread_id, + "active".to_string(), + AppServerThreadItemPayload::AgentMessage { id: "active-commentary".to_string(), text: commentary.to_string(), phase: Some(MessagePhase::Commentary), @@ -1638,7 +1637,9 @@ async fn slash_copy_picker_uses_completed_commentary_during_active_turn() { delivery: None, questions: None, }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); while rx.try_recv().is_ok() {} @@ -1660,15 +1661,16 @@ async fn slash_copy_state_tracks_plan_item_completion() { let plan_text = "## Plan\n\n1. Build it\n2. Test it\n\n```sh\r\njust test \r\n```".to_string(); chat.handle_server_notification( - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: String::new(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, - item: AppServerThreadItem::Plan { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + String::new(), + "turn-1".to_string(), + AppServerThreadItemPayload::Plan { id: "plan-1".to_string(), text: plan_text.clone(), }, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), /*replay_kind*/ None, ); handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None); diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index e9e1ab1bcd..00dab60b7d 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -5769,7 +5769,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { let cwd = chat.config.cwd.clone(); handle_exec_begin( &mut chat, - AppServerThreadItem::CommandExecution { + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "c1".into(), @@ -5788,7 +5788,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { ); handle_exec_end( &mut chat, - AppServerThreadItem::CommandExecution { + AppServerThreadItemPayload::CommandExecution { model_context: None, sandbox_type: None, id: "c1".into(), diff --git a/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs index 9f04098d5e..5079ecbba6 100644 --- a/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs @@ -14,7 +14,7 @@ enum FinalizedEvent { } fn complete_subagent(chat: &mut ChatWidget, id: &str) { - let item = AppServerThreadItem::SubAgentActivity { + let item = AppServerThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: id.to_string(), @@ -23,18 +23,19 @@ fn complete_subagent(chat: &mut ChatWidget, id: &str) { agent_path: "/root/stack_tip".to_string(), }; for notification in [ - ServerNotification::ItemStarted(ItemStartedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - started_at_ms: 0, - item: item.clone(), - }), - ServerNotification::ItemCompleted(ItemCompletedNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - completed_at_ms: 0, + ServerNotification::ItemStarted(ItemStartedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), + item.clone(), + /*started_at_ms*/ 0, + )), + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + "turn-1".to_string(), item, - }), + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )), ] { chat.handle_server_notification(notification, /*replay_kind*/ None); } diff --git a/codex-rs/tui/src/chatwidget/tests/tool_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/tool_activity_tests.rs index 4638d46b8c..572a10565b 100644 --- a/codex-rs/tui/src/chatwidget/tests/tool_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/tool_activity_tests.rs @@ -2,7 +2,7 @@ use super::*; #[tokio::test] async fn image_result_live_and_replay_render_in_the_owning_call() { - let item: AppServerThreadItem = serde_json::from_value(json!({ + let item: AppServerThreadItemPayload = serde_json::from_value(json!({ "type": "mcpToolCall", "id": "image", "server": "node_repl", "tool": "js", "status": "completed", "arguments": {"title": "Inspect screenshot", "code": "showImage()"}, "result": {"content": [ @@ -18,7 +18,7 @@ async fn image_result_live_and_replay_render_in_the_owning_call() { chat.replay_thread_item(item.clone(), "turn-1".into(), ReplayKind::ThreadSnapshot); } else { let mut started = item.clone(); - if let AppServerThreadItem::McpToolCall { status, result, .. } = &mut started { + if let AppServerThreadItemPayload::McpToolCall { status, result, .. } = &mut started { *status = codex_app_server_protocol::McpToolCallStatus::InProgress; *result = None; } diff --git a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs index 60617254f6..9f13cc572b 100644 --- a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs @@ -42,7 +42,7 @@ impl ChatWidget { self.request_redraw(); } - pub(super) fn on_file_change_completed(&mut self, item: ThreadItem) { + pub(super) fn on_file_change_completed(&mut self, item: ThreadItemPayload) { self.defer_or_handle( item, InterruptManager::push_item_completed, @@ -50,7 +50,7 @@ impl ChatWidget { ); } - pub(super) fn on_mcp_tool_call_started(&mut self, item: ThreadItem) { + pub(super) fn on_mcp_tool_call_started(&mut self, item: ThreadItemPayload) { self.defer_or_handle( item, InterruptManager::push_item_started, @@ -58,7 +58,7 @@ impl ChatWidget { ); } - pub(super) fn on_mcp_tool_call_completed(&mut self, item: ThreadItem) { + pub(super) fn on_mcp_tool_call_completed(&mut self, item: ThreadItemPayload) { self.defer_or_handle( item, InterruptManager::push_item_completed, @@ -111,8 +111,8 @@ impl ChatWidget { self.request_redraw(); } - pub(super) fn on_collab_agent_tool_call(&mut self, item: ThreadItem) { - let ThreadItem::CollabAgentToolCall { + pub(super) fn on_collab_agent_tool_call(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::CollabAgentToolCall { id, tool, status, .. } = &item else { @@ -142,7 +142,7 @@ impl ChatWidget { } } - pub(super) fn on_sub_agent_activity(&mut self, item: ThreadItem) { + pub(super) fn on_sub_agent_activity(&mut self, item: ThreadItemPayload) { // Background agents can finish while the parent answer is still streaming. // Keep that stream intact until its authoritative message completion. // After the turn stops, leftover prompts must not hold up late activity. @@ -157,14 +157,14 @@ impl ChatWidget { } } - fn handle_sub_agent_activity_now(&mut self, item: ThreadItem) { + fn handle_sub_agent_activity_now(&mut self, item: ThreadItemPayload) { if let Some(cell) = multi_agents::sub_agent_activity_history_cell(&item) { self.on_collab_event(cell); } } - pub(crate) fn handle_file_change_completed_now(&mut self, item: ThreadItem) { - let ThreadItem::FileChange { status, .. } = item else { + pub(crate) fn handle_file_change_completed_now(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::FileChange { status, .. } = item else { return; }; // If the patch was successful, just let the "Edited" block stand. @@ -174,8 +174,8 @@ impl ChatWidget { } } - pub(crate) fn handle_mcp_tool_call_started_now(&mut self, item: ThreadItem) { - let ThreadItem::McpToolCall { + pub(crate) fn handle_mcp_tool_call_started_now(&mut self, item: ThreadItemPayload) { + let ThreadItemPayload::McpToolCall { id, server, tool, @@ -212,7 +212,7 @@ impl ChatWidget { self.request_redraw(); } - pub(crate) fn handle_mcp_tool_call_completed_now(&mut self, item: ThreadItem) { + pub(crate) fn handle_mcp_tool_call_completed_now(&mut self, item: ThreadItemPayload) { self.flush_answer_stream_with_separator(); let Some(McpHistory { @@ -294,28 +294,38 @@ impl ChatWidget { } } - pub(crate) fn handle_queued_item_started_now(&mut self, item: ThreadItem) { + pub(crate) fn handle_queued_item_started_now(&mut self, item: ThreadItemPayload) { match item { - item @ ThreadItem::CommandExecution { .. } => { + item @ ThreadItemPayload::CommandExecution { .. } => { self.handle_command_execution_started_now(item); } - item @ ThreadItem::McpToolCall { .. } => { + item @ ThreadItemPayload::McpToolCall { .. } => { self.handle_mcp_tool_call_started_now(item); } - item @ ThreadItem::DynamicToolCall { .. } => self.handle_dynamic_tool_item_now(item), + item @ ThreadItemPayload::DynamicToolCall { .. } => { + self.handle_dynamic_tool_item_now(item) + } _ => {} } } - pub(crate) fn handle_queued_item_completed_now(&mut self, item: ThreadItem) { + pub(crate) fn handle_queued_item_completed_now(&mut self, item: ThreadItemPayload) { match item { - item @ ThreadItem::CommandExecution { .. } => { + item @ ThreadItemPayload::CommandExecution { .. } => { self.handle_command_execution_completed_now(item); } - item @ ThreadItem::FileChange { .. } => self.handle_file_change_completed_now(item), - item @ ThreadItem::McpToolCall { .. } => self.handle_mcp_tool_call_completed_now(item), - item @ ThreadItem::DynamicToolCall { .. } => self.handle_dynamic_tool_item_now(item), - item @ ThreadItem::SubAgentActivity { .. } => self.handle_sub_agent_activity_now(item), + item @ ThreadItemPayload::FileChange { .. } => { + self.handle_file_change_completed_now(item) + } + item @ ThreadItemPayload::McpToolCall { .. } => { + self.handle_mcp_tool_call_completed_now(item) + } + item @ ThreadItemPayload::DynamicToolCall { .. } => { + self.handle_dynamic_tool_item_now(item) + } + item @ ThreadItemPayload::SubAgentActivity { .. } => { + self.handle_sub_agent_activity_now(item) + } _ => {} } } diff --git a/codex-rs/tui/src/clipboard_copy/worker_tests.rs b/codex-rs/tui/src/clipboard_copy/worker_tests.rs index ed48e126d0..2088cfed4d 100644 --- a/codex-rs/tui/src/clipboard_copy/worker_tests.rs +++ b/codex-rs/tui/src/clipboard_copy/worker_tests.rs @@ -37,11 +37,10 @@ async fn blocked_copy_allows_overlay_exit_rejects_backlog_and_wakes_completion() crate::chatwidget::tests::make_chatwidget_manual_with_sender().await; chat.handle_server_notification( codex_app_server_protocol::ServerNotification::ItemCompleted( - codex_app_server_protocol::ItemCompletedNotification { - thread_id: String::new(), - turn_id: "turn-1".into(), - completed_at_ms: 0, - item: codex_app_server_protocol::ThreadItem::AgentMessage { + codex_app_server_protocol::ItemCompletedNotification::new( + String::new(), + "turn-1".into(), + codex_app_server_protocol::ThreadItemPayload::AgentMessage { id: "msg-1".into(), text: "cafรฉ\nsecond line".into(), phase: None, @@ -49,7 +48,9 @@ async fn blocked_copy_allows_overlay_exit_rejects_backlog_and_wakes_completion() delivery: None, questions: None, }, - }, + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + ), ), /*replay_kind*/ None, ); diff --git a/codex-rs/tui/src/dynamic_tools.rs b/codex-rs/tui/src/dynamic_tools.rs index 4b895da67b..184bc64ec1 100644 --- a/codex-rs/tui/src/dynamic_tools.rs +++ b/codex-rs/tui/src/dynamic_tools.rs @@ -29,8 +29,8 @@ use codex_app_server_protocol::ThreadArchiveResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadItemEntry; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadItemsListParams; use codex_app_server_protocol::ThreadItemsListResponse; use codex_app_server_protocol::ThreadListParams; @@ -912,9 +912,9 @@ async fn execute_inner( .cloned() .map(|item| ThreadItemEntry { turn_id: turn.id.clone(), + started_at_ms: item.started_at_ms, + completed_at_ms: item.completed_at_ms, item, - started_at_ms: None, - completed_at_ms: None, }) .collect(), ) @@ -967,8 +967,8 @@ async fn execute_inner( }; } let latest_assistant_message = latest_turn.as_ref().and_then(|turn| { - turn.items.iter().rev().find_map(|item| match item { - ThreadItem::AgentMessage { + turn.item_payloads().rev().find_map(|item| match item { + ThreadItemPayload::AgentMessage { id, text, phase, .. } => Some(json!({ "id": id, @@ -983,58 +983,58 @@ async fn execute_inner( .as_ref() .zip(latest_items.as_ref()) .and_then(|(turn, items)| { - items.iter().find_map(|entry| match &entry.item { - ThreadItem::CommandExecution { id, status, .. } => { + items.iter().find_map(|entry| match &entry.item.payload { + ThreadItemPayload::CommandExecution { id, status, .. } => { Some(json!({ "id": id, "turnId": turn.id, "type": "commandExecution", "name": "commandExecution", "status": status })) } - ThreadItem::FileChange { id, status, .. } => Some(json!({ + ThreadItemPayload::FileChange { id, status, .. } => Some(json!({ "id": id, "turnId": turn.id, "type": "fileChange", "name": "fileChange", "status": status })), - ThreadItem::ImageGeneration(item) => Some(json!({ + ThreadItemPayload::ImageGeneration(item) => Some(json!({ "id": item.id, "turnId": turn.id, "type": "imageGeneration", "name": "imageGeneration", "status": item.status })), - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { id, tool, status, .. } => Some(json!({ "id": id, "turnId": turn.id, "type": "mcpToolCall", "name": tool, "status": status })), - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { id, tool, status, .. } => Some(json!({ "id": id, "turnId": turn.id, "type": "dynamicToolCall", "name": tool, "status": status })), - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id, tool, status, .. } => Some(json!({ "id": id, "turnId": turn.id, "type": "collabAgentToolCall", "name": tool, "status": status })), - ThreadItem::Sleep(item) => Some(json!({ + ThreadItemPayload::Sleep(item) => Some(json!({ "id": item.id, "turnId": turn.id, "type": "sleep", "name": "sleep", "status": null })), - ThreadItem::WebSearch(item) => Some(json!({ + ThreadItemPayload::WebSearch(item) => Some(json!({ "id": item.id, "turnId": turn.id, "type": "webSearch", "name": "webSearch", "status": null })), - ThreadItem::UserMessage { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::AgentMessage { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::SubAgentActivity { .. } - | ThreadItem::ImageView { .. } - | ThreadItem::EnteredReviewMode { .. } - | ThreadItem::ExitedReviewMode { .. } - | ThreadItem::ContextCompaction { .. } => None, + ThreadItemPayload::UserMessage { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::HookPrompt { .. } + | ThreadItemPayload::AgentMessage { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::SubAgentActivity { .. } + | ThreadItemPayload::ImageView { .. } + | ThreadItemPayload::EnteredReviewMode { .. } + | ThreadItemPayload::ExitedReviewMode { .. } + | ThreadItemPayload::ContextCompaction { .. } => None, }) }); polls.push(json!({ @@ -1358,11 +1358,10 @@ fn thread_summary(thread: &Thread) -> Value { fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Value { let mut items: Vec = turn - .items - .iter() + .item_payloads() .rev() .map(|item| match item { - ThreadItem::UserMessage { id, content, .. } => json!({ + ThreadItemPayload::UserMessage { id, content, .. } => json!({ "type": "userMessage", "id": id, "content": content.iter().map(|input| match input { @@ -1392,10 +1391,10 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu UserInput::Mention { name, path } => json!({"type": "mention", "name": name, "path": path}), }).collect::>() }), - ThreadItem::HookPrompt { id, fragments } => json!({ + ThreadItemPayload::HookPrompt { id, fragments } => json!({ "type": "hookPrompt", "id": id, "fragmentCount": fragments.len() }), - ThreadItem::FunctionCallOutput { + ThreadItemPayload::FunctionCallOutput { id, name, namespace, @@ -1420,13 +1419,13 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu } item } - ThreadItem::AgentMessage { id, text, phase, .. } => json!({ + ThreadItemPayload::AgentMessage { id, text, phase, .. } => json!({ "type": "agentMessage", "id": id, "text": text, "phase": phase }), - ThreadItem::Plan { id, text } => json!({ + ThreadItemPayload::Plan { id, text } => json!({ "type": "plan", "id": id, "text": truncate(text, DEFAULT_OUTPUT_CHARS) }), - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id, summary, content, @@ -1443,7 +1442,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu } item } - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { id, command, cwd, @@ -1467,7 +1466,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu } item } - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id, changes, status, @@ -1483,7 +1482,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu item }).collect::>() }), - ThreadItem::McpToolCall { + ThreadItemPayload::McpToolCall { id, server, tool, @@ -1495,7 +1494,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu "type": "mcpToolCall", "id": id, "server": server, "tool": tool, "arguments": arguments, "status": status, "durationMs": duration_ms }), - ThreadItem::DynamicToolCall { + ThreadItemPayload::DynamicToolCall { id, namespace, tool, @@ -1509,7 +1508,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu "tool": tool, "arguments": arguments, "status": status, "success": success, "durationMs": duration_ms }), - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { id, tool, status, @@ -1525,7 +1524,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu "receiverThreadIds": receiver_thread_ids, "prompt": prompt, "model": model, "reasoningEffort": reasoning_effort }), - ThreadItem::SubAgentActivity { + ThreadItemPayload::SubAgentActivity { id, kind, agent_thread_id, @@ -1535,17 +1534,17 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu "type": "subAgentActivity", "id": id, "kind": kind, "agentThreadId": agent_thread_id, "agentPath": agent_path }), - ThreadItem::WebSearch(item) => json!({ + ThreadItemPayload::WebSearch(item) => json!({ "type": "webSearch", "id": item.id, "query": truncate(&item.query, DEFAULT_OUTPUT_CHARS), "action": item.action }), - ThreadItem::ImageView { id, path } => json!({ + ThreadItemPayload::ImageView { id, path } => json!({ "type": "imageView", "id": id, "path": path }), - ThreadItem::Sleep(item) => json!({ + ThreadItemPayload::Sleep(item) => json!({ "type": "sleep", "id": item.id, "durationMs": item.duration_ms }), - ThreadItem::ImageGeneration(item) => { + ThreadItemPayload::ImageGeneration(item) => { let mut image = json!({ "type": "imageGeneration", "id": item.id, "status": item.status, "revisedPrompt": item.revised_prompt.as_deref().map(|prompt| truncate(prompt, DEFAULT_OUTPUT_CHARS)), @@ -1563,13 +1562,13 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu } image } - ThreadItem::EnteredReviewMode { id, review } => json!({ + ThreadItemPayload::EnteredReviewMode { id, review } => json!({ "type": "enteredReviewMode", "id": id, "review": truncate(review, DEFAULT_OUTPUT_CHARS) }), - ThreadItem::ExitedReviewMode { id, review } => json!({ + ThreadItemPayload::ExitedReviewMode { id, review } => json!({ "type": "exitedReviewMode", "id": id, "review": truncate(review, DEFAULT_OUTPUT_CHARS) }), - ThreadItem::ContextCompaction { id } => json!({ + ThreadItemPayload::ContextCompaction { id } => json!({ "type": "contextCompaction", "id": id }), }) diff --git a/codex-rs/tui/src/history_cell/dynamic.rs b/codex-rs/tui/src/history_cell/dynamic.rs index 9c5c603064..1ac5eb7c37 100644 --- a/codex-rs/tui/src/history_cell/dynamic.rs +++ b/codex-rs/tui/src/history_cell/dynamic.rs @@ -12,7 +12,7 @@ use crate::terminal_hyperlinks::visible_lines; use crate::wrapping::RtOptions; use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallStatus; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use ratatui::style::Stylize; use ratatui::text::Line; use serde_json::Value; @@ -36,7 +36,7 @@ struct DynamicToolCallData { } impl DynamicToolCallCell { - pub(crate) fn from_item(item: ThreadItem) -> Option { + pub(crate) fn from_item(item: ThreadItemPayload) -> Option { let (call_id, data) = DynamicToolCallData::from_item(item)?; Some(Self { call_id, @@ -56,7 +56,7 @@ impl DynamicToolCallCell { } /// Updates the original retained row when concurrent calls complete in a different order. - pub(crate) fn update_from_item(&self, item: ThreadItem) -> bool { + pub(crate) fn update_from_item(&self, item: ThreadItemPayload) -> bool { let Some((call_id, data)) = DynamicToolCallData::from_item(item) else { return false; }; @@ -87,8 +87,8 @@ impl DynamicToolCallCell { } impl DynamicToolCallData { - fn from_item(item: ThreadItem) -> Option<(String, Self)> { - let ThreadItem::DynamicToolCall { + fn from_item(item: ThreadItemPayload) -> Option<(String, Self)> { + let ThreadItemPayload::DynamicToolCall { id, namespace, tool, diff --git a/codex-rs/tui/src/history_cell/dynamic_tests.rs b/codex-rs/tui/src/history_cell/dynamic_tests.rs index 18bc6f0b3d..e01278849c 100644 --- a/codex-rs/tui/src/history_cell/dynamic_tests.rs +++ b/codex-rs/tui/src/history_cell/dynamic_tests.rs @@ -8,8 +8,8 @@ use crate::thread_transcript::thread_items_to_transcript_cells; use pretty_assertions::assert_eq; use serde_json::json; -fn item(status: DynamicToolCallStatus, output: Option<&str>) -> ThreadItem { - ThreadItem::DynamicToolCall { +fn item(status: DynamicToolCallStatus, output: Option<&str>) -> ThreadItemPayload { + ThreadItemPayload::DynamicToolCall { id: "dynamic-1".to_string(), namespace: Some("example".to_string()), tool: "inspect".to_string(), diff --git a/codex-rs/tui/src/lib_tests.rs b/codex-rs/tui/src/lib_tests.rs index d911e9302f..e7eed00bbc 100644 --- a/codex-rs/tui/src/lib_tests.rs +++ b/codex-rs/tui/src/lib_tests.rs @@ -11,6 +11,7 @@ use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::Turn; use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -1489,11 +1490,11 @@ async fn resume_picker_loads_complete_paginated_and_legacy_transcripts() -> colo started .turns .iter() - .flat_map(|turn| &turn.items) + .flat_map(Turn::item_payloads) .any(|item| { matches!( item, - codex_app_server_protocol::ThreadItem::UserMessage { .. } + codex_app_server_protocol::ThreadItemPayload::UserMessage { .. } ) }) ); diff --git a/codex-rs/tui/src/multi_agents.rs b/codex-rs/tui/src/multi_agents.rs index 8210270580..e7f3d38951 100644 --- a/codex-rs/tui/src/multi_agents.rs +++ b/codex-rs/tui/src/multi_agents.rs @@ -13,7 +13,7 @@ use codex_app_server_protocol::CollabAgentStatus; use codex_app_server_protocol::CollabAgentTool; use codex_app_server_protocol::CollabAgentToolCallStatus; use codex_app_server_protocol::SubAgentActivityKind; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_protocol::ThreadId; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use crossterm::event::KeyCode; @@ -186,9 +186,9 @@ fn next_agent_word_motion_fallback( false } -pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option { +pub(crate) fn spawn_request_summary(item: &ThreadItemPayload) -> Option { match item { - ThreadItem::CollabAgentToolCall { + ThreadItemPayload::CollabAgentToolCall { tool: CollabAgentTool::SpawnAgent, model: Some(model), reasoning_effort: Some(reasoning_effort), @@ -202,11 +202,11 @@ pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option, mut agent_metadata: impl FnMut(ThreadId) -> AgentMetadata, ) -> Option { - let ThreadItem::CollabAgentToolCall { + let ThreadItemPayload::CollabAgentToolCall { tool, status, receiver_thread_ids, @@ -284,8 +284,10 @@ pub(crate) fn tool_call_history_cell( } } -pub(crate) fn sub_agent_activity_display(item: &ThreadItem) -> Option { - let ThreadItem::SubAgentActivity { +pub(crate) fn sub_agent_activity_display( + item: &ThreadItemPayload, +) -> Option { + let ThreadItemPayload::SubAgentActivity { kind, agent_thread_id, agent_path, @@ -306,8 +308,10 @@ pub(crate) fn sub_agent_activity_display(item: &ThreadItem) -> Option Option { - let ThreadItem::SubAgentActivity { +pub(crate) fn sub_agent_activity_history_cell( + item: &ThreadItemPayload, +) -> Option { + let ThreadItemPayload::SubAgentActivity { kind, agent_path, .. } = item else { @@ -680,7 +684,7 @@ mod tests { #[test] fn interacted_sub_agent_activity_does_not_change_liveness() { - let item = ThreadItem::SubAgentActivity { + let item = ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "activity-1".to_string(), @@ -695,7 +699,7 @@ mod tests { #[test] fn completed_sub_agent_activity_stops_running_liveness() { let thread_id = ThreadId::new(); - let item = ThreadItem::SubAgentActivity { + let item = ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "activity-1".to_string(), @@ -724,7 +728,7 @@ mod tests { .expect("valid bob thread id"); let spawn = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-spawn".to_string(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::Completed, @@ -744,7 +748,7 @@ mod tests { .expect("spawn item renders"); let send = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-send".to_string(), tool: CollabAgentTool::SendInput, status: CollabAgentToolCallStatus::Completed, @@ -764,7 +768,7 @@ mod tests { .expect("send-input item renders"); let waiting = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-wait".to_string(), tool: CollabAgentTool::Wait, status: CollabAgentToolCallStatus::InProgress, @@ -781,7 +785,7 @@ mod tests { .expect("wait begin item renders"); let finished = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-wait".to_string(), tool: CollabAgentTool::Wait, status: CollabAgentToolCallStatus::Completed, @@ -807,7 +811,7 @@ mod tests { .expect("wait end item renders"); let close = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-close".to_string(), tool: CollabAgentTool::CloseAgent, status: CollabAgentToolCallStatus::Completed, @@ -891,7 +895,7 @@ mod tests { let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002") .expect("valid robie thread id"); let cell = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-spawn".to_string(), tool: CollabAgentTool::SpawnAgent, status: CollabAgentToolCallStatus::Completed, @@ -930,7 +934,7 @@ mod tests { .expect("valid robie thread id"); let cell = tool_call_history_cell( - &ThreadItem::CollabAgentToolCall { + &ThreadItemPayload::CollabAgentToolCall { id: "call-resume".to_string(), tool: CollabAgentTool::ResumeAgent, status: CollabAgentToolCallStatus::Completed, diff --git a/codex-rs/tui/src/pager_overlay/transcript_tests.rs b/codex-rs/tui/src/pager_overlay/transcript_tests.rs index d0f9fc109f..4dd388f9cd 100644 --- a/codex-rs/tui/src/pager_overlay/transcript_tests.rs +++ b/codex-rs/tui/src/pager_overlay/transcript_tests.rs @@ -272,7 +272,7 @@ async fn focus_loss_stops_edge_drag_and_preserves_selection_after_focus_returns( #[tokio::test] async fn opening_find_retains_the_selected_mutable_revision() -> Result<()> { let cell = history_cell::DynamicToolCallCell::from_item( - codex_app_server_protocol::ThreadItem::DynamicToolCall { + codex_app_server_protocol::ThreadItemPayload::DynamicToolCall { id: "tool".into(), namespace: None, tool: "inspect".into(), diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index dfac576936..e3f1072d1d 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -3704,6 +3704,7 @@ mod tests { use super::*; use chrono::Duration; use codex_app_server_protocol::ThreadItem; + use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSourceKind; use codex_config::CONFIG_TOML_FILE; use codex_protocol::ThreadId; @@ -7011,26 +7012,26 @@ session_picker_view = "dense" root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ - ThreadItem::UserMessage { + ThreadItem::untimed(ThreadItemPayload::UserMessage { id: String::from("user-1"), client_id: None, content: vec![codex_app_server_protocol::UserInput::Text { text: String::from("hello from user"), text_elements: Vec::new(), }], - }, - ThreadItem::AgentMessage { + }), + ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: String::from("agent-1"), text: String::from("hello from assistant"), phase: None, memory_citation: None, delivery: None, questions: None, - }, - ThreadItem::Plan { + }), + ThreadItem::untimed(ThreadItemPayload::Plan { id: String::from("plan-1"), text: String::from("1. Do the thing"), - }, + }), ], status: codex_app_server_protocol::TurnStatus::Completed, error: None, @@ -7098,11 +7099,11 @@ session_picker_view = "dense" id: String::from("turn-1"), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::Reasoning { + items: vec![ThreadItem::untimed(ThreadItemPayload::Reasoning { id: String::from("reasoning-1"), summary: Vec::new(), content: vec![String::from("private raw chain of thought")], - }], + })], status: codex_app_server_protocol::TurnStatus::Completed, error: None, started_at: None, @@ -7177,11 +7178,11 @@ session_picker_view = "dense" id: String::from("turn-1"), root_turn_id: None, items_view: codex_app_server_protocol::TurnItemsView::Full, - items: vec![ThreadItem::Reasoning { + items: vec![ThreadItem::untimed(ThreadItemPayload::Reasoning { id: String::from("reasoning-1"), summary: vec![String::from("public summary")], content: vec![String::from("raw reasoning content")], - }], + })], status: codex_app_server_protocol::TurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/tui/src/resume_picker_transcript_preview.rs b/codex-rs/tui/src/resume_picker_transcript_preview.rs index fb642f5362..4f5ed57446 100644 --- a/codex-rs/tui/src/resume_picker_transcript_preview.rs +++ b/codex-rs/tui/src/resume_picker_transcript_preview.rs @@ -17,7 +17,7 @@ use crate::git_action_directives::parse_assistant_markdown; use crate::inline_visualization::InlineVisualizationContext; use crate::legacy_core::config::Config; use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_protocol::ThreadId; use codex_protocol::protocol::EventMsg; use codex_rollout::ReverseJsonlScanner; @@ -87,7 +87,7 @@ pub(crate) async fn load_transcript_preview( .turns .iter() .rev() - .flat_map(|turn| turn.items.iter().rev()), + .flat_map(|turn| turn.item_payloads().rev()), thread.cwd.as_path(), inline_visualization_context.as_ref(), ); @@ -118,7 +118,7 @@ pub(crate) async fn load_transcript_preview( page.data .iter() .take(remaining_items) - .map(|entry| &entry.item), + .map(|entry| &entry.item.payload), cwd, inline_visualization_context.as_ref(), ); @@ -228,13 +228,13 @@ impl Seek for BoundedRolloutTailReader { /// Appends the newest preview lines from items already ordered newest-first. fn append_transcript_preview_lines<'a>( lines: &mut Vec, - items: impl Iterator, + items: impl Iterator, cwd: &Path, inline_visualization_context: Option<&InlineVisualizationContext>, ) { for item in items { match item { - ThreadItem::UserMessage { content, .. } => { + ThreadItemPayload::UserMessage { content, .. } => { let text = content .iter() .filter_map(|input| match input { @@ -253,7 +253,7 @@ fn append_transcript_preview_lines<'a>( inline_visualization_context, ); } - ThreadItem::AgentMessage { text, .. } => { + ThreadItemPayload::AgentMessage { text, .. } => { append_transcript_preview_text( lines, TranscriptPreviewSpeaker::Assistant, diff --git a/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs b/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs index cfbb9ee5e7..fe516c6513 100644 --- a/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs +++ b/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs @@ -272,7 +272,7 @@ fn legacy_transcript_preview_falls_back_when_scan_budget_is_exhausted() { #[test] fn transcript_preview_reverse_scan_stops_before_older_items() { - let newest = ThreadItem::AgentMessage { + let newest = ThreadItemPayload::AgentMessage { id: String::from("final"), text: String::from("one\ntwo\nthree\nfour\nfive\nsix"), phase: None, @@ -421,7 +421,7 @@ async fn transcript_preview_for_history_mode( let mut first_page_lines = Vec::new(); append_transcript_preview_lines( &mut first_page_lines, - first_page.data.iter().map(|entry| &entry.item), + first_page.data.iter().map(|entry| &entry.item.payload), codex_home.path(), /*inline_visualization_context*/ None, ); diff --git a/codex-rs/tui/src/temporary_structured_request.rs b/codex-rs/tui/src/temporary_structured_request.rs index 8722aaa325..840eafb2ba 100644 --- a/codex-rs/tui/src/temporary_structured_request.rs +++ b/codex-rs/tui/src/temporary_structured_request.rs @@ -9,7 +9,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::SandboxPolicy; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -216,7 +216,7 @@ pub(crate) async fn collect_structured_response( while let Some(notification) = notifications.recv().await { match notification { ServerNotification::ItemCompleted(completed) if completed.turn_id == turn_id => { - if let ThreadItem::AgentMessage { text, .. } = completed.item { + if let ThreadItemPayload::AgentMessage { text, .. } = completed.item.payload { if text.len() > STRUCTURED_RESPONSE_MAX_BYTES { return Err(eyre!( "temporary structured response exceeds {STRUCTURED_RESPONSE_MAX_BYTES} bytes" diff --git a/codex-rs/tui/src/temporary_structured_request_tests.rs b/codex-rs/tui/src/temporary_structured_request_tests.rs index e86668e5ad..0cec337815 100644 --- a/codex-rs/tui/src/temporary_structured_request_tests.rs +++ b/codex-rs/tui/src/temporary_structured_request_tests.rs @@ -10,7 +10,7 @@ use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::SandboxPolicy; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; @@ -23,8 +23,10 @@ use tempfile::tempdir; use tokio::sync::mpsc::unbounded_channel; fn agent_message_notification(turn_id: &str, text: &str) -> ServerNotification { - ServerNotification::ItemCompleted(ItemCompletedNotification { - item: ThreadItem::AgentMessage { + ServerNotification::ItemCompleted(ItemCompletedNotification::new( + "thread-1".to_string(), + turn_id.to_string(), + ThreadItemPayload::AgentMessage { id: "message-1".to_string(), text: text.to_string(), phase: None, @@ -32,10 +34,9 @@ fn agent_message_notification(turn_id: &str, text: &str) -> ServerNotification { delivery: None, questions: None, }, - thread_id: "thread-1".to_string(), - turn_id: turn_id.to_string(), - completed_at_ms: 0, - }) + /*started_at_ms*/ None, + /*completed_at_ms*/ 0, + )) } fn turn_completed_notification(turn_id: &str, status: TurnStatus) -> ServerNotification { diff --git a/codex-rs/tui/src/thread_transcript.rs b/codex-rs/tui/src/thread_transcript.rs index babf331fa2..9327c77e77 100644 --- a/codex-rs/tui/src/thread_transcript.rs +++ b/codex-rs/tui/src/thread_transcript.rs @@ -17,7 +17,7 @@ use crate::history_cell::split_reasoning_summary_parts; use crate::inline_visualization::InlineVisualizationContext; use crate::legacy_core::config::Config; use codex_app_server_protocol::Thread; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::UserInput; use codex_protocol::ThreadId; use codex_protocol::items::UserMessageItem; @@ -107,7 +107,7 @@ pub(crate) fn thread_to_transcript_cells( thread_items_to_transcript_cells( thread_id, &cwd, - turn.items, + turn.items.into_iter().map(|entry| entry.payload), raw_reasoning_visibility, config, ) @@ -124,7 +124,7 @@ pub(crate) fn thread_to_transcript_cells( pub(crate) fn thread_items_to_transcript_cells( thread_id: Option, cwd: &AbsolutePathBuf, - items: impl IntoIterator, + items: impl IntoIterator, raw_reasoning_visibility: RawReasoningVisibility, config: Option<&Config>, ) -> TranscriptCells { @@ -134,7 +134,7 @@ pub(crate) fn thread_items_to_transcript_cells( let mut cells: TranscriptCells = Vec::new(); let mut pending = None; for item in items { - if matches!(item, ThreadItem::Reasoning { .. }) + if matches!(item, ThreadItemPayload::Reasoning { .. }) && let Some(group) = &mut pending { for cell in item_to_cells( @@ -156,7 +156,7 @@ pub(crate) fn thread_items_to_transcript_cells( continue; } match item { - item @ ThreadItem::McpToolCall { .. } => { + item @ ThreadItemPayload::McpToolCall { .. } => { let Some(call) = tools::McpHistory::from_item(item) else { PendingActivity::flush(&mut pending, &mut cells); continue; @@ -179,7 +179,7 @@ pub(crate) fn thread_items_to_transcript_cells( cells.push(Arc::new(cell)); } } - item @ ThreadItem::CommandExecution { .. } => { + item @ ThreadItemPayload::CommandExecution { .. } => { if matches!(pending, Some(PendingActivity::Computer(_))) { PendingActivity::flush(&mut pending, &mut cells); } @@ -220,14 +220,14 @@ pub(crate) fn thread_items_to_transcript_cells( /// Project one item without changing the active widget or its turn lifecycle. fn item_to_cells( - item: ThreadItem, + item: ThreadItemPayload, cwd: &AbsolutePathBuf, raw_reasoning_visibility: RawReasoningVisibility, inline_visualization_context: Option, ) -> TranscriptCells { let mut cells: TranscriptCells = Vec::new(); match item { - ThreadItem::UserMessage { + ThreadItemPayload::UserMessage { id, client_id, content, @@ -266,7 +266,7 @@ fn item_to_cells( remote_image_urls: item.image_urls(), })); } - ThreadItem::AgentMessage { text, .. } => { + ThreadItemPayload::AgentMessage { text, .. } => { let parsed = parse_assistant_markdown(&text, cwd.as_path()); if !parsed.visible_markdown.trim().is_empty() { cells.push(Arc::new(AgentMarkdownCell::new_with_inline_visualizations( @@ -276,7 +276,7 @@ fn item_to_cells( ))); } } - ThreadItem::FunctionCallOutput { + ThreadItemPayload::FunctionCallOutput { name, namespace, output, @@ -296,7 +296,7 @@ fn item_to_cells( ))); } } - ThreadItem::Plan { text, .. } => { + ThreadItemPayload::Plan { text, .. } => { if !text.trim().is_empty() { cells.push(Arc::new(crate::history_cell::new_proposed_plan( text, @@ -304,7 +304,7 @@ fn item_to_cells( ))); } } - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id, summary, content, @@ -329,7 +329,7 @@ fn item_to_cells( cells.push(Arc::new(cell)); } } - item @ ThreadItem::CommandExecution { .. } => { + item @ ThreadItemPayload::CommandExecution { .. } => { if let Some(command) = tools::CommandHistory::from_item(item) { cells.push(Arc::new(command.into_cell())); } diff --git a/codex-rs/tui/src/thread_transcript/computer_groups.rs b/codex-rs/tui/src/thread_transcript/computer_groups.rs index 90efce872e..59d33fe45c 100644 --- a/codex-rs/tui/src/thread_transcript/computer_groups.rs +++ b/codex-rs/tui/src/thread_transcript/computer_groups.rs @@ -4,7 +4,7 @@ use super::tools::McpHistory; use crate::history_cell::ComputerActivityCell; use crate::history_cell::HistoryCell; use crate::history_cell::McpToolCallCell; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use std::sync::Arc; @@ -40,7 +40,7 @@ pub(crate) fn older_computer_group( let items = adjacent_items(older, newer, turns)?; let older_items = items .iter() - .filter(|item| !matches!(item, ThreadItem::Reasoning { .. })) + .filter(|entry| !matches!(&entry.payload, ThreadItemPayload::Reasoning { .. })) .take(older.call_ids().count()) .cloned() .collect::>(); @@ -53,7 +53,7 @@ fn adjacent_items<'a>( older: &ComputerActivityCell, newer: &ComputerActivityCell, turns: &'a [Turn], -) -> Option<&'a [ThreadItem]> { +) -> Option<&'a [codex_app_server_protocol::ThreadItem]> { let ids = older.call_ids().chain(newer.call_ids()).collect::>(); adjacent_activity_items(&ids, turns) } @@ -62,7 +62,7 @@ fn adjacent_items<'a>( pub(super) fn adjacent_activity_items<'a>( ids: &[&str], turns: &'a [Turn], -) -> Option<&'a [ThreadItem]> { +) -> Option<&'a [codex_app_server_protocol::ThreadItem]> { let first = *ids.first()?; let (turn, start) = turns.iter().find_map(|turn| { turn.items @@ -71,9 +71,9 @@ pub(super) fn adjacent_activity_items<'a>( .map(|start| (turn, start)) })?; let mut matched = 0; - for (offset, item) in turn.items[start..].iter().enumerate() { + for (offset, item) in turn.item_payloads().skip(start).enumerate() { if item.id() != ids[matched] { - if matches!(item, ThreadItem::Reasoning { .. }) { + if matches!(item, ThreadItemPayload::Reasoning { .. }) { continue; } return None; @@ -86,10 +86,13 @@ pub(super) fn adjacent_activity_items<'a>( None } -pub(super) fn completed_group(items: &[ThreadItem]) -> Option { +pub(super) fn completed_group( + items: &[codex_app_server_protocol::ThreadItem], +) -> Option { let mut group = None; - for item in items { - if matches!(item, ThreadItem::Reasoning { .. }) { + for entry in items { + let item = &entry.payload; + if matches!(item, ThreadItemPayload::Reasoning { .. }) { continue; } let call = McpHistory::from_item(item.clone())?; diff --git a/codex-rs/tui/src/thread_transcript/computer_groups_tests.rs b/codex-rs/tui/src/thread_transcript/computer_groups_tests.rs index 6e24c27ef5..9d58bd7d73 100644 --- a/codex-rs/tui/src/thread_transcript/computer_groups_tests.rs +++ b/codex-rs/tui/src/thread_transcript/computer_groups_tests.rs @@ -5,12 +5,13 @@ use crate::thread_transcript::RawReasoningVisibility; use crate::thread_transcript::TranscriptCells; use crate::thread_transcript::thread_items_to_transcript_cells; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; -fn computer(id: &str) -> ThreadItem { +fn computer(id: &str) -> ThreadItemPayload { serde_json::from_value(serde_json::json!({ "type": "mcpToolCall", "id": id, "server": "cua_repl", "tool": "js", "status": "completed", "arguments": {"title": format!("Inspect {id}"), "code": "await cua.getState()"}, @@ -20,11 +21,11 @@ fn computer(id: &str) -> ThreadItem { })).unwrap() } -fn turn(id: &str, items: Vec) -> Turn { +fn turn(id: &str, items: Vec) -> Turn { Turn { id: id.to_string(), root_turn_id: None, - items, + items: items.into_iter().map(ThreadItem::untimed).collect(), items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, @@ -34,11 +35,11 @@ fn turn(id: &str, items: Vec) -> Turn { } } -fn project(items: &[ThreadItem]) -> TranscriptCells { +fn project(items: &[impl std::borrow::Borrow]) -> TranscriptCells { thread_items_to_transcript_cells( /*thread_id*/ None, &AbsolutePathBuf::current_dir().unwrap(), - items.to_vec(), + items.iter().map(|entry| entry.borrow().clone()), RawReasoningVisibility::Hidden, /*config*/ None, ) @@ -50,14 +51,14 @@ fn every_page_split_retains_the_same_rich_computer_group() { "turn", vec![ computer("first"), - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "reasoning-first".to_owned(), summary: vec!["Inspect the second screen".to_owned()], content: Vec::new(), }, computer("second"), computer("third"), - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "reasoning-second".to_owned(), summary: vec!["Confirm the final screen".to_owned()], content: Vec::new(), @@ -131,7 +132,7 @@ fn joins_respect_actual_turns_and_intervening_items() { assert!(join_computer_groups(&older[0], &newer[0], &turns).is_none()); let grouped = project(&[ first.clone(), - ThreadItem::Sleep(codex_app_server_protocol::SleepItem { + ThreadItemPayload::Sleep(codex_app_server_protocol::SleepItem { id: "sleep".into(), duration_ms: 10, }), @@ -147,7 +148,7 @@ fn joins_respect_actual_turns_and_intervening_items() { "turn", vec![ first, - ThreadItem::Reasoning { + ThreadItemPayload::Reasoning { id: "reasoning".to_string(), summary: Vec::new(), content: Vec::new(), @@ -162,13 +163,13 @@ fn joins_respect_actual_turns_and_intervening_items() { joined.transcript_lines(/*width*/ 80), expected[0].transcript_lines(/*width*/ 80), ); - turns[0].items[1] = ThreadItem::AgentMessage { + turns[0].items[1] = ThreadItem::untimed(ThreadItemPayload::AgentMessage { id: "message".to_string(), text: "Visible boundary".to_string(), phase: None, memory_citation: None, delivery: None, questions: None, - }; + }); assert!(join_computer_groups(&older[0], &newer[0], &turns).is_none()); } diff --git a/codex-rs/tui/src/thread_transcript/exploration_groups.rs b/codex-rs/tui/src/thread_transcript/exploration_groups.rs index 5b6194e5f2..b61e1234e3 100644 --- a/codex-rs/tui/src/thread_transcript/exploration_groups.rs +++ b/codex-rs/tui/src/thread_transcript/exploration_groups.rs @@ -3,7 +3,7 @@ use super::tools::CommandHistory; use crate::exec_cell::ExecCell; use crate::history_cell::HistoryCell; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use std::sync::Arc; @@ -33,7 +33,7 @@ pub(crate) fn older_exploration_group( let items = adjacent_items(older, newer, turns)?; let older_items = items .iter() - .filter(|item| !matches!(item, ThreadItem::Reasoning { .. })) + .filter(|entry| !matches!(&entry.payload, ThreadItemPayload::Reasoning { .. })) .take(older.group.calls.len()) .cloned() .collect::>(); @@ -46,7 +46,7 @@ fn adjacent_items<'a>( older: &ExecCell, newer: &ExecCell, turns: &'a [Turn], -) -> Option<&'a [ThreadItem]> { +) -> Option<&'a [codex_app_server_protocol::ThreadItem]> { if !older.is_exploring_cell() || older.should_flush() || !newer.is_exploring_cell() { return None; } @@ -58,10 +58,11 @@ fn adjacent_items<'a>( super::computer_groups::adjacent_activity_items(&ids, turns) } -pub(super) fn completed_group(items: &[ThreadItem]) -> Option { +pub(super) fn completed_group(items: &[codex_app_server_protocol::ThreadItem]) -> Option { let mut group: Option = None; - for item in items { - if matches!(item, ThreadItem::Reasoning { .. }) { + for entry in items { + let item = &entry.payload; + if matches!(item, ThreadItemPayload::Reasoning { .. }) { continue; } let call = CommandHistory::from_item(item.clone())?.into_cell(); diff --git a/codex-rs/tui/src/thread_transcript/other_items.rs b/codex-rs/tui/src/thread_transcript/other_items.rs index 3d81922dfa..b5f87b7b26 100644 --- a/codex-rs/tui/src/thread_transcript/other_items.rs +++ b/codex-rs/tui/src/thread_transcript/other_items.rs @@ -9,16 +9,16 @@ use crate::history_cell; use crate::history_cell::PlainHistoryCell; use crate::multi_agents; use codex_app_server_protocol::PatchApplyStatus; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::WebSearchAction; use codex_utils_absolute_path::AbsolutePathBuf; use ratatui::style::Stylize as _; use std::sync::Arc; -pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells { +pub(super) fn cells(item: ThreadItemPayload, cwd: &AbsolutePathBuf) -> TranscriptCells { let mut cells: TranscriptCells = Vec::new(); match item { - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id, changes, status, @@ -50,15 +50,17 @@ pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells ))); } }, - ThreadItem::WebSearch(item) => cells.push(Arc::new(history_cell::new_web_search_call( - item.id, - item.query, - item.action.unwrap_or(WebSearchAction::Other), - ))), - ThreadItem::ImageView { id, path } => { + ThreadItemPayload::WebSearch(item) => { + cells.push(Arc::new(history_cell::new_web_search_call( + item.id, + item.query, + item.action.unwrap_or(WebSearchAction::Other), + ))) + } + ThreadItemPayload::ImageView { id, path } => { cells.push(Arc::new(history_cell::new_view_image_tool_call(id, path))) } - ThreadItem::ImageGeneration(item) + ThreadItemPayload::ImageGeneration(item) if !matches!(item.status.as_str(), "completed" | "failed") => { let status = if item.status.is_empty() { @@ -71,7 +73,7 @@ pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells /*hint*/ None, ))); } - ThreadItem::ImageGeneration(item) => { + ThreadItemPayload::ImageGeneration(item) => { cells.push(Arc::new(history_cell::new_image_generation_call( item.id, &item.status, @@ -79,7 +81,7 @@ pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells item.saved_path, ))); } - item @ ThreadItem::CollabAgentToolCall { .. } => { + item @ ThreadItemPayload::CollabAgentToolCall { .. } => { if let Some(cell) = multi_agents::tool_call_history_cell( &item, /*cached_spawn_request*/ None, @@ -88,29 +90,29 @@ pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells cells.push(Arc::new(cell)); } } - item @ ThreadItem::SubAgentActivity { .. } => { + item @ ThreadItemPayload::SubAgentActivity { .. } => { if let Some(cell) = multi_agents::sub_agent_activity_history_cell(&item) { cells.push(Arc::new(cell)); } } - ThreadItem::EnteredReviewMode { review, .. } => { + ThreadItemPayload::EnteredReviewMode { review, .. } => { cells.push(Arc::new(history_cell::new_review_status_line(format!( ">> Code review started: {review} <<" )))); } - ThreadItem::ExitedReviewMode { review, .. } => { + ThreadItemPayload::ExitedReviewMode { review, .. } => { cells.push(Arc::new(history_cell::new_review_status_line(format!( "<< Code review finished: {review} >>" )))); } - ThreadItem::ContextCompaction { .. } => { + ThreadItemPayload::ContextCompaction { .. } => { cells.push(Arc::new(history_cell::new_info_event( "Context compacted".to_string(), /*hint*/ None, ))); } // These items do not have richer history-cell presentations yet. - ThreadItem::HookPrompt { fragments, .. } => { + ThreadItemPayload::HookPrompt { fragments, .. } => { if !fragments.is_empty() { cells.push(Arc::new(PlainHistoryCell::new( fragments @@ -126,19 +128,19 @@ pub(super) fn cells(item: ThreadItem, cwd: &AbsolutePathBuf) -> TranscriptCells ))); } } - item @ ThreadItem::DynamicToolCall { .. } => { + item @ ThreadItemPayload::DynamicToolCall { .. } => { if let Some(cell) = history_cell::DynamicToolCallCell::from_item(item) { cells.push(Arc::new(cell)); } } - ThreadItem::UserMessage { .. } - | ThreadItem::AgentMessage { .. } - | ThreadItem::FunctionCallOutput { .. } - | ThreadItem::Plan { .. } - | ThreadItem::Reasoning { .. } - | ThreadItem::CommandExecution { .. } - | ThreadItem::McpToolCall { .. } - | ThreadItem::Sleep(_) => {} + ThreadItemPayload::UserMessage { .. } + | ThreadItemPayload::AgentMessage { .. } + | ThreadItemPayload::FunctionCallOutput { .. } + | ThreadItemPayload::Plan { .. } + | ThreadItemPayload::Reasoning { .. } + | ThreadItemPayload::CommandExecution { .. } + | ThreadItemPayload::McpToolCall { .. } + | ThreadItemPayload::Sleep(_) => {} } cells } diff --git a/codex-rs/tui/src/thread_transcript/other_items_tests.rs b/codex-rs/tui/src/thread_transcript/other_items_tests.rs index de095cffce..6dfe8ad9ab 100644 --- a/codex-rs/tui/src/thread_transcript/other_items_tests.rs +++ b/codex-rs/tui/src/thread_transcript/other_items_tests.rs @@ -12,7 +12,7 @@ use codex_app_server_protocol::ImageGenerationItem; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; use codex_app_server_protocol::SubAgentActivityKind; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::WebSearchAction; use codex_app_server_protocol::WebSearchItem; use codex_utils_path_uri::LegacyAppPathString; @@ -68,7 +68,7 @@ fn completed_patch_restores_rich_diff_and_styles() { ); changes[2].diff.push_str("\n\nMoved to: src/after.rs"); let actual = cells( - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id: "patch-1".to_string(), changes, status: PatchApplyStatus::Completed, @@ -94,7 +94,7 @@ fn unfinished_and_rejected_patches_keep_their_outcome() { .into_iter() .flat_map(|status| { cells( - ThreadItem::FileChange { + ThreadItemPayload::FileChange { id: "patch-1".to_string(), changes: vec![FileUpdateChange { path: "main.rs".to_string(), @@ -136,11 +136,11 @@ fn tool_and_notice_projection_uses_normal_transcript_presentation() { generation_id: None, }; let items = vec![ - ThreadItem::EnteredReviewMode { + ThreadItemPayload::EnteredReviewMode { id: "review-start".to_string(), review: "current changes".to_string(), }, - ThreadItem::WebSearch(WebSearchItem { + ThreadItemPayload::WebSearch(WebSearchItem { id: "search-1".to_string(), query: "fallback query".to_string(), action: Some(WebSearchAction::FindInPage { @@ -149,16 +149,16 @@ fn tool_and_notice_projection_uses_normal_transcript_presentation() { }), results: None, }), - ThreadItem::ImageView { + ThreadItemPayload::ImageView { id: "view-1".to_string(), path: LegacyAppPathString::from_string("diagram.png".to_string()), }, - ThreadItem::ImageGeneration(image.clone()), - ThreadItem::ImageGeneration(ImageGenerationItem { + ThreadItemPayload::ImageGeneration(image.clone()), + ThreadItemPayload::ImageGeneration(ImageGenerationItem { status: "in_progress".into(), ..image }), - ThreadItem::SubAgentActivity { + ThreadItemPayload::SubAgentActivity { model: None, reasoning_effort: None, id: "agent-1".to_string(), @@ -166,11 +166,11 @@ fn tool_and_notice_projection_uses_normal_transcript_presentation() { agent_thread_id: "01912345-1234-7123-8123-123456789abc".to_string(), agent_path: "/root/reviewer".to_string(), }, - ThreadItem::ExitedReviewMode { + ThreadItemPayload::ExitedReviewMode { id: "review-end".to_string(), review: "No findings".to_string(), }, - ThreadItem::ContextCompaction { + ThreadItemPayload::ContextCompaction { id: "compact-1".to_string(), }, ]; diff --git a/codex-rs/tui/src/thread_transcript/tools.rs b/codex-rs/tui/src/thread_transcript/tools.rs index 73e80d617c..14fe7b1f02 100644 --- a/codex-rs/tui/src/thread_transcript/tools.rs +++ b/codex-rs/tui/src/thread_transcript/tools.rs @@ -18,7 +18,7 @@ use codex_app_server_protocol::CollabAgentToolCallStatus; use codex_app_server_protocol::CommandExecutionSource; use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::McpToolCallStatus; -use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadItemPayload; use codex_protocol::mcp::CallToolResult; use codex_protocol::parse_command::ParsedCommand; use ratatui::style::Stylize as _; @@ -27,9 +27,9 @@ use ratatui::text::Line; /// Preserve status and output when replay cannot reconstruct a rich completed tool cell. /// Declined commands never ran, and historical terminal interactions do not carry stdin details. /// Pending items retain their last known state without starting a running clock. -pub(crate) fn historical_tool_fallback(item: &ThreadItem) -> Option { +pub(crate) fn historical_tool_fallback(item: &ThreadItemPayload) -> Option { let lines = match item { - ThreadItem::CommandExecution { + ThreadItemPayload::CommandExecution { command, source, status, @@ -73,7 +73,7 @@ pub(crate) fn historical_tool_fallback(item: &ThreadItem) -> Option Option Option { - let ThreadItem::CommandExecution { + pub(crate) fn from_item(item: ThreadItemPayload) -> Option { + let ThreadItemPayload::CommandExecution { id, command, source, @@ -192,8 +192,8 @@ pub(crate) struct McpHistory { } impl McpHistory { - pub(crate) fn from_item(item: ThreadItem) -> Option { - let ThreadItem::McpToolCall { + pub(crate) fn from_item(item: ThreadItemPayload) -> Option { + let ThreadItemPayload::McpToolCall { id, server, tool, diff --git a/codex-rs/tui/src/thread_transcript/tools_tests.rs b/codex-rs/tui/src/thread_transcript/tools_tests.rs index 267168c876..c3249b1614 100644 --- a/codex-rs/tui/src/thread_transcript/tools_tests.rs +++ b/codex-rs/tui/src/thread_transcript/tools_tests.rs @@ -9,8 +9,8 @@ use codex_utils_path_uri::LegacyAppPathString; use pretty_assertions::assert_eq; use serde_json::json; -fn command_item(status: CommandExecutionStatus) -> ThreadItem { - ThreadItem::CommandExecution { +fn command_item(status: CommandExecutionStatus) -> ThreadItemPayload { + ThreadItemPayload::CommandExecution { id: "command".to_string(), plugin_id: None, script_path: None, @@ -35,8 +35,8 @@ fn command_item(status: CommandExecutionStatus) -> ThreadItem { } } -fn mcp_item(server: &str, id: &str) -> ThreadItem { - ThreadItem::McpToolCall { +fn mcp_item(server: &str, id: &str) -> ThreadItemPayload { + ThreadItemPayload::McpToolCall { id: id.to_string(), server: server.to_string(), tool: "js".to_string(), @@ -100,7 +100,7 @@ fn completed_tools_keep_compact_and_detailed_presentations() { fn failed_commands_keep_failure_when_exit_code_is_missing_or_zero() { for exit in [None, Some(0)] { let mut item = command_item(CommandExecutionStatus::Failed); - if let ThreadItem::CommandExecution { exit_code, .. } = &mut item { + if let ThreadItemPayload::CommandExecution { exit_code, .. } = &mut item { *exit_code = exit; } let command = CommandHistory::from_item(item).unwrap(); @@ -112,12 +112,12 @@ fn failed_commands_keep_failure_when_exit_code_is_missing_or_zero() { fn incomplete_tool_payloads_preserve_last_known_status_and_output() { let command = command_item(CommandExecutionStatus::InProgress); let mut mcp = mcp_item("example", "pending"); - if let ThreadItem::McpToolCall { status, result, .. } = &mut mcp { + if let ThreadItemPayload::McpToolCall { status, result, .. } = &mut mcp { *status = McpToolCallStatus::InProgress; *result = None; } let mut missing_result = mcp_item("example", "completed"); - if let ThreadItem::McpToolCall { result, .. } = &mut missing_result { + if let ThreadItemPayload::McpToolCall { result, .. } = &mut missing_result { *result = None; } let cwd = test_path_buf("/workspace").abs(); @@ -141,7 +141,7 @@ fn historical_command_fallbacks_preserve_status_output_and_group_boundaries() { let cwd = test_path_buf("/workspace").abs(); let exploration = |call_id: &str| { let mut item = command_item(CommandExecutionStatus::Completed); - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { id, command, command_actions, @@ -204,7 +204,7 @@ fn historical_command_fallbacks_preserve_status_output_and_group_boundaries() { ), ] { let mut item = command_item(status); - if let ThreadItem::CommandExecution { + if let ThreadItemPayload::CommandExecution { source: item_source, command, aggregated_output, @@ -305,7 +305,7 @@ fn agent_tool_fallbacks_preserve_status_without_duplicating_v2_activity() { | CollabAgentTool::ResumeAgent | CollabAgentTool::Wait ); - let item = ThreadItem::CollabAgentToolCall { + let item = ThreadItemPayload::CollabAgentToolCall { id: "pending-agent-call".to_string(), tool, status, diff --git a/codex-rs/tui/src/transcript_view/mutations_tests.rs b/codex-rs/tui/src/transcript_view/mutations_tests.rs index 25edcdf7b0..fa79740834 100644 --- a/codex-rs/tui/src/transcript_view/mutations_tests.rs +++ b/codex-rs/tui/src/transcript_view/mutations_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::history_cell::PlainHistoryCell; +use codex_app_server_protocol::ThreadItem; use pretty_assertions::assert_eq; fn cell(text: &str) -> Arc { @@ -360,7 +361,7 @@ fn folding_a_reasoning_only_page_retains_its_find_and_copy_revision() { use crate::thread_transcript::RawReasoningVisibility; use crate::thread_transcript::fold_trailing_activity_details; use crate::thread_transcript::thread_items_to_transcript_cells; - use codex_app_server_protocol::ThreadItem; + use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; @@ -371,7 +372,7 @@ fn folding_a_reasoning_only_page_retains_its_find_and_copy_revision() { "result": {"content": [{"type": "text", "text": "retained call output"}]}, "durationMs": 1 })) .expect("computer call"); - let reasoning = ThreadItem::Reasoning { + let reasoning = ThreadItemPayload::Reasoning { id: "reasoning".to_owned(), summary: vec!["retained needle text with exact spacing".to_owned()], content: Vec::new(), @@ -379,7 +380,7 @@ fn folding_a_reasoning_only_page_retains_its_find_and_copy_revision() { let current = Turn { id: "turn".to_owned(), root_turn_id: None, - items: vec![call, reasoning], + items: vec![call, ThreadItem::untimed(reasoning)], items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, @@ -387,11 +388,11 @@ fn folding_a_reasoning_only_page_retains_its_find_and_copy_revision() { completed_at: None, duration_ms: None, }; - let project = |items: &[ThreadItem]| { + let project = |items: &[codex_app_server_protocol::ThreadItem]| { thread_items_to_transcript_cells( /*thread_id*/ None, &codex_utils_absolute_path::AbsolutePathBuf::current_dir().unwrap(), - items.iter().cloned(), + items.iter().map(|entry| entry.payload.clone()), RawReasoningVisibility::Hidden, /*config*/ None, ) diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index d03cdbe345..de3734c381 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -529,6 +529,13 @@ def _normalized_schema_bundle_text(schema_dir: Path) -> str: _preserve_guardian_approval_path_wrappers(schema) definitions = schema.get("definitions", {}) if isinstance(definitions, dict): + # Pydantic represents the tagged item union as its existing nominal variant classes. + # Give each variant the shared fields so live and persisted items parse identically. + thread_item = definitions.get("ThreadItem", {}) + timing = thread_item.pop("properties", {}) + for variant in thread_item.get("oneOf", []): + variant["properties"].update(timing) + # Older SDKs accept omission for these nullable project and section IDs. for model, field in ( ("Thread", "projectId"), diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 6bceb284b9..3af00d6150 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -5309,8 +5309,22 @@ class HookPromptThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None fragments: list[HookPromptFragment] id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["hookPrompt"], Field(title="HookPromptThreadItemType")] @@ -5318,7 +5332,21 @@ class PlanThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None text: str type: Annotated[Literal["plan"], Field(title="PlanThreadItemType")] @@ -5327,8 +5355,22 @@ class ReasoningThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None content: list[str] | None = [] id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None summary: list[str] | None = [] type: Annotated[Literal["reasoning"], Field(title="ReasoningThreadItemType")] @@ -5339,6 +5381,13 @@ class McpToolCallThreadItem(BaseModel): ) app_context: Annotated[McpToolCallAppContext | None, Field(alias="appContext")] = None arguments: Any + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None duration_ms: Annotated[ int | None, Field(alias="durationMs", description="The duration of the MCP tool call in milliseconds."), @@ -5363,6 +5412,13 @@ class McpToolCallThreadItem(BaseModel): read_only_hint: Annotated[bool | None, Field(alias="readOnlyHint")] = None result: McpToolCallResult | None = None server: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: McpToolCallStatus tool: str type: Annotated[Literal["mcpToolCall"], Field(title="McpToolCallThreadItemType")] @@ -5373,6 +5429,13 @@ class DynamicToolCallThreadItem(BaseModel): populate_by_name=True, ) arguments: Any + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None content_items: Annotated[ list[DynamicToolCallOutputContentItem] | None, Field(alias="contentItems") ] = None @@ -5384,6 +5447,13 @@ class DynamicToolCallThreadItem(BaseModel): ] = None id: str namespace: str | None = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: DynamicToolCallStatus success: bool | None = None tool: str @@ -5396,6 +5466,13 @@ class SubAgentActivityThreadItem(BaseModel): ) agent_path: Annotated[str, Field(alias="agentPath")] agent_thread_id: Annotated[str, Field(alias="agentThreadId")] + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str kind: SubAgentActivityKind model: Annotated[ @@ -5411,6 +5488,13 @@ class SubAgentActivityThreadItem(BaseModel): description="Resolved reasoning effort at sub-agent creation, when known.", ), ] = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["subAgentActivity"], Field(title="SubAgentActivityThreadItemType")] @@ -5418,8 +5502,22 @@ class ImageViewThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str path: LegacyAppPathString + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["imageView"], Field(title="ImageViewThreadItemType")] @@ -5427,8 +5525,22 @@ class SleepThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None duration_ms: Annotated[int, Field(alias="durationMs", ge=0)] id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["sleep"], Field(title="SleepThreadItemType")] @@ -5436,6 +5548,13 @@ class FileIdThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None failure: ImageGenerationFailure | None = None id: str input_images: Annotated[ @@ -5447,6 +5566,13 @@ class FileIdThreadItem(BaseModel): ] = None revised_prompt: Annotated[str | None, Field(alias="revisedPrompt")] = None saved_path: Annotated[AbsolutePathBuf | None, Field(alias="savedPath")] = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: str transparent_background: Annotated[bool | None, Field(alias="transparentBackground")] = None type: Annotated[Literal["imageGeneration"], Field(title="ImageGenerationThreadItemType")] @@ -5457,6 +5583,13 @@ class ResultThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None failure: ImageGenerationFailure | None = None id: str input_images: Annotated[ @@ -5468,6 +5601,13 @@ class ResultThreadItem(BaseModel): ] = None revised_prompt: Annotated[str | None, Field(alias="revisedPrompt")] = None saved_path: Annotated[AbsolutePathBuf | None, Field(alias="savedPath")] = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: str transparent_background: Annotated[bool | None, Field(alias="transparentBackground")] = None type: Annotated[Literal["imageGeneration"], Field(title="ImageGenerationThreadItemType")] @@ -5481,8 +5621,22 @@ class EnteredReviewModeThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str review: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["enteredReviewMode"], Field(title="EnteredReviewModeThreadItemType")] @@ -5490,8 +5644,22 @@ class ExitedReviewModeThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str review: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["exitedReviewMode"], Field(title="ExitedReviewModeThreadItemType")] @@ -5499,7 +5667,21 @@ class ContextCompactionThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["contextCompaction"], Field(title="ContextCompactionThreadItemType")] @@ -9993,8 +10175,22 @@ class UserMessageThreadItem(BaseModel): populate_by_name=True, ) client_id: Annotated[str | None, Field(alias="clientId")] = None + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None content: list[UserInput] id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["userMessage"], Field(title="UserMessageThreadItemType")] @@ -10002,11 +10198,25 @@ class AgentMessageThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None delivery: AgentMessageDelivery | None = None id: str memory_citation: Annotated[MemoryCitation | None, Field(alias="memoryCitation")] = None phase: MessagePhase | None = None questions: list[AsyncUserInputQuestion] | None = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None text: str type: Annotated[Literal["agentMessage"], Field(title="AgentMessageThreadItemType")] @@ -10030,6 +10240,13 @@ class CommandExecutionThreadItem(BaseModel): description="A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", ), ] + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None cwd: Annotated[LegacyAppPathString, Field(description="The command's working directory.")] duration_ms: Annotated[ int | None, @@ -10063,6 +10280,13 @@ class CommandExecutionThreadItem(BaseModel): ), ] = None source: CommandExecutionSource | None = "agent" + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: CommandExecutionStatus type: Annotated[Literal["commandExecution"], Field(title="CommandExecutionThreadItemType")] @@ -10072,7 +10296,21 @@ class FileChangeThreadItem(BaseModel): populate_by_name=True, ) changes: list[FileUpdateChange] + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: PatchApplyStatus type: Annotated[Literal["fileChange"], Field(title="FileChangeThreadItemType")] @@ -10088,6 +10326,13 @@ class CollabAgentToolCallThreadItem(BaseModel): description="Last known status of the target agents, when available.", ), ] + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: Annotated[str, Field(description="Unique identifier for this collab tool call.")] model: Annotated[ str | None, Field(description="Model requested for the spawned agent, when applicable.") @@ -10116,6 +10361,13 @@ class CollabAgentToolCallThreadItem(BaseModel): alias="senderThreadId", description="Thread ID of the agent issuing the collab request." ), ] + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None status: Annotated[ CollabAgentToolCallStatus, Field(description="Current status of the collab tool call.") ] @@ -10130,6 +10382,13 @@ class WebSearchThreadItem(BaseModel): populate_by_name=True, ) action: WebSearchAction | None = None + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str query: str results: Annotated[ @@ -10138,6 +10397,13 @@ class WebSearchThreadItem(BaseModel): description="Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release." ), ] = None + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["webSearch"], Field(title="WebSearchThreadItemType")] @@ -11575,10 +11841,24 @@ class FunctionCallOutputThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int | None, + Field( + alias="completedAtMs", + description="Unix timestamp in milliseconds when the item completed, if recorded.", + ), + ] = None id: str name: str namespace: str | None = None output: FunctionCallOutputBody + started_at_ms: Annotated[ + int | None, + Field( + alias="startedAtMs", + description="Unix timestamp in milliseconds when the item started, if recorded.", + ), + ] = None type: Annotated[Literal["functionCallOutput"], Field(title="FunctionCallOutputThreadItemType")] @@ -11606,7 +11886,7 @@ class ThreadItem( | ContextCompactionThreadItem ] ): - root: ( + root: Annotated[ UserMessageThreadItem | HookPromptThreadItem | AgentMessageThreadItem @@ -11626,8 +11906,11 @@ class ThreadItem( | ResultThreadItem | EnteredReviewModeThreadItem | ExitedReviewModeThreadItem - | ContextCompactionThreadItem - ) + | ContextCompactionThreadItem, + Field( + description="A thread item and its recorded lifecycle times. The payload stays flat on the wire." + ), + ] class ThreadItemEntry(BaseModel): diff --git a/sdk/python/tests/test_app_server_inputs.py b/sdk/python/tests/test_app_server_inputs.py index 26e0bfa659..0bc9100091 100644 --- a/sdk/python/tests/test_app_server_inputs.py +++ b/sdk/python/tests/test_app_server_inputs.py @@ -16,6 +16,8 @@ from openai_codex import ( SkillInput, TextInput, ) +from openai_codex.generated.v2_all import FunctionCallOutputThreadItem +from openai_codex.types import ThreadItem def _external_items(request) -> list[dict]: @@ -49,7 +51,16 @@ def test_external_message_preserves_tool_authority_through_resume(tmp_path) -> N with Codex(config=harness.app_server_config()) as codex: resumed = codex.thread_resume(thread.id, include_turns=False) history = resumed.read(include_turns=True) - assert external_item in history.thread.turns[0].items + persisted = next( + item + for item in history.thread.turns[0].items + if item.root.id == external_item.root.id + ) + assert isinstance(persisted, ThreadItem) + assert isinstance(persisted.root, FunctionCallOutputThreadItem) + assert persisted.root.started_at_ms is not None + assert persisted.root.completed_at_ms is not None + assert persisted == external_item resumed.run("Summarize the external update.") requests = harness.responses.requests()