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

  1. Quit the menu-bar app explicitly (its menu Quit action or Activity Monitor) and retry the update.
  2. Check Activity Monitor / logs for a hung menu-bar process and force-quit it before updating.
  3. Disable 'reopen windows' / login-item restore for the app during the update to avoid relaunch interference.
  4. 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

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


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)