thedotmack/claude-mem · error · ChromaUnavailableError

Chroma data dir ${normalizedDataDir} is already owned by PID

Error message

Chroma data dir ${normalizedDataDir} is already owned by PID ${existing.pid}; refusing to start a second writer

What it means

Thrown when acquiring the Chroma writer lock fails with EEXIST, the existing lock is readable AND its owner PID is still alive (isChromaWriterLockLive true) AND it does not belong to this manager (different pid/ownerId). The manager deliberately refuses to start a second writer against the same data directory because SQLite/Chroma require single-writer access. Surfaced as ChromaUnavailableError (503, CHROMA_UNAVAILABLE).

Source

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

        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;
    }
    this.chromaWriterLock = null;

    const existing = ChromaMcpManager.readChromaWriterLock(lock.path);
    if (!existing) {
      logger.debug('CHROMA_MCP', 'Chroma writer lock already missing or unreadable during release', {

View on GitHub (pinned to d768ba3643)

Solutions

  1. Identify the owning process: read existing.pid from the log and run `ps -p <pid>` (or the value embedded in the message) to see what it is.
  2. Shut down the other legitimate writer (close the second editor/session, stop the duplicate worker) and let it release the lock cleanly.
  3. If that PID is a zombie/orphaned chroma-mcp uvx child from a crashed parent, kill it (kill <pid> or kill the process tree) then retry.
  4. If you intentionally need parallel processes, give each its own data directory (separate CHROMA data dir per instance) instead of sharing one.
  5. As a last resort, confirm the PID is truly gone, then delete the lock file at the lockPath manually.

Example fix

// before: two instances share one data dir
new ChromaMcpManager({ dataDir: '~/.claude-mem/chroma' });
// after: each instance gets an isolated data dir
new ChromaMcpManager({ dataDir: `~/.claude-mem/chroma-${process.env.WORKER_ID}` });
Defensive patterns

Strategy: validation

Validate before calling

// Before connecting, check whether another live process owns the lock
function isLockHeldByLiveOther(lockPath: string): boolean {
  try {
    const raw = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
    if (typeof raw.pid !== 'number') return false;
    return isPidAlive(raw.pid) && raw.pid !== process.pid;
  } catch { return false; }
}

Type guard

function isSecondWriterRefusal(e: unknown): boolean {
  return e instanceof Error && /already owned by PID.*refusing to start a second writer/i.test(e.message);
}

Try / catch

try { await manager.ensureConnected(); }
catch (e) {
  if (isSecondWriterRefusal(e)) {
    // The other PID is legitimate; degrade vector search rather than double-writing
    markVectorSearchUnavailable(e.message);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: acquireChromaWriterLock finds an EEXIST lock whose pid is alive and ownerId differs; this happens when another plugin instance, a parallel worker process, or a previous session that did not release its lock is actively using ${normalizedDataDir}.

Common situations: Two editor windows / two Claude Code instances both pointing at the same ~/.claude-mem/chroma data dir; a long-running worker that crashed the JS process but whose chroma-mcp uvx subprocess is still alive holding the lock; running under a process supervisor that auto-restarted the worker while the old chroma-mcp child still owns the directory.

Related errors


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