ruvnet/ruflo · error

Storage is locked by another process (holder: ${lockData.hol

Error message

Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). Lock file: ${this.lockPath}

What it means

EventStore.acquireLock() implements single-writer file locking: if the lock file exists, parses as JSON, and is younger than LOCK_STALE_MS (30 seconds), it throws with the recorded holder and age. Locks older than 30s are treated as stale and deleted, and unparseable/corrupted lock files are also removed — so this error means a genuinely recent lock: another process is writing, or a crashed process died less than 30s ago.

Source

Thrown at v3/@claude-flow/guidance/src/persistence.ts:221

    };
  }

  /**
   * Acquire a file-based lock for concurrent access prevention.
   * Throws if the lock is already held by another process.
   */
  async acquireLock(): Promise<void> {
    await this.ensureDirectory();

    // Check for stale locks
    if (existsSync(this.lockPath)) {
      try {
        const lockContent = await readFile(this.lockPath, 'utf-8');
        const lockData = JSON.parse(lockContent);
        const age = Date.now() - lockData.timestamp;

        if (age < LOCK_STALE_MS) {
          throw new Error(
            `Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). ` +
            `Lock file: ${this.lockPath}`
          );
        }
        // Stale lock, remove it
        await unlink(this.lockPath);
      } catch (err) {
        if (err instanceof Error && err.message.startsWith('Storage is locked')) {
          throw err;
        }
        // Corrupted lock file, remove it
        try { await unlink(this.lockPath); } catch { /* ignore */ }
      }
    }

    const holder = randomUUID();
    const lockData = { holder, timestamp: Date.now(), pid: process.pid };
    await writeFile(this.lockPath, JSON.stringify(lockData), 'utf-8');

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Serialize all writes to a storage path through a single owner process
  2. Retry acquireLock() with backoff — locks older than 30s self-heal via the stale check
  3. If you can verify the holder process is dead (holder PID), manually remove the lock file; otherwise wait out the stale window
  4. Give each concurrent process its own storagePath so no contention exists

Example fix

// before
await store.acquireLock(); // throws if another writer holds it
// after
async function acquireWithRetry(store: EventStore, tries = 5, delayMs = 2_000) {
  for (let i = 0; i < tries; i++) {
    try { return await store.acquireLock(); }
    catch (err) {
      if (i === tries - 1 || !String(err.message).startsWith('Storage is locked')) throw err;
      await new Promise(r => setTimeout(r, delayMs));
    }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

import { existsSync, readFileSync } from 'node:fs';

function lockAgeMs(lockPath: string): number | null {
  if (!existsSync(lockPath)) return null; // free
  try {
    const { timestamp } = JSON.parse(readFileSync(lockPath, 'utf-8'));
    return Date.now() - timestamp;
  } catch {
    return 0; // corrupted: acquireLock will clear it
  }
}
const age = lockAgeMs(store['lockPath'] ?? lockPath);
if (age !== null && age < 30_000) {
  // another writer is active; wait or route to the owning process
}

Try / catch

async function withLock<T>(store: EventStore, fn: () => Promise<T>): Promise<T> {
  const maxTries = 10;
  for (let i = 0; ; i++) {
    try {
      await store.acquireLock();
      break;
    } catch (err) {
      const msg = err instanceof Error ? err.message : '';
      if (!msg.startsWith('Storage is locked') || i === maxTries - 1) throw err;
      await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** i, 30_000)));
    }
  }
  try { return await fn(); } finally { await store.releaseLock(); }
}

Prevention

When it happens

Trigger: Two processes using the same storage directory calling save()/append() concurrently; the previous writer crashed moments ago and left its lock file; a long-running write still holding the lock while a second one starts.

Common situations: Multiple workers/CLI invocations pointed at one storagePath; orchestrators restarting a crashed job immediately; tests running in parallel against shared fixture storage.

Related errors


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