thedotmack/claude-mem · error · ChromaUnavailableError

Unable to acquire Chroma writer lock at

Error message

Unable to acquire Chroma writer lock at ${lockPath} after removing stale lock

What it means

Thrown by acquireChromaWriterLock after its two-attempt loop exhausted: a stale/unreadable lock was removed, the loop retried the exclusive write, and it still failed with EEXIST - meaning some other process recreated the lock between removal and retry. The manager cannot claim the lock, marks vector search unavailable, and throws ChromaUnavailableError.

Solutions

  1. Identify the concurrently starting process (ps aux | grep chroma-mem / chroma-mcp) and stagger or stop the duplicate launch
  2. Retry the connect after a short delay once the competing startup has settled - this is inherently a race
  3. Serialize startup: launch only one Claude-mem instance per data dir, or use a lockfile/bootstrap mutex before connect
  4. Point each concurrent instance at a distinct chroma data dir to eliminate contention

Example fix

// before: immediate connect races with another startup
await manager.connect(); // throws after lock removal race
// after: bounded retry with backoff
for (let i = 0; i < 5; i++) {
  try { await manager.connect(); break; }
  catch (e) {
    if (e instanceof ChromaUnavailableError && /after removing stale lock/.test(e.message)) {
      await new Promise(r => setTimeout(r, 250 * (i + 1))); continue;
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

import fs from 'fs';
function writerLockExists(dataDir: string): boolean {
  return fs.existsSync(`${dataDir}/writer.lock`);
}
// before connect, ensure no other instance is mid-startup:
// while (writerLockJustChanged(dataDir)) await sleep(100);

Type guard

function isChromaLockRaceError(e: unknown): e is ChromaUnavailableError {
  return e instanceof ChromaUnavailableError && e.message.includes('after removing stale lock');
}

Try / catch

try {
  await manager.connect();
} catch (e) {
  if (isChromaLockRaceError(e)) {
    await new Promise(r => setTimeout(r, 500));
    await manager.connect(); // one bounded retry after the competing startup settles
  } else throw e;
}

Prevention

When it happens

Trigger: The exclusive fs.writeFileSync(flag:'wx') fails with EEXIST on both attempts even after fs.rmSync succeeded - a racing process (another Claude-mem instance or spawned chroma-mcp) re-acquired the lock within milliseconds, or removal raced with a concurrent creator.

Common situations: Two processes starting Claude-mem simultaneously (IDE + CLI, or two IDE windows) contending for the same chroma data dir; a supervisor auto-restarting chroma-mcp that re-grabs the lock; very fast restart loops (watchdog respawn) where locks are continuously re-created.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/27ec614fcd3a21b5. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/ChromaMcpManager.ts:538

              priorStartedAt: existing.acquiredAt,
            });
            continue;
          } catch (removeError) {
            const message = `Unable to remove stale Chroma writer lock at ${lockPath}: ${removeError instanceof Error ? removeError.message : String(removeError)}`;
            recordChromaVectorSearchUnavailable(message);
            throw new ChromaUnavailableError(message, removeError instanceof Error ? removeError : undefined);
          }
        }

        const message = `Chroma data dir ${normalizedDataDir} is already owned by PID ${existing.pid}; refusing to start a second writer`;
        recordChromaVectorSearchUnavailable(message);
        throw new ChromaUnavailableError(message);
      }
    }

    const message = `Unable to acquire Chroma writer lock at ${lockPath} after removing stale lock`;
    recordChromaVectorSearchUnavailable(message);
    throw new ChromaUnavailableError(message);
  }

  private releaseChromaWriterLock(): void {
    const lock = this.chromaWriterLock;
    if (!lock) {
      return;
    }
    this.chromaWriterLock = null;

    const existing = ChromaMcpManager.readChromaWriterLock(lock.path);
    if (!existing) {
      logger.debug('CHROMA_MCP', 'Chroma writer lock already missing or unreadable during release', {
        lockPath: lock.path,
      });
      return;
    }

    if (existing.pid !== process.pid || existing.ownerId !== lock.ownerId) {

View on GitHub (pinned to d8bc9755e7)