JuliusBrussee/caveman · error

MCP transaction state kept changing; refusing mutation

Error message

${agent} ${serverName} MCP transaction state kept changing; refusing mutation

What it means

A safety guard in withOwnedMcpTransactionLock: the lock retry loop kept observing changing transaction state (different lock paths / canonical config paths) after exhausting retries, so it refuses to mutate rather than corrupt the config.

Solutions

  1. Ensure HOME/config environment variables are stable for the process lifetime
  2. Check that the agent's config path is not a symlink chain that resolves differently per attempt
  3. Re-run the command once; transient path instability may have resolved
  4. Report a bug if the path is stable — this indicates a canonicalization defect
Defensive patterns

Strategy: retry

Validate before calling

const before = canonicalMcpConfigPath(resourcePath());
await sleep(50);
if (canonicalMcpConfigPath(resourcePath()) !== before) throw new Error('unstable config path');

Try / catch

try { installMcp(...) } catch (e) {
  if (String(e).includes('kept changing')) { /* stabilize env, retry once */ }
}

Prevention

When it happens

Trigger: Running an MCP install/remove for kilo/qwen where canonicalMcpConfigPath(resourcePath()) returns a different lockPath than the one acquired across repeated attempts, exhausting the retry budget.

Common situations: resourcePath() (home dir / config root) moving between attempts; symlinked or remapped config directories; environment changes (HOME, XDG vars) mid-process.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/fa45e9eddc3bd6aa. Report an issue: GitHub.

Appendix: source

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

      const lockPath = canonicalMcpConfigPath(resourcePath());
      const outcome = withMcpConfigLock<{ retry: true } | { retry: false; value: T }>(lockPath, () => {
        const locatorPending = readOwnedMcpPendingLocator(agent, serverName);
        if (locatorPending) {
          if (locatorPending.journal.config_path !== lockPath) return { retry: true };
          recoverOwnedMcpTransaction(locatorPending);
          return { retry: true };
        }
        const configPending = readOwnedMcpConfigPending(lockPath);
        if (configPending) {
          recoverOwnedMcpTransaction(configPending);
          return { retry: true };
        }
        if (canonicalMcpConfigPath(resourcePath()) !== lockPath) return { retry: true };
        return { retry: false, value: run(lockPath) };
      });
      if (!outcome.retry) return outcome.value;
    }
    throw new Error(`${agent} ${serverName} MCP transaction state kept changing; refusing mutation`);
  });
}

function mcpInstall(target?: string, serverName = "caveman"): number {
  if (serverName !== "caveman" && serverName !== "caveman-browse" && serverName !== "caveman-cloud" && serverName !== "caveman-delegate") {
    console.error(`unknown MCP server '${serverName}'. valid: caveman, caveman-browse, caveman-cloud, caveman-delegate`);
    process.exit(2);
  }
  let mcp: { command: string; args: string[] };
  if (serverName === "caveman") {
    mcp = resolveMcpCommand();
  } else if (serverName === "caveman-browse") {
    const binary = resolveGoBin("caveman-browse", "CAVEMAN_BROWSE_BIN");
    if (!binary) {
      console.error("caveman-browse binary not found — run `caveman setup --install` first");
      process.exit(1);
    }
    mcp = { command: binary, args: [] };

View on GitHub (pinned to 3ee70a1026)