From f7046293689cbff9588b0c88c2852b1830436f80 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 22 Feb 2026 19:32:00 -0300 Subject: [PATCH] fix(core): harden realtime input routing and mirroring Ignore empty or whitespace-only realtime transcript input so we do not create no-op user turns from blank transcriptions. Move realtime text mirroring off the `send_event` hot path and add bounded truncation for mirrored patch output and file lists, while adding a serde alias for `background_terminal_timeout` compatibility. --- codex-rs/core/src/codex.rs | 66 +++++++++++++++++++++++++++++++-- codex-rs/core/src/config/mod.rs | 1 + 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 758b592047..3fb57d3519 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2164,6 +2164,9 @@ impl Session { } pub(crate) async fn route_realtime_text_input(self: &Arc, text: String) { + if text.trim().is_empty() { + return; + } handlers::user_input_or_turn( self, self.next_internal_sub_id(), @@ -7021,12 +7024,72 @@ fn agent_message_text(item: &codex_protocol::items::AgentMessageItem) -> String } fn realtime_text_for_event(msg: &EventMsg) -> Option { + const REALTIME_MIRROR_MAX_CHARS: usize = 2_000; + const REALTIME_MIRROR_MAX_FILES: usize = 50; + match msg { EventMsg::AgentMessage(event) => Some(event.message.clone()), EventMsg::ItemCompleted(event) => match &event.item { TurnItem::AgentMessage(item) => Some(agent_message_text(item)), _ => None, }, + EventMsg::ExecCommandBegin(event) => { + let command = event.command.join(" "); + Some(format!( + "Exec command started: {command}\nWorking directory: {}", + event.cwd.display() + )) + } + EventMsg::PatchApplyBegin(event) => { + let mut files: Vec = event + .changes + .keys() + .map(|path| path.display().to_string()) + .collect(); + files.sort(); + let total_file_count = files.len(); + let truncated_file_count = total_file_count.saturating_sub(REALTIME_MIRROR_MAX_FILES); + if truncated_file_count > 0 { + files.truncate(REALTIME_MIRROR_MAX_FILES); + } + let file_list = if files.is_empty() { + "none".to_string() + } else { + files.join(", ") + }; + let file_list = if truncated_file_count > 0 { + format!("{file_list} ...(truncated, +{truncated_file_count} more)") + } else { + file_list + }; + Some(format!( + "apply_patch started ({count} file change(s))\nFiles: {file_list}", + count = total_file_count + )) + } + EventMsg::PatchApplyEnd(event) => { + let status = match event.status { + codex_protocol::protocol::PatchApplyStatus::Completed => "completed", + codex_protocol::protocol::PatchApplyStatus::Failed => "failed", + codex_protocol::protocol::PatchApplyStatus::Declined => "declined", + }; + let mut text = format!("apply_patch {status}"); + if !event.stdout.is_empty() { + text.push_str(&format!("\nstdout:\n{}", event.stdout)); + } + if !event.stderr.is_empty() { + text.push_str(&format!("\nstderr:\n{}", event.stderr)); + } + if text.len() > REALTIME_MIRROR_MAX_CHARS { + let mut end = REALTIME_MIRROR_MAX_CHARS; + while !text.is_char_boundary(end) { + end -= 1; + } + text.truncate(end); + text.push_str("\n...(truncated)"); + } + Some(text) + } EventMsg::Error(_) | EventMsg::Warning(_) | EventMsg::RealtimeConversationStarted(_) @@ -7053,12 +7116,9 @@ fn realtime_text_for_event(msg: &EventMsg) -> Option { | EventMsg::McpToolCallEnd(_) | EventMsg::WebSearchBegin(_) | EventMsg::WebSearchEnd(_) - | EventMsg::ExecCommandBegin(_) | EventMsg::ExecCommandOutputDelta(_) | EventMsg::TerminalInteraction(_) | EventMsg::ExecCommandEnd(_) - | EventMsg::PatchApplyBegin(_) - | EventMsg::PatchApplyEnd(_) | EventMsg::ViewImageToolCall(_) | EventMsg::ImageGenerationBegin(_) | EventMsg::ImageGenerationEnd(_) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8eb0691330..6d5d28709a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1270,6 +1270,7 @@ pub struct ConfigToml { /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. /// Default: `300000` (5 minutes). + #[serde(alias = "background_terminal_timeout")] pub background_terminal_max_timeout: Option, /// Optional absolute path to the Node runtime used by `js_repl`.