{"record":{"id":"5533bfd120cc3315","repo":"JuliusBrussee/caveman","slug":"agent-servername-mcp-config-or-ownership-jou","errorCode":null,"errorMessage":"${agent} ${serverName} MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery","messagePattern":"(.+?) (.+?) MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/cli/src/index.ts","lineNumber":12847,"sourceCode":"  const markerIsBefore = markerHash === journal.marker_before_sha256;\n  const markerIsAfter = markerHash === journal.marker_after_sha256;\n  const removePendingCopies = () => {\n    if (configPending) durableUnlink(configPendingPath);\n    if (locatorPending) durableUnlink(locatorPath);\n  };\n\n  if (configIsAfter && markerIsAfter) {\n    removePendingCopies();\n    process.stderr.write(`${mark(\"warn\")} finalized interrupted ${agent} ${serverName} MCP transaction\\n`);\n    return \"finalized\";\n  }\n  if (configIsBefore && markerIsBefore) {\n    removePendingCopies();\n    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\";","sourceCodeStart":12829,"sourceCodeEnd":12865,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L12829-L12865","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the current config and marker files; if the current content is correct, delete both pending journal copies so recovery becomes a no-op.","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.","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.","Never hand-edit while a .pending journal exists; finish or clear transactions before modifying configs."],"exampleFix":"// before: config edited while .pending journal exists\n// config hash matches neither before nor after\n// after: clear journals once the live files are confirmed good\nrm <config>.pending <marker>.pending","handlingStrategy":"validation","validationCode":"import { existsSync } from 'node:fs';\nconst hasPending = existsSync(markerPath + '.pending') || existsSync(configPath + '.pending');\nif (hasPending) {\n  console.warn('pending MCP transaction exists — resolve recovery BEFORE editing the agent config');\n}","typeGuard":"function isSafeToEditConfig(configPath: string, markerPath: string): boolean {\n  return !existsSync(markerPath + '.pending') && !existsSync(configPath + '.pending');\n}","tryCatchPattern":"try {\n  runMcpCommand();\n} catch (e) {\n  if (String(e.message).includes('refusing destructive recovery')) {\n    // confirm which state you want, restore config+marker to it, then delete both pending copies\n    restoreFromBackup(configPath, markerPath);\n    rmBothPending(markerPath, configPath);\n  }\n}","preventionTips":["Never edit an agent's MCP config while a .pending journal exists","Disable agent config auto-save/watchers during caveman mcp operations","Resolve interrupted transactions promptly via any caveman mcp command","Keep backups of agent configs so you can restore a known-good state"],"tags":["mcp","recovery","state-conflict","data-safety"],"backgroundTag":"invalid-state-transition","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"}