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

  1. 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.
  2. 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'.
  3. Restore the agent config and marker from backup before re-running, since neither copy can be trusted.
  4. 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

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


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)