Hmbown/CodeWhale · error · Error

Control changed, but its restart preference could not be…

Error message

Control changed, but its restart preference could not be saved. Check disk space before reopening the app.

What it means

setControlMode persists the requested daemon control mode ('ready'/'paused'/'stopped') by atomically writing a JSON control file via writeFileSync + renameSync. If either write fails, the mode change is still applied in memory but the restart preference is lost, so a descriptive Error is thrown after the work completes to warn the user their preference will not survive restarts.

Solutions

  1. Free disk space on the volume holding the control file, then retry the control change
  2. Check write permissions on the directory containing controlFile and fix ownership/chmod
  3. Ensure the temp file and controlFile are on the same filesystem so renameSync works
  4. Check disk health / read-only mount status (diskutil verifyVolume on macOS) and restart the app

Example fix

// before (no preflight, fails at write time)
fs.writeFileSync(temporary, JSON.stringify({ mode }), { mode: 0o600 });
// after (preflight disk space before attempting the change)
const stat = fs.statfsSync(path.dirname(controlFile));
if (stat.bavail * stat.bsize < 4096) throw new Error("Insufficient disk space to save control preference.");
fs.writeFileSync(temporary, JSON.stringify({ mode }), { mode: 0o600 });
Defensive patterns

Strategy: try-catch

Validate before calling

const dir = path.dirname(controlFile);
if (!fs.existsSync(dir)) throw new Error("control dir missing");
const st = fs.statfsSync(dir);
if (st.bavail * st.bsize < 4096) throw new Error("low disk space");

Try / catch

try {
  await userControl(mode, work);
} catch (e) {
  if (e.message.includes("restart preference could not be saved")) {
    notifyUser("Preference not persisted; check disk space.");
  } else throw e;
}

Prevention

When it happens

Trigger: Calling userControl (or the startup setControlMode('stopped') fallback) when the filesystem cannot write the control file: full disk, read-only volume, permission denied on the control file's directory, or renameSync failing across filesystems.

Common situations: Disk full on the machine running the computer-use daemon; app installed in a location without write permissions; antivirus/quarantine locking the temp file; temp write succeeds but rename fails because temporary and controlFile land on different mounts.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/016d136e2670ba2b. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/daemon.mjs:64

const leases = new Map();
let shuttingDown = false;
let backgroundCheck = null;
let checking = false;
let update = readUpdateResult();
let updating = false;
let controlError = null;
const controlFile = path.join(stateDir(), "control.json");
async function userControl(mode) {
  const work = setControlMode(mode);
  if (mode === "ready") await work;
  const temporary = `${controlFile}.${process.pid}.tmp`;
  let storageError;
  try {
    fs.writeFileSync(temporary, JSON.stringify({ mode }), { mode: 0o600 });
    fs.renameSync(temporary, controlFile);
  } catch (error) { storageError = error; }
  const result = await work;
  if (storageError) throw new Error("Control changed, but its restart preference could not be saved. Check disk space before reopening the app.");
  return result;
}
try {
  const saved = JSON.parse(fs.readFileSync(controlFile, "utf8"));
  if (saved.mode !== "ready") await setControlMode(["paused", "stopped"].includes(saved.mode) ? saved.mode : "stopped");
} catch (error) { if (error.code !== "ENOENT") await setControlMode("stopped"); }

// An inherited socketpair joins the menu-bar owner and its child. This has
// no filesystem endpoint, no reusable credential and no MCP equivalent.
// Losing the human control process fails closed before accepting more work.
if (process.env.CODEWHALE_CU_CONTROL_FD === "3") {
  const control = new net.Socket({ fd: 3, readable: true, writable: true });
  controlOwner = true;
  delete process.env.CODEWHALE_CU_CONTROL_FD;
  let input = "";
  control.setEncoding("utf8");
  const status = () => ({ ...controlStatus(), version: APP_VERSION, checking, backgroundCheck, error: controlError,
    update: update ? { available: update.available, version: update.version, message: update.message, busy: updating } : null });

View on GitHub (pinned to 73e0f67d83)