thedotmack/claude-mem · error · ChromaUnavailableError

Unable to remove stale Chroma writer lock at ${lockPath}: ${

Error message

Unable to remove stale Chroma writer lock at ${lockPath}: ${removeError instanceof Error ? removeError.message : String(removeError)}

What it means

Thrown by ChromaMcpManager.acquireChromaWriterLock after it detected an existing writer lock whose owning PID is dead (isChromaWriterLockLive returned false) but fs.rmSync(lockPath, { force: true }) still failed to delete the stale lock file. It is surfaced as a ChromaUnavailableError (HTTP 503, code CHROMA_UNAVAILABLE) via recordChromaVectorSearchUnavailable, so semantic/vector search is treated as down. The lock is the single-writer guard over the Chroma data directory, so the manager refuses to proceed without removing it.

Source

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

        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)}`;
            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;

View on GitHub (pinned to d768ba3643)

Solutions

  1. Check the lockPath in the message and inspect filesystem permissions: ensure the process user owns or has write/delete rights on the directory containing the lock file.
  2. Look for the prior pid in the log (priorPid field) and confirm no stray chroma/uvx/python process still holds the file open; kill it if present.
  3. Remove the stale lock file manually (rm the path from the message) once you confirm the PID is truly dead, then retry.
  4. If on a read-only / network mount, move the Chroma data dir (CHROMA data dir / configured path) to a local writable filesystem.
  5. Disable any antivirus/SELinux policy that locks files in the Chroma directory, or add an exclusion for the lock path.

Example fix

// before: lock sits on a read-only mount
const lockPath = path.join(readOnlyDataDir, '.chroma-writer-lock.json');
// after: data dir on a local writable volume
const lockPath = path.join(os.homedir(), '.claude-mem', 'chroma', '.chroma-writer-lock.json');
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting the manager, confirm the lock dir is writable+deletable
import fs from 'node:fs';
function canManageLock(lockPath: string): boolean {
  try {
    fs.accessSync(lockPath, fs.constants.W_OK);
    // try a temp create+delete in the same dir
    const probe = lockPath + '.probe-' + process.pid;
    fs.writeFileSync(probe, 'x', { flag: 'wx' });
    fs.rmSync(probe, { force: true });
    return true;
  } catch { return false; }
}

Type guard

import { ChromaUnavailableError } from '...';
function isChromaUnavailable(e: unknown): e is ChromaUnavailableError {
  return e instanceof ChromaUnavailableError || (e instanceof Error && /stale Chroma writer lock|already owned by PID|Unable to acquire Chroma writer lock/i.test(e.message));
}

Try / catch

try {
  await manager.ensureConnected();
} catch (e) {
  if (isChromaUnavailable(e) && /remove stale Chroma writer lock/i.test(e.message)) {
    // surface a 'vector search unavailable' state to the UI; do not crash the worker
    markVectorSearchUnavailable(e.message);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Called during ensureConnected/prewarm when acquiring the writer lock at ${lockPath}; readChromaWriterLock returns a payload whose pid is no longer alive, then fs.rmSync throws (EACCES, EBUSY, read-only filesystem, antivirus/SELinux hold, or the path was removed by a race between isChromaWriterLockLive and rmSync).

Common situations: Running the plugin under a restricted user without delete permission on the Chroma data directory; the data dir living on a read-only or network mount (NFS/SMB) that rejects rmSync; an OS-level file lock (Windows Defender, SELinux, open file handle from a crashed process) preventing removal; two managers racing to reap the same stale lock.

Related errors


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