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
- Ensure HOME/config environment variables are stable for the process lifetime
- Check that the agent's config path is not a symlink chain that resolves differently per attempt
- Re-run the command once; transient path instability may have resolved
- 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
- Do not mutate HOME/XDG env vars while the CLI runs
- Avoid symlinked config roots that resolve inconsistently
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
- : MCP ownership journal failed; native config may already…
- MCP transaction failed and rolled back
- MCP transaction failed and safe recovery was blocked: …
- changed while planning MCP update; refusing overwrite
- mcp must be a JSON object; refusing to overwrite it
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)