JuliusBrussee/caveman · critical · Error

MCP transaction lost one durable journal copy; refusing…

Error message

${agent} ${serverName} MCP transaction lost one durable journal copy; refusing destructive recovery

What it means

The transaction protocol requires both durable journal copies (locator '<markerPath>.pending' and the config-adjacent copy) to exist before performing a mid-state rollback. If the current files are in a partial state (neither fully before nor fully after) but one journal copy is missing, the CLI lacks the snapshot data needed to roll back safely and refuses destructive recovery.

Solutions

  1. Restore the missing .pending copy: if you have the other copy, duplicate it to the missing path (they must be byte-identical) and re-run to let recovery proceed.
  2. Manually restore the config and marker to a known-good state (from the journal's base64 snapshots or a backup), then delete remaining pending files.
  3. Check for the files under the exact expected paths (journal.config_path-adjacent and '<marker_path>.pending') — a moved config makes the copy appear 'lost'.
  4. Re-run 'caveman mcp install/uninstall' after clearing state to rebuild configs deterministically.

Example fix

// before: only one copy exists
ls marker.mcp.pending        # exists
ls config.json.pending       # missing
// after: duplicate the surviving copy
cp marker.mcp.pending config.json.pending
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from 'node:fs';
const a = markerPath + '.pending', b = configPath + '.pending';
if (existsSync(a) !== existsSync(b) && existsSync(a) || existsSync(a) && !existsSync(b)) {
  console.warn('one durable journal copy is missing — duplicate the surviving copy or restore from backup');
}

Type guard

function hasBothJournalCopies(markerPath: string, configPath: string): boolean {
  return existsSync(markerPath + '.pending') && existsSync(configPath + '.pending');
}

Try / catch

try {
  runMcpCommand();
} catch (e) {
  if (String(e.message).includes('lost one durable journal copy')) {
    // duplicate the surviving copy to the missing path, or restore before-state manually
    ensureBothCopies(locatorPath, configPendingPath);
    runMcpCommand();
  }
}

Prevention

When it happens

Trigger: recoverOwnedMcpTransaction: thrown when configPending or locatorPending is null (readOwnedMcpPendingJournalAt returned null for the config pending path or the locator path) while the config/marker files are in a mixed before/after state requiring rollback.

Common situations: A crash occurred between durableCreateFile(locatorPath) and durableCreateFile(configPendingPath) (the CLI unlinks the locator on that failure, but a crash can leave neither or one); a cleanup script or antivirus removed one .pending file; a partial transaction happened after the config file was moved/renamed so the adjacent pending path no longer resolves.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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

Appendix: source

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

    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";
  }
  if (configIsBefore && markerIsBefore) {
    removePendingCopies();
    process.stderr.write(`${mark("warn")} discarded uncommitted ${agent} ${serverName} MCP transaction\n`);
    return "discarded";
  }
  if ((!configIsBefore && !configIsAfter) || (!markerIsBefore && !markerIsAfter)) {
    throw new Error(`${agent} ${serverName} MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery`);
  }
  if (!configPending || !locatorPending) {
    throw new Error(`${agent} ${serverName} MCP transaction lost one durable journal copy; refusing destructive recovery`);
  }

  if (!configIsBefore) {
    durableReplaceFileIfUnchanged(configPath, configCurrent, configBefore, journal.config_before_mode);
  }
  if (!markerIsBefore) {
    durableReplaceFileIfUnchanged(markerPath, markerCurrent, markerBefore, journal.marker_before_mode);
  }
  if (optionalBytesHash(fileBytes(configPath)) !== journal.config_before_sha256
    || optionalBytesHash(fileBytes(markerPath)) !== journal.marker_before_sha256) {
    throw new Error(`${agent} ${serverName} MCP rollback postflight mismatch`);
  }
  removePendingCopies();
  process.stderr.write(`${mark("warn")} rolled back interrupted ${agent} ${serverName} MCP transaction\n`);
  return "rolled-back";
}

function transactOwnedMcpConfig(

View on GitHub (pinned to 3ee70a1026)