{"record":{"id":"0d6fff98fe0c671b","repo":"BigPizzaV3/CodexPlusPlus","slug":"native-browser-cleanup-is-still-running-launcher-was-not","errorCode":null,"errorMessage":"Native browser cleanup is still running; launcher was not terminated","messagePattern":"Native browser cleanup is still running; launcher was not terminated","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":795,"sourceCode":"    plain_path(&path)?;\n    let deadline = std::time::Instant::now() + timeout;\n    loop {\n        match file.try_lock_exclusive() {\n            Ok(()) => {\n                ensure!(file.metadata()?.len() <= 1024, \"Invalid native cleanup receipt\");\n                file.seek(SeekFrom::Start(0))?;\n                let mut bytes = Vec::new();\n                Read::by_ref(&mut file).take(1025).read_to_end(&mut bytes)?;\n                let receipt: MonitorReceipt = serde_json::from_slice(&bytes)?;\n                ensure!(\n                    receipt.schema == 1 && uuid::Uuid::parse_str(&receipt.generation).is_ok()\n                        && receipt.state == \"restored\",\n                    \"Native browser cleanup did not complete successfully\"\n                );\n                return Ok(());\n            }\n            Err(error) if error.kind() == fs2::lock_contended_error().kind() => {\n                ensure!(\n                    std::time::Instant::now() < deadline,\n                    \"Native browser cleanup is still running; launcher was not terminated\"\n                );\n                std::thread::sleep(Duration::from_millis(50).min(\n                    deadline.saturating_duration_since(std::time::Instant::now()),\n                ));\n            }\n            Err(error) => return Err(error.into()),\n        }\n    }\n}\n\nasync fn start_monitor_with_contract(\n    paths: BrowserPaths,\n    enabled: bool,\n    contract: RuntimeContract,\n) -> Result<BrowserMonitor> {\n    let mut owner = acquire_monitor_owner(&paths)?;","sourceCodeStart":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L777-L813","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nwait_for_monitor_shutdown(Duration::from_secs(2))?;  // too short\n// after\nwait_for_monitor_shutdown(Duration::from_secs(30))?;  # allow cleanup to finish","handlingStrategy":"retry","validationCode":"// Pre-check: is any monitor/launcher process still alive?\n// (e.g. probe the PID recorded by the manager or scan for the monitor executable)\nlet monitor_alive = process_exists(\"codex-native-browser-monitor\");\nif monitor_alive { /* terminate or wait before calling wait_for_monitor_shutdown */ }","typeGuard":null,"tryCatchPattern":"loop {\n    match wait_for_monitor_shutdown(Duration::from_secs(30)) {\n        Ok(()) => break,\n        Err(e) if e.to_string().contains(\"still running\") => {\n            // escalate: find and kill the stale holder of monitor.lock, then retry\n        }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["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"],"tags":["timeout","lock-contention","process-lifecycle","windows"],"backgroundTag":"request-timeout","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}