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

  1. 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).
  2. Free the port the daemon tries to bind, or pick a free one and set it consistently.
  3. Rebuild native modules for the active Node version (`pnpm install` / `pnpm rebuild better-sqlite3`).
  4. Increase timeoutMs in ensureMcpDaemonUrl options if the machine is legitimately slow.
  5. 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

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

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/e273ce52965f7b01. Report an issue: GitHub.