Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,79 @@ impl App {
self.chat_widget.prepare_local_op_submission(&op);
self.submit_active_thread_op(app_server, op).await?;
}
AppEvent::RetrySafetyBufferedTurn {
thread_id,
turn_id,
model,
mut turn,
} => {
if self.active_thread_id != Some(thread_id)
|| self.chat_widget.thread_id() != Some(thread_id)
{
return Ok(AppRunControl::Continue);
}
if !self.chat_widget.can_retry_safety_buffered_turn(&turn_id) {
self.app_event_tx.send(AppEvent::UpdateModel(model));
self.app_event_tx.send(AppEvent::UpdateReasoningEffort(Some(
ReasoningEffortConfig::Low,
)));
return Ok(AppRunControl::Continue);
}

let AppCommand::UserTurn {
model: turn_model,
effort,
collaboration_mode,
..
} = &mut turn
else {
self.chat_widget.add_error_message(
"Failed to retry with a faster model: original turn is unavailable."
.to_string(),
);
return Ok(AppRunControl::Continue);
};
*turn_model = model.clone();
*effort = Some(ReasoningEffortConfig::Low);
*collaboration_mode = collaboration_mode.as_ref().map(|mode| {
mode.with_updates(
Some(model),
Some(Some(ReasoningEffortConfig::Low)),
/*developer_instructions*/ None,
)
});

if let Err(err) = app_server.turn_interrupt(thread_id, turn_id).await {
self.chat_widget
.add_error_message(format!("Failed to retry with a faster model: {err}"));
return Ok(AppRunControl::Continue);
}
let rollback_response =
match app_server.thread_rollback(thread_id, /*num_turns*/ 1).await {
Ok(response) => response,
Err(err) => {
self.chat_widget.add_error_message(format!(
"Failed to retry with a faster model: {err}"
));
return Ok(AppRunControl::Continue);
}
};

self.chat_widget.prepare_safety_buffering_retry();
self.handle_thread_rollback_response_with_origin(
thread_id,
/*num_turns*/ 1,
&rollback_response,
super::thread_routing::ThreadRollbackOrigin::SafetyBufferingRetry,
)
.await;

if let Err(err) = self.submit_thread_op(app_server, thread_id, turn).await {
Comment thread
etraut-openai marked this conversation as resolved.
Comment thread
etraut-openai marked this conversation as resolved.
self.chat_widget.fail_safety_buffering_retry();
self.chat_widget
.add_error_message(format!("Failed to retry with a faster model: {err}"));
Comment thread
etraut-openai marked this conversation as resolved.
}
}
AppEvent::RestoreCancelledTurn(prompt) => {
self.apply_cancelled_turn_edit(prompt);
}
Expand Down
37 changes: 35 additions & 2 deletions codex-rs/tui/src/app/thread_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
use super::*;
use crate::session_resume::read_session_model;

#[derive(Clone, Copy)]
pub(super) enum ThreadRollbackOrigin {
Backtrack,
SafetyBufferingRetry,
}

impl App {
pub(super) async fn shutdown_current_thread(&mut self, app_server: &mut AppServerSession) {
if let Some(thread_id) = self.chat_widget.thread_id() {
Expand Down Expand Up @@ -643,7 +649,7 @@ impl App {
.as_ref()
.map(|profile| &profile.permission_profile),
);
app_server
let response = app_server
.turn_start(
thread_id,
items.to_vec(),
Expand All @@ -661,6 +667,12 @@ impl App {
final_output_json_schema.clone(),
)
.await?;
if self.active_thread_id == Some(thread_id)
&& self.chat_widget.thread_id() == Some(thread_id)
{
self.chat_widget
.record_safety_buffering_turn(response.turn.id, op);
}
}
Ok(true)
}
Expand Down Expand Up @@ -1395,6 +1407,22 @@ impl App {
thread_id: ThreadId,
num_turns: u32,
response: &ThreadRollbackResponse,
) {
self.handle_thread_rollback_response_with_origin(
thread_id,
num_turns,
response,
ThreadRollbackOrigin::Backtrack,
)
.await;
}

pub(super) async fn handle_thread_rollback_response_with_origin(
&mut self,
thread_id: ThreadId,
num_turns: u32,
response: &ThreadRollbackResponse,
origin: ThreadRollbackOrigin,
) {
if let Some(channel) = self.thread_event_channels.get(&thread_id) {
let mut store = channel.store.lock().await;
Expand All @@ -1421,7 +1449,12 @@ impl App {
self.clear_active_thread().await;
}
}
self.handle_backtrack_rollback_succeeded(num_turns);
match origin {
ThreadRollbackOrigin::Backtrack => self.handle_backtrack_rollback_succeeded(num_turns),
ThreadRollbackOrigin::SafetyBufferingRetry => {
self.apply_non_pending_thread_rollback(num_turns);
}
}
}

pub(super) fn handle_thread_event_now(&mut self, event: ThreadBufferedEvent) {
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ pub(crate) enum AppEvent {
op: AppCommand,
},

/// Interrupt, roll back, and retry a safety-buffered turn with the server-selected model.
RetrySafetyBufferedTurn {
thread_id: ThreadId,
turn_id: String,
model: String,
turn: AppCommand,
},

/// Deliver a synthetic history lookup response to a specific thread channel.
ThreadHistoryEntryResponse {
thread_id: ThreadId,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,12 @@ mod review_popups;
use self::review::ReviewState;
#[cfg(test)]
pub(crate) use self::review_popups::show_review_commit_picker_with_entries;
mod safety_buffering;
mod service_tiers;
mod settings;
mod settings_popups;
mod side;
use self::safety_buffering::SafetyBufferingState;
mod status_state;
mod windows_sandbox_prompts;
use self::status_state::StatusIndicatorState;
Expand Down Expand Up @@ -584,6 +586,7 @@ pub(crate) struct ChatWidget {
last_unified_wait: Option<UnifiedExecWaitState>,
unified_exec_wait_streak: Option<UnifiedExecWaitStreak>,
turn_lifecycle: TurnLifecycleState,
safety_buffering: SafetyBufferingState,
task_complete_pending: bool,
unified_exec_processes: Vec<UnifiedExecProcessSummary>,
/// Tracks per-server MCP startup state while startup is in progress.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ impl ChatWidget {
last_unified_wait: None,
unified_exec_wait_streak: None,
turn_lifecycle: TurnLifecycleState::new(prevent_idle_sleep),
safety_buffering: SafetyBufferingState::default(),
task_complete_pending: false,
unified_exec_processes: Vec::new(),
mcp_startup_status: None,
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/tui/src/chatwidget/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ impl ChatWidget {
ServerNotification::ModelVerification(notification) => {
self.on_app_server_model_verification(&notification.verifications)
}
ServerNotification::ModelSafetyBufferingUpdated(notification) => {
self.on_model_safety_buffering_updated(notification, replay_kind)
}
ServerNotification::Warning(notification) => self.on_warning(notification.message),
ServerNotification::GuardianWarning(notification) => {
self.on_warning(notification.message)
Expand Down Expand Up @@ -208,7 +211,6 @@ impl ChatWidget {
| ServerNotification::ExternalAgentConfigImportProgress(_)
| ServerNotification::ExternalAgentConfigImportCompleted(_)
| ServerNotification::FsChanged(_)
| ServerNotification::ModelSafetyBufferingUpdated(_)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have too much context on this code. What is the effect of deleting this? Will it effect the app server code that was relying on it to send to the Codex App?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TUI was previously ignoring this notification from the app server. This line was present to ensure that the case statement was exhaustive. Our linter rules require that.

The TUI is now handling this notification by calling self.on_model_safety_buffering_updated (see insertion above).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok. Thanks for the explanation!

| ServerNotification::TurnModerationMetadata(_)
| ServerNotification::FuzzyFileSearchSessionUpdated(_)
| ServerNotification::FuzzyFileSearchSessionCompleted(_)
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ impl ChatWidget {
duration_ms,
} = turn;
if matches!(status, TurnStatus::InProgress) {
self.turn_lifecycle.last_turn_id = Some(turn_id.clone());
self.last_non_retry_error = None;
self.on_task_started();
}
Expand Down
Loading
Loading