{"record":{"id":"2215e08d05e0c924","repo":"thedotmack/claude-mem","slug":"mutation-body-must-be-an-object","errorCode":null,"errorMessage":"mutation body must be an object","messagePattern":"mutation body must be an object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":798,"sourceCode":"      };\n      for (const row of rows) {\n        try {\n          let op: WireOp;\n          if (row.canonical_body !== null || row.operation_sha256 !== null) {\n            if (row.canonical_body === null || row.operation_sha256 === null) {\n              throw new Error('partial canonical mutation snapshot');\n            }\n            op = { body: row.canonical_body, operation_sha256: row.operation_sha256 };\n            parseCanonicalOperation(op);\n          } else {\n            let parsed: unknown;\n            try {\n              parsed = JSON.parse(row.body);\n            } catch {\n              throw new Error('mutation body is not JSON');\n            }\n            if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n              throw new Error('mutation body must be an object');\n            }\n            const body = parsed as OpBody;\n            if (body.op === 'set_prompt_session' && typeof body.target === 'object' && body.target !== null) {\n              const target = body.target as Record<string, unknown>;\n              if (target.origin_device_id == null) target.origin_device_id = this.deviceId;\n            }\n            op = buildMutationOperation({\n              originDeviceId: this.deviceId,\n              mutationId: row.op_uuid,\n              entityRev: row.rev,\n              mutation: body as unknown as Parameters<typeof buildMutationOperation>[0]['mutation'],\n            });\n            // First accepted serialization is frozen before its first send.\n            this.db.prepare(`\n              UPDATE sync_outbox\n              SET canonical_body = ?, operation_sha256 = ?\n              WHERE id = ? AND canonical_body IS NULL AND operation_sha256 IS NULL\n            `).run(op.body, op.operation_sha256, row.id);","sourceCodeStart":780,"sourceCodeEnd":816,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/CloudSync.ts#L780-L816","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: array envelope — parses to an array, not an object\nenqueue(JSON.stringify([{ op: 'set_title', target }]));\n\n// after: single mutation object\nenqueue(JSON.stringify({ op: 'set_title', target }));","handlingStrategy":"type-guard","validationCode":"// Reject non-object bodies at enqueue time so they never reach the drain:\nfunction enqueueMutation(db: Database, opUuid: string, rev: string, body: unknown): void {\n  if (!body || typeof body !== 'object' || Array.isArray(body)) {\n    throw new TypeError('mutation body must be a plain object');\n  }\n  db.prepare('INSERT INTO sync_outbox (op_uuid, rev, body) VALUES (?, ?, ?)')\n    .run(opUuid, rev, JSON.stringify(body));\n}","typeGuard":"function isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"// Quarantined internally; monitor:\nconst rows = db.prepare(`SELECT * FROM sync_dead_letter WHERE reason = 'mutation body must be an object'`).all();\n// inspect raw_body shape, fix the producer, re-issue the mutation","preventionTips":["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"],"tags":["sync","json","type-mismatch","outbox","mutation"],"backgroundTag":"schema-validation-failed","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}