ruvnet/ruflo · error · Error

timed out acquiring flywheel transaction lock

Error message

timed out acquiring flywheel transaction lock

What it means

Thrown by withStateLock() when the O_EXCL lock file (.claude-flow/flywheel-v1/transaction-state.lock) cannot be acquired within LOCK_TIMEOUT_MS (10 seconds). The lock serialises all cross-process mutation of the authoritative promotion state. Stale locks older than LOCK_STALE_MS (60s) are automatically taken over, so this timeout means a live process held the lock for the full 10s window.

Source

Thrown at v3/@claude-flow/cli/src/services/flywheel-transaction.ts:258

    try {
      const fd = fs.openSync(lock, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
      fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }), 'utf8');
      fs.closeSync(fd);
      try {
        return await fn();
      } finally {
        try { fs.unlinkSync(lock); } catch { /* lock already gone */ }
      }
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
      try {
        const stat = fs.lstatSync(lock);
        if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
          fs.unlinkSync(lock);
          continue;
        }
      } catch { /* raced with owner */ }
      if (Date.now() >= deadline) throw new Error('timed out acquiring flywheel transaction lock');
      await delay(5);
    }
  }
}

function validateReceiptId(receiptId: string): void {
  if (!/^sha256:[a-f0-9]{64}$/.test(receiptId)) throw new Error('invalid receipt ID');
}

function receiptPath(root: string, receiptId: string): string {
  validateReceiptId(receiptId);
  return path.join(receiptDir(root), `${receiptId.slice('sha256:'.length)}.json`);
}

export function readFlywheelReceipt(root: string, receiptId: string): FlywheelEvaluationReceipt | null {
  try {
    const file = receiptPath(root, receiptId);
    assertSafeFile(file);

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Serialize promotion transactions — only one writer per project root at a time.
  2. Identify and stop the process holding the lock (check the PID inside transaction-state.lock).
  3. If the holder is a confirmed zombie, remove the stale lock file manually after verifying the PID is dead.
  4. Move the state directory off network/low-IOPS storage (it defaults to <projectRoot>/.claude-flow/flywheel-v1/).
  5. Ensure applyFn callbacks (policy materialization) complete quickly.

Example fix

# diagnose
ps -p $(cat .claude-flow/flywheel-v1/transaction-state.lock | jq .pid)
# if PID is dead, remove stale lock
rm .claude-flow/flywheel-v1/transaction-state.lock
Defensive patterns

Strategy: retry

Validate before calling

function isLockHeldByLiveProcess(lockFile: string): boolean {
  try {
    const { pid } = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
    return pid && isProcessAlive(pid);
  } catch { return false; }
}
function isProcessAlive(pid: number): boolean {
  try { process.kill(pid, 0); return true; } catch { return false; }
}

Try / catch

try {
  await promoteFlywheel(root, receipt, opts);
} catch (e) {
  if (e instanceof Error && /timed out acquiring flywheel transaction lock/.test(e.message)) {
    // inspect the holder; if dead, remove stale lock and retry ONCE
    if (!isLockHeldByLiveProcess(lockPath(root))) {
      fs.unlinkSync(lockPath(root));
      await promoteFlywheel(root, receipt, opts);
    } else {
      throw new Error('flywheel lock held by a live process — serialize promotions');
    }
  } else throw e;
}

Prevention

When it happens

Trigger: Two or more processes concurrently promoting/reading-modifying-writing the flywheel transaction state; a long-running commit (e.g. slow fsync or policy materialization) holding the lock beyond 10s; heavy I/O contention on the state directory's filesystem.

Common situations: Parallel CI jobs or multiple daemon instances hitting the same project root's state; a hung applyFn (policy materialization callback) holding the lock; network filesystem latency on the state dir.

Understand the failure class

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/e22466e7e9c976af. Report an issue: GitHub.