JuliusBrussee/caveman · error · Error

MCP rollback postflight mismatch

Error message

${agent} ${serverName} MCP rollback postflight mismatch

What it means

After rolling back an interrupted MCP transaction (replacing the config and/or marker with their recorded before-state), the CLI re-hashes both files and verifies they now equal the journal's before hashes. If the postflight check fails, the rollback did not actually take effect (or something concurrently rewrote the files), and the error is thrown rather than silently claiming recovery succeeded.

Solutions

  1. Re-run the caveman command: recovery is re-attempted on next transaction and may succeed once the concurrent writer is gone.
  2. Ensure no agent/editor/sync daemon is holding or auto-saving the MCP config while running recovery; pause watchers and retry.
  3. Check file permissions on the config and marker paths (durableReplaceFileIfUnchanged needs write access); fix with chmod/chown and retry.
  4. As a last resort, manually restore config and marker from the journal's base64 before-snapshots, then delete both pending copies.

Example fix

// before: recovery races an editor auto-saving the config
// after: close the agent/editor, then re-run
caveman mcp install kilo   # retries recovery atomically
Defensive patterns

Strategy: retry

Validate before calling

import { statSync } from 'node:fs';
// ensure we can write before triggering recovery
try { accessSync(configPath, W_OK); accessSync(markerPath, W_OK); }
catch { console.error('fix permissions on config and marker before recovery'); }

Type guard

function canRecoverSafely(configPath: string, markerPath: string): boolean {
  try { accessSync(configPath, W_OK); accessSync(markerPath, W_OK); return true; }
  catch { return false; }
}

Try / catch

try {
  runMcpCommand();
} catch (e) {
  if (String(e.message).includes('rollback postflight mismatch')) {
    stopConcurrentWriters(); // pause agents/watchers/sync
    runMcpCommand(); // recovery retries on next run
  }
}

Prevention

When it happens

Trigger: recoverOwnedMcpTransaction: thrown at the postflight when optionalBytesHash(fileBytes(configPath)) !== journal.config_before_sha256 or the marker hash !== marker_before_sha256 after durableReplaceFileIfUnchanged calls — i.e. the conditional replace skipped because the file changed underneath, or a concurrent writer modified files between restore and verification.

Common situations: Another process (the agent itself, a config watcher, sync software) rewrote the config during recovery; the durableReplaceFileIfUnchanged guard detected a mid-restore change and skipped; filesystem caching/quirks on network mounts; permissions preventing the rewrite.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

    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(
  agent: "kilo" | "qwen",
  serverName: string,
  action: "install" | "uninstall",
  mcp?: { command: string; args: string[] },
  lockedConfigPath?: string,
): boolean {
  const plan = action === "install"
    ? agent === "kilo" ? planMcpKiloJson(mcp!, serverName) : planMcpQwenJson(mcp!, serverName)
    : agent === "kilo" ? planRemoveMcpKiloJson(serverName) : planRemoveMcpQwenJson(serverName);
  if (!plan) return false;
  if (lockedConfigPath && plan.path !== canonicalMcpConfigPath(lockedConfigPath)) {

View on GitHub (pinned to 3ee70a1026)