{"record":{"id":"50674cfd866a2ea4","repo":"JuliusBrussee/caveman","slug":"mcp-config-change-already-running-for-canonicalp","errorCode":null,"errorMessage":"MCP config change already running for ${canonicalPath}","messagePattern":"MCP config change already running for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7792,"sourceCode":"          || Object.keys(existing).sort().join(\"\\0\") !== keys.join(\"\\0\")\n          || existing.schema_version !== 1\n          || typeof existing.pid !== \"number\" || !Number.isInteger(existing.pid) || existing.pid <= 1\n          || typeof existing.token !== \"string\"\n          || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(existing.token)\n          || existing.config_path !== canonicalPath\n          || typeof existing.started_at !== \"string\"\n          || new Date(existing.started_at).toISOString() !== existing.started_at\n          || process.platform !== \"win32\" && ((lockStat.mode | ownerStat.mode) & 0o077) !== 0) {\n          throw new Error(\"invalid MCP lock owner\");\n        }\n        try { process.kill(existing.pid, 0); }\n        catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === \"ESRCH\"; }\n      } catch {\n        // Populated claim is durable before publication, so malformed or\n        // ownerless lock can never be our crash residue. Never delete it.\n        stale = false;\n      }\n      if (!stale) throw new Error(`MCP config change already running for ${canonicalPath}`);\n      const quarantine = `${lock}.stale-${token}`;\n      try {\n        renameSync(lock, quarantine);\n        renameSync(claim, lock);\n        fsyncParentDirectory(lock);\n        process.stderr.write(`${mark(\"warn\")} reclaimed stale MCP config lock for ${canonicalPath}\\n`);\n      } catch {\n        throw new Error(`MCP config change already running for ${canonicalPath}`);\n      } finally {\n        try { rmSync(quarantine, { recursive: true, force: true }); } catch { /* isolated stale lock only */ }\n      }\n    }\n    return run();\n  } finally {\n    try { rmSync(claim, { recursive: true, force: true }); } catch { /* published or absent */ }\n    try {\n      const current = JSON.parse(readFileSync(join(lock, \"owner.json\"), \"utf8\")) as { token?: unknown };\n      if (current.token === token) {","sourceCodeStart":7774,"sourceCodeEnd":7810,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L7774-L7810","documentation":"withMcpConfigLock serializes MCP config mutations (e.g. `caveman mcp install/uninstall`) with a lock directory published via atomic rename of a claim directory. When the rename hits an existing lock, the code validates the lock's owner.json and probes the owning PID; if the lock is not provably stale (owner process still alive, or owner file malformed/ownerless so it can never be crash residue), it throws this error. The library refuses to delete a lock it cannot prove is dead, so a concurrent or un-cleaned-up mutation blocks a new one.","triggerScenarios":"Calling any MCP config mutation that runs inside withMcpConfigLock(configPath, run) when the canonical lock directory `.caveman-mcp-<sha256-20>.lock` already exists AND either (a) its owner.json validates and process.kill(pid, 0) succeeds (a live concurrent mutation), or (b) the owner data fails validation / is unreadable (malformed or ownerless lock, treated as authoritative and never reclaimed).","commonSituations":"Running two `caveman mcp install`/`uninstall` commands at once (e.g. two terminals, CI racing a local run); a previous mutation crashed after publishing the lock but with a malformed owner.json; permissions or platform quirks left the lock in a state the validator rejects; a wrapped agent spawned its own caveman mutation while the operator ran one.","solutions":["Wait for the other MCP config change to finish (check for a running caveman process) and retry the command.","Inspect the lock directory next to your MCP config file (`.caveman-mcp-*.lock`) and read its owner.json to see which PID owns it.","If the owning PID is dead but the lock is malformed/ownerless, remove the stale lock directory manually, then re-run the command.","Avoid launching concurrent caveman MCP mutations from scripts/CI; serialize them or use a job-level mutex."],"exampleFix":"// before: racing parallel mutations\nawait Promise.all([caveman([\"mcp\", \"install\", \"claude\"]), caveman([\"mcp\", \"install\", \"codex\"])]);\n// after: serialize\nawait caveman([\"mcp\", \"install\", \"claude\"]);\nawait caveman([\"mcp\", \"install\", \"codex\"]);","handlingStrategy":"retry","validationCode":"import { existsSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { execSync } from \"node:child_process\";\nfunction mcpLockIsBusy(configPath: string): boolean {\n  const canonical = configPath; // simplify: canonicalize as the CLI does\n  const key = createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 20);\n  const lock = join(dirname(canonical), `.caveman-mcp-${key}.lock`);\n  if (!existsSync(lock)) return false;\n  try {\n    const owner = JSON.parse(readFileSync(join(lock, \"owner.json\"), \"utf8\"));\n    try { process.kill(owner.pid, 0); return true; } catch (e: any) { return e.code !== \"ESRCH\"; }\n  } catch { return true; } // malformed/ownerless lock is treated as authoritative\n}","typeGuard":null,"tryCatchPattern":"try {\n  runMcpMutation();\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"MCP config change already running\")) {\n    await sleep(backoffMs); return retryMcpMutation(); // bounded retry with backoff\n  }\n  throw e;\n}","preventionTips":["Never run multiple caveman mcp install/uninstall commands concurrently for the same config.","Serialize MCP mutations in CI with a job-level lock.","After crashes, check for `.caveman-mcp-*.lock` next to your MCP config and remove it only if its owner PID is dead and owner.json is malformed.","Keep the config directory on a local filesystem without ACL quirks."],"tags":["lock-contention","concurrency","filesystem","cli"],"backgroundTag":"resource-already-locked","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-09-06T12:00:26.372Z","contentChangedAt":"2026-09-06T12:00:26.372Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}