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
- Identify the concurrently starting process (ps aux | grep chroma-mem / chroma-mcp) and stagger or stop the duplicate launch
- Retry the connect after a short delay once the competing startup has settled - this is inherently a race
- Serialize startup: launch only one Claude-mem instance per data dir, or use a lockfile/bootstrap mutex before connect
- 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
- Serialize Claude-mem startup across instances (IDE and CLI) rather than launching both at once
- Add jittered retry with backoff around connect when lock contention is expected
- Use one shared manager instance per process instead of repeated connect attempts
- Give each concurrent Claude-mem instance its own chroma data dir
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
- Chroma data dir is already owned by PID ; refusing to start…
- Chroma writer lock at
- Unable to remove stale Chroma writer lock at
- Backfill failed
- chroma-mcp call cancelled during shutdown
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)