JuliusBrussee/caveman · warning

integration change already running for ${agent}

Error message

integration change already running for ${agent}

What it means

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.

Source

Thrown at packages/cli/src/index.ts:6802

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 27d5a3981a)

Solutions

  1. Check for a running caveman process for that agent (`ps aux | grep caveman`) and let it finish before retrying
  2. If no process is running, remove the stale lock directory: rm -rf ~/.caveman/integrations/.lock-<agent>
  3. Serialize your automation: run one integration change per agent at a time (the lock exists to prevent torn config writes)

Example fix

# before
$ caveman native install claude &  $ caveman native install claude
Error: integration change already running for claude

# after
$ wait   # or: rm -rf ~/.caveman/integrations/.lock-claude  (only if no live process)
$ caveman native install claude
Defensive patterns

Strategy: retry

Validate before calling

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
function integrationLockHeld(cavemanHome: string, agent: string): boolean {
  return existsSync(join(cavemanHome, "integrations", `.lock-${agent}`));
}

Try / catch

try { withIntegration(agent, run); } catch (e) {
  if (e instanceof Error && /integration change already running/.test(e.message)) {
    await waitForConcurrentRun(agent); // or: rm -rf ~/.caveman/integrations/.lock-<agent> after verifying no live owner
    withIntegration(agent, run);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/cc616426f94f0ed9. Report an issue: GitHub.