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

  1. Serialize flywheel generation runs — one generation per project root at a time.
  2. Check the PID inside attempts.lock and confirm the holder is alive.
  3. Remove a confirmed-stale lock (older than ATTEMPTS_LOCK_STALE_MS) manually.
  4. 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

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

Related errors


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