BigPizzaV3/CodexPlusPlus · error

Native browser cleanup is still running; launcher was not…

Error message

Native browser cleanup is still running; launcher was not terminated

What it means

wait_for_monitor_shutdown_at retries try_lock_exclusive on monitor.lock every 50ms until a deadline (the caller-supplied timeout). If the lock remains contended — i.e. the previous monitor still holds it and has not finished cleanup — when the deadline passes, this error is thrown. It signals the caller (typically before launching a replacement launcher) that the old monitor/launcher is still alive and shutdown did not complete in time.

Solutions

  1. Identify the process holding monitor.lock (e.g. Sysinternals handle.exe / Resource Monitor → CPU → Associated Handles) and terminate it if it is a stale monitor.
  2. Increase the timeout passed to wait_for_monitor_shutdown to cover realistic cleanup durations.
  3. Ensure the old launcher/monitor is actually stopped before calling wait_for_monitor_shutdown (the API contract requires the caller to stop it first).
  4. Retry the wait after killing the process; if it recurs, collect the diagnostic log (native_browser.compatibility) to see what the monitor was doing.

Example fix

// before
wait_for_monitor_shutdown(Duration::from_secs(2))?;  // too short
// after
wait_for_monitor_shutdown(Duration::from_secs(30))?;  # allow cleanup to finish
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check: is any monitor/launcher process still alive?
// (e.g. probe the PID recorded by the manager or scan for the monitor executable)
let monitor_alive = process_exists("codex-native-browser-monitor");
if monitor_alive { /* terminate or wait before calling wait_for_monitor_shutdown */ }

Try / catch

loop {
    match wait_for_monitor_shutdown(Duration::from_secs(30)) {
        Ok(()) => break,
        Err(e) if e.to_string().contains("still running") => {
            // escalate: find and kill the stale holder of monitor.lock, then retry
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: Calling wait_for_monitor_shutdown(timeout) with a timeout shorter than the previous monitor's cleanup time, while the monitor process is hung, blocked on I/O, or still running. The lock stays exclusively held past the deadline.

Common situations: Monitor process wedged on a slow disk or antivirus scan; previous launcher never terminated (parent still running); timeout set too aggressively in the manager; Windows reboot lag or a zombie process holding the file handle.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/0d6fff98fe0c671b. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/native_browser.rs:795

    plain_path(&path)?;
    let deadline = std::time::Instant::now() + timeout;
    loop {
        match file.try_lock_exclusive() {
            Ok(()) => {
                ensure!(file.metadata()?.len() <= 1024, "Invalid native cleanup receipt");
                file.seek(SeekFrom::Start(0))?;
                let mut bytes = Vec::new();
                Read::by_ref(&mut file).take(1025).read_to_end(&mut bytes)?;
                let receipt: MonitorReceipt = serde_json::from_slice(&bytes)?;
                ensure!(
                    receipt.schema == 1 && uuid::Uuid::parse_str(&receipt.generation).is_ok()
                        && receipt.state == "restored",
                    "Native browser cleanup did not complete successfully"
                );
                return Ok(());
            }
            Err(error) if error.kind() == fs2::lock_contended_error().kind() => {
                ensure!(
                    std::time::Instant::now() < deadline,
                    "Native browser cleanup is still running; launcher was not terminated"
                );
                std::thread::sleep(Duration::from_millis(50).min(
                    deadline.saturating_duration_since(std::time::Instant::now()),
                ));
            }
            Err(error) => return Err(error.into()),
        }
    }
}

async fn start_monitor_with_contract(
    paths: BrowserPaths,
    enabled: bool,
    contract: RuntimeContract,
) -> Result<BrowserMonitor> {
    let mut owner = acquire_monitor_owner(&paths)?;

View on GitHub (pinned to b1ed92e5e4)