{"record":{"id":"54de62892c1babca","repo":"JuliusBrussee/caveman","slug":"agent-servername-mcp-transaction-journals-di","errorCode":null,"errorMessage":"${agent} ${serverName} MCP transaction journals disagree; refusing recovery","messagePattern":"(.+?) (.+?) MCP transaction journals disagree; refusing recovery","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":12821,"sourceCode":"  const canonicalPath = canonicalMcpConfigPath(configPath);\n  return readOwnedMcpPendingJournalAt(mcpConfigPendingJournalPath(canonicalPath), { configPath: canonicalPath });\n}\n\ntype OwnedMcpRecovery = \"none\" | \"discarded\" | \"finalized\" | \"rolled-back\";\n\nfunction recoverOwnedMcpTransaction(pending: ReadOwnedMcpPendingJournal | null): OwnedMcpRecovery {\n  if (!pending) return \"none\";\n  const { journal, configBefore, markerBefore } = pending;\n  const agent = journal.agent;\n  const serverName = journal.server_name;\n  const configPath = journal.config_path;\n  const markerPath = journal.marker_path;\n  const configPendingPath = mcpConfigPendingJournalPath(configPath);\n  const locatorPath = `${markerPath}.pending`;\n  const configPending = readOwnedMcpPendingJournalAt(configPendingPath, { agent, serverName, configPath });\n  const locatorPending = readOwnedMcpPendingJournalAt(locatorPath, { agent, serverName, configPath, locatorPath });\n  if (configPending && locatorPending && !configPending.bytes.equals(locatorPending.bytes)) {\n    throw new Error(`${agent} ${serverName} MCP transaction journals disagree; refusing recovery`);\n  }\n  const configCurrent = fileBytes(configPath);\n  const markerCurrent = fileBytes(markerPath);\n  const configHash = optionalBytesHash(configCurrent);\n  const markerHash = optionalBytesHash(markerCurrent);\n  const configIsBefore = configHash === journal.config_before_sha256;\n  const configIsAfter = configHash === journal.config_after_sha256;\n  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\";","sourceCodeStart":12803,"sourceCodeEnd":12839,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L12803-L12839","documentation":"MCP transactions keep two durable copies of the pending journal: a locator copy at '<markerPath>.pending' and a config-adjacent copy next to the agent config file. During recovery, if both copies exist but their bytes differ, the CLI cannot know which reflects the true transaction state and refuses recovery rather than guessing.","triggerScenarios":"recoverOwnedMcpTransaction reads both copies via readOwnedMcpPendingJournalAt; thrown when configPending.bytes does not equal locatorPending.bytes, i.e. one copy was created, truncated, or modified independently of the other (e.g. a crash between the two durableCreateFile calls, or manual deletion/rewrite of one copy).","commonSituations":"Power loss or kill -9 during journal creation leaving divergent copies; a user or cleanup script deleted one .pending file and a later transaction recreated only one; disk-full during the second durableCreateFile after partial first write; syncing tools (Dropbox) desyncing the two copies.","solutions":["Compare the two pending files; keep the newer one (check mtime and transaction_id) and copy it over the other so both match, then re-run the command to trigger recovery.","Delete BOTH pending copies to abort recovery, then verify the agent config and marker file are the desired state and re-run 'caveman mcp install/uninstall'.","Restore the agent config and marker from backup before re-running, since neither copy can be trusted.","Investigate what is racing the journal (another caveman process, sync software) and serialize operations."],"exampleFix":"// before: divergent copies\nconfig.json.pending bytes != marker.mcp.pending bytes\n// after: converge on the newer copy, or clear both\nrm <config>.pending <marker>.pending\ncaveman mcp install kilo","handlingStrategy":"retry","validationCode":"import { readFileSync, existsSync } from 'node:fs';\nconst a = markerPath + '.pending', b = configPath + '.pending';\nif (existsSync(a) && existsSync(b) && !readFileSync(a).equals(readFileSync(b))) {\n  console.error('journal copies diverged — converge or delete both before running caveman mcp');\n}","typeGuard":"function journalsAgree(a: Buffer | null, b: Buffer | null): boolean {\n  return a === null || b === null || a.equals(b);\n}","tryCatchPattern":"try {\n  runMcpCommand();\n} catch (e) {\n  if (String(e.message).includes('journals disagree')) {\n    // keep the newer copy, duplicate over the other, then retry\n    copyNewerOverOlder(locatorPath, configPendingPath);\n    runMcpCommand();\n  }\n}","preventionTips":["Don't delete only one of the two .pending copies — always remove both together","Avoid kill -9 during caveman mcp commands; use graceful termination","Exclude marker and config directories from file-sync tools","Run caveman mcp commands serially, never in parallel shells"],"tags":["mcp","journal","state-conflict","recovery"],"backgroundTag":"conflicting-config-options","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"}