mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
feat(tui): prevent macOS idle sleep while turns run (#11711)
## Summary - add a shared `codex-core` sleep inhibitor that uses native macOS IOKit assertions (`IOPMAssertionCreateWithName` / `IOPMAssertionRelease`) instead of spawning `caffeinate` - wire sleep inhibition to turn lifecycle in `tui` (`TurnStarted` enables; `TurnComplete` and abort/error finalization disable) - gate this behavior behind a `/experimental` feature toggle (`[features].prevent_idle_sleep`) instead of a dedicated `[tui]` config flag - expose the toggle in `/experimental` on macOS; keep it under development on other platforms - keep behavior no-op on non-macOS targets <img width="1326" height="577" alt="image" src="https://github.com/user-attachments/assets/73fac06b-97ae-46a2-800a-30f9516cf8a3" /> ## Testing - `cargo check -p codex-core -p codex-tui` - `cargo test -p codex-core sleep_inhibitor::tests -- --nocapture` - `cargo test -p codex-core tui_config_missing_notifications_field_defaults_to_enabled -- --nocapture` - `cargo test -p codex-core prevent_idle_sleep_is_ -- --nocapture` ## Semantics and API references - This PR targets `caffeinate -i` semantics: prevent *idle system sleep* while allowing display idle sleep. - `caffeinate -i` mapping in Apple open source (`assertionMap`): - `kIdleAssertionFlag -> kIOPMAssertionTypePreventUserIdleSystemSleep` - Source: https://github.com/apple-oss-distributions/PowerManagement/blob/PowerManagement-1846.60.12/caffeinate/caffeinate.c#L52-L54 - Apple IOKit docs for assertion types and API: - https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes - https://developer.apple.com/documentation/iokit/1557092-iopmassertioncreatewithname - https://developer.apple.com/library/archive/qa/qa1340/_index.html ## Codex Electron vs this PR (full stack path) - Codex Electron app requests sleep blocking with `powerSaveBlocker.start("prevent-app-suspension")`: - https://github.com/openai/codex/blob/main/codex/codex-vscode/electron/src/electron-message-handler.ts - Electron maps that string to Chromium wake lock type `kPreventAppSuspension`: - https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_power_save_blocker.cc - Chromium macOS backend maps wake lock types to IOKit assertion constants and calls IOKit: - `kPreventAppSuspension -> kIOPMAssertionTypeNoIdleSleep` - `kPreventDisplaySleep / kPreventDisplaySleepAllowDimming -> kIOPMAssertionTypeNoDisplaySleep` - https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_mac.cc ## Why this PR uses a different macOS constant name - This PR uses `"PreventUserIdleSystemSleep"` directly, via `IOPMAssertionCreateWithName`, in `codex-rs/core/src/sleep_inhibitor.rs`. - Apple’s IOKit header documents `kIOPMAssertionTypeNoIdleSleep` as deprecated and recommends `kIOPMAssertPreventUserIdleSystemSleep` / `kIOPMAssertionTypePreventUserIdleSystemSleep`: - https://github.com/apple-oss-distributions/IOKitUser/blob/IOKitUser-100222.60.2/pwr_mgt.subproj/IOPMLib.h#L1000-L1030 - So Chromium and this PR are using different constant names, but semantically equivalent idle-system-sleep prevention behavior. ## Future platform support The architecture is intentionally set up for multi-platform extensions: - UI code (`tui`) only calls `SleepInhibitor::set_turn_running(...)` on turn lifecycle boundaries. - Platform-specific behavior is isolated in `codex-rs/core/src/sleep_inhibitor.rs` behind `cfg(...)` blocks. - Feature exposure is centralized in `core/src/features.rs` and surfaced via `/experimental`. - Adding new OS backends should not require additional TUI wiring; only the backend internals and feature stage metadata need to change. Potential follow-up implementations: - Windows: - Add a backend using Win32 power APIs (`SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)` as baseline). - Optionally move to `PowerCreateRequest` / `PowerSetRequest` / `PowerClearRequest` for richer assertion semantics. - Linux: - Add a backend using logind inhibitors over D-Bus (`org.freedesktop.login1.Manager.Inhibit` with `what="sleep"`). - Keep a no-op fallback where logind/D-Bus is unavailable. This PR keeps the cross-platform API surface minimal so future PRs can add Windows/Linux support incrementally with low churn. --------- Co-authored-by: jif-oai <jif@openai.com>
This commit is contained in:
committed by
GitHub
parent
851fcc377b
commit
32da5eb358
94
codex-rs/utils/sleep-inhibitor/src/lib.rs
Normal file
94
codex-rs/utils/sleep-inhibitor/src/lib.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Cross-platform helper for preventing idle sleep while a turn is running.
|
||||
//!
|
||||
//! On macOS this uses native IOKit power assertions instead of spawning
|
||||
//! `caffeinate`, so assertion lifecycle is tied directly to Rust object lifetime.
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod dummy;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_inhibitor;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Keeps the machine awake while a turn is in progress when enabled.
|
||||
#[derive(Debug)]
|
||||
pub struct SleepInhibitor {
|
||||
enabled: bool,
|
||||
platform: Box<dyn PlatformSleepInhibitor>,
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformSleepInhibitor: Debug {
|
||||
fn acquire(&mut self);
|
||||
fn release(&mut self);
|
||||
}
|
||||
|
||||
impl SleepInhibitor {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform: Box<dyn PlatformSleepInhibitor> =
|
||||
Box::new(macos_inhibitor::MacOsSleepInhibitor::new());
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let platform: Box<dyn PlatformSleepInhibitor> = Box::new(dummy::DummySleepInhibitor::new());
|
||||
|
||||
Self { enabled, platform }
|
||||
}
|
||||
|
||||
/// Update the active turn state; turns sleep prevention on/off as needed.
|
||||
pub fn set_turn_running(&mut self, turn_running: bool) {
|
||||
if !self.enabled {
|
||||
self.release();
|
||||
return;
|
||||
}
|
||||
|
||||
if turn_running {
|
||||
self.acquire();
|
||||
} else {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire(&mut self) {
|
||||
self.platform.acquire();
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.platform.release();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SleepInhibitor;
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_toggles_without_panicking() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_disabled_does_not_panic() {
|
||||
let mut inhibitor = SleepInhibitor::new(false);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_multiple_true_calls_are_idempotent() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_can_toggle_multiple_times() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user