{"record":{"id":"27ec614fcd3a21b5","repo":"thedotmack/claude-mem","slug":"unable-to-acquire-chroma-writer-lock-at-lockpath-after","errorCode":null,"errorMessage":"Unable to acquire Chroma writer lock at ${lockPath} after removing stale lock","messagePattern":"Unable to acquire Chroma writer lock at (.+?) after removing stale lock","errorType":"exception","errorClass":"ChromaUnavailableError","httpStatus":null,"severity":"error","filePath":"src/services/sync/ChromaMcpManager.ts","lineNumber":538,"sourceCode":"              priorStartedAt: existing.acquiredAt,\n            });\n            continue;\n          } catch (removeError) {\n            const message = `Unable to remove stale Chroma writer lock at ${lockPath}: ${removeError instanceof Error ? removeError.message : String(removeError)}`;\n            recordChromaVectorSearchUnavailable(message);\n            throw new ChromaUnavailableError(message, removeError instanceof Error ? removeError : undefined);\n          }\n        }\n\n        const message = `Chroma data dir ${normalizedDataDir} is already owned by PID ${existing.pid}; refusing to start a second writer`;\n        recordChromaVectorSearchUnavailable(message);\n        throw new ChromaUnavailableError(message);\n      }\n    }\n\n    const message = `Unable to acquire Chroma writer lock at ${lockPath} after removing stale lock`;\n    recordChromaVectorSearchUnavailable(message);\n    throw new ChromaUnavailableError(message);\n  }\n\n  private releaseChromaWriterLock(): void {\n    const lock = this.chromaWriterLock;\n    if (!lock) {\n      return;\n    }\n    this.chromaWriterLock = null;\n\n    const existing = ChromaMcpManager.readChromaWriterLock(lock.path);\n    if (!existing) {\n      logger.debug('CHROMA_MCP', 'Chroma writer lock already missing or unreadable during release', {\n        lockPath: lock.path,\n      });\n      return;\n    }\n\n    if (existing.pid !== process.pid || existing.ownerId !== lock.ownerId) {","sourceCodeStart":520,"sourceCodeEnd":556,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/ChromaMcpManager.ts#L520-L556","documentation":"Thrown by acquireChromaWriterLock after its two-attempt loop exhausted: a stale/unreadable lock was removed, the loop retried the exclusive write, and it still failed with EEXIST - meaning some other process recreated the lock between removal and retry. The manager cannot claim the lock, marks vector search unavailable, and throws ChromaUnavailableError.","triggerScenarios":"The exclusive fs.writeFileSync(flag:'wx') fails with EEXIST on both attempts even after fs.rmSync succeeded - a racing process (another Claude-mem instance or spawned chroma-mcp) re-acquired the lock within milliseconds, or removal raced with a concurrent creator.","commonSituations":"Two processes starting Claude-mem simultaneously (IDE + CLI, or two IDE windows) contending for the same chroma data dir; a supervisor auto-restarting chroma-mcp that re-grabs the lock; very fast restart loops (watchdog respawn) where locks are continuously re-created.","solutions":["Identify the concurrently starting process (ps aux | grep chroma-mem / chroma-mcp) and stagger or stop the duplicate launch","Retry the connect after a short delay once the competing startup has settled - this is inherently a race","Serialize startup: launch only one Claude-mem instance per data dir, or use a lockfile/bootstrap mutex before connect","Point each concurrent instance at a distinct chroma data dir to eliminate contention"],"exampleFix":"// before: immediate connect races with another startup\nawait manager.connect(); // throws after lock removal race\n// after: bounded retry with backoff\nfor (let i = 0; i < 5; i++) {\n  try { await manager.connect(); break; }\n  catch (e) {\n    if (e instanceof ChromaUnavailableError && /after removing stale lock/.test(e.message)) {\n      await new Promise(r => setTimeout(r, 250 * (i + 1))); continue;\n    }\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"import fs from 'fs';\nfunction writerLockExists(dataDir: string): boolean {\n  return fs.existsSync(`${dataDir}/writer.lock`);\n}\n// before connect, ensure no other instance is mid-startup:\n// while (writerLockJustChanged(dataDir)) await sleep(100);","typeGuard":"function isChromaLockRaceError(e: unknown): e is ChromaUnavailableError {\n  return e instanceof ChromaUnavailableError && e.message.includes('after removing stale lock');\n}","tryCatchPattern":"try {\n  await manager.connect();\n} catch (e) {\n  if (isChromaLockRaceError(e)) {\n    await new Promise(r => setTimeout(r, 500));\n    await manager.connect(); // one bounded retry after the competing startup settles\n  } else throw e;\n}","preventionTips":["Serialize Claude-mem startup across instances (IDE and CLI) rather than launching both at once","Add jittered retry with backoff around connect when lock contention is expected","Use one shared manager instance per process instead of repeated connect attempts","Give each concurrent Claude-mem instance its own chroma data dir"],"tags":["chroma","file-lock","race-condition","lock-contention"],"backgroundTag":"address-already-in-use","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}