JuliusBrussee/caveman · error · Error

MCP transaction failed and rolled back

Error message

${agent} ${serverName} MCP transaction failed and rolled back: ${(error as Error).message}

What it means

Thrown when a kilo/qwen MCP config transaction fails but the ownership journal successfully rolled the change back ('failed and rolled back'). The original error is preserved in the message; the native config should be in its pre-transaction state.

Solutions

  1. Read the original error embedded in the message and fix that root cause
  2. Verify the agent's native config was rolled back and re-run the install command
  3. Ensure the agent process is not holding the config file open/locked during install
  4. Check disk space and file permissions on the config directory
Defensive patterns

Strategy: try-catch

Validate before calling

assertWritable(mcpConfigDir(agent));

Try / catch

try { installMcp(agent, serverName) } catch (e) {
  // message ends with 'rolled back'; log original cause and retry after fixing
  console.error(String(e));
}

Prevention

When it happens

Trigger: Any failure inside the kilo/qwen MCP install/remove transaction body (write error, spawn failure, validation failure) where recoverOwnedMcpTransaction succeeded in finalizing/rolling back.

Common situations: Agent binary rejected the config change; native config file read-only; schema change in the agent's config format broke a mid-transaction step.

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

Appendix: source

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

    } 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());
      const outcome = withMcpConfigLock<{ retry: true } | { retry: false; value: T }>(lockPath, () => {
        const locatorPending = readOwnedMcpPendingLocator(agent, serverName);

View on GitHub (pinned to 3ee70a1026)