thedotmack/claude-mem · error · Error
SyncApply: invalid op seq=
Error message
SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: body is not parseable JSON What it means
parseBody() JSON-parses each sync op's body column. It throws when op.body is not valid JSON at all — e.g. raw text, truncated data, or binary bytes stored in the body field. The op is rejected rather than partially applied.
Solutions
- Check op.body for seq=${op.seq} in the sync log and fix or delete the corrupt row, then re-pull the op from the origin device.
- Fix the producer to always persist JSON.stringify(body) — never raw objects or formatted text.
- If rows were truncated by a DB migration, restore from backup and re-run sync.
- Add a producer-side assertion that the serialized body round-trips through JSON.parse before enqueueing.
Example fix
// before
await log.insert({ body: `user asked about sync` });
// after
await log.insert({ body: JSON.stringify({ op: 'set_title', text: 'user asked about sync' }) }); Defensive patterns
Strategy: try-catch
Validate before calling
function bodyParses(body) {
try { JSON.parse(body); return true; } catch { return false; }
}
if (!bodyParses(op.body)) deadLetterQueue.push(op); Type guard
function isParseableObject(body) {
try {
const p = JSON.parse(body);
return typeof p === 'object' && p !== null && !Array.isArray(p);
} catch { return false; }
} Try / catch
try {
applySyncOp(op);
} catch (e) {
if (String(e.message).includes('body is not parseable JSON')) {
logger.error({ seq: op.seq }, 'corrupt body — quarantining op');
quarantine(op);
} else throw e;
} Prevention
- Always JSON.stringify on the producer; never store raw text/objects in body
- Round-trip test: JSON.parse(JSON.stringify(body)) before persisting
- Monitor for truncated writes; use transactions when inserting sync log rows
When it happens
Trigger: The sync log row's body column contains malformed or truncated JSON — a crashed writer, manual DB edit, a queue message that was cut off, or a body stored with a non-JSON encoding.
Common situations: A migration or import script wrote plain text into body; a producer logged debug output instead of JSON.stringify'd payload; network payload was truncated before persistence; double-encoding bugs where a quoted string of JSON got mangled.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- Unreadable chroma-sync-state.json, treating as empty
- canonical content
- Expected JSON array in observation list field, using plain…
- mutation body is not JSON
- mutation body must be an object
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/a99437ca8692eca0.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/SyncApply.ts:520
}).catch((error) => {
logger.error('SYNC_APPLY', 'Chroma forward of applied row failed, continuing without vector search', {},
error instanceof Error ? error : new Error(String(error)));
});
}
return result;
}
// -------------------------------------------------------------------------
// Row ops
// -------------------------------------------------------------------------
private parseBody(op: SyncOp): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(op.body);
} catch {
throw invalidOp(op, 'body is not parseable JSON');
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw invalidOp(op, 'body must be a JSON object');
}
return parsed as Record<string, unknown>;
}
private findByOrigin(table: string, originDevice: string, originId: string): RowIdRev | undefined {
return this.db.prepare(
`SELECT id, CAST(sync_rev AS TEXT) AS sync_rev
FROM ${table} WHERE origin_device_id = ? AND origin_local_id = ?`
).get(originDevice, originId) as RowIdRev | undefined;
}
/**
* Canonical-v2 head ledger. It survives local row deletion and epoch replay,
* so a stale live op cannot resurrect a tombstoned entity.
*/View on GitHub (pinned to d8bc9755e7)