{"record":{"id":"c280dd3f20755672","repo":"NousResearch/hermes-agent","slug":"could-not-move-existing-app-aside-at-leaving-i","errorCode":null,"errorMessage":"could not move existing app aside at {} (leaving it in place): {err}","messagePattern":"could not move existing app aside at (.+?) \\(leaving it in place\\): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"apps/bootstrap-installer/src-tauri/src/update.rs","lineNumber":1084,"sourceCode":"\n    Ok(target_app.to_path_buf())\n}\n\n/// Move a freshly-staged bundle (`tmp`) into place at `target`, parking any\n/// existing bundle at `old` so the move can succeed (macOS `rename` won't\n/// overwrite a non-empty directory).\n///\n/// Invariant: on ANY failure path, `target` is left pointing at a working\n/// bundle — either the original (rolled back from `old`) or untouched — and we\n/// never delete the running app with no replacement in place. The staged `tmp`\n/// copy is cleaned up on failure.\nasync fn swap_in_new_bundle(tmp: &Path, target: &Path, old: &Path) -> Result<()> {\n    let moved_old = if target.exists() {\n        if let Err(err) = tokio::fs::rename(target, old).await {\n            // Could not move the existing app aside. Leave it untouched and\n            // bail — a failed update must not brick the install.\n            remove_dir_if_exists(tmp).await;\n            return Err(anyhow!(\n                \"could not move existing app aside at {} (leaving it in place): {err}\",\n                target.display()\n            ));\n        }\n        true\n    } else {\n        false\n    };\n    if let Err(err) = tokio::fs::rename(tmp, target).await {\n        // Restore the original app from the backup so the user keeps a working\n        // install, and clean up the staged copy.\n        if moved_old {\n            let _ = tokio::fs::rename(old, target).await;\n        }\n        remove_dir_if_exists(tmp).await;\n        return Err(anyhow!(\"installing updated app at {}: {err}\", target.display()));\n    }\n    remove_dir_if_exists(old).await;","sourceCodeStart":1066,"sourceCodeEnd":1102,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/bootstrap-installer/src-tauri/src/update.rs#L1066-L1102","documentation":"First phase of the atomic bundle swap in swap_in_new_bundle: renaming the existing target .app to <target>.hermes-update-old failed, so the updater removes the staged tmp copy and leaves the original app untouched. The invariant is that a failed update must never brick the install — bail before touching the original rather than risk deleting the running app with no replacement.","triggerScenarios":"The target .app is busy — macOS refuses to rename a bundle that is running or has files held open; permission denied on /Applications (app installed without user-writable perms); the .hermes-update-old leftover from a prior crashed update already exists as a file where a directory rename is required.","commonSituations":"The old Hermes.app is still running when the swap starts (stage-1 race); user-installed into /Applications under an admin context so the rename needs elevation; leftover stale .hermes-update-old path from an earlier failed run.","solutions":["Quit Hermes completely (check Activity Monitor) and retry the update.","Remove leftover <app>.hermes-update-old / -new siblings from the previous failed attempt.","Fix permissions on the target's parent directory (chown / reinstall into user-writable location).","If /Applications requires admin, run the updater with the necessary privileges or move the app to ~/Applications."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Pre-swap checks: old app not running, no stale leftovers, writable parent.\nfn swap_precheck(target: &std::path::Path) -> Result<(), String> {\n    let old = std::path::PathBuf::from(format!(\"{}.hermes-update-old\", target.display()));\n    let tmp = std::path::PathBuf::from(format!(\"{}.hermes-update-new\", target.display()));\n    if old.exists() { return Err(format!(\"stale leftover {} from a failed update — remove it first\", old.display())); }\n    if tmp.exists() { return Err(format!(\"stale leftover {} — remove it first\", tmp.display())); }\n    if target.parent().map(|p| p.metadata().is_ok_and(|m| m.permissions().readonly())).unwrap_or(true) {\n        return Err(\"target parent directory is read-only\".into());\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"// The function already implements the correct pattern: on failed move-aside, delete tmp and\n// leave the original untouched — never continue into a state where the target is deleted.\nmatch tokio::fs::rename(target, old).await {\n    Ok(()) => { /* proceed to rename tmp -> target */ }\n    Err(err) => {\n        remove_dir_if_exists(tmp).await; // staged copy cleaned\n        return Err(anyhow!(\"could not move existing app aside at {} (leaving it in place): {err}\", target.display()));\n    }\n}","preventionTips":["Quit the app fully before updating — a running bundle cannot be renamed on macOS.","Clear .hermes-update-old/.hermes-update-new leftovers after any failed update.","Install the app in a user-writable location (~/Applications) if /Applications needs elevation."],"tags":["macos","filesystem","atomic-swap","update","bootstrap-installer"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}