thedotmack/claude-mem · error · ChromaUnavailableError
Chroma writer lock at
Error message
Chroma writer lock at ${lockPath} is unreadable; refusing to start a second writer What it means
Thrown by acquireChromaWriterLock when the Chroma writer lock file exists (writeFileSync with flag 'wx' hit EEXIST) but its contents cannot be parsed into a valid ChromaWriterLockPayload. Since liveness/staleness cannot be determined without a readable owner, the manager refuses to start a second Chroma writer to avoid corrupting the shared data dir, marks vector search unavailable, and throws ChromaUnavailableError.
Solutions
- Inspect the lock file in the Chroma data dir (a writer.lock-style file) and confirm it is valid JSON with pid/ownerId/dataDir/acquiredAt fields
- Delete the corrupt lock file manually once no chroma-mcp process is running, then retry the connection
- Wait past the abandoned-lock grace period and retry - the manager reaps unreadable locks once they are old enough
- Check for disk-full (ENOSPC) or permission issues that could have caused a truncated lock write
Example fix
// before: blind retry fails because the corrupt lock is within grace period
await manager.connect();
// after: remove the corrupt lock when no chroma process is alive
import fs from 'fs';
const lockPath = `${dataDir}/writer.lock`;
const raw = fs.readFileSync(lockPath, 'utf-8');
try { JSON.parse(raw); } catch {
if (!isChromaRunning()) fs.rmSync(lockPath, { force: true });
}
await manager.connect(); Defensive patterns
Strategy: try-catch
Validate before calling
import fs from 'fs';
function isChromaWriterLockSane(lockPath: string): boolean {
try {
const p = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
return typeof p.pid === 'number' && typeof p.ownerId === 'string';
} catch { return false; }
}
const lockPath = `${dataDir}/writer.lock`;
if (fs.existsSync(lockPath) && !isChromaWriterLockSane(lockPath)) {
// let the abandoned-lock grace period pass or clean up before connect
} Type guard
function isChromaUnavailableError(e: unknown): e is ChromaUnavailableError {
return e instanceof ChromaUnavailableError;
} Try / catch
try {
await manager.connect();
} catch (e) {
if (e instanceof ChromaUnavailableError && /unreadable; refusing to start a second writer/.test(e.message)) {
// wait out grace period, clean corrupt lock if no chroma process alive, retry once
if (!isChromaProcessAlive()) fs.rmSync(lockPath, { force: true });
await manager.connect();
} else throw e;
} Prevention
- Avoid killing processes with SIGKILL while chroma-mcp startup is in flight; prefer graceful shutdown so locks are released
- Never hand-edit files in the chroma data dir, especially lock files
- Monitor disk space on the volume hosting the chroma data dir
- Catch ChromaUnavailableError and degrade gracefully - vector sync is optional, the plugin still works without it
When it happens
Trigger: Another process created the lock file but it is corrupt, empty, partially written (crash mid-write), or written in an unexpected format, and it is still within the abandoned-lock grace period (isChromaWriterLockAbandoned returned false).
Common situations: A previous Claude-mem process was killed (SIGKILL/power loss) while serializing the lock JSON; manual editing of files under ~/.claude-mem/chroma; disk-full leaving a truncated lock file; an older plugin version wrote a lock payload with a different schema.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- Chroma data dir is already owned by PID ; refusing to start…
- Unable to acquire 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/3a4b89fd981d9aa7.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/ChromaMcpManager.ts:506
const existing = ChromaMcpManager.readChromaWriterLock(lockPath);
if (!existing) {
// Without a readable owner the liveness reaper below can never run,
// so vector sync would stay dead until someone deletes the file (#3916).
// Treat an unreadable lock that is past the write grace period as stale.
if (ChromaMcpManager.isChromaWriterLockAbandoned(lockPath)) {
try {
fs.rmSync(lockPath, { force: true });
logger.info('CHROMA_MCP', 'Removed unreadable Chroma writer lock', { lockPath });
continue;
} catch (removeError) {
const message = `Unable to remove unreadable 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 writer lock at ${lockPath} is unreadable; refusing to start a second writer`;
recordChromaVectorSearchUnavailable(message);
throw new ChromaUnavailableError(message);
}
if (existing.pid === process.pid && existing.ownerId === this.chromaWriterOwnerId) {
this.chromaWriterLock = { path: lockPath, dataDir: normalizedDataDir, ownerId: this.chromaWriterOwnerId };
return;
}
if (!ChromaMcpManager.isChromaWriterLockLive(existing)) {
try {
fs.rmSync(lockPath, { force: true });
logger.info('CHROMA_MCP', 'Removed stale Chroma writer lock', {
lockPath,
priorPid: existing.pid,
priorStartedAt: existing.acquiredAt,
});
continue;
} catch (removeError) {
const message = `Unable to remove stale Chroma writer lock at ${lockPath}: ${removeError instanceof Error ? removeError.message : String(removeError)}`;View on GitHub (pinned to d8bc9755e7)