can1357/oh-my-pi · error · Error

Failed to acquire lock for ${filePath} after ${opts.retries}

Error message

Failed to acquire lock for ${filePath} after ${opts.retries} attempts

What it means

acquireLock uses an OS-backed exclusive lock (abstract Unix socket / named mutex / flock) and retries acquisition opts.retries times with opts.retryDelayMs between attempts (defaults: 50 retries x 100ms = ~5s). If the lock stays held the whole time, it throws this error naming the contended file path. It indicates another live process (or a hung one) still owns the lock for that resource.

Source

Thrown at packages/utils/src/file-lock.ts:42

	return `${path.resolve(filePath)}.lock`;
}

function tryAcquireLock(lockPath: string): NativeFileLock | null {
	const lock = NativeFileLock.tryAcquire(lockPath);
	return lock.acquired ? lock : null;
}

async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<NativeFileLock> {
	const opts = { ...DEFAULT_OPTIONS, ...options };
	const lockPath = getLockPath(filePath);

	for (let attempt = 0; attempt < opts.retries; attempt++) {
		const lock = tryAcquireLock(lockPath);
		if (lock) return lock;
		if (attempt + 1 < opts.retries) await Bun.sleep(opts.retryDelayMs);
	}

	throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
}

/** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
export async function withFileLock<T>(
	filePath: string,
	fn: () => Promise<T>,
	options: FileLockOptions = {},
): Promise<T> {
	const lock = await acquireLock(filePath, options);
	try {
		return await fn();
	} finally {
		lock.release();
	}
}

/**
 * Test-only acquisition handle for forcing ownership handoffs. This is not

View on GitHub (pinned to 9690622007)

Solutions

  1. Increase retries/retryDelayMs in FileLockOptions if contention is expected and short-lived
  2. Find and stop the process holding the lock (locks are released automatically on process exit, so a persistent holder is a live process)
  3. Serialize work at a higher level (queue) instead of hammering the same lock file
  4. Ensure the critical section under the lock is short so holders release promptly

Example fix

// before
await withFileLock(path, writeState); // default 50 x 100ms
// after
await withFileLock(path, writeState, { retries: 200, retryDelayMs: 250 }); // ~50s window for heavy contention
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-check can prove the lock is free (TOCTOU); configure a sane window instead.
const opts = { retries: 100, retryDelayMs: 100 }; // ~10s budget

Try / catch

try {
  return await withFileLock(path, fn);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Failed to acquire lock for")) {
    // fallback: skip, queue, or surface 'resource busy'
    return fallbackValue;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling withFileLock(filePath, fn) or acquireLock while another process holds the lock on `${filePath}.lock` for longer than the total retry window — e.g. retries: 50, retryDelayMs: 100 exhausted.

Common situations: Two agent instances writing the same session/state file; a crashed-but-alive background process holding the lock indefinitely; a long-running worker exceeding the 5s default window; NFS environments where flock semantics degrade.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/fce42d55cf47bb7d. Report an issue: GitHub.