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
- 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.
- 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.
- Run `happy doctor` (and `happy doctor clean` if needed) to find and kill runaway happy processes holding the lock.
- 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
- Don't launch `happy daemon start` from multiple scripts/processes at once; serialize daemon startups.
- Prefer graceful `happy daemon stop` over SIGKILL so the lock file is released on shutdown.
- After a crash or force-kill, run `happy doctor` / `doctor clean` to clear orphan processes and stale locks.
- Verify write permissions on the happy home directory (~/.happy or $HAPPY_HOME_DIR).
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
- Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS *
- The chosen rewind point is no longer present in the source C
- Daemon-spawned sessions cannot use local/interactive mode. U
- Claude goal action already in progress
- Process did not die within timeout
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/32993d5f18224ff4.
Report an issue: GitHub.