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}: same entity revision has a different canonical operation hash What it means
applyCanonicalRowOp implements last-writer-wins by comparing canonical decimal revisions. When an incoming op has the SAME entity_rev as the stored head but a different operation_sha256, two divergent operations claim the same revision — an invariant violation that would make replicas diverge — so the op is rejected instead of arbitrarily picking one.
Solutions
- Investigate the conflicting op: dump the stored head's operation_sha256 vs the incoming one for entity_id=${op.entity_id}.
- Fix the producer to always bump entity_rev when content changes — never rewrite a body at an existing rev.
- If a version skew changed canonicalization, redeploy the matching version to all devices and re-canonicalize the affected rows (rebuild the sync log with recomputed hashes).
- After restoring a device from backup, reset/advance its revision counter before it produces new ops.
Example fix
// before (producer re-emit without rev bump)
publish({ entity_id, entity_rev: existingRev, body: newBody });
// after
const nextRev = incrementCanonicalDecimal(existingRev);
publish({ entity_id, entity_rev: nextRev, body: newBody }); Defensive patterns
Strategy: try-catch
Validate before calling
function willConflict(op, head) {
return head && op.entity_rev === head.entity_rev && op.operation_sha256 !== head.operation_sha256;
}
// skip or re-emit with bumped rev before applying Type guard
function isReplayOfHead(op, head) { return head != null && op.entity_rev === head.entity_rev && op.operation_sha256 === head.operation_sha256; } Try / catch
try {
applySyncOp(op);
} catch (e) {
if (String(e.message).includes('different canonical operation hash')) {
logger.error({ seq: op.seq, entity: op.entity_id }, 'rev collision — needs operator investigation');
alertDivergence(op);
} else throw e;
} Prevention
- Always bump entity_rev when body content changes — never rewrite at an existing rev
- Advance revision counters after restoring a device from backup
- Keep canonicalization/hashing code identical across all node versions
When it happens
Trigger: Two devices independently generate ops for the same entity with identical rev values but different canonical bodies (rev counter collision), or a producer re-emits a modified body without bumping rev, or canonical hash computation differs between versions (hashing algorithm/normalization change).
Common situations: Clock/counter collision after restoring a device from backup without advancing its rev counter; a bug where edits are written at the same rev as an existing op; a library upgrade changed canonicalization so the same logical body hashes differently.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- cloud sync ack conflict for
- projection_error: invalid projected through_seq
- projection_error: target_seq exceeds head_seq
- revision_hash_conflict
- stale_revision: : <
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/c9cc945151e59682.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/SyncApply.ts:552
/**
* Canonical-v2 head ledger. It survives local row deletion and epoch replay,
* so a stale live op cannot resurrect a tombstoned entity.
*/
private applyCanonicalRowOp(op: SyncOp, chromaJobs: ChromaJob[]): 'applied' | 'stale' {
if (!op.entity_id || !op.entity_rev || !op.operation_sha256) {
return this.applyRowOp(op, chromaJobs); // internal legacy fixtures only
}
const head = this.db.prepare(`
SELECT entity_rev, operation_sha256
FROM sync_entity_heads WHERE entity_id = ?
`).get(op.entity_id) as { entity_rev: string; operation_sha256: string } | undefined;
if (head) {
const order = compareCanonicalDecimals(op.entity_rev, head.entity_rev);
if (order < 0) return 'stale';
if (order === 0) {
if (op.operation_sha256 !== head.operation_sha256) {
throw invalidOp(op, 'same entity revision has a different canonical operation hash');
}
return 'stale';
}
}
let outcome: 'applied' | 'stale';
if (op.deleted === true) {
const table = op.kind === 'observation'
? 'observations'
: op.kind === 'summary'
? 'session_summaries'
: 'user_prompts';
this.db.prepare(
`DELETE FROM ${table} WHERE origin_device_id = ? AND origin_local_id = ?`
).run(op.origin_device, op.origin_id);
outcome = 'applied';
} else {
outcome = this.applyRowOp(op, chromaJobs);View on GitHub (pinned to d8bc9755e7)