{"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":"error","filePath":"packages/cli/src/index.ts","lineNumber":7840,"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":7822,"sourceCodeEnd":7858,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7822-L7858","documentation":"Integration changes are serialized per-agent with a lock file plus an owner file recording the holding PID and token. If the lock exists and its owner process is still alive (or liveness cannot be proven), the tool refuses to start another change. Only a lock provably owned by a dead process (stale via ESRCH or older than 30s when stat fails) is reclaimed.","triggerScenarios":"Running a native-integration command for an agent while a lock file for that agent already exists and the recorded owner PID responds to `process.kill(pid, 0)`, or the lock cannot be proven stale.","commonSituations":"Two caveman CLI invocations run concurrently (e.g. CI plus a local terminal); a previous run is hung but its process is alive; lock file mtime checks race with an active run.","solutions":["Wait for the other caveman integration command to finish, then retry.","Find and terminate the stale holding process (`ps` / check the owner file next to the lock) before rerunning.","Remove the lock and owner files only after confirming no caveman process is running, then retry.","Run commands sequentially rather than in parallel for the same agent."],"exampleFix":"// before\n// two parallel spawns of: caveman doctor hermes\n// after\nawait runCommand(\"caveman doctor hermes\"); // then\nawait runCommand(\"caveman install hermes\");","handlingStrategy":"retry","validationCode":"import { existsSync, readFileSync } from \"node:fs\";\nif (existsSync(lockPath)) {\n  const owner = JSON.parse(readFileSync(lockPath, \"utf8\"));\n  try { process.kill(owner.pid, 0); console.error(\"another change is live\"); } catch { /* stale, safe to proceed */ }\n}","typeGuard":"const lockIsLive = (owner: { pid: number }) => { try { process.kill(owner.pid, 0); return true; } catch { return false; } };","tryCatchPattern":"try { runIntegration(agent); } catch (e) { if (String(e.message).includes(\"already running\")) await waitForLockRelease(agent); }","preventionTips":["Never run two caveman integration commands for the same agent in parallel.","Use a job queue or file locking in CI to serialize caveman runs.","Kill hung caveman processes and verify the owner file PID is dead before removing locks."],"tags":["concurrency","locking","cli"],"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"}