From 57f865c069c4acc213d43371a82671b2deed4e1c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 17 Mar 2026 00:50:25 -0600 Subject: [PATCH] Fix tui_app_server: ignore duplicate legacy stream events (#14892) The in-process app-server currently emits both typed `ServerNotification`s and legacy `codex/event/*` notifications for the same live turn updates. `tui_app_server` was consuming both paths, so message deltas and completed items could be enqueued twice and rendered as duplicated output in the transcript. Ignore legacy notifications for event types that already have typed (app server) notification handling, while keeping legacy fallback behavior for events that still only arrive on the old path. This preserves compatibility without duplicating streamed commentary or final agent output. We will remove all of the legacy event handlers over time; they're here only during the short window where we're moving the tui to use the app server. --- .../src/app/app_server_adapter.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index 889c47202..d21542b8b 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -89,6 +89,17 @@ impl App { ); } notification => { + if !app_server_client.is_remote() + && matches!( + notification, + ServerNotification::TurnCompleted(_) + | ServerNotification::ThreadRealtimeItemAdded(_) + | ServerNotification::ThreadRealtimeOutputAudioDelta(_) + | ServerNotification::ThreadRealtimeError(_) + ) + { + return; + } if let Some((thread_id, events)) = server_notification_thread_events(notification) { @@ -116,6 +127,9 @@ impl App { AppServerEvent::LegacyNotification(notification) => { if let Some((thread_id, event)) = legacy_thread_event(notification.params) { self.pending_app_server_requests.note_legacy_event(&event); + if legacy_event_is_shadowed_by_server_notification(&event.msg) { + return; + } if self.primary_thread_id.is_none() || matches!(event.msg, EventMsg::SessionConfigured(_)) && self.primary_thread_id == Some(thread_id) @@ -198,6 +212,24 @@ fn legacy_thread_event(params: Option) -> Option<(ThreadId, Event)> { Some((thread_id, event)) } +fn legacy_event_is_shadowed_by_server_notification(msg: &EventMsg) -> bool { + matches!( + msg, + EventMsg::TokenCount(_) + | EventMsg::Error(_) + | EventMsg::ThreadNameUpdated(_) + | EventMsg::TurnStarted(_) + | EventMsg::ItemStarted(_) + | EventMsg::ItemCompleted(_) + | EventMsg::AgentMessageDelta(_) + | EventMsg::PlanDelta(_) + | EventMsg::AgentReasoningDelta(_) + | EventMsg::AgentReasoningRawContentDelta(_) + | EventMsg::RealtimeConversationStarted(_) + | EventMsg::RealtimeConversationClosed(_) + ) +} + fn server_notification_thread_events( notification: ServerNotification, ) -> Option<(ThreadId, Vec)> {