{"record":{"id":"d5ac60fed9805610","repo":"thedotmack/claude-mem","slug":"mutation-body-is-not-json","errorCode":null,"errorMessage":"mutation body is not JSON","messagePattern":"mutation body is not JSON","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":795,"sourceCode":"        await this.sendOps(buf);\n        buf = [];\n        bufBytes = 0;\n      };\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","sourceCodeStart":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/CloudSync.ts#L777-L813","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: incremental string build can truncate/concatenate\nstmt.run(`${prefix}${chunk}`);\n\n// after: build the full value, write once, atomically\nconst body = JSON.stringify({ op, target });\ndb.prepare('INSERT INTO sync_outbox (op_uuid, rev, body) VALUES (?, ?, ?)').run(uuid, rev, body);","handlingStrategy":"validation","validationCode":"// Validate queued legacy bodies before flush:\nconst bad = db.prepare('SELECT id, body FROM sync_outbox WHERE canonical_body IS NULL').all()\n  .filter(r => { try { JSON.parse(r.body); return false; } catch { return true; } });\n// bad rows: re-issue the mutation or repair body, before the drain quarantines them","typeGuard":"function parsesAsJson(value: string): boolean {\n  try { JSON.parse(value); return true; } catch { return false; }\n}","tryCatchPattern":"// Quarantined internally. Handle via dead-letter review:\nfor (const row of db.prepare(`SELECT * FROM sync_dead_letter WHERE reason = 'mutation body is not JSON'`).all()) {\n  // raw_body preserved: diagnose the writer, then re-issue or discard\n}","preventionTips":["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"],"tags":["sync","json","outbox","quarantine","mutation"],"backgroundTag":"invalid-json","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}