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
- Open the referenced log file (<stateDir>/server.log) — it contains the server's actual startup error.
- Check if a stale server already holds the port (`lsof -i :<port>` / `ss -ltnp`) and kill it or pick another port.
- Verify the state directory exists and is writable.
- 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
- Check server.log first — it holds the real startup failure
- Detect and kill stale listeners on the port before launching
- Ensure the state directory exists and is writable before spawning
- Allow headless/CI environments extra startup time or increase the poll budget
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
- createPlanCanvasServer requires a session store
- ETIMEDOUT
- health check result does not match persisted assertion
- Standard input remained unavailable after
- timeout is outside the 10-120 second safety range
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)