JuliusBrussee/caveman · error · Error
pending transaction is malformed
Error message
pending ${label} transaction is malformed: ${(error as Error).message} What it means
readOwnedMcpPendingJournalAt validates a saved MCP transaction journal (schema fields, hashes, and the base64-encoded config/marker snapshots). After the structural checks pass, it decodes the snapshot bytes and verifies their SHA-256 hashes against the journal's declared hashes and marker consistency. If that inner verification throws, the function rethrows as 'pending <label> transaction is malformed: <reason>', meaning the journal file is internally inconsistent and safe recovery is refused.
Solutions
- Delete both pending journal copies (<markerPath>.pending and the config-adjacent pending journal) so recovery falls back to 'none' and the CLI can re-plan the transaction.
- Restore the config file and ownership marker from backup or re-run 'caveman mcp install/uninstall <agent>' to rebuild state from scratch.
- If you must inspect, decode the *_base64 fields and recompute SHA-256 to find which snapshot is corrupt; do not edit the journal in place.
- Reinstall or upgrade the CLI to ensure journal schema version compatibility (schema_version must be 1).
Example fix
// before: hand-edited journal with mismatched hash
{ "config_before_base64": "e30=", "config_before_sha256": "abc..." }
// after: remove stale pending journals and re-run the command
rm ~/.caveman/markers/kilo/<server>.mcp.pending <config-dir>/<config>.pending
caveman mcp install kilo Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync, existsSync } from 'node:fs';
const pendings = [markerPath + '.pending', configPath + '.pending'];
if (pendings.some(p => existsSync(p))) {
const j = JSON.parse(readFileSync(pendings[0], 'utf8'));
if (j.schema_version !== 1) throw new Error('stale/foreign journal — run caveman mcp recovery or delete pending copies');
} Type guard
function isWellFormedJournal(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v)
&& (v as any).schema_version === 1
&& typeof (v as any).transaction_id === 'string';
} Try / catch
try {
await runMcpCommand();
} catch (e) {
if (String(e.message).includes('transaction is malformed')) {
for (const p of [markerPath + '.pending', configPath + '.pending']) rmSafe(p);
await runMcpCommand(); // replans from scratch
}
} Prevention
- Never hand-edit .pending journal files or the ownership marker
- Let the CLI finish transactions; don't kill it mid 'caveman mcp' commands
- Exclude ~/.caveman and agent config dirs from sync/backup tools that rewrite files
- After a crash, run a benign 'caveman mcp' command to trigger recovery before editing configs
When it happens
Trigger: Called during MCP transaction recovery (recoverOwnedMcpTransaction via readOwnedMcpConfigPending or readOwnedMcpPendingLocator). Thrown when decodePendingBytes fails on config_before_base64/marker_before_base64/marker_after_base64, when the decoded bytes' SHA-256 does not match the journal's *_sha256 fields, when marker bytes fail validMcpMarkerBytes, when marker config_path disagrees with journal.config_path, or when action ('install') does not correspond to a non-null marker_after snapshot.
Common situations: A .pending journal file was hand-edited or truncated mid-write; a user copied a journal between machines with different line endings; an interrupted durableCreateFile left a partial file; an old-schema journal from a previous CLI version sits in the marker path.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- MCP config or ownership journal changed during interrupted…
- MCP rollback postflight mismatch
- MCP transaction failed and safe recovery was blocked: …
- MCP transaction journal already exists; refusing overwrite
- MCP transaction journals disagree; refusing recovery
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c1ed88c563807d45.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:12793
|| optionalBytesHash(markerAfter) !== value.marker_after_sha256
|| (markerBefore !== null && !validMcpMarkerBytes(markerBefore, journalAgent, journalServer))
|| (markerAfter !== null && !validMcpMarkerBytes(markerAfter, journalAgent, journalServer))
|| (markerBefore !== null && parseMcpServerMarkerBytes(journalAgent, journalServer, markerBefore)?.config_path !== undefined
&& parseMcpServerMarkerBytes(journalAgent, journalServer, markerBefore)?.config_path !== value.config_path)
|| (markerAfter !== null && parseMcpServerMarkerBytes(journalAgent, journalServer, markerAfter)?.config_path !== value.config_path)
|| (value.action === "install") !== (markerAfter !== null)) {
throw new Error("journal bytes do not match declared transaction state");
}
return {
journal: value as OwnedMcpPendingJournal,
configBefore,
markerBefore,
markerAfter,
path,
bytes,
};
} catch (error) {
throw new Error(`pending ${label} transaction is malformed: ${(error as Error).message}`);
}
}
function readOwnedMcpPendingLocator(agent: "kilo" | "qwen", serverName: string): ReadOwnedMcpPendingJournal | null {
const path = canonicalMcpConfigPath(mcpPendingJournalPath(agent, serverName));
return readOwnedMcpPendingJournalAt(path, { agent, serverName, locatorPath: path });
}
function readOwnedMcpConfigPending(configPath: string): ReadOwnedMcpPendingJournal | null {
const canonicalPath = canonicalMcpConfigPath(configPath);
return readOwnedMcpPendingJournalAt(mcpConfigPendingJournalPath(canonicalPath), { configPath: canonicalPath });
}
type OwnedMcpRecovery = "none" | "discarded" | "finalized" | "rolled-back";
function recoverOwnedMcpTransaction(pending: ReadOwnedMcpPendingJournal | null): OwnedMcpRecovery {
if (!pending) return "none";
const { journal, configBefore, markerBefore } = pending;View on GitHub (pinned to 3ee70a1026)