nexu-io/open-design · error · Error
Open Design was launched headlessly but its daemon did not b
Error message
Open Design was launched headlessly but its daemon did not become ready within ${timeoutMs}ms. What it means
Thrown by ensureMcpDaemonUrl() after it successfully spawned the headless bootstrap process but the daemon failed to become healthy within timeoutMs (default DEFAULT_BOOTSTRAP_TIMEOUT_MS). The loop polls discoverTargetDaemonUrl/resolveDaemonUrl and probeDaemon every DEFAULT_BOOTSTRAP_POLL_MS until the deadline; if none of the probes succeed, the spawned daemon is presumed stuck or crashed.
Source
Thrown at apps/daemon/src/mcp-bootstrap.ts:147
throw new Error(
`The registered Open Design runtime is unavailable and cannot be launched (${plan.reason}).`,
);
}
await spawnBootstrap(plan);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await sleep(DEFAULT_BOOTSTRAP_POLL_MS);
daemonUrl = registeredBootstrapTarget
? await discoverTargetDaemonUrl(env, 300)
: await resolveDaemonUrl({
env,
flagUrl: null,
timeoutMs: 300,
});
if (daemonUrl != null && await probeDaemon(daemonUrl)) return daemonUrl;
}
throw new Error(
`Open Design was launched headlessly but its daemon did not become ready within ${timeoutMs}ms.`,
);
}
async function discoverDaemonUrlFromRegisteredIpc(
env: NodeJS.ProcessEnv,
timeoutMs: number,
): Promise<string | null> {
const socketPath = env[SIDECAR_ENV.IPC_PATH];
if (socketPath == null || socketPath.length === 0) return null;
try {
const status = await requestJsonIpc<DaemonStatusSnapshot>(
socketPath,
{ type: SIDECAR_MESSAGES.STATUS },
{ timeoutMs },
);
return status.url;
} catch {View on GitHub (pinned to 5be4028344)
Solutions
- Check the daemon's log output (it is spawned detached — look in the packaged log dir or run the daemon in the foreground to see the startup error).
- Free the port the daemon tries to bind, or pick a free one and set it consistently.
- Rebuild native modules for the active Node version (`pnpm install` / `pnpm rebuild better-sqlite3`).
- Increase timeoutMs in ensureMcpDaemonUrl options if the machine is legitimately slow.
- Verify the daemon data directory is writable and not corrupted.
Example fix
# before
ensureMcpDaemonUrl(); // default timeout, daemon crashes silently
# after — foreground daemon to see the error, then retry
pnpm tools-dev # observe startup errors here
ensureMcpDaemonUrl({ timeoutMs: 30000 }); Defensive patterns
Strategy: retry
Try / catch
import { ensureMcpDaemonUrl } from './mcp-bootstrap';
async function ensureDaemonWithDiagnostic(): Promise<string> {
try {
return await ensureMcpDaemonUrl({ timeoutMs: 30000 });
} catch (error) {
if (error instanceof Error && error.message.includes('did not become ready')) {
// Spawned but unhealthy: surface a pointer to the daemon log and bail.
throw new Error('Daemon launched but did not become healthy. Inspect the daemon log and port usage, then retry.');
}
throw error;
}
} Prevention
- Run the daemon in the foreground first to confirm it boots cleanly before relying on headless bootstrap.
- Rebuild native modules (better-sqlite3) for the active Node version after every Node upgrade.
- Keep the daemon's port free; pick a deterministic free port and set it consistently.
- Allow a longer timeoutMs on slow/CI machines.
When it happens
Trigger: The bootstrap command ran but the daemon exited early, never bound its port, or its /api/health endpoint never returned 200. Commonly: port already in use, missing/broken native module (better-sqlite3 not built for this Node), the daemon hit a fatal error during startup, the IPC socket path is unreachable, or the machine is too slow to boot within the timeout.
Common situations: Port conflict on the daemon's expected port; better-sqlite3 native binary missing for the running Node version (especially Windows — see repo AGENTS.md Windows notes); daemon data dir permissions issue; CI environment with slow disk; OD_DAEMON_URL in env points somewhere the daemon did not bind.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- The registered Open Design runtime is unavailable and cannot
- project arg omitted and active context lookup failed: ${erro
- daemon ${resp.status} on ${url}: ${body || resp.statusText}
- ARTIFACT_MANIFEST_INVALID
- ARTIFACT_MANIFEST_REQUIRED
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/e273ce52965f7b01.
Report an issue: GitHub.