JuliusBrussee/caveman · error · Error
pending transaction is malformed; refusing recovery
Error message
pending ${label} transaction is malformed; refusing recovery What it means
After reading the pending transaction journal, the CLI validates its top-level shape: it must be a non-null, non-array JSON object. Anything else (a JSON string, number, array, or literal null) fails this structural check and the journal is rejected for recovery.
Solutions
- Restore the journal to the expected object schema with all required keys (action, agent, config_path, schema_version, etc.)
- Delete the invalid journal and re-run the MCP operation to generate a fresh one
- Verify with jq that the file parses as an object: jq -e 'type == "object"' journal.json
Example fix
// before
[]
// after
{ "schema_version": 1, "action": "install", "agent": "kilo", "config_path": "...", "transaction_id": "..." } Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from 'fs';
function journalIsObject(path: string): boolean {
try {
const v = JSON.parse(readFileSync(path, 'utf8'));
return !!v && typeof v === 'object' && !Array.isArray(v);
} catch { return false; }
} Type guard
function isJournalObject(v: unknown): v is Record<string, unknown> {
return !!v && typeof v === 'object' && !Array.isArray(v);
} Try / catch
try {
recoverPendingTransaction(label, path);
} catch (e) {
if (e instanceof Error && e.message.includes('transaction is malformed')) {
// journal unusable: remove it and re-run the MCP operation
} else throw e;
} Prevention
- Never overwrite the journal path with arbitrary JSON payloads
- Verify journal shape with jq after any manual edit
- Ensure only the CLI writes to the journal file
- Validate JSON type == object before restoring a journal from backup
When it happens
Trigger: The journal file contains valid JSON but not an object — e.g. the file was overwritten with '[]', '"text"', 'null', or a bare number.
Common situations: A user or script accidentally wrote a different JSON payload to the journal path; a partially-overwritten file that happens to still parse; testing the journal mechanism by writing placeholder content.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- cursor has unknown key(s)
- must be base64 or null
- filter monitor has unknown key(s)
- sort has unknown key(s)
- : MCP ownership journal failed; native config may already…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b654c91becd9950c.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:12733
return agent && serverName ? `${agent} ${serverName} MCP` : "owned MCP";
}
function readOwnedMcpPendingJournalAt(
path: string,
expected: { agent?: "kilo" | "qwen"; serverName?: string; configPath?: string; locatorPath?: string } = {},
): ReadOwnedMcpPendingJournal | null {
const label = ownedMcpPendingLabel(expected.agent, expected.serverName);
let bytes: Buffer;
let value: Record<string, unknown>;
try {
bytes = readFileSync(path);
value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw new Error(`cannot read pending ${label} transaction: ${(error as Error).message}`);
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`pending ${label} transaction is malformed; refusing recovery`);
}
if (process.platform !== "win32" && (statSync(path).mode & 0o077) !== 0) {
throw new Error(`pending ${label} transaction has unsafe permissions; refusing recovery`);
}
const keys = [
"action", "agent", "config_after_sha256", "config_before_base64", "config_before_mode", "config_path",
"config_before_sha256", "marker_after_base64", "marker_after_sha256", "marker_before_base64", "marker_before_mode",
"marker_before_sha256", "marker_path", "schema_version", "server_name", "transaction_id",
].sort();
const journalAgent = value.agent;
const journalServer = value.server_name;
if (Object.keys(value).sort().join("\0") !== keys.join("\0")
|| value.schema_version !== 1
|| typeof value.transaction_id !== "string"
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value.transaction_id)
|| (journalAgent !== "kilo" && journalAgent !== "qwen")
|| typeof journalServer !== "string"
|| !mcpServerToolName(journalServer)View on GitHub (pinned to 3ee70a1026)