Hmbown/CodeWhale · error
The menu-bar app did not exit; update cancelled.
Error message
The menu-bar app did not exit; update cancelled.
What it means
After the helper exits, the install step waits (up to 10 seconds) for the menu-bar launcher process to exit, because LaunchServices could otherwise send a Reopen event to the retiring menu-bar process during the swap. If the launcher is still alive after the grace period, the install is cancelled; only ESRCH (process gone) is accepted.
Solutions
- Quit the menu-bar app explicitly (its menu Quit action or Activity Monitor) and retry the update.
- Check Activity Monitor / logs for a hung menu-bar process and force-quit it before updating.
- Disable 'reopen windows' / login-item restore for the app during the update to avoid relaunch interference.
- Reboot if a stale or PID-reused launcher keeps appearing, then rerun the update.
Defensive patterns
Strategy: try-catch
Validate before calling
try { process.kill(launcherPid, 0); console.log('menu-bar app still running; quit it before updating'); } catch { /* launcher already exited */ } Try / catch
try { await restartWithUpdate(prepared, destination); } catch (e) { if (/menu-bar app did not exit/.test(e.message)) { /* quit/force-quit the menu-bar app, then retry */ } else throw e; } Prevention
- Quit the menu-bar app (close any open panels) before updating.
- Disable login-item window restore to avoid relaunch interference.
- Investigate hung menu-bar processes instead of retrying blindly.
When it happens
Trigger: The menu-bar app ignored SIGTERM-style shutdown beyond 10 seconds; the launcher PID is stale or was reused by another long-lived process; the launcher is stuck in a modal system dialog or blocked syscall and never processes its exit; permission issues prevent signaling it.
Common situations: Updating while the menu-bar app is showing a panel or alert; a hung menu-bar process from a previous crashed session; macOS re-launching or holding the app due to login-item/Restore- windows behavior; PID reuse.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- The running helper did not stop; update cancelled.
- The update could not be unpacked.
- application not found — call list_apps for exact names/pids
- background preview capture failed
- Cannot resolve current binary path
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/42afb9d3b5eca710.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:130
export async function restartWithUpdate(prepared,destination) {
const logDir=path.join(os.homedir(),"Library","Logs",APP_NAME); fs.mkdirSync(logDir,{recursive:true});
const log=fs.openSync(path.join(logDir,"update.log"),"a",0o600);
const child=spawn(process.execPath,[fileURLToPath(import.meta.url),"--apply",prepared.bundle,destination,String(process.pid),String(process.ppid)],{detached:true,stdio:["ignore",log,log]});
try { await new Promise((resolve,reject)=>{child.once("spawn",resolve);child.once("error",reject);}); child.unref(); }
finally { fs.closeSync(log); }
}
if(process.argv[1]===fileURLToPath(import.meta.url)&&process.argv[2]==="--apply") {
const [source,destination,owner,launcher]=process.argv.slice(3);
let result;
try {
verifyReleaseBundle(source);
process.kill(Number(owner),"SIGTERM");
for(let i=0;i<100;i++) { try { process.kill(Number(owner),0); } catch { break; } await new Promise(resolve=>setTimeout(resolve,100)); }
try { process.kill(Number(owner),0); throw new Error("The running helper did not stop; update cancelled."); } catch(error) { if(error.code!=="ESRCH") throw error; }
// LaunchServices must not send Reopen to the retiring menu-bar process.
for(let i=0;i<100;i++) { try { process.kill(Number(launcher),0); } catch { break; } await new Promise(resolve=>setTimeout(resolve,100)); }
try { process.kill(Number(launcher),0); throw new Error("The menu-bar app did not exit; update cancelled."); } catch(error) { if(error.code!=="ESRCH") throw error; }
const receipt=replaceMacBundle(source,destination,{verify:verifyReleaseBundle});
const installed=spawnSync("/usr/libexec/PlistBuddy",["-c","Print :CFBundleShortVersionString",path.join(destination,"Contents","Info.plist")],{encoding:"utf8"});
if(installed.status!==0) throw new Error("The installed version could not be read.");
console.log(JSON.stringify({version:installed.stdout.trim(),...receipt,installedAt:new Date().toISOString()}));
result={ok:true,message:`Updated to ${installed.stdout.trim()}. Choose Allow new sessions when you are ready.`};
} catch(error) {
console.error(error.message); process.exitCode=1;
result={ok:false,message:`The update could not be completed: ${String(error.message).slice(0,600)} Computer sessions remain stopped.`};
} finally {
// The apply process outlives the menu app. Carry its result into the next
// launch so a failed install is visible without hunting for update.log.
try {
fs.mkdirSync(stateDir(),{recursive:true});
const temporary=`${updateResultPath()}.${process.pid}.tmp`;
fs.writeFileSync(temporary,JSON.stringify({...result,completedAt:new Date().toISOString()})+"\n",{mode:0o600});
fs.renameSync(temporary,updateResultPath());
} catch(error) { console.error(`Could not save the update result: ${error.message}`); }
if(destination&&fs.existsSync(destination)) spawnSync("open",["-g","-a",destination]);View on GitHub (pinned to 73e0f67d83)