Hmbown/CodeWhale · error

The running helper did not stop; update cancelled.

Error message

The running helper did not stop; update cancelled.

What it means

The privileged install step (run via restartWithUpdate) sends SIGTERM to the running helper process (owner) and polls kill(pid, 0) up to 100 times (10 seconds). If the helper is still alive after that grace period, the install is cancelled with this error so it never replaces a bundle while its process is running. The error is only swallowed when kill reports ESRCH (process gone).

Solutions

  1. Stop any active computer sessions and retry the update so the helper can exit promptly on SIGTERM.
  2. Check the helper's state (ps / logs under ~/Library/Logs/<APP_NAME>) for a hang or crash; kill it manually if stuck, then retry.
  3. Ensure the updater has permission to signal the helper (same user/root as the process that launched it).
  4. Reboot or manually terminate the stale helper if PID reuse is suspected, then rerun the update.
Defensive patterns

Strategy: try-catch

Validate before calling

try { process.kill(ownerPid, 0); console.log('helper still running; stop sessions before updating'); } catch { /* helper already stopped */ }

Try / catch

try { await restartWithUpdate(prepared, destination); } catch (e) { if (/helper did not stop/.test(e.message)) { /* terminate or diagnose the hung helper, then retry */ } else throw e; }

Prevention

When it happens

Trigger: The helper process ignored or was slow to handle SIGTERM beyond 10 seconds (long in-flight operation, blocked event loop, stuck in a syscall); the owner PID was reused by another process that refuses to die; permission problems making SIGTERM ineffective; the helper trapped SIGTERM and deliberately kept running.

Common situations: Updating while the computer-use helper is mid-session or hung; an elevated helper that the updater cannot signal; a system under heavy load delaying exit; PID reuse causing kill(0) to keep succeeding on an unrelated process.

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/20e00b284e81bc8f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/updates.mjs:127

  } catch(error) { fs.rmSync(stage,{recursive:true,force:true}); throw error; }
}

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});

View on GitHub (pinned to 73e0f67d83)