ruvnet/ruflo · error · Error

timed out acquiring flywheel transaction lock

Error message

timed out acquiring flywheel transaction lock

What it means

Flywheel transactions serialize cross-process access with an O_EXCL lock file (`transaction-state.lock`): exclusive creation wins the lock, the holder unlinks it in `finally`, and a lock whose mtime is older than 60s (LOCK_STALE_MS) is treated as abandoned and reclaimed. When the lock exists, is still fresh, and the caller's deadline (LOCK_TIMEOUT_MS = 10s) expires while polling every 5ms, this error is thrown.

Solutions

  1. Serialize the runs — run one mutating flywheel command per checkout at a time and retry once the current holder finishes
  2. If no other process is active (`ps aux | grep claude-flow`), wait 60s for the stale-lock takeover, or manually delete `.claude-flow/flywheel-v1/transaction-state.lock` once the holder is confirmed dead
  3. Kill zombie claude-flow/daemon processes still holding the lock
  4. Give each CI shard its own workspace instead of sharing one checkout

Example fix

// before: fire-and-forget, no tolerance for a concurrent holder
await openFlywheelTransaction(root);

// after: bounded retry around the lock deadline
for (let attempt = 0; attempt < 3; attempt++) {
  try { return await openFlywheelTransaction(root); }
  catch (e) {
    if (e?.message !== 'timed out acquiring flywheel transaction lock') throw e;
    await new Promise((r) => setTimeout(r, 15_000)); // > LOCK_TIMEOUT_MS
  }
}
throw new Error('flywheel transaction lock stayed contended');
Defensive patterns

Strategy: retry

Validate before calling

import { lstatSync, existsSync } from 'node:fs';
import { join } from 'node:path';

const LOCK_STALE_MS = 60_000;

// Best-effort preflight: report whether another holder is active.
function lockStatus(root: string): 'free' | 'stale' | 'held' {
  const lock = join(root, '.claude-flow', 'flywheel-v1', 'transaction-state.lock');
  if (!existsSync(lock)) return 'free';
  const age = Date.now() - lstatSync(lock).mtimeMs;
  return age > LOCK_STALE_MS ? 'stale' : 'held';
}

Try / catch

const DEADLINE_MINUTES = 2;
const deadline = Date.now() + DEADLINE_MINUTES * 60_000;
let delayMs = 5_000;
while (true) {
  try {
    txn = await openFlywheelTransaction(root);
    break;
  } catch (e) {
    if (e?.message !== 'timed out acquiring flywheel transaction lock') throw e;
    if (Date.now() > deadline) throw e; // escalate after bounded retries
    await new Promise((r) => setTimeout(r, delayMs));
    delayMs = Math.min(delayMs * 2, 30_000);
  }
}

Prevention

When it happens

Trigger: Two or more claude-flow processes in the same checkout both open a flywheel transaction (parallel CI shards on one workspace, a forgotten interactive session, a hung daemon) and the second polls past its 10s deadline; or a crashed holder left a lock younger than 60s, so stale-takeover cannot fire yet.

Common situations: CI matrices that share one workspace; a long promotion running in one terminal while another flywheel command runs elsewhere; a killed process whose lock has not yet aged past the 60s staleness window.

Understand the failure class

Related errors


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

Appendix: source

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

    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 5234333c34)