JuliusBrussee/caveman · error · Error
MCP transaction journals disagree; refusing recovery
Error message
${agent} ${serverName} MCP transaction journals disagree; refusing recovery What it means
MCP transactions keep two durable copies of the pending journal: a locator copy at '<markerPath>.pending' and a config-adjacent copy next to the agent config file. During recovery, if both copies exist but their bytes differ, the CLI cannot know which reflects the true transaction state and refuses recovery rather than guessing.
Solutions
- Compare the two pending files; keep the newer one (check mtime and transaction_id) and copy it over the other so both match, then re-run the command to trigger recovery.
- Delete BOTH pending copies to abort recovery, then verify the agent config and marker file are the desired state and re-run 'caveman mcp install/uninstall'.
- Restore the agent config and marker from backup before re-running, since neither copy can be trusted.
- Investigate what is racing the journal (another caveman process, sync software) and serialize operations.
Example fix
// before: divergent copies config.json.pending bytes != marker.mcp.pending bytes // after: converge on the newer copy, or clear both rm <config>.pending <marker>.pending caveman mcp install kilo
Defensive patterns
Strategy: retry
Validate before calling
import { readFileSync, existsSync } from 'node:fs';
const a = markerPath + '.pending', b = configPath + '.pending';
if (existsSync(a) && existsSync(b) && !readFileSync(a).equals(readFileSync(b))) {
console.error('journal copies diverged — converge or delete both before running caveman mcp');
} Type guard
function journalsAgree(a: Buffer | null, b: Buffer | null): boolean {
return a === null || b === null || a.equals(b);
} Try / catch
try {
runMcpCommand();
} catch (e) {
if (String(e.message).includes('journals disagree')) {
// keep the newer copy, duplicate over the other, then retry
copyNewerOverOlder(locatorPath, configPendingPath);
runMcpCommand();
}
} Prevention
- Don't delete only one of the two .pending copies — always remove both together
- Avoid kill -9 during caveman mcp commands; use graceful termination
- Exclude marker and config directories from file-sync tools
- Run caveman mcp commands serially, never in parallel shells
When it happens
Trigger: recoverOwnedMcpTransaction reads both copies via readOwnedMcpPendingJournalAt; thrown when configPending.bytes does not equal locatorPending.bytes, i.e. one copy was created, truncated, or modified independently of the other (e.g. a crash between the two durableCreateFile calls, or manual deletion/rewrite of one copy).
Common situations: Power loss or kill -9 during journal creation leaving divergent copies; a user or cleanup script deleted one .pending file and a later transaction recreated only one; disk-full during the second durableCreateFile after partial first write; syncing tools (Dropbox) desyncing the two copies.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- MCP config or ownership journal changed during interrupted…
- MCP transaction journal already exists; refusing overwrite
- : MCP ownership journal failed; native config may already…
- MCP transaction failed and safe recovery was blocked: …
- MCP transaction lost one durable journal copy; refusing…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/54de62892c1babca.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:12821
const canonicalPath = canonicalMcpConfigPath(configPath);
return readOwnedMcpPendingJournalAt(mcpConfigPendingJournalPath(canonicalPath), { configPath: canonicalPath });
}
type OwnedMcpRecovery = "none" | "discarded" | "finalized" | "rolled-back";
function recoverOwnedMcpTransaction(pending: ReadOwnedMcpPendingJournal | null): OwnedMcpRecovery {
if (!pending) return "none";
const { journal, configBefore, markerBefore } = pending;
const agent = journal.agent;
const serverName = journal.server_name;
const configPath = journal.config_path;
const markerPath = journal.marker_path;
const configPendingPath = mcpConfigPendingJournalPath(configPath);
const locatorPath = `${markerPath}.pending`;
const configPending = readOwnedMcpPendingJournalAt(configPendingPath, { agent, serverName, configPath });
const locatorPending = readOwnedMcpPendingJournalAt(locatorPath, { agent, serverName, configPath, locatorPath });
if (configPending && locatorPending && !configPending.bytes.equals(locatorPending.bytes)) {
throw new Error(`${agent} ${serverName} MCP transaction journals disagree; refusing recovery`);
}
const configCurrent = fileBytes(configPath);
const markerCurrent = fileBytes(markerPath);
const configHash = optionalBytesHash(configCurrent);
const markerHash = optionalBytesHash(markerCurrent);
const configIsBefore = configHash === journal.config_before_sha256;
const configIsAfter = configHash === journal.config_after_sha256;
const markerIsBefore = markerHash === journal.marker_before_sha256;
const markerIsAfter = markerHash === journal.marker_after_sha256;
const removePendingCopies = () => {
if (configPending) durableUnlink(configPendingPath);
if (locatorPending) durableUnlink(locatorPath);
};
if (configIsAfter && markerIsAfter) {
removePendingCopies();
process.stderr.write(`${mark("warn")} finalized interrupted ${agent} ${serverName} MCP transaction\n`);
return "finalized";View on GitHub (pinned to 3ee70a1026)