slopus/happy · warning

[DAEMON RUN] Failed to acquire daemon lock; daemon startup d

Error message

[DAEMON RUN] Failed to acquire daemon lock; daemon startup did not complete

What it means

startDaemon() serializes daemon startup with an exclusive lock file (acquireDaemonLock, retried 5 times every 200ms). If the lock cannot be acquired, another daemon process is presumed to be starting/running (or a stale lock was left behind), so this process logs 'Failed to acquire daemon lock; daemon startup did not complete' and exits with code 1 instead of starting a second daemon.

Source

Thrown at packages/happy-cli/src/daemon/run.ts:150

  // Check if running daemon version matches current CLI version
  const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
  if (!runningDaemonVersionMatches) {
    // TODO: This hand-rolled self-restart path is awkward to reason about and awkward to test.
    // We should probably migrate this daemon to native system service management
    // (launchd/systemd, similar to OpenClaw's model), so startup/start-at-login and upgrades
    // are owned by the OS instead of by the daemon trying to replace itself in-process.
    logger.debug('[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version');
    await stopDaemon();
  } else {
    logger.debug('[DAEMON RUN] Daemon version matches, keeping existing daemon');
    console.log('Daemon already running with matching version');
    process.exit(0);
  }

  // Acquire exclusive lock (proves daemon is running)
  const daemonLockHandle = await acquireDaemonLock(5, 200);
  if (!daemonLockHandle) {
    logger.warn('[DAEMON RUN] Failed to acquire daemon lock; daemon startup did not complete');
    process.exit(1);
  }

  // At this point we should be safe to startup the daemon:
  // 1. Not have a stale daemon state
  // 2. Should not have another daemon process running

  try {
    // Happy Agent is a machine-level service shared by the mobile app and
    // Happy Terminal. Start it concurrently and keep this daemon boot path
    // independent from its install/download/network state.
    startHappyTerminalDaemon();

    // Start caffeinate
    const caffeinateStarted = startCaffeinate();
    if (caffeinateStarted) {
      logger.debug('[DAEMON RUN] Sleep prevention enabled');
    }

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Check whether a daemon is actually running: `happy daemon status` (or inspect daemon.state.json / ps). If it is, no action needed — the new start intentionally exits.
  2. If no daemon is running, remove the stale lock file in the happy home directory (~/.happy or $HAPPY_HOME_DIR), then run `happy daemon start` again.
  3. Run `happy doctor` (and `happy doctor clean` if needed) to find and kill runaway happy processes holding the lock.
  4. Avoid launching the daemon concurrently from scripts; serialize daemon start calls and confirm HOME/HAPPY_HOME_DIR permissions are correct.

Example fix

// before: stale lock after a SIGKILLed daemon
happy daemon start   # exits 1: Failed to acquire daemon lock
// after: clean up, then restart
happy doctor clean   # or: rm ~/.happy/<daemon-lock-file>
happy daemon start
Defensive patterns

Strategy: validation

Validate before calling

// Before starting the daemon, check whether one is already running
import { readFileSync, existsSync, unlinkSync } from 'fs';
import { configuration } from './configuration';

const stateFile = configuration.daemonStateFile; // daemon.state.json path
if (existsSync(stateFile)) {
  try {
    const state = JSON.parse(readFileSync(stateFile, 'utf8'));
    const alive = (() => { try { process.kill(state.pid, 0); return true; } catch { return false; } })();
    if (alive) throw new Error(`Daemon already running (pid ${state.pid}) — no need to start`);
    // stale state from a dead daemon: clean up lock/state before starting
    unlinkSync(stateFile);
  } catch { /* corrupt state: safe to remove */ }
}

Try / catch

// Wrap daemon start and interpret exit code 1 lock failure
const { status } = spawnSync('happy', ['daemon', 'start']);
if (status === 1) {
  // lock not acquired: either a daemon is already running or a stale lock exists.
  // run `happy daemon status`; if nothing is running, `happy doctor clean`, remove the lock file, and retry once.
}

Prevention

When it happens

Trigger: acquireDaemonLock(5, 200) returns null: another daemon instance is mid-startup or already holds the lock, a previous daemon crashed and left a stale lock file (O_EXCL creation fails), or filesystem permission problems prevent creating the lock file in the happy home directory.

Common situations: Running `happy daemon start` twice in quick succession (two detached processes race); daemon killed with SIGKILL (crash, OOM, machine power-off) without cleanup so the lock file survives; multiple-user/permission mismatch in ~/.happy; automation scripts launching daemons concurrently.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/32993d5f18224ff4. Report an issue: GitHub.