JuliusBrussee/caveman · error
integration change already running for
Error message
integration change already running for ${agent} What it means
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.
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.
Example fix
// before
// two parallel spawns of: caveman doctor hermes
// after
await runCommand("caveman doctor hermes"); // then
await runCommand("caveman install hermes"); Defensive patterns
Strategy: retry
Validate before calling
import { existsSync, readFileSync } from "node:fs";
if (existsSync(lockPath)) {
const owner = JSON.parse(readFileSync(lockPath, "utf8"));
try { process.kill(owner.pid, 0); console.error("another change is live"); } catch { /* stale, safe to proceed */ }
} Type guard
const lockIsLive = (owner: { pid: number }) => { try { process.kill(owner.pid, 0); return true; } catch { return false; } }; Try / catch
try { runIntegration(agent); } catch (e) { if (String(e.message).includes("already running")) await waitForLockRelease(agent); } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MCP config change already running for
- caveman-cloud MCP changed after setup; refusing destructive…
- caveman-cloud MCP changed during interrupted setup…
- caveman-cloud MCP changed during interrupted removal…
- caveman-cloud MCP postflight mismatch
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/cc616426f94f0ed9.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:7840
function withIntegrationLock<T>(agent: string, run: () => T): T {
const lock = join(cavemanHome(), "integrations", `.lock-${agent}`);
const ownerPath = join(lock, "owner.json");
const token = randomUUID();
mkdirSync(dirname(lock), { recursive: true, mode: 0o700 });
try { mkdirSync(lock, { recursive: false, mode: 0o700 }); }
catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
let stale = false;
try {
const owner = JSON.parse(readFileSync(ownerPath, "utf8")) as { pid?: unknown };
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 1) {
try { process.kill(owner.pid, 0); }
catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === "ESRCH"; }
}
} catch {
try { stale = Date.now() - statSync(lock).mtimeMs > 30_000; } catch { /* raced; treated live */ }
}
if (!stale) throw new Error(`integration change already running for ${agent}`);
const quarantine = `${lock}.stale-${token}`;
try {
renameSync(lock, quarantine);
mkdirSync(lock, { recursive: false, mode: 0o700 });
process.stderr.write(`${mark("warn")} reclaimed stale integration lock for ${agent}\n`);
} catch {
throw new Error(`integration change already running for ${agent}`);
} finally {
try { rmSync(quarantine, { recursive: true, force: true }); } catch { /* isolated stale lock only */ }
}
}
try {
atomicWriteFile(ownerPath, Buffer.from(JSON.stringify({ pid: process.pid, token, started_at: new Date().toISOString() }) + "\n"));
} catch (error) {
try { rmSync(lock, { recursive: true, force: true }); } catch { /* original error remains authority */ }
throw error;
}
try { return run(); }View on GitHub (pinned to 3ee70a1026)