ruvnet/ruflo · error · Error
timed out acquiring flywheel attempts lock
Error message
timed out acquiring flywheel attempts lock
What it means
The flywheel harness serializes generation-attempt accounting with the same O_EXCL + stale-mtime lock protocol used by the transaction service (`withAttemptsLock` in harness-flywheel-generations.ts). If the attempts lock exists, is younger than ATTEMPTS_LOCK_STALE_MS, and the caller's deadline expires while polling every 5ms, this error surfaces — protecting the attempt counters from concurrent mutation.
Solutions
- Serialize flywheel generation commands per checkout and retry after the current run finishes
- Check for and terminate stray claude-flow processes, then let the stale window elapse or delete the attempts lock file in the flywheel state directory
- Give parallel jobs separate workspaces instead of a shared checkout
Example fix
# before: two jobs race the same checkout in CI - run: npx ruflo flywheel promote & npx ruflo flywheel eval & # after: serialized - run: npx ruflo flywheel promote && npx ruflo flywheel eval
Defensive patterns
Strategy: retry
Validate before calling
// Best-effort: detect contention before invoking harness generation commands.
import { existsSync, lstatSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
function flywheelLocksPresent(root: string): string[] {
const dir = join(root, '.claude-flow');
return existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.lock')) : [];
} Try / catch
let done = false;
for (let attempt = 0; attempt < 3 && !done; attempt++) {
try {
await runFlywheelGeneration(root);
done = true;
} catch (e) {
if (e?.message !== 'timed out acquiring flywheel attempts lock') throw e;
await new Promise((r) => setTimeout(r, 15_000));
}
}
if (!done) throw new Error('flywheel attempts lock remained contended after retries'); Prevention
- Serialize harness/flywheel commands per checkout (promote, eval, status mutations)
- Use separate workspaces for parallel CI jobs
- Kill stray claude-flow processes before scheduling new harness runs
When it happens
Trigger: Two harness/flywheel commands mutating generation attempts concurrently in one checkout (e.g. a promotion run racing a status or eval command), or a stale-but-not-yet-old lock left by a crashed run.
Common situations: Parallel CI jobs or scripts invoking flywheel generation commands on the same workspace; a previously killed run leaving a fresh lock behind; an interactive session plus an automated timer both triggering the harness.
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
- timed out acquiring repo-supervisor lock
- timed out acquiring workspace-lease lock
- Another Ruflo/MetaHarness installer still owns the…
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/a8902e3f031ccb8a.
Report an issue: GitHub.
Appendix: 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 fa13ee4ad6)