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

  1. Wait for the other caveman integration command to finish, then retry.
  2. Find and terminate the stale holding process (`ps` / check the owner file next to the lock) before rerunning.
  3. Remove the lock and owner files only after confirming no caveman process is running, then retry.
  4. 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

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


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)