@@ -730,6 +730,7 @@ pub(crate) struct App {
730730 primary_thread_id : Option < ThreadId > ,
731731 primary_session_configured : Option < SessionConfiguredEvent > ,
732732 pending_primary_events : VecDeque < Event > ,
733+ pending_async_queue_resume_barriers : usize ,
733734}
734735
735736#[ derive( Default ) ]
@@ -757,8 +758,22 @@ fn normalize_harness_overrides_for_cwd(
757758}
758759
759760impl App {
761+ #[ cfg_attr( not( target_os = "windows" ) , allow( dead_code) ) ]
762+ fn begin_async_queue_resume_barrier ( & mut self ) {
763+ self . pending_async_queue_resume_barriers += 1 ;
764+ }
765+
766+ #[ cfg_attr( not( target_os = "windows" ) , allow( dead_code) ) ]
767+ fn finish_async_queue_resume_barrier ( & mut self ) {
768+ if self . pending_async_queue_resume_barriers == 0 {
769+ tracing:: warn!( "finished async queue-resume barrier with no pending barrier" ) ;
770+ return ;
771+ }
772+ self . pending_async_queue_resume_barriers -= 1 ;
773+ }
774+
760775 fn maybe_resume_queued_inputs_after_app_events ( & mut self , app_events_drained : bool ) {
761- if !app_events_drained {
776+ if !app_events_drained || self . pending_async_queue_resume_barriers != 0 {
762777 return ;
763778 }
764779
@@ -2301,6 +2316,7 @@ impl App {
23012316 primary_thread_id : None ,
23022317 primary_session_configured : None ,
23032318 pending_primary_events : VecDeque :: new ( ) ,
2319+ pending_async_queue_resume_barriers : 0 ,
23042320 } ;
23052321
23062322 // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
@@ -2319,6 +2335,7 @@ impl App {
23192335 . hide_world_writable_warning
23202336 . unwrap_or ( false ) ;
23212337 if should_check {
2338+ app. begin_async_queue_resume_barrier ( ) ;
23222339 let cwd = app. config . cwd . clone ( ) ;
23232340 let env_map: std:: collections:: HashMap < String , String > = std:: env:: vars ( ) . collect ( ) ;
23242341 let tx = app. app_event_tx . clone ( ) ;
@@ -2429,10 +2446,10 @@ impl App {
24292446 ) {
24302447 waiting_for_initial_session_configured = false ;
24312448 }
2432- // Some replayed slash commands pause queue draining until their app-side updates
2433- // or popup flows settle. Only resume once the app-event queue is fully drained so
2434- // multi-event commands (for example approvals updates) cannot interleave later
2435- // queued input.
2449+ // Some replayed slash commands pause queue draining until their app-side updates,
2450+ // popup flows, or async follow-up work settle. Only resume once the app-event
2451+ // queue is fully drained and no background slash-command completions are still
2452+ // pending, so later queued input cannot interleave with those updates .
24362453 app. maybe_resume_queued_inputs_after_app_events ( app_event_rx. is_empty ( ) ) ;
24372454 match control {
24382455 AppRunControl :: Continue => { }
@@ -2890,6 +2907,7 @@ impl App {
28902907
28912908 self . chat_widget . show_windows_sandbox_setup_status ( ) ;
28922909 self . windows_sandbox . setup_started_at = Some ( Instant :: now ( ) ) ;
2910+ self . begin_async_queue_resume_barrier ( ) ;
28932911 let session_telemetry = self . session_telemetry . clone ( ) ;
28942912 tokio:: task:: spawn_blocking ( move || {
28952913 let result = codex_core:: windows_sandbox:: run_elevated_setup (
@@ -2906,10 +2924,10 @@ impl App {
29062924 1 ,
29072925 & [ ] ,
29082926 ) ;
2909- AppEvent :: EnableWindowsSandboxForAgentMode {
2927+ AppEvent :: WindowsSandboxElevatedSetupCompleted {
29102928 preset : preset. clone ( ) ,
2911- mode : WindowsSandboxEnableMode :: Elevated ,
29122929 approvals_reviewer,
2930+ setup_succeeded : true ,
29132931 }
29142932 }
29152933 Err ( err) => {
@@ -2941,9 +2959,10 @@ impl App {
29412959 error = %err,
29422960 "failed to run elevated Windows sandbox setup"
29432961 ) ;
2944- AppEvent :: OpenWindowsSandboxFallbackPrompt {
2962+ AppEvent :: WindowsSandboxElevatedSetupCompleted {
29452963 preset,
29462964 approvals_reviewer,
2965+ setup_succeeded : false ,
29472966 }
29482967 }
29492968 } ;
@@ -2955,6 +2974,33 @@ impl App {
29552974 let _ = ( preset, approvals_reviewer) ;
29562975 }
29572976 }
2977+ AppEvent :: WindowsSandboxElevatedSetupCompleted {
2978+ preset,
2979+ approvals_reviewer,
2980+ setup_succeeded,
2981+ } => {
2982+ #[ cfg( target_os = "windows" ) ]
2983+ {
2984+ self . finish_async_queue_resume_barrier ( ) ;
2985+ let event = if setup_succeeded {
2986+ AppEvent :: EnableWindowsSandboxForAgentMode {
2987+ preset,
2988+ mode : WindowsSandboxEnableMode :: Elevated ,
2989+ approvals_reviewer,
2990+ }
2991+ } else {
2992+ AppEvent :: OpenWindowsSandboxFallbackPrompt {
2993+ preset,
2994+ approvals_reviewer,
2995+ }
2996+ } ;
2997+ self . app_event_tx . send ( event) ;
2998+ }
2999+ #[ cfg( not( target_os = "windows" ) ) ]
3000+ {
3001+ let _ = ( preset, approvals_reviewer, setup_succeeded) ;
3002+ }
3003+ }
29583004 AppEvent :: BeginWindowsSandboxLegacySetup {
29593005 preset,
29603006 approvals_reviewer,
@@ -2969,6 +3015,7 @@ impl App {
29693015 let codex_home = self . config . codex_home . clone ( ) ;
29703016 let tx = self . app_event_tx . clone ( ) ;
29713017
3018+ self . begin_async_queue_resume_barrier ( ) ;
29723019 tokio:: task:: spawn_blocking ( move || {
29733020 let preset_for_error = preset. clone ( ) ;
29743021 let result = codex_core:: windows_sandbox:: run_legacy_setup_preflight (
@@ -3010,19 +3057,22 @@ impl App {
30103057 error,
30113058 } => {
30123059 #[ cfg( target_os = "windows" ) ]
3013- match error {
3014- None => {
3015- self . app_event_tx
3016- . send ( AppEvent :: EnableWindowsSandboxForAgentMode {
3017- preset,
3018- mode : WindowsSandboxEnableMode :: Legacy ,
3019- approvals_reviewer,
3020- } ) ;
3021- }
3022- Some ( err) => {
3023- self . chat_widget . add_error_message ( format ! (
3024- "Failed to enable the Windows sandbox feature: {err}"
3025- ) ) ;
3060+ {
3061+ self . finish_async_queue_resume_barrier ( ) ;
3062+ match error {
3063+ None => {
3064+ self . app_event_tx
3065+ . send ( AppEvent :: EnableWindowsSandboxForAgentMode {
3066+ preset,
3067+ mode : WindowsSandboxEnableMode :: Legacy ,
3068+ approvals_reviewer,
3069+ } ) ;
3070+ }
3071+ Some ( err) => {
3072+ self . chat_widget . add_error_message ( format ! (
3073+ "Failed to enable the Windows sandbox feature: {err}"
3074+ ) ) ;
3075+ }
30263076 }
30273077 }
30283078 #[ cfg( not( target_os = "windows" ) ) ]
@@ -3419,6 +3469,7 @@ impl App {
34193469 && policy_is_workspace_write_or_ro
34203470 && !self . chat_widget . world_writable_warning_hidden ( ) ;
34213471 if should_check {
3472+ self . begin_async_queue_resume_barrier ( ) ;
34223473 let cwd = self . config . cwd . clone ( ) ;
34233474 let env_map: std:: collections:: HashMap < String , String > =
34243475 std:: env:: vars ( ) . collect ( ) ;
@@ -3591,6 +3642,10 @@ impl App {
35913642 AppEvent :: OpenApprovalsPopup => {
35923643 self . chat_widget . open_approvals_popup ( ) ;
35933644 }
3645+ AppEvent :: WorldWritableScanCompleted => {
3646+ #[ cfg( target_os = "windows" ) ]
3647+ self . finish_async_queue_resume_barrier ( ) ;
3648+ }
35943649 AppEvent :: OpenAgentPicker => {
35953650 self . open_agent_picker ( ) . await ;
35963651 }
@@ -4285,6 +4340,7 @@ impl App {
42854340 failed_scan : true ,
42864341 } ) ;
42874342 }
4343+ tx. send ( AppEvent :: WorldWritableScanCompleted ) ;
42884344 } ) ;
42894345 }
42904346}
@@ -6555,6 +6611,7 @@ guardian_approval = true
65556611 primary_thread_id : None ,
65566612 primary_session_configured : None ,
65576613 pending_primary_events : VecDeque :: new ( ) ,
6614+ pending_async_queue_resume_barriers : 0 ,
65586615 }
65596616 }
65606617
@@ -6615,6 +6672,7 @@ guardian_approval = true
66156672 primary_thread_id : None ,
66166673 primary_session_configured : None ,
66176674 pending_primary_events : VecDeque :: new ( ) ,
6675+ pending_async_queue_resume_barriers : 0 ,
66186676 } ,
66196677 rx,
66206678 op_rx,
@@ -7728,6 +7786,109 @@ guardian_approval = true
77287786 }
77297787 }
77307788
7789+ #[ tokio:: test]
7790+ async fn queued_followup_waits_for_pending_async_resume_barrier ( ) {
7791+ let ( mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels ( ) . await ;
7792+ app. chat_widget . set_model ( "gpt-5.2-codex" ) ;
7793+ app. chat_widget
7794+ . set_feature_enabled ( Feature :: Personality , true ) ;
7795+ app. chat_widget . handle_codex_event ( Event {
7796+ id : "configured" . into ( ) ,
7797+ msg : EventMsg :: SessionConfigured ( SessionConfiguredEvent {
7798+ session_id : ThreadId :: new ( ) ,
7799+ forked_from_id : None ,
7800+ thread_name : None ,
7801+ model : "gpt-5.2-codex" . to_string ( ) ,
7802+ model_provider_id : "test-provider" . to_string ( ) ,
7803+ service_tier : None ,
7804+ approval_policy : AskForApproval :: Never ,
7805+ approvals_reviewer : ApprovalsReviewer :: User ,
7806+ sandbox_policy : SandboxPolicy :: new_read_only_policy ( ) ,
7807+ cwd : PathBuf :: from ( "/home/user/project" ) ,
7808+ reasoning_effort : Some ( ReasoningEffortConfig :: default ( ) ) ,
7809+ history_log_id : 0 ,
7810+ history_entry_count : 0 ,
7811+ initial_messages : None ,
7812+ network_proxy : None ,
7813+ rollout_path : None ,
7814+ } ) ,
7815+ } ) ;
7816+ app. chat_widget . handle_codex_event ( Event {
7817+ id : "turn-started" . into ( ) ,
7818+ msg : EventMsg :: TurnStarted ( TurnStartedEvent {
7819+ turn_id : "turn-1" . to_string ( ) ,
7820+ model_context_window : None ,
7821+ collaboration_mode_kind : Default :: default ( ) ,
7822+ } ) ,
7823+ } ) ;
7824+ while app_event_rx. try_recv ( ) . is_ok ( ) { }
7825+ while op_rx. try_recv ( ) . is_ok ( ) { }
7826+
7827+ app. chat_widget
7828+ . handle_serialized_slash_command ( UserMessage :: from ( "/personality pragmatic" ) ) ;
7829+ app. chat_widget
7830+ . set_composer_text ( "followup" . to_string ( ) , Vec :: new ( ) , Vec :: new ( ) ) ;
7831+ app. chat_widget
7832+ . handle_key_event ( KeyEvent :: new ( KeyCode :: Tab , KeyModifiers :: NONE ) ) ;
7833+
7834+ app. chat_widget . handle_codex_event ( Event {
7835+ id : "turn-complete" . into ( ) ,
7836+ msg : EventMsg :: TurnComplete ( TurnCompleteEvent {
7837+ turn_id : "turn-1" . to_string ( ) ,
7838+ last_agent_message : None ,
7839+ } ) ,
7840+ } ) ;
7841+
7842+ loop {
7843+ match app_event_rx. try_recv ( ) {
7844+ Ok ( AppEvent :: CodexOp ( Op :: OverrideTurnContext {
7845+ personality : Some ( Personality :: Pragmatic ) ,
7846+ ..
7847+ } ) ) => continue ,
7848+ Ok ( AppEvent :: UpdatePersonality ( Personality :: Pragmatic ) ) => {
7849+ app. on_update_personality ( Personality :: Pragmatic ) ;
7850+ break ;
7851+ }
7852+ Ok ( AppEvent :: PersistPersonalitySelection {
7853+ personality : Personality :: Pragmatic ,
7854+ } ) => continue ,
7855+ Ok ( _) => continue ,
7856+ Err ( TryRecvError :: Empty ) => panic ! ( "expected personality update events" ) ,
7857+ Err ( TryRecvError :: Disconnected ) => panic ! ( "expected personality update events" ) ,
7858+ }
7859+ }
7860+
7861+ app. pending_async_queue_resume_barriers = 1 ;
7862+ app. maybe_resume_queued_inputs_after_app_events ( true ) ;
7863+
7864+ assert_eq ! (
7865+ app. chat_widget. queued_user_message_texts( ) ,
7866+ vec![ "followup" . to_string( ) ]
7867+ ) ;
7868+ assert ! (
7869+ op_rx. try_recv( ) . is_err( ) ,
7870+ "queued follow-up should stay queued while async replay barriers are pending"
7871+ ) ;
7872+
7873+ app. pending_async_queue_resume_barriers = 0 ;
7874+ app. maybe_resume_queued_inputs_after_app_events ( true ) ;
7875+
7876+ match next_user_turn_op ( & mut op_rx) {
7877+ Op :: UserTurn {
7878+ items,
7879+ personality : Some ( Personality :: Pragmatic ) ,
7880+ ..
7881+ } => assert_eq ! (
7882+ items,
7883+ vec![ UserInput :: Text {
7884+ text: "followup" . to_string( ) ,
7885+ text_elements: Vec :: new( ) ,
7886+ } ]
7887+ ) ,
7888+ other => panic ! ( "expected Op::UserTurn with pragmatic personality, got {other:?}" ) ,
7889+ }
7890+ }
7891+
77317892 #[ tokio:: test]
77327893 async fn shutdown_first_exit_returns_immediate_exit_when_shutdown_submit_fails ( ) {
77337894 let mut app = make_test_app ( ) . await ;
0 commit comments