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

  1. Ensure no other happy processes are running (`pgrep -f happy`), then retry the command
  2. 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
  3. Increase MAX_LOCK_ATTEMPTS or LOCK_RETRY_INTERVAL_MS in persistence.ts if contention is expected
  4. 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

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


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