{"record":{"id":"3a4b89fd981d9aa7","repo":"thedotmack/claude-mem","slug":"chroma-writer-lock-at-lockpath-is-unreadable-r","errorCode":null,"errorMessage":"Chroma writer lock at ${lockPath} is unreadable; refusing to start a second writer","messagePattern":"Chroma writer lock at (.+?) is unreadable; refusing to start a second writer","errorType":"exception","errorClass":"ChromaUnavailableError","httpStatus":null,"severity":"error","filePath":"src/services/sync/ChromaMcpManager.ts","lineNumber":425,"sourceCode":"          encoding: 'utf-8',\n          flag: 'wx',\n        });\n        this.chromaWriterLock = { path: lockPath, dataDir: normalizedDataDir, ownerId: this.chromaWriterOwnerId };\n        logger.debug('CHROMA_MCP', 'Acquired Chroma writer lock', { lockPath, dataDir: normalizedDataDir });\n        return;\n      } catch (error) {\n        const errno = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;\n        if (errno !== 'EEXIST') {\n          const message = `Unable to acquire Chroma writer lock at ${lockPath}: ${error instanceof Error ? error.message : String(error)}`;\n          recordChromaVectorSearchUnavailable(message);\n          throw new ChromaUnavailableError(message, error instanceof Error ? error : undefined);\n        }\n\n        const existing = ChromaMcpManager.readChromaWriterLock(lockPath);\n        if (!existing) {\n          const message = `Chroma writer lock at ${lockPath} is unreadable; refusing to start a second writer`;\n          recordChromaVectorSearchUnavailable(message);\n          throw new ChromaUnavailableError(message);\n        }\n\n        if (existing.pid === process.pid && existing.ownerId === this.chromaWriterOwnerId) {\n          this.chromaWriterLock = { path: lockPath, dataDir: normalizedDataDir, ownerId: this.chromaWriterOwnerId };\n          return;\n        }\n\n        if (!ChromaMcpManager.isChromaWriterLockLive(existing)) {\n          try {\n            fs.rmSync(lockPath, { force: true });\n            logger.info('CHROMA_MCP', 'Removed stale Chroma writer lock', {\n              lockPath,\n              priorPid: existing.pid,\n              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)}`;","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/ChromaMcpManager.ts#L407-L443","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Verify no live process owns it first (check the pid field if partially readable, and `ps -p <pid>`), then remove the file.","Restart the worker so it re-acquires a clean lock.","If this recurs, find what is corrupting the lock file (concurrent runs, disk issue)."],"exampleFix":"# before — corrupt lock, no live chroma process\nps aux | grep chroma   # none running\nls -la ~/.claude-mem/chroma/.claude-mem-chroma-writer.lock\n\n# after — remove the unreadable lock, restart\nrm ~/.claude-mem/chroma/.claude-mem-chroma-writer.lock\n# restart the claude-mem worker","handlingStrategy":"try-catch","validationCode":"import { readFileSync, existsSync } from 'fs';\n\nfunction lockReadable(lockPath: string): boolean {\n  if (!existsSync(lockPath)) return true; // absent is fine\n  try {\n    const raw = JSON.parse(readFileSync(lockPath, 'utf-8'));\n    return typeof raw.pid === 'number' && typeof raw.ownerId === 'string';\n  } catch { return false; }\n}","typeGuard":"import { ChromaUnavailableError } from '../worker/search/errors.js';\n\nfunction isLockUnreadable(e: unknown): boolean {\n  return e instanceof ChromaUnavailableError && /lock .* is unreadable/i.test(e.message);\n}","tryCatchPattern":"import { rmSync } from 'fs';\n\ntry {\n  await chromaManager.search(query);\n} catch (e) {\n  if (e instanceof ChromaUnavailableError && /is unreadable/i.test(e.message)) {\n    // Only safe if no live chroma process holds it.\n    const lockPath = /at (.+\\.claude-mem-chroma-writer\\.lock)/.exec(e.message)?.[1];\n    if (lockPath && noLiveChromaProcess()) {\n      rmSync(lockPath, { force: true });\n      return await chromaManager.search(query); // one retry after cleanup\n    }\n  }\n  throw e;\n}","preventionTips":["Before deleting a lock file, confirm no live chroma/claude-mem process owns it (ps -p <pid>).","Investigate recurring lock corruption — it implies concurrent writers or disk faults.","Stop the worker cleanly so it releases its lock on exit."],"tags":["chroma","filesystem","lock","corruption","vector-search"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}