{"record":{"id":"fb743f2ce580058f","repo":"JuliusBrussee/caveman","slug":"agent-servername-mcp-rollback-postflight-mis","errorCode":null,"errorMessage":"${agent} ${serverName} MCP rollback postflight mismatch","messagePattern":"(.+?) (.+?) MCP rollback postflight mismatch","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":12861,"sourceCode":"    process.stderr.write(`${mark(\"warn\")} discarded uncommitted ${agent} ${serverName} MCP transaction\\n`);\n    return \"discarded\";\n  }\n  if ((!configIsBefore && !configIsAfter) || (!markerIsBefore && !markerIsAfter)) {\n    throw new Error(`${agent} ${serverName} MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery`);\n  }\n  if (!configPending || !locatorPending) {\n    throw new Error(`${agent} ${serverName} MCP transaction lost one durable journal copy; refusing destructive recovery`);\n  }\n\n  if (!configIsBefore) {\n    durableReplaceFileIfUnchanged(configPath, configCurrent, configBefore, journal.config_before_mode);\n  }\n  if (!markerIsBefore) {\n    durableReplaceFileIfUnchanged(markerPath, markerCurrent, markerBefore, journal.marker_before_mode);\n  }\n  if (optionalBytesHash(fileBytes(configPath)) !== journal.config_before_sha256\n    || optionalBytesHash(fileBytes(markerPath)) !== journal.marker_before_sha256) {\n    throw new Error(`${agent} ${serverName} MCP rollback postflight mismatch`);\n  }\n  removePendingCopies();\n  process.stderr.write(`${mark(\"warn\")} rolled back interrupted ${agent} ${serverName} MCP transaction\\n`);\n  return \"rolled-back\";\n}\n\nfunction transactOwnedMcpConfig(\n  agent: \"kilo\" | \"qwen\",\n  serverName: string,\n  action: \"install\" | \"uninstall\",\n  mcp?: { command: string; args: string[] },\n  lockedConfigPath?: string,\n): boolean {\n  const plan = action === \"install\"\n    ? agent === \"kilo\" ? planMcpKiloJson(mcp!, serverName) : planMcpQwenJson(mcp!, serverName)\n    : agent === \"kilo\" ? planRemoveMcpKiloJson(serverName) : planRemoveMcpQwenJson(serverName);\n  if (!plan) return false;\n  if (lockedConfigPath && plan.path !== canonicalMcpConfigPath(lockedConfigPath)) {","sourceCodeStart":12843,"sourceCodeEnd":12879,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L12843-L12879","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run the caveman command: recovery is re-attempted on next transaction and may succeed once the concurrent writer is gone.","Ensure no agent/editor/sync daemon is holding or auto-saving the MCP config while running recovery; pause watchers and retry.","Check file permissions on the config and marker paths (durableReplaceFileIfUnchanged needs write access); fix with chmod/chown and retry.","As a last resort, manually restore config and marker from the journal's base64 before-snapshots, then delete both pending copies."],"exampleFix":"// before: recovery races an editor auto-saving the config\n// after: close the agent/editor, then re-run\ncaveman mcp install kilo   # retries recovery atomically","handlingStrategy":"retry","validationCode":"import { statSync } from 'node:fs';\n// ensure we can write before triggering recovery\ntry { accessSync(configPath, W_OK); accessSync(markerPath, W_OK); }\ncatch { console.error('fix permissions on config and marker before recovery'); }","typeGuard":"function canRecoverSafely(configPath: string, markerPath: string): boolean {\n  try { accessSync(configPath, W_OK); accessSync(markerPath, W_OK); return true; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  runMcpCommand();\n} catch (e) {\n  if (String(e.message).includes('rollback postflight mismatch')) {\n    stopConcurrentWriters(); // pause agents/watchers/sync\n    runMcpCommand(); // recovery retries on next run\n  }\n}","preventionTips":["Pause agents, editors, and sync daemons that touch MCP configs while running caveman mcp","Ensure the CLI user owns the config and marker files with write permission","Avoid network mounts for agent configs where rename/replace semantics are unreliable","Re-run the command after a mismatch — recovery is designed to be re-attempted"],"tags":["mcp","rollback","race-condition","integrity-check"],"backgroundTag":"checksum-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}