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 d95e19e63a..83628bab4d 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -313,8 +313,11 @@ impl EventProcessor for EventProcessorWithHumanOutput { } TypedExecEvent::TurnCompleted(notification) => match notification.turn.status { TurnStatus::Completed => { - self.final_message = - final_message_from_turn_items(notification.turn.items.as_slice()); + if let Some(final_message) = + final_message_from_turn_items(notification.turn.items.as_slice()) + { + self.final_message = Some(final_message); + } self.print_usage(); CodexStatus::InitiateShutdown } @@ -599,4 +602,39 @@ mod tests { ); assert_eq!(processor.final_message.as_deref(), Some("final answer")); } + + #[test] + fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() { + let mut processor = EventProcessorWithHumanOutput { + bold: Style::new(), + cyan: Style::new(), + dimmed: Style::new(), + green: Style::new(), + red: Style::new(), + yellow: Style::new(), + show_agent_reasoning: true, + show_raw_agent_reasoning: false, + last_message_path: None, + final_message: Some("streamed answer".to_string()), + last_total_token_usage: None, + }; + + let status = processor.process_event(TypedExecEvent::TurnCompleted( + codex_app_server_protocol::TurnCompletedNotification { + thread_id: "thread-1".to_string(), + turn: Turn { + id: "turn-1".to_string(), + items: Vec::new(), + status: TurnStatus::Completed, + error: None, + }, + }, + )); + + assert_eq!( + status, + crate::event_processor::CodexStatus::InitiateShutdown + ); + assert_eq!(processor.final_message.as_deref(), Some("streamed answer")); + } } 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 878e7619d1..ecee916231 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -406,8 +406,11 @@ impl EventProcessorWithJsonOutput { } match notification.turn.status { TurnStatus::Completed => { - self.final_message = - Self::final_message_from_turn_items(notification.turn.items.as_slice()); + if let Some(final_message) = + Self::final_message_from_turn_items(notification.turn.items.as_slice()) + { + self.final_message = Some(final_message); + } events.push(ThreadEvent::TurnCompleted(TurnCompletedEvent { usage: self.usage_from_last_total(), })); 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 0793af6fc3..5e9f1142ac 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 @@ -515,6 +515,34 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { assert_eq!(processor.final_message.as_deref(), Some("final answer")); } +#[test] +fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() { + let mut processor = EventProcessorWithJsonOutput::new(None); + processor.final_message = Some("streamed answer".to_string()); + + let completed = + processor.collect_thread_events(TypedExecEvent::TurnCompleted(TurnCompletedNotification { + thread_id: "thread-1".to_string(), + turn: Turn { + id: "turn-1".to_string(), + items: Vec::new(), + status: TurnStatus::Completed, + error: None, + }, + })); + + assert_eq!( + completed, + CollectedThreadEvents { + events: vec![ThreadEvent::TurnCompleted(TurnCompletedEvent { + usage: Usage::default(), + })], + status: CodexStatus::InitiateShutdown, + } + ); + assert_eq!(processor.final_message.as_deref(), Some("streamed answer")); +} + #[test] fn turn_completion_falls_back_to_final_plan_text() { let mut processor = EventProcessorWithJsonOutput::new(None);