JuliusBrussee/caveman · critical · Error

MCP config or ownership journal changed during interrupted…

Error message

${agent} ${serverName} MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery

What it means

During recovery of an interrupted MCP transaction, the CLI hashes the current config file and ownership marker and compares against the journal's before/after SHA-256 values. If either file matches NEITHER its before nor after hash, it was modified by something other than this transaction (or advanced past it), so a destructive rollback would corrupt live state and is refused.

Solutions

  1. Inspect the current config and marker files; if the current content is correct, delete both pending journal copies so recovery becomes a no-op.
  2. If you want the pre-transaction state, restore the config and marker to the journal's recorded before state (the before bytes are base64 inside the journal) and re-run to let recovery discard.
  3. If the after-state is what you want, set the files to the journal's after state so recovery can finalize and clean the pending copies.
  4. Never hand-edit while a .pending journal exists; finish or clear transactions before modifying configs.

Example fix

// before: config edited while .pending journal exists
// config hash matches neither before nor after
// after: clear journals once the live files are confirmed good
rm <config>.pending <marker>.pending
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from 'node:fs';
const hasPending = existsSync(markerPath + '.pending') || existsSync(configPath + '.pending');
if (hasPending) {
  console.warn('pending MCP transaction exists — resolve recovery BEFORE editing the agent config');
}

Type guard

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

Try / catch

try {
  runMcpCommand();
} catch (e) {
  if (String(e.message).includes('refusing destructive recovery')) {
    // confirm which state you want, restore config+marker to it, then delete both pending copies
    restoreFromBackup(configPath, markerPath);
    rmBothPending(markerPath, configPath);
  }
}

Prevention

When it happens

Trigger: recoverOwnedMcpTransaction: thrown when (!configIsBefore && !configIsAfter) || (!markerIsBefore && !markerIsAfter) — the agent config file or the ownership marker currently on disk matches neither the journal's recorded before-hash nor after-hash.

Common situations: The user (or the agent) edited the MCP config after the transaction was interrupted; another tool rewrote the config with reformatted JSON; a second caveman transaction ran and changed state; clock-skewed manual restores replaced the files.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

  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";
  }
  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";

View on GitHub (pinned to 3ee70a1026)