slopus/happy · error
Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS *
Error message
Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1000} seconds What it means
updateSettings() serializes settings writes with an exclusive file lock, retrying up to MAX_LOCK_ATTEMPTS with LOCK_RETRY_INTERVAL_MS between tries. If no lock handle is obtained after all retries, it throws to avoid concurrent processes corrupting the settings file. This is a contention/timeout error, not a data error.
Source
Thrown at packages/happy-cli/src/persistence.ts:182
// Lock file exists, wait and retry
attempts++;
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
// Check for stale lock
try {
const stats = await stat(lockFile);
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
await unlink(lockFile).catch(() => { });
}
} catch { }
} else {
throw err;
}
}
}
if (!fileHandle) {
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1000} seconds`);
}
try {
// Read current settings with defaults
const current = await readSettings() || { ...defaultSettings };
// Apply update
const updated = await updater(current);
// Ensure directory exists
if (!existsSync(configuration.happyHomeDir)) {
await mkdir(configuration.happyHomeDir, { recursive: true });
}
// Write atomically using rename
await writeFile(tmpFile, JSON.stringify(updated, null, 2));
await rename(tmpFile, configuration.settingsFile); // Atomic on POSIX
View on GitHub (pinned to b824cd0a46)
Solutions
- Ensure no other happy processes are running (`pgrep -f happy`), then retry the command
- Remove a stale lock file left by a crashed process (check the settings directory for the lock artifact) if you are certain nothing holds it
- Increase MAX_LOCK_ATTEMPTS or LOCK_RETRY_INTERVAL_MS in persistence.ts if contention is expected
- Serialize settings updates in your tooling so concurrent writers cannot occur
Example fix
// before await Promise.all([updateSettings(a), updateSettings(b)]); // concurrent writers // after await updateSettings(a); await updateSettings(b); // sequential writes avoid lock contention
Defensive patterns
Strategy: retry
Validate before calling
// Check no other happy process holds the lock
const { execSync } = require('child_process');
if (execSync('pgrep -f happy || true').toString().trim()) {
await waitForOtherHappyProcessesToExit();
} Try / catch
const MAX_RETRIES = 5;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
await updateSettings(fn);
break;
} catch (err) {
if (err.message.startsWith('Failed to acquire settings lock') && i < MAX_RETRIES - 1) {
await new Promise(r => setTimeout(r, 2000));
continue;
}
throw err;
}
} Prevention
- Avoid invoking multiple happy commands that write settings concurrently
- Serialize settings updates in scripts rather than running them in parallel
- After a crashed run, verify and clean stale lock files before retrying
When it happens
Trigger: Another process holds the settings lock (e.g. a concurrent `happy` command, the daemon, or a crashed process that left a stale lock) for longer than MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS (total wait in seconds shown in the message).
Common situations: Multiple happy CLI instances or the daemon writing settings simultaneously; a previous run crashed leaving a stale lock; slow filesystem (network mount) making lock acquisition exceed the timeout; sandbox configure/disable handlers racing with the main process.
Related errors
- [DAEMON RUN] Failed to acquire daemon lock; daemon startup d
- No machine ID found in settings
- No machine ID found in settings
- Claude local launcher not found. Please ensure HAPPY_PROJECT
- Claude goal action already in progress
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/7c8c92bab2d22832.
Report an issue: GitHub.