From 131dde2c2242c3d0298bce422ac56f2ec90263f0 Mon Sep 17 00:00:00 2001 From: fengqigang Date: Wed, 24 Jun 2026 10:38:17 +0800 Subject: [PATCH] fix: prevent Computer Use subprocess leak on macOS Codex spawns a SkyComputerUseClient subprocess for each Computer Use session via the openai-bundled computer-use plugin, but does not reliably clean them up when conversations end. On macOS, these orphaned processes accumulate (~20MB RSS each), eventually causing swap pressure and UI freezes. Changes: - computer_use_guard.rs: Add kill_orphaned_computer_use_processes() for macOS that uses pkill to clean up SkyComputerUseClient processes. Codex re-spawns them lazily on the next Computer Use session, so this is safe to run periodically. - launcher.rs: Add a macOS-specific computer_use_guard_watchdog that runs every 120 seconds and kills orphaned Computer Use subprocesses. The watchdog is always active on macOS (not gated on the computer_use_guard_enabled setting, which is Windows-specific). - relay_config.rs: Replace && let_chains syntax with nested if let blocks for broader Rust version compatibility. --- .../codex-plus-core/src/computer_use_guard.rs | 24 +++++++++++++++ crates/codex-plus-core/src/launcher.rs | 30 +++++++++++++++++-- crates/codex-plus-core/src/relay_config.rs | 26 +++++++++------- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/crates/codex-plus-core/src/computer_use_guard.rs b/crates/codex-plus-core/src/computer_use_guard.rs index fa3c09ff..bb8b9f29 100644 --- a/crates/codex-plus-core/src/computer_use_guard.rs +++ b/crates/codex-plus-core/src/computer_use_guard.rs @@ -1160,3 +1160,27 @@ mod tests { assert_eq!(guard_staging_count, 1); } } + + /// Kill orphaned SkyComputerUseClient processes on macOS. + /// + /// On macOS, Codex spawns a `SkyComputerUseClient` subprocess for each + /// Computer Use session via the bundled openai-bundled computer-use plugin. + /// Codex does not reliably clean these up when conversations end — they + /// accumulate and consume significant memory (~20MB RSS each), eventually + /// causing swap pressure and UI freezes. + /// + /// This function kills all `SkyComputerUseClient` processes it can find. + /// Codex re-spawns them lazily on the next Computer Use session, so killing + /// them is safe and does not affect active conversations. + /// + /// We intentionally leave `node_repl` processes alone — they are lightweight + /// (~1MB RSS) and killing them could disrupt in-flight code execution. + #[cfg(target_os = "macos")] + pub fn kill_orphaned_computer_use_processes() { + let _ = std::process::Command::new("pkill") + .arg("-f") + .arg("SkyComputerUseClient") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + } diff --git a/crates/codex-plus-core/src/launcher.rs b/crates/codex-plus-core/src/launcher.rs index e4618115..0ba96db9 100644 --- a/crates/codex-plus-core/src/launcher.rs +++ b/crates/codex-plus-core/src/launcher.rs @@ -689,11 +689,11 @@ impl LaunchHooks for DefaultLaunchHooks { &self, settings: &BackendSettings, ) -> anyhow::Result<()> { - if !settings.computer_use_guard_enabled { - return Ok(()); - } #[cfg(windows)] { + if !settings.computer_use_guard_enabled { + return Ok(()); + } let home = crate::relay_config::default_codex_home_dir(); let artifacts = self.computer_use_guard_artifacts.lock().await.clone(); let (shutdown, mut shutdown_rx) = tokio::sync::oneshot::channel(); @@ -710,6 +710,30 @@ impl LaunchHooks for DefaultLaunchHooks { let _ = runtime.task.await; } } + #[cfg(target_os = "macos")] + { + let _ = &settings; + let (shutdown, mut shutdown_rx) = tokio::sync::oneshot::channel(); + let task = tokio::spawn(async move { + loop { + tokio::select! { + _ = &mut shutdown_rx => break, + _ = tokio::time::sleep(std::time::Duration::from_secs(120)) => { + crate::computer_use_guard::kill_orphaned_computer_use_processes(); + } + } + } + }); + if let Some(runtime) = self + .computer_use_guard_watchdog + .lock() + .await + .replace(ComputerUseGuardWatchdogRuntime { shutdown, task }) + { + let _ = runtime.shutdown.send(()); + let _ = runtime.task.await; + } + } Ok(()) } diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index 0c0b606a..da8938fb 100644 --- a/crates/codex-plus-core/src/relay_config.rs +++ b/crates/codex-plus-core/src/relay_config.rs @@ -1230,11 +1230,12 @@ fn normalize_duplicate_toml_text(contents: &str) -> String { if in_root && !trimmed.is_empty() && !trimmed.starts_with('#') - && let Some((key, _)) = trimmed.split_once('=') { - let key = key.trim(); - if !key.is_empty() && !key.contains('.') && !seen_root_keys.insert(key.to_string()) { - continue; + if let Some((key, _)) = trimmed.split_once('=') { + let key = key.trim(); + if !key.is_empty() && !key.contains('.') && !seen_root_keys.insert(key.to_string()) { + continue; + } } } @@ -1271,10 +1272,12 @@ fn strip_common_config_text_fallback(config_text: &str, common_config: &str) -> if !trimmed.is_empty() && !trimmed.starts_with('#') - && let Some((key, _)) = trimmed.split_once('=') - && anchors.root_keys.contains(key.trim()) { - continue; + if let Some((key, _)) = trimmed.split_once('=') { + if anchors.root_keys.contains(key.trim()) { + continue; + } + } } kept.push(line); @@ -1304,11 +1307,12 @@ fn common_config_anchors(common_config: &str) -> CommonConfigAnchors { if in_root && !trimmed.is_empty() && !trimmed.starts_with('#') - && let Some((key, _)) = trimmed.split_once('=') { - let key = key.trim(); - if !key.is_empty() { - root_keys.insert(key.to_string()); + if let Some((key, _)) = trimmed.split_once('=') { + let key = key.trim(); + if !key.is_empty() { + root_keys.insert(key.to_string()); + } } } }