JuliusBrussee/caveman · critical · Error
MCP transaction lost one durable journal copy; refusing…
Error message
${agent} ${serverName} MCP transaction lost one durable journal copy; refusing destructive recovery What it means
The transaction protocol requires both durable journal copies (locator '<markerPath>.pending' and the config-adjacent copy) to exist before performing a mid-state rollback. If the current files are in a partial state (neither fully before nor fully after) but one journal copy is missing, the CLI lacks the snapshot data needed to roll back safely and refuses destructive recovery.
Solutions
- Restore the missing .pending copy: if you have the other copy, duplicate it to the missing path (they must be byte-identical) and re-run to let recovery proceed.
- Manually restore the config and marker to a known-good state (from the journal's base64 snapshots or a backup), then delete remaining pending files.
- Check for the files under the exact expected paths (journal.config_path-adjacent and '<marker_path>.pending') — a moved config makes the copy appear 'lost'.
- Re-run 'caveman mcp install/uninstall' after clearing state to rebuild configs deterministically.
Example fix
// before: only one copy exists ls marker.mcp.pending # exists ls config.json.pending # missing // after: duplicate the surviving copy cp marker.mcp.pending config.json.pending
Defensive patterns
Strategy: validation
Validate before calling
import { existsSync } from 'node:fs';
const a = markerPath + '.pending', b = configPath + '.pending';
if (existsSync(a) !== existsSync(b) && existsSync(a) || existsSync(a) && !existsSync(b)) {
console.warn('one durable journal copy is missing — duplicate the surviving copy or restore from backup');
} Type guard
function hasBothJournalCopies(markerPath: string, configPath: string): boolean {
return existsSync(markerPath + '.pending') && existsSync(configPath + '.pending');
} Try / catch
try {
runMcpCommand();
} catch (e) {
if (String(e.message).includes('lost one durable journal copy')) {
// duplicate the surviving copy to the missing path, or restore before-state manually
ensureBothCopies(locatorPath, configPendingPath);
runMcpCommand();
}
} Prevention
- Keep agent config files at their canonical paths — moving them orphans the config-adjacent journal copy
- Exclude caveman state dirs from aggressive cleanup scripts and antivirus scans
- After any crash, run recovery before issuing new mcp commands
- Monitor disk-full conditions: partial journal creation is a common cause
When it happens
Trigger: recoverOwnedMcpTransaction: thrown when configPending or locatorPending is null (readOwnedMcpPendingJournalAt returned null for the config pending path or the locator path) while the config/marker files are in a mixed before/after state requiring rollback.
Common situations: A crash occurred between durableCreateFile(locatorPath) and durableCreateFile(configPendingPath) (the CLI unlinks the locator on that failure, but a crash can leave neither or one); a cleanup script or antivirus removed one .pending file; a partial transaction happened after the config file was moved/renamed so the adjacent pending path no longer resolves.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- MCP config or ownership journal changed during interrupted…
- MCP transaction failed and safe recovery was blocked: …
- MCP transaction journal already exists; refusing overwrite
- MCP transaction journals disagree; refusing recovery
- pending transaction is malformed
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/3f01147cfa0a6dbd.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:12850
if (configPending) durableUnlink(configPendingPath);
if (locatorPending) durableUnlink(locatorPath);
};
if (configIsAfter && markerIsAfter) {
removePendingCopies();
process.stderr.write(`${mark("warn")} finalized interrupted ${agent} ${serverName} MCP transaction\n`);
return "finalized";
}
if (configIsBefore && markerIsBefore) {
removePendingCopies();
process.stderr.write(`${mark("warn")} discarded uncommitted ${agent} ${serverName} MCP transaction\n`);
return "discarded";
}
if ((!configIsBefore && !configIsAfter) || (!markerIsBefore && !markerIsAfter)) {
throw new Error(`${agent} ${serverName} MCP config or ownership journal changed during interrupted transaction; refusing destructive recovery`);
}
if (!configPending || !locatorPending) {
throw new Error(`${agent} ${serverName} MCP transaction lost one durable journal copy; refusing destructive recovery`);
}
if (!configIsBefore) {
durableReplaceFileIfUnchanged(configPath, configCurrent, configBefore, journal.config_before_mode);
}
if (!markerIsBefore) {
durableReplaceFileIfUnchanged(markerPath, markerCurrent, markerBefore, journal.marker_before_mode);
}
if (optionalBytesHash(fileBytes(configPath)) !== journal.config_before_sha256
|| optionalBytesHash(fileBytes(markerPath)) !== journal.marker_before_sha256) {
throw new Error(`${agent} ${serverName} MCP rollback postflight mismatch`);
}
removePendingCopies();
process.stderr.write(`${mark("warn")} rolled back interrupted ${agent} ${serverName} MCP transaction\n`);
return "rolled-back";
}
function transactOwnedMcpConfig(View on GitHub (pinned to 3ee70a1026)