ruvnet/ruflo · error · Error
timed out acquiring flywheel attempts lock
Error message
timed out acquiring flywheel attempts lock
What it means
Thrown by withAttemptsLock() in harness-flywheel-generations.ts when the O_EXCL attempts lock cannot be acquired within ATTEMPTS_LOCK_TIMEOUT_MS. This lock serializes the read-testIndex→build-bundle→append critical section (ADR-381 §3): attempts.jsonl's length IS the sequential-evidence test stream position, so concurrent readers would both get the same index and silently double-spend alpha_k.
Source
Thrown at v3/@claude-flow/cli/src/services/harness-flywheel-generations.ts:130
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() }), 'utf-8');
fs.closeSync(fd);
try {
return 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 > ATTEMPTS_LOCK_STALE_MS) {
fs.unlinkSync(lock);
continue;
}
} catch { /* raced with owner */ }
if (Date.now() >= deadline) throw new Error('timed out acquiring flywheel attempts lock');
await delay(5);
}
}
}
/** The current operating champion (last promotion's config), or defaults. */
export function currentChampion(root: string): { config: Record<string, number>; hash: string | null; generation: number } {
const p = loadPromotions(root);
if (!p.length) return { config: { ...DEFAULT_CONFIG }, hash: null, generation: 0 };
const last = p[p.length - 1];
return { config: (last.candidateManifest.policy.value ?? { ...DEFAULT_CONFIG }) as Record<string, number>, hash: last.candidateManifestHash, generation: p.length };
}
export interface ServedState { championHash: string | null; config: Record<string, number> | null; servedAt: number | null; fromGeneration: number | null; }
export function servedChampion(root: string): ServedState {
return readJson<ServedState>(path.join(dir(root), SERVED_FILE)) ?? { championHash: null, config: null, servedAt: null, fromGeneration: null };
}
View on GitHub (pinned to 6b01dc5a68)
Solutions
- Serialize flywheel generation runs — one generation per project root at a time.
- Check the PID inside attempts.lock and confirm the holder is alive.
- Remove a confirmed-stale lock (older than ATTEMPTS_LOCK_STALE_MS) manually.
- Ensure the generations state directory is on fast local storage.
Example fix
# diagnose ps -p "$(cat .claude-flow/flywheel-generations/attempts.lock | jq .pid)" # if the PID is dead, remove the stale lock rm .claude-flow/flywheel-generations/attempts.lock
Defensive patterns
Strategy: retry
Validate before calling
function attemptsLockHeldByLiveProcess(root: string, lockFile: string): boolean {
try {
const { pid } = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
try { process.kill(pid, 0); return true; } catch { return false; }
} catch { return false; }
} Try / catch
try {
await runFlywheelGeneration(root, ...);
} catch (e) {
if (e instanceof Error && /timed out acquiring flywheel attempts lock/.test(e.message)) {
if (!attemptsLockHeldByLiveProcess(root, lockFile)) {
fs.unlinkSync(lockFile);
await runFlywheelGeneration(root, ...); // retry once
} else {
throw new Error('generation in progress — serialize flywheel runs');
}
} else throw e;
} Prevention
- Run one flywheel generation per project root at a time.
- Keep the generations state dir on local storage.
- Ensure the build-bundle phase completes without hanging.
- Never run parallel daemon ticks that both trigger generation.
When it happens
Trigger: Two concurrent runFlywheelGeneration() calls against the same project root; a process holding attempts.lock beyond the deadline; slow I/O during the append-to-attempts.jsonl phase.
Common situations: Parallel CI pipelines or multiple daemon ticks triggering generation simultaneously; a hung build-bundle phase; network filesystem on the flywheel generations 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 flywheel transaction lock
- timed out acquiring ai-budget 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/a8902e3f031ccb8a.
Report an issue: GitHub.