thedotmack/claude-mem · error
mutation body is not JSON
Error message
mutation body is not JSON
What it means
While draining sync_outbox, a legacy (not yet frozen) row's body column failed JSON.parse before it could be rebuilt into a canonical mutation operation. The row is treated as poison: quarantined into sync_dead_letter with the raw body preserved, then DELETEd from sync_outbox so it cannot wedge later work. Later rows in the same page continue normally.
Source
Thrown at src/services/sync/CloudSync.ts:795
await this.sendOps(buf);
buf = [];
bufBytes = 0;
};
for (const row of rows) {
try {
let op: WireOp;
if (row.canonical_body !== null || row.operation_sha256 !== null) {
if (row.canonical_body === null || row.operation_sha256 === null) {
throw new Error('partial canonical mutation snapshot');
}
op = { body: row.canonical_body, operation_sha256: row.operation_sha256 };
parseCanonicalOperation(op);
} else {
let parsed: unknown;
try {
parsed = JSON.parse(row.body);
} catch {
throw new Error('mutation body is not JSON');
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('mutation body must be an object');
}
const body = parsed as OpBody;
if (body.op === 'set_prompt_session' && typeof body.target === 'object' && body.target !== null) {
const target = body.target as Record<string, unknown>;
if (target.origin_device_id == null) target.origin_device_id = this.deviceId;
}
op = buildMutationOperation({
originDeviceId: this.deviceId,
mutationId: row.op_uuid,
entityRev: row.rev,
mutation: body as unknown as Parameters<typeof buildMutationOperation>[0]['mutation'],
});
// First accepted serialization is frozen before its first send.
this.db.prepare(`
UPDATE sync_outboxView on GitHub (pinned to e2d1df569a)
Solutions
- Inspect the raw payload: SELECT raw_body FROM sync_dead_letter WHERE reason = 'mutation body is not JSON'
- Re-issue the intended mutation from the app so a clean row is enqueued
- Find the writer that produced invalid JSON (check for string concatenation or partial updates) and fix it
- If the body is recoverable by hand (e.g. truncated tail), fix it and re-insert into sync_outbox with both canonical columns NULL
Example fix
// before: incremental string build can truncate/concatenate
stmt.run(`${prefix}${chunk}`);
// after: build the full value, write once, atomically
const body = JSON.stringify({ op, target });
db.prepare('INSERT INTO sync_outbox (op_uuid, rev, body) VALUES (?, ?, ?)').run(uuid, rev, body); Defensive patterns
Strategy: validation
Validate before calling
// Validate queued legacy bodies before flush:
const bad = db.prepare('SELECT id, body FROM sync_outbox WHERE canonical_body IS NULL').all()
.filter(r => { try { JSON.parse(r.body); return false; } catch { return true; } });
// bad rows: re-issue the mutation or repair body, before the drain quarantines them Type guard
function parsesAsJson(value: string): boolean {
try { JSON.parse(value); return true; } catch { return false; }
} Try / catch
// Quarantined internally. Handle via dead-letter review:
for (const row of db.prepare(`SELECT * FROM sync_dead_letter WHERE reason = 'mutation body is not JSON'`).all()) {
// raw_body preserved: diagnose the writer, then re-issue or discard
} Prevention
- Serialize mutation bodies once, atomically, at enqueue time — never build them incrementally
- Assert JSON.parse round-trips in the enqueue path during development
- Keep external tools away from sync_outbox; route writes through the app
When it happens
Trigger: The body column holds truncated JSON (interrupted write), concatenated fragments from a double-write bug, a UTF-16/BOM-mangled string, or plain non-JSON text; an older build serialized the body differently.
Common situations: App killed mid-write before WAL checkpoint; a writer appending to the body column instead of replacing; schema drift across versions; external tools writing the table.
Related errors
- mutation body must be an object
- partial canonical mutation snapshot
- sync hub status: response requires decimal-string epoch/head
- sync hub push: response is not JSON
- sync hub pull: malformed /changes response
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/d5ac60fed9805610.
Report an issue: GitHub.