{"record":{"id":"f07d390370b1f879","repo":"BigPizzaV3/CodexPlusPlus","slug":"native-browser-cleanup-did-not-complete-successfully","errorCode":null,"errorMessage":"Native browser cleanup did not complete successfully","messagePattern":"Native browser cleanup did not complete successfully","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":787,"sourceCode":"    }\n    let mut file = match options.open(&path) {\n        Ok(file) => file,\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {\n            return verify_restored_state(paths);\n        }\n        Err(error) => return Err(error.into()),\n    };\n    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    }","sourceCodeStart":769,"sourceCodeEnd":805,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L769-L805","documentation":"After acquiring the released monitor.lock, the library deserializes the MonitorReceipt JSON and requires schema == 1, a parseable UUID in `generation`, and state == \"restored\". This error means the lock was free (the previous monitor finished) but its receipt does not record a successful cleanup — either the fields are malformed or the recorded state is not `restored` (e.g. \"active\" or an error state), so the shutdown cannot be considered complete.","triggerScenarios":"Calling wait_for_monitor_shutdown when the receipt was left in a non-restored state (monitor died mid-cleanup), or the receipt is corrupt/invalid JSON so schema/generation checks fail; also stale receipts from an interrupted earlier generation.","commonSituations":"The native browser monitor process was killed (taskkill, crash, reboot) between writing \"active\" and \"restored\"; power loss during cleanup; hand-edited or corrupted receipt; version skew writing different schema numbers.","solutions":["Read monitor.lock and inspect the JSON: check `schema`, `generation`, and `state` fields.","If state is not \"restored\", run the cleanup/restore path (or verify_restored_state's checks manually: control.json requireIdentification=false, service files match originals) before launching a replacement.","If the JSON is corrupt, delete monitor.lock and restore browser service files from backups in the state directory.","Check the diagnostic log for the monitor's last recorded action to determine how far cleanup got."],"exampleFix":"// before: stale receipt\n{\"schema\":1,\"generation\":\"...\",\"state\":\"active\"}\n// after: rerun cleanup so it writes\n{\"schema\":1,\"generation\":\"<uuid>\",\"state\":\"restored\"}","handlingStrategy":"try-catch","validationCode":"if let Ok(bytes) = std::fs::read(state_root.join(\"monitor.lock\")) {\n    if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) {\n        let ok = v[\"schema\"] == 1 && v[\"state\"] == \"restored\";\n        if !ok { /* stale/failed receipt: run cleanup before launching replacement */ }\n    }\n}","typeGuard":"fn receipt_restored(bytes: &[u8]) -> bool {\n    serde_json::from_slice::<serde_json::Value>(bytes)\n        .map(|v| v[\"schema\"] == 1 && v[\"state\"] == \"restored\" && v[\"generation\"].is_string())\n        .unwrap_or(false)\n}","tryCatchPattern":"match wait_for_monitor_shutdown(Duration::from_secs(30)) {\n    Err(e) if e.to_string().contains(\"cleanup did not complete successfully\") => {\n        // previous monitor died mid-cleanup: run restore/verify before launching the new launcher\n    }\n    other => other?,\n}","preventionTips":["Always stop the monitor via the library's graceful shutdown so it writes state=\"restored\"","Avoid killing monitor processes (taskkill / power loss) mid-cleanup; use UPS/graceful shutdown on machines running the manager","Run verify_restored_state-equivalent checks (control.json, service files) before launching a replacement launcher"],"tags":["state-machine","cleanup","validation","json"],"backgroundTag":"invalid-state-transition","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"}