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
- 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.
- Increase the timeout passed to wait_for_monitor_shutdown to cover realistic cleanup durations.
- Ensure the old launcher/monitor is actually stopped before calling wait_for_monitor_shutdown (the API contract requires the caller to stop it first).
- 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
- Terminate the previous launcher/monitor before calling wait_for_monitor_shutdown (this is the API contract)
- Use a generous timeout (tens of seconds) rather than a couple of seconds
- On timeout, locate the lock holder with handle.exe/Resource Monitor instead of blindly retrying
- Alert on hung monitor processes via the diagnostic log so cleanup stalls are caught early
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
- macOS app did not exit before debug relaunch
- Previous launcher has not exited; no process was forcibly…
- API 请求超时( 秒),本批未提交
- cannot terminate Windows process id
- Cannot verify launcher process identity
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)