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

  1. 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
  2. Delete the corrupt lock file manually once no chroma-mcp process is running, then retry the connection
  3. Wait past the abandoned-lock grace period and retry - the manager reaps unreadable locks once they are old enough
  4. 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

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


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)