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

  1. 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.
  2. Restore the config file and ownership marker from backup or re-run 'caveman mcp install/uninstall <agent>' to rebuild state from scratch.
  3. 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.
  4. 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

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.

Related errors


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)