{"record":{"id":"cc616426f94f0ed9","repo":"JuliusBrussee/caveman","slug":"integration-change-already-running-for-agent","errorCode":null,"errorMessage":"integration change already running for ${agent}","messagePattern":"integration change already running for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/cli/src/index.ts","lineNumber":6802,"sourceCode":"function withIntegrationLock<T>(agent: string, run: () => T): T {\n  const lock = join(cavemanHome(), \"integrations\", `.lock-${agent}`);\n  const ownerPath = join(lock, \"owner.json\");\n  const token = randomUUID();\n  mkdirSync(dirname(lock), { recursive: true, mode: 0o700 });\n  try { mkdirSync(lock, { recursive: false, mode: 0o700 }); }\n  catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n    let stale = false;\n    try {\n      const owner = JSON.parse(readFileSync(ownerPath, \"utf8\")) as { pid?: unknown };\n      if (typeof owner.pid === \"number\" && Number.isInteger(owner.pid) && owner.pid > 1) {\n        try { process.kill(owner.pid, 0); }\n        catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === \"ESRCH\"; }\n      }\n    } catch {\n      try { stale = Date.now() - statSync(lock).mtimeMs > 30_000; } catch { /* raced; treated live */ }\n    }\n    if (!stale) throw new Error(`integration change already running for ${agent}`);\n    const quarantine = `${lock}.stale-${token}`;\n    try {\n      renameSync(lock, quarantine);\n      mkdirSync(lock, { recursive: false, mode: 0o700 });\n      process.stderr.write(`${mark(\"warn\")} reclaimed stale integration lock for ${agent}\\n`);\n    } catch {\n      throw new Error(`integration change already running for ${agent}`);\n    } finally {\n      try { rmSync(quarantine, { recursive: true, force: true }); } catch { /* isolated stale lock only */ }\n    }\n  }\n  try {\n    atomicWriteFile(ownerPath, Buffer.from(JSON.stringify({ pid: process.pid, token, started_at: new Date().toISOString() }) + \"\\n\"));\n  } catch (error) {\n    try { rmSync(lock, { recursive: true, force: true }); } catch { /* original error remains authority */ }\n    throw error;\n  }\n  try { return run(); }","sourceCodeStart":6784,"sourceCodeEnd":6820,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L6784-L6820","documentation":"withIntegrationLock() serializes config mutations per agent using a lock directory under ~/.caveman/integrations/.lock-<agent>. On EEXIST it reads owner.json and probes the recorded PID: the lock is only reclaimed if the owner is dead (ESRCH) or, failing that, the lock is older than 30s. This error means a live concurrent process holds the lock, or the stale-reclaim race was lost.","triggerScenarios":"Two caveman commands mutating the same agent's integration concurrently (e.g. `caveman setup` racing `caveman native install claude`); or a stale lock whose owner.json is unreadable and younger than 30s; or the renameSync reclaim lost a race with another process.","commonSituations":"Scripts firing multiple caveman install commands in parallel; a cron/CI job overlapping an interactive session; a crashed process leaving a lock with a PID now reused by a live process (probe succeeds, lock looks live).","solutions":["Check for a running caveman process for that agent (`ps aux | grep caveman`) and let it finish before retrying","If no process is running, remove the stale lock directory: rm -rf ~/.caveman/integrations/.lock-<agent>","Serialize your automation: run one integration change per agent at a time (the lock exists to prevent torn config writes)"],"exampleFix":"# before\n$ caveman native install claude &  $ caveman native install claude\nError: integration change already running for claude\n\n# after\n$ wait   # or: rm -rf ~/.caveman/integrations/.lock-claude  (only if no live process)\n$ caveman native install claude","handlingStrategy":"retry","validationCode":"import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nfunction integrationLockHeld(cavemanHome: string, agent: string): boolean {\n  return existsSync(join(cavemanHome, \"integrations\", `.lock-${agent}`));\n}","typeGuard":null,"tryCatchPattern":"try { withIntegration(agent, run); } catch (e) {\n  if (e instanceof Error && /integration change already running/.test(e.message)) {\n    await waitForConcurrentRun(agent); // or: rm -rf ~/.caveman/integrations/.lock-<agent> after verifying no live owner\n    withIntegration(agent, run);\n  } else throw e;\n}","preventionTips":["Serialize caveman integration commands per agent in scripts and CI; never run them in parallel","After a crashed run, verify no owner process lives (ps), then delete the .lock-<agent> directory before retrying","Treat this error as advisory: the lock prevents torn writes to agent configs, so prefer waiting over force-clearing"],"tags":["concurrency","locking","integrations","stale-lock"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}