JuliusBrussee/caveman · error · Error

MCP transaction failed and safe recovery was blocked: …

Error message

${agent} ${serverName} MCP transaction failed and safe recovery was blocked: ${(recoveryError as Error).message}; original error: ${(error as Error).message}

What it means

Thrown when a kilo/qwen MCP config transaction (a journaled, locked multi-step write to the agent's native config) fails and the automatic recovery of the ownership journal also throws. The library combines both error messages so the developer sees the original failure and the reason safe recovery was blocked.

Solutions

  1. Inspect the pending journal file referenced by the locator and repair/remove it so recovery can proceed
  2. Fix the underlying recovery failure reported in the message (permissions, disk space, corrupt JSON)
  3. Re-run the MCP install/remove command; the transaction retries cleanly once pending state is consistent
  4. Manually restore the agent's native MCP config from a backup and delete pending state, then re-install

Example fix

// before: stale pending journal blocks recovery
recoverOwnedMcpTransaction(readOwnedMcpConfigPending(plan.path));
// after: clear stale pending state before retry
removeOwnedMcpConfigPending(plan.path); // then re-run install
recoverOwnedMcpTransaction(readOwnedMcpConfigPending(plan.path));
Defensive patterns

Strategy: try-catch

Validate before calling

const pending = readOwnedMcpConfigPending(plan.path) ?? readOwnedMcpPendingLocator(agent, serverName);
if (pending) verifyPendingJournalIntegrity(pending);

Try / catch

try { runMcpTransaction(agent, serverName) } catch (e) {
  if (String(e).includes('safe recovery was blocked')) {
    // inspect/clear pending journal, then retry once
  }
}

Prevention

When it happens

Trigger: Calling an MCP install/remove flow for agent 'kilo' or 'qwen' where the transaction body throws (e.g. config write fails mid-write) AND recoverOwnedMcpTransaction cannot finalize/roll back from the pending journal (corrupt or unwritable pending state).

Common situations: Config file permissions changed mid-transaction; journal file manually edited or truncated; disk full during recovery; concurrent processes raced outside the lock.

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/f97acc0577d59a71. Report an issue: GitHub.

Appendix: source

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

      if (plan.changed) durableReplaceFileIfUnchanged(plan.path, plan.before, plan.after, plan.beforeMode);
      if (!optionalBytesEqual(markerBefore, markerAfter)) durableReplaceFileIfUnchanged(markerPath, markerBefore, markerAfter);
    } else {
      if (!optionalBytesEqual(markerBefore, markerAfter)) durableReplaceFileIfUnchanged(markerPath, markerBefore, markerAfter);
      if (plan.changed) durableReplaceFileIfUnchanged(plan.path, plan.before, plan.after, plan.beforeMode);
    }
    if (optionalBytesHash(fileBytes(plan.path)) !== journal.config_after_sha256
      || optionalBytesHash(fileBytes(markerPath)) !== journal.marker_after_sha256) {
      throw new Error(`${agent} ${serverName} MCP transaction postflight mismatch`);
    }
    durableUnlink(configPendingPath);
    durableUnlink(locatorPath);
    return true;
  } catch (error) {
    try {
      const recovery = recoverOwnedMcpTransaction(readOwnedMcpConfigPending(plan.path) ?? readOwnedMcpPendingLocator(agent, serverName));
      if (recovery === "finalized") return true;
    } catch (recoveryError) {
      throw new Error(`${agent} ${serverName} MCP transaction failed and safe recovery was blocked: ${(recoveryError as Error).message}; original error: ${(error as Error).message}`);
    }
    throw new Error(`${agent} ${serverName} MCP transaction failed and rolled back: ${(error as Error).message}`);
  }
}

function withOwnedMcpTransactionLock<T>(
  agent: "kilo" | "qwen",
  serverName: string,
  run: (lockedConfigPath: string) => T,
): T {
  const markerPath = canonicalOwnedMcpMarkerPath(agent, serverName);
  return withMcpConfigLock(markerPath, () => {
    const activeConfigPath = () => canonicalMcpConfigPath(agent === "kilo" ? kiloConfigPath() : qwenConfigPath());
    const resourcePath = () => readOwnedMcpPendingLocator(agent, serverName)?.journal.config_path
      ?? readMcpServerMarker(agent, serverName)?.config_path
      ?? activeConfigPath();
    for (let attempt = 0; attempt < 8; attempt++) {
      const lockPath = canonicalMcpConfigPath(resourcePath());

View on GitHub (pinned to 3ee70a1026)