affaan-m/ECC · error · Error

plan-canvas server did not become healthy on port

Error message

plan-canvas server did not become healthy on port ${port}; check ${path.join(stateDir, 'server.log')}

What it means

ensureServer spawns the plan-canvas server as a detached child, then polls the health endpoint up to 50 times with 100ms sleeps (about 5s total). If the server never reports healthy within that window — crash on boot, port conflict, missing runtime — this error is thrown, pointing at the server.log file in the state directory for the real cause.

Solutions

  1. Open the referenced log file (<stateDir>/server.log) — it contains the server's actual startup error.
  2. Check if a stale server already holds the port (`lsof -i :<port>` / `ss -ltnp`) and kill it or pick another port.
  3. Verify the state directory exists and is writable.
  4. Retry once — a transiently slow machine can exceed the 5s health window; if it recurs, increase the poll count/timeout in ensureServer.

Example fix

// before
PORT=4173 node scripts/plan-canvas.js open plan.html   # fails: stale server on 4173
// after
lsof -ti :4173 | xargs kill -9
PORT=4174 node scripts/plan-canvas.js open plan.html
Defensive patterns

Strategy: try-catch

Validate before calling

async function isHealthy(port) {
  try {
    const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1000) });
    return res.ok;
  } catch { return false; }
}
// check for a stale/conflicting listener before starting
const inUse = await isHealthy(port);

Try / catch

try {
  await cmdOpen(file, args, { stateDir, port });
} catch (err) {
  if (err.message.includes('did not become healthy')) {
    const logPath = path.join(stateDir, 'server.log');
    console.error(`Server failed to start. Tail of ${logPath}:`);
    console.error(fs.readFileSync(logPath, 'utf8').split('\n').slice(-20).join('\n'));
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: Starting cmdOpen (or any command needing the server) when the spawned process exits immediately (bad state dir, port already bound by another process), when the server binary crashes during init, or when the machine is too slow for the ~5s polling budget.

Common situations: Port already in use by a stale server instance; stateDir not writable so server.log/child fails; Node/runtime version mismatch; firewall or sandbox blocking loopback binds; first-run dependency missing.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/2f162ae2e9047730. Report an issue: GitHub.

Appendix: source

Thrown at scripts/plan-canvas.js:194

  if (health && health.version === VERSION) return port;
  if (health) {
    await request(port, 'POST', '/shutdown').catch(() => {});
    for (let i = 0; i < 20 && (await healthCheck(port)); i++) await sleep(100);
  }
  fs.mkdirSync(stateDir, { recursive: true });
  const logFd = fs.openSync(path.join(stateDir, 'server.log'), 'a');
  const child = spawn(process.execPath, [__filename, 'server', '--port', String(port)], {
    detached: true,
    stdio: ['ignore', logFd, logFd],
    env: { ...process.env, ECC_PLAN_CANVAS_STATE_DIR: stateDir }
  });
  child.unref();
  fs.closeSync(logFd);
  for (let i = 0; i < 50; i++) {
    await sleep(100);
    if (await healthCheck(port)) return port;
  }
  throw new Error(`plan-canvas server did not become healthy on port ${port}; check ${path.join(stateDir, 'server.log')}`);
}

function openBrowser(url) {
  const platform = process.platform;
  const [cmd, args] =
    platform === 'darwin' ? ['open', [url]]
      : platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
        : ['xdg-open', [url]];
  try {
    spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
    return true;
  } catch {
    return false;
  }
}

function output(payload) {
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);

View on GitHub (pinned to 8321021c54)