{"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":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7913,"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":7895,"sourceCodeEnd":7931,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7895-L7931","documentation":"When the MCP config lock owner is verified live (its PID answers a kill(0) probe) or its state cannot be proven stale (e.g. the owner file is unreadable/malformed and treated as live), a new config change is refused. Stale locks are reclaimed only after a verified-ESRCH probe.","triggerScenarios":"Starting an MCP config change while another live process holds the lock for the same canonical config path, or the lock state is ambiguous (owner file unreadable) and treated as live.","commonSituations":"Concurrent caveman MCP commands; a hung but running process holding the lock; permission errors reading the owner claim making staleness undecidable.","solutions":["Wait for the running caveman MCP command to complete and retry.","Identify the PID in the owner file and terminate it if it is stuck, then retry.","Fix read permissions on the lock/owner files so liveness can be evaluated.","If the lock is genuinely orphaned and no process matches, remove lock and owner files manually, then rerun."],"exampleFix":"// before\ncaveman mcp add & caveman mcp remove &  # concurrent\n// after\ncaveman mcp add && caveman mcp remove","handlingStrategy":"retry","validationCode":"const owner = readOwner(lockPath);\nif (owner && isLivePid(owner.pid)) console.error(\"wait for the running caveman mcp command to finish\");","typeGuard":"const isLivePid = (pid: number) => { try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code !== \"EPERM\"; } };","tryCatchPattern":"try { mcpChange(); } catch (e) { if (String(e.message).includes(\"already running\")) { await waitForProcessExit(ownerPid); retry(mcpChange); } }","preventionTips":["Run MCP config mutations sequentially per config path.","Use a CI mutex when multiple jobs touch the same MCP config.","Ensure lock/owner files are readable so staleness can be evaluated."],"tags":["concurrency","locking","mcp"],"backgroundTag":"address-already-in-use","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}