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
- Serialize promotion transactions — only one writer per project root at a time.
- Identify and stop the process holding the lock (check the PID inside transaction-state.lock).
- If the holder is a confirmed zombie, remove the stale lock file manually after verifying the PID is dead.
- Move the state directory off network/low-IOPS storage (it defaults to <projectRoot>/.claude-flow/flywheel-v1/).
- 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
- Serialize promotion transactions — one writer per project root.
- Keep the state dir on fast local storage, not NFS.
- Ensure applyFn (policy materialization) callbacks are fast.
- Monitor for orphaned daemon processes that may hold locks.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out acquiring ai-budget lock
- timed out acquiring flywheel attempts lock
- Could not find repository root (no package.json found)
- trajectory envelope not found: ${path}
- Issue ${input.issueId} is already claimed by ${issue.claimed
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/e22466e7e9c976af.
Report an issue: GitHub.