From f28dfff641804947fa53981db0b1173d369f537d Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:02:02 +0800 Subject: [PATCH 1/8] fix(tui): drain redraw bursts in frame scheduler --- codex-rs/tui/src/tui/frame_requester.rs | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/tui/frame_requester.rs b/codex-rs/tui/src/tui/frame_requester.rs index d7e54d82cc9c..7ad2b2ee0d0c 100644 --- a/codex-rs/tui/src/tui/frame_requester.rs +++ b/codex-rs/tui/src/tui/frame_requester.rs @@ -107,12 +107,18 @@ impl FrameScheduler { // All senders dropped; exit the scheduler. break }; - let draw_at = self.rate_limiter.clamp_deadline(draw_at); + let mut draw_at = self.rate_limiter.clamp_deadline(draw_at); + while let Ok(pending_draw_at) = self.receiver.try_recv() { + let pending_draw_at = self.rate_limiter.clamp_deadline(pending_draw_at); + draw_at = draw_at.min(pending_draw_at); + } next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at))); // Do not send a draw immediately here. By continuing the loop, // we recompute the sleep target so the draw fires once via the // sleep branch, coalescing multiple requests into a single draw. + // Draining the ready queue here avoids leaving a long tail of + // stale redraw requests after bursty streaming updates. continue; } _ = &mut deadline => { @@ -208,6 +214,32 @@ mod tests { assert!(second.is_err(), "unexpected extra draw received"); } + #[tokio::test(flavor = "current_thread", start_paused = true)] + async fn test_drains_bursty_requests_without_backlog_tail() { + let (draw_tx, mut draw_rx) = broadcast::channel(16); + let requester = FrameRequester::new(draw_tx); + + for _ in 0..64 { + requester.schedule_frame(); + } + + time::advance(Duration::from_millis(1)).await; + + let first = draw_rx + .recv() + .timeout(Duration::from_millis(50)) + .await + .expect("timed out waiting for bursty draw"); + assert!(first.is_ok(), "broadcast closed unexpectedly"); + + time::advance(MIN_FRAME_INTERVAL * 4).await; + let second = draw_rx.recv().timeout(Duration::from_millis(5)).await; + assert!( + second.is_err(), + "unexpected extra draw received from drained redraw backlog" + ); + } + #[tokio::test(flavor = "current_thread", start_paused = true)] async fn test_coalesces_mixed_immediate_and_delayed_requests() { let (draw_tx, mut draw_rx) = broadcast::channel(16); From a2aaf27f7688ed105eab94f975d95ae7a1fee3ef Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:29:40 +0800 Subject: [PATCH 2/8] Retry transient turn failures before queue advance --- codex-rs/tui/src/chatwidget.rs | 95 +++++++++++++- codex-rs/tui/src/chatwidget/tests.rs | 184 +++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2666f00ecdfe..2c988eb35e8e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -522,6 +522,43 @@ fn rate_limit_error_kind(info: &CodexErrorInfo) -> Option { } } +fn is_transient_turn_failure(codex_error_info: Option<&CodexErrorInfo>, message: &str) -> bool { + match codex_error_info { + Some( + CodexErrorInfo::ServerOverloaded + | CodexErrorInfo::HttpConnectionFailed { .. } + | CodexErrorInfo::ResponseStreamConnectionFailed { .. } + | CodexErrorInfo::InternalServerError + | CodexErrorInfo::ResponseStreamDisconnected { .. } + | CodexErrorInfo::ResponseTooManyFailedAttempts { .. }, + ) => true, + Some( + CodexErrorInfo::ContextWindowExceeded + | CodexErrorInfo::UsageLimitExceeded + | CodexErrorInfo::Unauthorized + | CodexErrorInfo::BadRequest + | CodexErrorInfo::SandboxError + | CodexErrorInfo::ThreadRollbackFailed, + ) => false, + Some(CodexErrorInfo::Other) | None => { + let message = message.to_ascii_lowercase(); + [ + "429", + "too many requests", + "retry limit", + "connection failed", + "temporarily unavailable", + "timeout", + "timed out", + "server overloaded", + "stream disconnected", + ] + .iter() + .any(|pattern| message.contains(pattern)) + } + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) enum ExternalEditorState { #[default] @@ -633,6 +670,10 @@ pub(crate) struct ChatWidget { queued_user_messages: VecDeque, // Monotonic sequence source used when enqueuing `queued_user_messages`. next_queued_message_seq: u64, + // User message most recently submitted to core and still awaiting a terminal turn outcome. + in_flight_user_message: Option, + // Retries the failed in-flight message before advancing queued drafts after transient failures. + retry_current_user_message: Option, // RLPH `/exec` commands are submitted as user shell commands and bridged // back into a follow-up prompt on command completion. pending_rlph_exec_commands: VecDeque, @@ -1626,6 +1667,8 @@ impl ChatWidget { self.last_unified_wait = None; self.unified_exec_wait_streak = None; self.request_redraw(); + self.in_flight_user_message = None; + self.retry_current_user_message = None; let had_pending_steers = !self.pending_steers.is_empty(); self.refresh_pending_input_preview(); @@ -1921,13 +1964,20 @@ impl ChatWidget { self.add_to_history(history_cell::new_warning_event(message)); self.request_redraw(); + self.retry_current_user_message = self.in_flight_user_message.take(); self.maybe_send_next_queued_input(); } - fn on_error(&mut self, message: String) { + fn on_error(&mut self, message: String, retry_current_message: bool) { self.finalize_turn(); self.add_to_history(history_cell::new_error_event(message)); self.request_redraw(); + if retry_current_message { + self.retry_current_user_message = self.in_flight_user_message.take(); + } else { + self.in_flight_user_message = None; + self.retry_current_user_message = None; + } // After an error ends the turn, try sending the next queued input. self.maybe_send_next_queued_input(); @@ -2012,6 +2062,8 @@ impl ChatWidget { fn on_interrupted_turn(&mut self, reason: TurnAbortReason) { // Finalize, log a gentle prompt, and clear running state. self.finalize_turn(); + self.in_flight_user_message = None; + self.retry_current_user_message = None; if reason == TurnAbortReason::Interrupted { self.clear_unified_exec_processes(); } @@ -2187,6 +2239,8 @@ impl ChatWidget { self.queued_user_messages = input_state.pending_steers; self.queued_user_messages .extend(input_state.queued_user_messages); + self.in_flight_user_message = None; + self.retry_current_user_message = None; self.pending_rlph_exec_commands.clear(); self.active_rlph_exec_commands.clear(); } else { @@ -2201,6 +2255,8 @@ impl ChatWidget { ); self.bottom_pane.set_composer_pending_pastes(Vec::new()); self.queued_user_messages.clear(); + self.in_flight_user_message = None; + self.retry_current_user_message = None; self.pending_rlph_exec_commands.clear(); self.active_rlph_exec_commands.clear(); } @@ -3223,6 +3279,8 @@ impl ChatWidget { forked_from: None, queued_user_messages: VecDeque::new(), next_queued_message_seq: 0, + in_flight_user_message: None, + retry_current_user_message: None, pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -3412,6 +3470,8 @@ impl ChatWidget { plan_item_active: false, queued_user_messages: VecDeque::new(), next_queued_message_seq: 0, + in_flight_user_message: None, + retry_current_user_message: None, pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -3585,6 +3645,8 @@ impl ChatWidget { forked_from: None, queued_user_messages: VecDeque::new(), next_queued_message_seq: 0, + in_flight_user_message: None, + retry_current_user_message: None, pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -4613,6 +4675,10 @@ impl ChatWidget { } fn submit_user_message(&mut self, user_message: UserMessage) { + self.submit_user_message_internal(user_message, false); + } + + fn submit_user_message_internal(&mut self, user_message: UserMessage, is_retry: bool) { if !self.is_session_configured() { tracing::warn!("cannot submit user message before session is configured; queueing"); self.push_queued_user_message_front(user_message); @@ -4625,6 +4691,7 @@ impl ChatWidget { return; } + let submitted_user_message = user_message.clone(); let UserMessage { text, local_images, @@ -4816,6 +4883,13 @@ impl ChatWidget { if !self.submit_op(op) { return; } + self.in_flight_user_message = Some(submitted_user_message); + self.retry_current_user_message = None; + + if is_retry { + self.needs_final_message_separator = false; + return; + } // Persist the text to cross-session message history. if !text.is_empty() { @@ -5016,19 +5090,21 @@ impl ChatWidget { message, codex_error_info, }) => { - if let Some(info) = codex_error_info - && let Some(kind) = rate_limit_error_kind(&info) + let retry_current_message = + is_transient_turn_failure(codex_error_info.as_ref(), &message); + if let Some(info) = codex_error_info.as_ref() + && let Some(kind) = rate_limit_error_kind(info) { match kind { RateLimitErrorKind::ServerOverloaded => { self.on_server_overloaded_error(message) } RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => { - self.on_error(message) + self.on_error(message, retry_current_message) } } } else { - self.on_error(message); + self.on_error(message, retry_current_message); } } EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev), @@ -5040,7 +5116,7 @@ impl ChatWidget { TurnAbortReason::Replaced => { self.pending_steers.clear(); self.refresh_pending_input_preview(); - self.on_error("Turn aborted: replaced by a new task".to_owned()) + self.on_error("Turn aborted: replaced by a new task".to_owned(), false) } TurnAbortReason::ReviewEnded => { self.on_interrupted_turn(ev.reason); @@ -5347,6 +5423,13 @@ impl ChatWidget { if self.suppress_queue_autosend { return; } + if !self.bottom_pane.is_task_running() + && let Some(user_message) = self.retry_current_user_message.take() + { + self.submit_user_message_internal(user_message, true); + self.refresh_pending_input_preview(); + return; + } if self.bottom_pane.is_task_running() && !self .queued_user_messages diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bdbd3f0d4aff..6938ccedcec5 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -62,6 +62,7 @@ use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::BackgroundEventEvent; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot; +use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; @@ -1837,6 +1838,8 @@ async fn make_chatwidget_manual( startup_tooltip_override: None, queued_user_messages: VecDeque::new(), next_queued_message_seq: 0, + in_flight_user_message: None, + retry_current_user_message: None, pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -7171,6 +7174,187 @@ async fn server_overloaded_error_does_not_switch_models() { } } +#[tokio::test] +async fn transient_retry_limit_error_retries_current_message_before_advancing_queue() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("first".to_string())); + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "first".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn, got {other:?}"), + } + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("second".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "exceeded retry limit, last status: 429 Too Many Requests".to_string(), + codex_error_info: Some(CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + }), + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "first".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn retry, got {other:?}"), + } + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["second".to_string()] + ); + + chat.handle_codex_event(Event { + id: "turn-start-2".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-2".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.handle_codex_event(Event { + id: "turn-complete-2".into(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-2".to_string(), + last_agent_message: None, + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "second".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected queued Op::UserTurn, got {other:?}"), + } +} + +#[tokio::test] +async fn transient_connection_error_retries_current_message_before_advancing_queue() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("alpha".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("beta".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "connection failed while streaming response".to_string(), + codex_error_info: Some(CodexErrorInfo::HttpConnectionFailed { + http_status_code: Some(502), + }), + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "alpha".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn retry, got {other:?}"), + } + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["beta".to_string()] + ); +} + +#[tokio::test] +async fn transient_steer_retry_does_not_duplicate_pending_steers() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage { + text: "retry steer".to_string(), + local_images: Vec::new(), + remote_image_urls: Vec::new(), + text_elements: Vec::new(), + mention_bindings: Vec::new(), + repeat_mode: false, + steer_mode: true, + enqueue_seq: 0, + }); + let _ = next_submit_op(&mut op_rx); + assert_eq!(chat.pending_steers.len(), 1); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "response stream disconnected".to_string(), + codex_error_info: Some(CodexErrorInfo::ResponseStreamDisconnected { + http_status_code: Some(502), + }), + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "retry steer".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn retry, got {other:?}"), + } + assert_eq!(chat.pending_steers.len(), 1); + + complete_user_message_for_inputs( + &mut chat, + "user-1", + vec![UserInput::Text { + text: "retry steer".to_string(), + text_elements: Vec::new(), + }], + ); + assert!(chat.pending_steers.is_empty()); +} + #[tokio::test] async fn approvals_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; From b2ddfd4570ef7e3bf00e63a557ddc76153112d82 Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:21:01 +0800 Subject: [PATCH 3/8] Block queue advance during retry submission gap --- codex-rs/tui/src/chatwidget.rs | 6 ++ codex-rs/tui/src/chatwidget/tests.rs | 100 +++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2c988eb35e8e..a8d2eabecc06 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4545,6 +4545,8 @@ impl ChatWidget { if !self.is_session_configured() || self.bottom_pane.is_task_running() || self.is_review_mode + || self.in_flight_user_message.is_some() + || self.retry_current_user_message.is_some() { self.push_queued_user_message_back(user_message); self.refresh_pending_input_preview(); @@ -5430,6 +5432,10 @@ impl ChatWidget { self.refresh_pending_input_preview(); return; } + if self.in_flight_user_message.is_some() { + self.refresh_pending_input_preview(); + return; + } if self.bottom_pane.is_task_running() && !self .queued_user_messages diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 6938ccedcec5..c56c42899205 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -7304,6 +7304,106 @@ async fn transient_connection_error_retries_current_message_before_advancing_que ); } +#[tokio::test] +async fn repeated_retry_limit_errors_do_not_advance_queue_during_retry_gap() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("first".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("second".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "exceeded retry limit, last status: 429 Too Many Requests".to_string(), + codex_error_info: Some(CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + }), + }), + }); + let _ = next_submit_op(&mut op_rx); + + chat.maybe_send_next_queued_input(); + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["second".to_string()] + ); + + chat.handle_codex_event(Event { + id: "err-2".into(), + msg: EventMsg::Error(ErrorEvent { + message: "exceeded retry limit, last status: 429 Too Many Requests".to_string(), + codex_error_info: Some(CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + }), + }), + }); + let _ = next_submit_op(&mut op_rx); + + chat.maybe_send_next_queued_input(); + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["second".to_string()] + ); +} + +#[tokio::test] +async fn queue_user_message_during_retry_gap_stays_queued() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("first".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "exceeded retry limit, last status: 429 Too Many Requests".to_string(), + codex_error_info: Some(CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + }), + }), + }); + let _ = next_submit_op(&mut op_rx); + + chat.queue_user_message(UserMessage::from("late queued".to_string())); + + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["late queued".to_string()] + ); +} + #[tokio::test] async fn transient_steer_retry_does_not_duplicate_pending_steers() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; From 2e0e9fdc1d6e2b39c913115038278adf5de3a734 Mon Sep 17 00:00:00 2001 From: "Argon@Termux" Date: Thu, 12 Mar 2026 17:02:36 +0800 Subject: [PATCH 4/8] fix(tui): hold queued drafts on incomplete turn failures --- AGENTS.md | 6 + codex-rs/tui/src/chatwidget.rs | 144 +++++++++++++++++-- codex-rs/tui/src/chatwidget/tests.rs | 200 +++++++++++++++++++++++++++ 3 files changed, 337 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 09c32d02f197..e769fbbabd2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,12 @@ In the codex-rs folder where the rust code lives: - After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught locally before CI. - Do not create small helper methods that are referenced only once. +- On Termux/Android, before long-running build/test commands, load the latest safe shell snapshot with + `. "$(ls -t "$HOME"/.codex/shell_snapshots/*.sh | head -n 1)"`. +- On Termux/Android, continuously monitor system pressure while builds/tests are running. At minimum, + watch `MemAvailable` from `/proc/meminfo`, `df -P /data`, and top RSS processes via + `ps -eo rss,pcpu,comm --sort=-rss`, and stop or scale back if memory or disk pressure approaches + instability. Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests: diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a8d2eabecc06..618add567189 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -135,6 +135,7 @@ use codex_protocol::protocol::TokenUsageInfo; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnCompleteEvent; use codex_protocol::protocol::TurnDiffEvent; +use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::UndoCompletedEvent; use codex_protocol::protocol::UndoStartedEvent; use codex_protocol::protocol::UserMessageEvent; @@ -522,7 +523,10 @@ fn rate_limit_error_kind(info: &CodexErrorInfo) -> Option { } } -fn is_transient_turn_failure(codex_error_info: Option<&CodexErrorInfo>, message: &str) -> bool { +fn should_retry_current_message_immediately( + codex_error_info: Option<&CodexErrorInfo>, + message: &str, +) -> bool { match codex_error_info { Some( CodexErrorInfo::ServerOverloaded @@ -559,6 +563,30 @@ fn is_transient_turn_failure(codex_error_info: Option<&CodexErrorInfo>, message: } } +fn should_hold_queue_after_error( + codex_error_info: Option<&CodexErrorInfo>, + _message: &str, +) -> bool { + match codex_error_info { + Some( + CodexErrorInfo::ContextWindowExceeded + | CodexErrorInfo::UsageLimitExceeded + | CodexErrorInfo::ServerOverloaded + | CodexErrorInfo::HttpConnectionFailed { .. } + | CodexErrorInfo::ResponseStreamConnectionFailed { .. } + | CodexErrorInfo::InternalServerError + | CodexErrorInfo::Unauthorized + | CodexErrorInfo::BadRequest + | CodexErrorInfo::SandboxError + | CodexErrorInfo::ResponseStreamDisconnected { .. } + | CodexErrorInfo::ResponseTooManyFailedAttempts { .. } + | CodexErrorInfo::ThreadRollbackFailed + | CodexErrorInfo::Other, + ) + | None => true, + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) enum ExternalEditorState { #[default] @@ -674,6 +702,13 @@ pub(crate) struct ChatWidget { in_flight_user_message: Option, // Retries the failed in-flight message before advancing queued drafts after transient failures. retry_current_user_message: Option, + // Preserves the failed current message when the queue must pause but should not auto-retry. + paused_current_user_message: Option, + // The live turn id most recently started by core, if any. + active_turn_id: Option, + // Turn ids that already ended locally via error/abort, so late TurnComplete events must not + // release queued follow-ups or clear preserved retry/pause state. + ignored_turn_complete_ids: HashSet, // RLPH `/exec` commands are submitted as user shell commands and bridged // back into a follow-up prompt on command completion. pending_rlph_exec_commands: VecDeque, @@ -1029,6 +1064,20 @@ enum ReplayKind { } impl ChatWidget { + fn retry_or_pause_blocks_submission(&self) -> bool { + self.retry_current_user_message.is_some() || self.paused_current_user_message.is_some() + } + + fn queue_advancement_blocked(&self) -> bool { + self.in_flight_user_message.is_some() || self.retry_or_pause_blocks_submission() + } + + fn remember_active_turn_complete_as_stale(&mut self) { + if let Some(turn_id) = self.active_turn_id.take() { + self.ignored_turn_complete_ids.insert(turn_id); + } + } + fn realtime_conversation_enabled(&self) -> bool { self.config.features.enabled(Feature::RealtimeConversation) && cfg!(not(target_os = "linux")) @@ -1625,6 +1674,7 @@ impl ChatWidget { { self.last_copyable_output = Some(message.clone()); } + self.active_turn_id = None; // If a stream is currently active, finalize it. self.flush_answer_stream_with_separator(); if let Some(mut controller) = self.plan_stream_controller.take() @@ -1669,6 +1719,7 @@ impl ChatWidget { self.request_redraw(); self.in_flight_user_message = None; self.retry_current_user_message = None; + self.paused_current_user_message = None; let had_pending_steers = !self.pending_steers.is_empty(); self.refresh_pending_input_preview(); @@ -1954,6 +2005,7 @@ impl ChatWidget { } fn on_server_overloaded_error(&mut self, message: String) { + self.remember_active_turn_complete_as_stale(); self.finalize_turn(); let message = if message.trim().is_empty() { @@ -1965,18 +2017,25 @@ impl ChatWidget { self.add_to_history(history_cell::new_warning_event(message)); self.request_redraw(); self.retry_current_user_message = self.in_flight_user_message.take(); + self.paused_current_user_message = None; self.maybe_send_next_queued_input(); } - fn on_error(&mut self, message: String, retry_current_message: bool) { + fn on_error(&mut self, message: String, retry_current_message: bool, hold_queue: bool) { + self.remember_active_turn_complete_as_stale(); self.finalize_turn(); self.add_to_history(history_cell::new_error_event(message)); self.request_redraw(); if retry_current_message { self.retry_current_user_message = self.in_flight_user_message.take(); + self.paused_current_user_message = None; + } else if hold_queue { + self.paused_current_user_message = self.in_flight_user_message.take(); + self.retry_current_user_message = None; } else { self.in_flight_user_message = None; self.retry_current_user_message = None; + self.paused_current_user_message = None; } // After an error ends the turn, try sending the next queued input. @@ -2061,9 +2120,11 @@ impl ChatWidget { /// separated by newlines rather than auto‑submitting the next one. fn on_interrupted_turn(&mut self, reason: TurnAbortReason) { // Finalize, log a gentle prompt, and clear running state. + self.remember_active_turn_complete_as_stale(); self.finalize_turn(); self.in_flight_user_message = None; self.retry_current_user_message = None; + self.paused_current_user_message = None; if reason == TurnAbortReason::Interrupted { self.clear_unified_exec_processes(); } @@ -2241,6 +2302,9 @@ impl ChatWidget { .extend(input_state.queued_user_messages); self.in_flight_user_message = None; self.retry_current_user_message = None; + self.paused_current_user_message = None; + self.active_turn_id = None; + self.ignored_turn_complete_ids.clear(); self.pending_rlph_exec_commands.clear(); self.active_rlph_exec_commands.clear(); } else { @@ -2257,6 +2321,9 @@ impl ChatWidget { self.queued_user_messages.clear(); self.in_flight_user_message = None; self.retry_current_user_message = None; + self.paused_current_user_message = None; + self.active_turn_id = None; + self.ignored_turn_complete_ids.clear(); self.pending_rlph_exec_commands.clear(); self.active_rlph_exec_commands.clear(); } @@ -3281,6 +3348,9 @@ impl ChatWidget { next_queued_message_seq: 0, in_flight_user_message: None, retry_current_user_message: None, + paused_current_user_message: None, + active_turn_id: None, + ignored_turn_complete_ids: HashSet::new(), pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -3472,6 +3542,9 @@ impl ChatWidget { next_queued_message_seq: 0, in_flight_user_message: None, retry_current_user_message: None, + paused_current_user_message: None, + active_turn_id: None, + ignored_turn_complete_ids: HashSet::new(), pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -3647,6 +3720,9 @@ impl ChatWidget { next_queued_message_seq: 0, in_flight_user_message: None, retry_current_user_message: None, + paused_current_user_message: None, + active_turn_id: None, + ignored_turn_complete_ids: HashSet::new(), pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -3789,6 +3865,21 @@ impl ChatWidget { return; } + if key_event.kind == KeyEventKind::Press + && key_event.code == KeyCode::Enter + && key_event.modifiers == KeyModifiers::NONE + && self.bottom_pane.no_modal_or_popup_active() + && self.bottom_pane.composer_is_empty() + && let Some(user_message) = self.paused_current_user_message.take() + { + self.reasoning_buffer.clear(); + self.full_reasoning_buffer.clear(); + self.set_status_header(String::from("Working")); + self.submit_user_message_internal(user_message, true); + self.refresh_pending_input_preview(); + return; + } + match key_event { _ if is_cycle_mode_or_repeat_queue_key(key_event) && self.collaboration_modes_enabled() @@ -3830,8 +3921,9 @@ impl ChatWidget { else { return; }; - let should_submit_now = - self.is_session_configured() && !self.is_plan_streaming_in_tui(); + let should_submit_now = self.is_session_configured() + && !self.is_plan_streaming_in_tui() + && !self.retry_or_pause_blocks_submission(); if should_submit_now { // Submitted is emitted when user submits. // Reset any reasoning header only when we are actually submitting a turn. @@ -4545,8 +4637,7 @@ impl ChatWidget { if !self.is_session_configured() || self.bottom_pane.is_task_running() || self.is_review_mode - || self.in_flight_user_message.is_some() - || self.retry_current_user_message.is_some() + || self.queue_advancement_blocked() { self.push_queued_user_message_back(user_message); self.refresh_pending_input_preview(); @@ -4887,6 +4978,7 @@ impl ChatWidget { } self.in_flight_user_message = Some(submitted_user_message); self.retry_current_user_message = None; + self.paused_current_user_message = None; if is_retry { self.needs_final_message_separator = false; @@ -5074,14 +5166,31 @@ impl ChatWidget { self.on_agent_reasoning_final(); } EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(), - EventMsg::TurnStarted(_) => { + EventMsg::TurnStarted(TurnStartedEvent { turn_id, .. }) => { if !is_resume_initial_replay { + self.active_turn_id = Some(turn_id); self.on_task_started(); } } EventMsg::TurnComplete(TurnCompleteEvent { - last_agent_message, .. - }) => self.on_task_complete(last_agent_message, from_replay), + turn_id, + last_agent_message, + .. + }) => { + if !from_replay { + if self.ignored_turn_complete_ids.remove(&turn_id) { + return; + } + if self + .active_turn_id + .as_ref() + .is_some_and(|active_turn_id| active_turn_id != &turn_id) + { + return; + } + } + self.on_task_complete(last_agent_message, from_replay) + } EventMsg::TokenCount(ev) => { self.set_token_info(ev.info); self.on_rate_limit_snapshot(ev.rate_limits); @@ -5093,7 +5202,8 @@ impl ChatWidget { codex_error_info, }) => { let retry_current_message = - is_transient_turn_failure(codex_error_info.as_ref(), &message); + should_retry_current_message_immediately(codex_error_info.as_ref(), &message); + let hold_queue = should_hold_queue_after_error(codex_error_info.as_ref(), &message); if let Some(info) = codex_error_info.as_ref() && let Some(kind) = rate_limit_error_kind(info) { @@ -5102,11 +5212,11 @@ impl ChatWidget { self.on_server_overloaded_error(message) } RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => { - self.on_error(message, retry_current_message) + self.on_error(message, retry_current_message, hold_queue) } } } else { - self.on_error(message, retry_current_message); + self.on_error(message, retry_current_message, hold_queue); } } EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev), @@ -5118,7 +5228,11 @@ impl ChatWidget { TurnAbortReason::Replaced => { self.pending_steers.clear(); self.refresh_pending_input_preview(); - self.on_error("Turn aborted: replaced by a new task".to_owned(), false) + self.on_error( + "Turn aborted: replaced by a new task".to_owned(), + false, + false, + ) } TurnAbortReason::ReviewEnded => { self.on_interrupted_turn(ev.reason); @@ -5436,6 +5550,10 @@ impl ChatWidget { self.refresh_pending_input_preview(); return; } + if self.paused_current_user_message.is_some() { + self.refresh_pending_input_preview(); + return; + } if self.bottom_pane.is_task_running() && !self .queued_user_messages diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c56c42899205..053132383917 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1840,6 +1840,9 @@ async fn make_chatwidget_manual( next_queued_message_seq: 0, in_flight_user_message: None, retry_current_user_message: None, + paused_current_user_message: None, + active_turn_id: None, + ignored_turn_complete_ids: HashSet::new(), pending_rlph_exec_commands: VecDeque::new(), active_rlph_exec_commands: HashMap::new(), pending_steers: VecDeque::new(), @@ -7304,6 +7307,203 @@ async fn transient_connection_error_retries_current_message_before_advancing_que ); } +#[tokio::test] +async fn late_turn_complete_after_transient_retry_does_not_advance_queue() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("alpha".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("beta".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "connection failed while streaming response".to_string(), + codex_error_info: Some(CodexErrorInfo::HttpConnectionFailed { + http_status_code: Some(502), + }), + }), + }); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-complete-1".into(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-1".to_string(), + last_agent_message: None, + }), + }); + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["beta".to_string()] + ); + + chat.handle_codex_event(Event { + id: "turn-start-2".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-2".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.handle_codex_event(Event { + id: "turn-complete-2".into(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-2".to_string(), + last_agent_message: None, + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "beta".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected queued follow-up after retry turn completed, got {other:?}"), + } +} + +#[tokio::test] +async fn bad_request_holds_queue_until_empty_enter_retries_preserved_message() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("first".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("second".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "bad request".to_string(), + codex_error_info: Some(CodexErrorInfo::BadRequest), + }), + }); + assert_no_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-complete-1".into(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-1".to_string(), + last_agent_message: None, + }), + }); + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["second".to_string()] + ); + + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "first".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected preserved message retry, got {other:?}"), + } + + chat.handle_codex_event(Event { + id: "turn-start-2".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-2".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.handle_codex_event(Event { + id: "turn-complete-2".into(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-2".to_string(), + last_agent_message: None, + }), + }); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "second".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected queued follow-up after preserved retry, got {other:?}"), + } +} + +#[tokio::test] +async fn non_empty_submit_while_queue_paused_stays_queued() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + + chat.submit_user_message(UserMessage::from("first".to_string())); + let _ = next_submit_op(&mut op_rx); + + chat.handle_codex_event(Event { + id: "turn-start-1".into(), + msg: EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".to_string(), + model_context_window: None, + collaboration_mode_kind: ModeKind::Default, + }), + }); + chat.queue_user_message(UserMessage::from("second".to_string())); + + chat.handle_codex_event(Event { + id: "err-1".into(), + msg: EventMsg::Error(ErrorEvent { + message: "bad request".to_string(), + codex_error_info: Some(CodexErrorInfo::BadRequest), + }), + }); + assert_no_submit_op(&mut op_rx); + + chat.bottom_pane + .set_composer_text("third".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_no_submit_op(&mut op_rx); + assert_eq!( + chat.queued_user_messages + .iter() + .map(|message| message.text.clone()) + .collect::>(), + vec!["second".to_string(), "third".to_string()] + ); +} + #[tokio::test] async fn repeated_retry_limit_errors_do_not_advance_queue_during_retry_gap() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; From ff963750c810f4751e3cfec8c84a5a0c8b301bb0 Mon Sep 17 00:00:00 2001 From: "Argon@Termux" Date: Thu, 12 Mar 2026 17:02:48 +0800 Subject: [PATCH 5/8] fix(tui): avoid Android voice context panics --- codex-rs/tui/src/clipboard_paste.rs | 1 - codex-rs/tui/src/voice.rs | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/clipboard_paste.rs b/codex-rs/tui/src/clipboard_paste.rs index 4d28b365fed8..3904d56f0a22 100644 --- a/codex-rs/tui/src/clipboard_paste.rs +++ b/codex-rs/tui/src/clipboard_paste.rs @@ -1,6 +1,5 @@ use std::path::Path; use std::path::PathBuf; -use tempfile::Builder; #[derive(Debug, Clone)] pub enum PasteImageError { diff --git a/codex-rs/tui/src/voice.rs b/codex-rs/tui/src/voice.rs index 227d27c88fb8..e43fb13245cb 100644 --- a/codex-rs/tui/src/voice.rs +++ b/codex-rs/tui/src/voice.rs @@ -55,6 +55,12 @@ pub struct VoiceCapture { impl VoiceCapture { pub fn start() -> Result { + #[cfg(target_os = "android")] + let (device, config) = std::panic::catch_unwind(select_default_input_device_and_config) + .map_err(|_| { + "audio input unavailable: Android app context was not initialized".to_string() + })??; + #[cfg(not(target_os = "android"))] let (device, config) = select_default_input_device_and_config()?; let sample_rate = config.sample_rate().0; @@ -79,6 +85,14 @@ impl VoiceCapture { } pub fn start_realtime(config: &Config, tx: AppEventSender) -> Result { + #[cfg(target_os = "android")] + let (device, config) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + select_realtime_input_device_and_config(config) + })) + .map_err(|_| { + "audio input unavailable: Android app context was not initialized".to_string() + })??; + #[cfg(not(target_os = "android"))] let (device, config) = select_realtime_input_device_and_config(config)?; let sample_rate = config.sample_rate().0; From a7fd003c52d14bea86103337e1e431b4af864c42 Mon Sep 17 00:00:00 2001 From: "Argon@Termux" Date: Thu, 12 Mar 2026 19:37:46 +0800 Subject: [PATCH 6/8] chore(release): align exomind line to 0.114.0 stable --- .github/workflows/rust-release.yml | 2 +- README.md | 14 +-- codex-rs/Cargo.lock | 142 ++++++++++++++--------------- codex-rs/Cargo.toml | 2 +- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index d4797342e709..c83b697f95cb 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -4,7 +4,7 @@ # git tag -a rust-v0.1.0 -m "Release 0.1.0" # git push origin rust-v0.1.0 # # Exomind fork releases use the upstream version with an `-exomind` suffix. -# # Example: rust-v0.112.0-alpha.11-exomind +# # Example: rust-v0.114.0-exomind # ``` name: rust-release diff --git a/README.md b/README.md index 05d6946e75ad..8d4a372d0baa 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,20 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), Date: Thu, 12 Mar 2026 19:58:06 +0800 Subject: [PATCH 7/8] codex: fix CI failure on PR #27 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8d4a372d0baa..87cdaa5db513 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ If you want Codex in your code editor (VS Code, Cursor, Windsurf), Date: Fri, 13 Mar 2026 01:04:53 +0800 Subject: [PATCH 8/8] android: safe-build 0.114.0-exomind release --- codex-rs/Cargo.toml | 5 +++++ codex-rs/core/Cargo.toml | 4 +++- codex-rs/core/src/tools/handlers/mod.rs | 8 ++++++++ codex-rs/core/src/tools/spec.rs | 3 +++ scripts/termux/README.md | 6 +++++- scripts/termux/build-safe.sh | 7 +++++-- 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f0dd9f106564..403c8c9d8c2c 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -368,6 +368,11 @@ strip = "symbols" # See https://github.com/openai/codex/issues/1411 for details. codegen-units = 1 +[profile.release.package.bm25] +opt-level = 3 +codegen-units = 1 +strip = "symbols" + [profile.ci-test] debug = 1 # Reduce debug symbol size inherits = "test" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index e40658652447..2600a4a91612 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -23,7 +23,6 @@ async-channel = { workspace = true } async-trait = { workspace = true } askama = { workspace = true } base64 = { workspace = true } -bm25 = { workspace = true } chardetng = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } @@ -116,6 +115,9 @@ which = { workspace = true } wildmatch = { workspace = true } zip = { workspace = true } +[target.'cfg(not(target_os = "android"))'.dependencies] +bm25 = { workspace = true } + [target.'cfg(target_os = "linux")'.dependencies] keyring = { workspace = true, features = ["linux-native-async-persistent"] } landlock = { workspace = true } diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 086701d9c242..4a8f52db2407 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod multi_agents; mod plan; mod read_file; mod request_user_input; +#[cfg(not(target_os = "android"))] mod search_tool_bm25; mod shell; mod test_sync; @@ -43,8 +44,15 @@ pub use plan::PlanHandler; pub use read_file::ReadFileHandler; pub use request_user_input::RequestUserInputHandler; pub(crate) use request_user_input::request_user_input_tool_description; +#[cfg(target_os = "android")] +pub(crate) const SEARCH_TOOL_BM25_DEFAULT_LIMIT: usize = 8; +#[cfg(target_os = "android")] +pub(crate) const SEARCH_TOOL_BM25_TOOL_NAME: &str = "search_tool_bm25"; +#[cfg(not(target_os = "android"))] pub(crate) use search_tool_bm25::DEFAULT_LIMIT as SEARCH_TOOL_BM25_DEFAULT_LIMIT; +#[cfg(not(target_os = "android"))] pub(crate) use search_tool_bm25::SEARCH_TOOL_BM25_TOOL_NAME; +#[cfg(not(target_os = "android"))] pub use search_tool_bm25::SearchToolBm25Handler; pub use shell::ShellCommandHandler; pub use shell::ShellHandler; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 9c6cb95a6484..8eb4d090fe50 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1820,6 +1820,7 @@ pub(crate) fn build_specs( use crate::tools::handlers::PlanHandler; use crate::tools::handlers::ReadFileHandler; use crate::tools::handlers::RequestUserInputHandler; + #[cfg(not(target_os = "android"))] use crate::tools::handlers::SearchToolBm25Handler; use crate::tools::handlers::ShellCommandHandler; use crate::tools::handlers::ShellHandler; @@ -1842,6 +1843,7 @@ pub(crate) fn build_specs( let request_user_input_handler = Arc::new(RequestUserInputHandler { default_mode_request_user_input: config.default_mode_request_user_input, }); + #[cfg(not(target_os = "android"))] let search_tool_handler = Arc::new(SearchToolBm25Handler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); @@ -1912,6 +1914,7 @@ pub(crate) fn build_specs( builder.register_handler("request_user_input", request_user_input_handler); } + #[cfg(not(target_os = "android"))] if config.search_tool && let Some(app_tools) = app_tools { diff --git a/scripts/termux/README.md b/scripts/termux/README.md index 378a6e629a5a..4e0b1792becb 100644 --- a/scripts/termux/README.md +++ b/scripts/termux/README.md @@ -8,9 +8,13 @@ Builds `codex-cli` and `codex-exec` for Android/Termux with low-memory defaults: - computes a conservative `CARGO_BUILD_JOBS` from current `MemAvailable` - forces `release` overrides to reduce linker memory spikes: + - `RUSTFLAGS="-C llvm-args=--threads=1"` + - `CARGO_PROFILE_RELEASE_OPT_LEVEL=2` - `CARGO_PROFILE_RELEASE_LTO=off` - - `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16` + - `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=2` - `CARGO_PROFILE_RELEASE_DEBUG=0` + - the workspace `Cargo.toml` pins `bm25` to `codegen-units = 1` in + `profile.release.package.bm25` Usage: diff --git a/scripts/termux/build-safe.sh b/scripts/termux/build-safe.sh index b446ecada6cb..2c3edc06221c 100755 --- a/scripts/termux/build-safe.sh +++ b/scripts/termux/build-safe.sh @@ -37,13 +37,16 @@ echo "[termux-build-safe] target: $TARGET" echo "[termux-build-safe] cores: $cores" echo "[termux-build-safe] MemAvailable: ${mem_kb} kB" echo "[termux-build-safe] CARGO_BUILD_JOBS=$jobs" -echo "[termux-build-safe] release overrides: LTO=off, codegen-units=16, debug=0" +echo "[termux-build-safe] release overrides: opt-level=2, LTO=off, codegen-units=2, debug=0" +echo "[termux-build-safe] rustflags: -C llvm-args=--threads=1" cd "$ROOT_DIR/codex-rs" CARGO_BUILD_JOBS="$jobs" \ +RUSTFLAGS="-C llvm-args=--threads=1" \ +CARGO_PROFILE_RELEASE_OPT_LEVEL=2 \ CARGO_PROFILE_RELEASE_LTO=off \ -CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \ +CARGO_PROFILE_RELEASE_CODEGEN_UNITS=2 \ CARGO_PROFILE_RELEASE_DEBUG=0 \ cargo build --release -p codex-cli -p codex-exec --target "$TARGET"