From c7e5a9d95e259b8b79e3f7f659b3f6d19c4521cc Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 21 Apr 2026 20:39:23 -0700 Subject: [PATCH 1/3] Keep TUI status surfaces in sync (#18935) --- codex-rs/tui/src/chatwidget.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index ba00a1c8a1..46850c8304 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1792,7 +1792,7 @@ impl ChatWidget { fn update_task_running_state(&mut self) { self.bottom_pane .set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some()); - self.refresh_terminal_title(); + self.refresh_status_surfaces(); } fn restore_reasoning_status_header(&mut self) { @@ -1911,7 +1911,7 @@ impl ChatWidget { || (title_uses_spinner && self.terminal_title_status_kind == TerminalTitleStatusKind::Undoing) { - self.refresh_terminal_title(); + self.refresh_status_surfaces(); } } @@ -2287,7 +2287,6 @@ impl ChatWidget { self.add_boxed_history(Box::new(cell)); } self.thread_name = event.thread_name; - self.refresh_terminal_title(); self.refresh_status_surfaces(); self.request_redraw(); self.maybe_send_next_queued_input(); @@ -2491,7 +2490,6 @@ impl ChatWidget { self.saw_plan_update_this_turn = false; self.saw_plan_item_this_turn = false; self.latest_proposed_plan_markdown = None; - self.last_plan_progress = None; self.plan_delta_buffer.clear(); self.plan_item_active = false; self.adaptive_chunking.reset(); @@ -3563,7 +3561,7 @@ impl ChatWidget { self.update_task_running_state(); if restored_task_running && !self.bottom_pane.is_task_running() { self.bottom_pane.set_task_running(/*running*/ true); - self.refresh_terminal_title(); + self.refresh_status_surfaces(); } self.refresh_pending_input_preview(); self.request_redraw(); @@ -3585,7 +3583,7 @@ impl ChatWidget { }) .count(); self.last_plan_progress = (total > 0).then_some((completed, total)); - self.refresh_terminal_title(); + self.refresh_status_surfaces(); self.add_to_history(history_cell::new_plan_update(update)); } From 0e39614d8775c10cc686562dc661f3edfdc5998b Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Tue, 21 Apr 2026 21:00:23 -0700 Subject: [PATCH 2/3] chore(tui) debug-config guardian_policy_config (#18923) ## Summary List guardian_policy_config_source in `/debug-config` output ## Testing - [x] Ran locally --- codex-rs/config/src/config_requirements.rs | 7 ++++++- codex-rs/core/src/config/mod.rs | 1 + codex-rs/tui/src/debug_config.rs | 14 +++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 57ee0601e9..800a688768 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -93,6 +93,8 @@ pub struct ConfigRequirements { pub network: Option>, /// Managed filesystem constraints derived from requirements. pub filesystem: Option>, + /// Source for the managed guardian policy config, when one is configured. + pub guardian_policy_config_source: Option, } impl Default for ConfigRequirements { @@ -123,6 +125,7 @@ impl Default for ConfigRequirements { ), network: None, filesystem: None, + guardian_policy_config_source: None, } } } @@ -887,7 +890,7 @@ impl TryFrom for ConfigRequirements { enforce_residency, network, permissions, - guardian_policy_config: _guardian_policy_config, + guardian_policy_config, } = toml; let approval_policy = match allowed_approval_policies { @@ -1096,6 +1099,7 @@ impl TryFrom for ConfigRequirements { let Sourced { value, source } = sourced_permissions; Sourced::new(FilesystemConstraints::from(value), source) }); + let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source); Ok(ConfigRequirements { approval_policy, approvals_reviewer, @@ -1107,6 +1111,7 @@ impl TryFrom for ConfigRequirements { enforce_residency, network, filesystem, + guardian_policy_config_source, }) } } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 995c2008c5..a8fd687e86 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1565,6 +1565,7 @@ impl Config { enforce_residency, network: network_requirements, filesystem: filesystem_requirements, + guardian_policy_config_source: _, } = config_layer_stack.requirements().clone(); let user_instructions = AgentsMdManager::load_global_instructions(Some(&codex_home)) diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 6cb71792ff..b16121c595 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -144,6 +144,14 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { )); } + if requirements_toml.guardian_policy_config.is_some() { + requirement_lines.push(requirement_line( + "guardian_policy_config", + "configured".to_string(), + requirements.guardian_policy_config_source.as_ref(), + )); + } + if let Some(feature_requirements) = requirements.feature_requirements.as_ref() { let value = join_or_empty( feature_requirements @@ -633,6 +641,7 @@ mod tests { file: requirements_file.clone(), }, )), + guardian_policy_config_source: Some(RequirementSource::CloudRequirements), ..ConfigRequirements::default() }; @@ -642,7 +651,7 @@ mod tests { allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), - guardian_policy_config: None, + guardian_policy_config: Some("Use the managed guardian policy.".to_string()), feature_requirements: Some(FeatureRequirementsToml { entries: BTreeMap::from([("guardian_approval".to_string(), true)]), }), @@ -697,6 +706,9 @@ mod tests { "allowed_web_search_modes: cached, disabled (source: cloud requirements)" ) ); + assert!( + rendered.contains("guardian_policy_config: configured (source: cloud requirements)") + ); assert!(rendered.contains("features: guardian_approval=true (source: cloud requirements)")); assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))")); assert!(rendered.contains("enforce_residency: us (source: cloud requirements)")); From 448f71e7d0cf94228196d3e66ae813e0cee8d5ee Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 21 Apr 2026 21:12:52 -0700 Subject: [PATCH 3/3] fix(exec-server): retain output until streams close --- codex-rs/exec-server/src/local_process.rs | 111 ++++++++++++++++++---- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index aa88e37fdd..d285418a1a 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -323,8 +323,10 @@ impl LocalProcess { ) }; + let has_new_lifecycle_event = response.next_seq > after_seq.saturating_add(1); if !response.chunks.is_empty() - || response.exited + || has_new_lifecycle_event + || response.closed || tokio::time::Instant::now() >= deadline { let _total_bytes: usize = response @@ -625,16 +627,7 @@ async fn watch_exit( .await; } - maybe_emit_closed(process_id.clone(), Arc::clone(&inner)).await; - - tokio::time::sleep(EXITED_PROCESS_RETENTION).await; - let mut processes = inner.processes.lock().await; - if matches!( - processes.get(&process_id), - Some(ProcessEntry::Running(process)) if process.exit_code == Some(exit_code) - ) { - processes.remove(&process_id); - } + maybe_emit_closed(process_id, Arc::clone(&inner)).await; } async fn finish_output_stream(process_id: ProcessId, inner: Arc) { @@ -653,7 +646,7 @@ async fn finish_output_stream(process_id: ProcessId, inner: Arc) { } async fn maybe_emit_closed(process_id: ProcessId, inner: Arc) { - let notification = { + let closed = { let mut processes = inner.processes.lock().await; let Some(ProcessEntry::Running(process)) = processes.get_mut(&process_id) else { return; @@ -668,16 +661,33 @@ async fn maybe_emit_closed(process_id: ProcessId, inner: Arc) { process.next_seq += 1; let _ = process.wake_tx.send(seq); process.events.publish(ExecProcessEvent::Closed { seq }); - Some(ExecClosedNotification { - process_id: process_id.clone(), - seq, - }) + Some(( + ExecClosedNotification { + process_id: process_id.clone(), + seq, + }, + Arc::clone(&process.output_notify), + )) }; - let Some(notification) = notification else { + let Some((notification, output_notify)) = closed else { return; }; + output_notify.notify_waiters(); + let cleanup_process_id = process_id.clone(); + let cleanup_inner = Arc::clone(&inner); + tokio::spawn(async move { + tokio::time::sleep(EXITED_PROCESS_RETENTION).await; + let mut processes = cleanup_inner.processes.lock().await; + if matches!( + processes.get(&cleanup_process_id), + Some(ProcessEntry::Running(process)) if process.closed + ) { + processes.remove(&cleanup_process_id); + } + }); + if let Some(notifications) = notification_sender(&inner) { let _ = notifications .notify(EXEC_CLOSED_METHOD, ¬ification) @@ -697,6 +707,7 @@ fn notification_sender(inner: &Inner) -> Option { mod tests { use super::*; use codex_config::types::ShellEnvironmentPolicyInherit; + use pretty_assertions::assert_eq; fn test_exec_params(env: HashMap) -> ExecParams { ExecParams { @@ -745,4 +756,70 @@ mod tests { assert_eq!(child_env(¶ms), expected); } + + #[cfg(unix)] + #[tokio::test] + async fn exited_process_is_retained_until_output_streams_close() { + let process_id = ProcessId::from("retained-until-stream-close"); + let spawned = codex_utils_pty::spawn_pipe_process_no_stdin( + "/bin/sleep", + &["5".to_string()], + std::env::current_dir().expect("cwd").as_path(), + &HashMap::new(), + &None::, + ) + .await + .expect("spawn sleep"); + let (outgoing_tx, _outgoing_rx) = mpsc::channel(16); + let (wake_tx, _wake_rx) = watch::channel(0); + let output_notify = Arc::new(Notify::new()); + let inner = Arc::new(Inner { + notifications: std::sync::RwLock::new(Some(RpcNotificationSender::new(outgoing_tx))), + processes: Mutex::new(HashMap::from([( + process_id.clone(), + ProcessEntry::Running(Box::new(RunningProcess { + session: spawned.session, + tty: false, + pipe_stdin: false, + output: VecDeque::new(), + retained_bytes: 0, + next_seq: 1, + exit_code: None, + wake_tx, + events: ExecProcessEventLog::new( + PROCESS_EVENT_CHANNEL_CAPACITY, + RETAINED_OUTPUT_BYTES_PER_PROCESS, + ), + output_notify: Arc::clone(&output_notify), + open_streams: 1, + closed: false, + })), + )])), + }); + let (exit_tx, exit_rx) = tokio::sync::oneshot::channel(); + let watch_exit_task = tokio::spawn(watch_exit( + process_id.clone(), + exit_rx, + Arc::clone(&inner), + output_notify, + )); + + exit_tx.send(0).expect("send exit"); + tokio::time::sleep(EXITED_PROCESS_RETENTION + Duration::from_millis(20)).await; + { + let processes = inner.processes.lock().await; + let Some(ProcessEntry::Running(process)) = processes.get(&process_id) else { + panic!("exited process should stay retained while output streams are open"); + }; + assert_eq!(process.exit_code, Some(0)); + assert!(!process.closed); + } + + finish_output_stream(process_id.clone(), Arc::clone(&inner)).await; + watch_exit_task.await.expect("watch exit task should join"); + tokio::time::sleep(EXITED_PROCESS_RETENTION + Duration::from_millis(20)).await; + + let processes = inner.processes.lock().await; + assert!(!processes.contains_key(&process_id)); + } }