thedotmack/claude-mem · error
mutation body must be an object
Error message
mutation body must be an object
What it means
A sync_outbox row's body parsed as JSON but is not an object — it is an array, string, number, boolean, or null. buildMutationOperation needs an object describing the mutation (op, target, ...), so the row is unusable. Like other per-row drain failures it is quarantined to sync_dead_letter with this reason and removed from the queue; the drain keeps making progress.
Source
Thrown at src/services/sync/CloudSync.ts:798
};
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_outbox
SET canonical_body = ?, operation_sha256 = ?
WHERE id = ? AND canonical_body IS NULL AND operation_sha256 IS NULL
`).run(op.body, op.operation_sha256, row.id);View on GitHub (pinned to e2d1df569a)
Solutions
- Inspect the dead-letter entry: SELECT raw_body FROM sync_dead_letter WHERE reason = 'mutation body must be an object'
- Re-enqueue the correct object shape: { op: '...', target: { ... } } with canonical columns NULL
- Fix the producer that queued a non-object (add a shape assertion at enqueue time)
- Unwrap double-encoded bodies: JSON.parse twice is the symptom — serialize once at the boundary
Example fix
// before: array envelope — parses to an array, not an object
enqueue(JSON.stringify([{ op: 'set_title', target }]));
// after: single mutation object
enqueue(JSON.stringify({ op: 'set_title', target })); Defensive patterns
Strategy: type-guard
Validate before calling
// Reject non-object bodies at enqueue time so they never reach the drain:
function enqueueMutation(db: Database, opUuid: string, rev: string, body: unknown): void {
if (!body || typeof body !== 'object' || Array.isArray(body)) {
throw new TypeError('mutation body must be a plain object');
}
db.prepare('INSERT INTO sync_outbox (op_uuid, rev, body) VALUES (?, ?, ?)')
.run(opUuid, rev, JSON.stringify(body));
} Type guard
function isJsonObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Try / catch
// Quarantined internally; monitor: const rows = db.prepare(`SELECT * FROM sync_dead_letter WHERE reason = 'mutation body must be an object'`).all(); // inspect raw_body shape, fix the producer, re-issue the mutation
Prevention
- Type the enqueue API to accept only the mutation object shape, not unknown JSON
- Guard with isJsonObject before serializing into sync_outbox
- Add unit tests that enqueue each mutation type and drain them end-to-end
When it happens
Trigger: A writer stored JSON.stringify of a scalar/array (e.g. wrapped the mutation in a list); the body was double-encoded (a JSON string containing JSON text parses to a string); schema drift changed the envelope shape between versions.
Common situations: Calling code passed an array of mutations instead of one; an older build wrapped bodies in [ ... ]; test harness enqueuing malformed fixtures directly into sync_outbox.
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.
Related errors
- mutation body is not JSON
- sync hub status: response requires decimal-string epoch/head
- partial canonical mutation snapshot
- sync hub push: response is not JSON
- sync hub push: response requires decimal-string head_seq/pro
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/2215e08d05e0c924.
Report an issue: GitHub.