thedotmack/claude-mem · error · ChromaUnavailableError

Chroma writer lock at ${lockPath} is unreadable; refusing to

Error message

Chroma writer lock at ${lockPath} is unreadable; refusing to start a second writer

What it means

When the lock file already exists (EEXIST), acquireChromaWriterLock reads and validates it via readChromaWriterLock(). If that returns null — the file is present but unreadable (corrupt JSON, wrong schema, missing fields) — the manager refuses to start a second writer and throws ChromaUnavailableError rather than guess whether the existing owner is live. This avoids concurrent-writer corruption when the lock state cannot be trusted.

Source

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

          encoding: 'utf-8',
          flag: 'wx',
        });
        this.chromaWriterLock = { path: lockPath, dataDir: normalizedDataDir, ownerId: this.chromaWriterOwnerId };
        logger.debug('CHROMA_MCP', 'Acquired Chroma writer lock', { lockPath, dataDir: normalizedDataDir });
        return;
      } catch (error) {
        const errno = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
        if (errno !== 'EEXIST') {
          const message = `Unable to acquire Chroma writer lock at ${lockPath}: ${error instanceof Error ? error.message : String(error)}`;
          recordChromaVectorSearchUnavailable(message);
          throw new ChromaUnavailableError(message, error instanceof Error ? error : undefined);
        }

        const existing = ChromaMcpManager.readChromaWriterLock(lockPath);
        if (!existing) {
          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 d768ba3643)

Solutions

  1. Inspect the lock file contents at the path in the message; if it is clearly stale/corrupt and no claude-mem/chroma process is running, delete it manually.
  2. Verify no live process owns it first (check the pid field if partially readable, and `ps -p <pid>`), then remove the file.
  3. Restart the worker so it re-acquires a clean lock.
  4. If this recurs, find what is corrupting the lock file (concurrent runs, disk issue).

Example fix

# before — corrupt lock, no live chroma process
ps aux | grep chroma   # none running
ls -la ~/.claude-mem/chroma/.claude-mem-chroma-writer.lock

# after — remove the unreadable lock, restart
rm ~/.claude-mem/chroma/.claude-mem-chroma-writer.lock
# restart the claude-mem worker
Defensive patterns

Strategy: try-catch

Validate before calling

import { readFileSync, existsSync } from 'fs';

function lockReadable(lockPath: string): boolean {
  if (!existsSync(lockPath)) return true; // absent is fine
  try {
    const raw = JSON.parse(readFileSync(lockPath, 'utf-8'));
    return typeof raw.pid === 'number' && typeof raw.ownerId === 'string';
  } catch { return false; }
}

Type guard

import { ChromaUnavailableError } from '../worker/search/errors.js';

function isLockUnreadable(e: unknown): boolean {
  return e instanceof ChromaUnavailableError && /lock .* is unreadable/i.test(e.message);
}

Try / catch

import { rmSync } from 'fs';

try {
  await chromaManager.search(query);
} catch (e) {
  if (e instanceof ChromaUnavailableError && /is unreadable/i.test(e.message)) {
    // Only safe if no live chroma process holds it.
    const lockPath = /at (.+\.claude-mem-chroma-writer\.lock)/.exec(e.message)?.[1];
    if (lockPath && noLiveChromaProcess()) {
      rmSync(lockPath, { force: true });
      return await chromaManager.search(query); // one retry after cleanup
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: The lock file .claude-mem-chroma-writer.lock exists in the data dir but cannot be parsed into a valid ChromaWriterLockPayload: contents are not JSON, are JSON but missing/invalid pid/ownerId/dataDir/acquiredAt fields, or the file is empty/garbage.

Common situations: A previous process crashed mid-write leaving a truncated lock file; someone hand-edited or emptied the lock file; a different/older claude-mem version wrote a schema the reader rejects; disk corruption touched the lock file.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/3a4b89fd981d9aa7. Report an issue: GitHub.