thedotmack/claude-mem · error
SyncApply: ops out of order
Error message
SyncApply: ops out of order (seq ${op.seq} after ${lastSeq}) What it means
Thrown by SyncApply.applyOps when, after the cursor skip (compareCanonicalDecimals(seq, cursor) <= 0), an op's seq is still <= lastSeq — i.e. the page contains a duplicate or regressed sequence relative to ops already accepted in this batch. It is the in-batch ordering guard; with requireContiguous enabled the gap check at SyncApply.ts:462 fires first, so this error is effectively the non-strict (socket push) path's protection against at-least-once delivery duplicating an op inside one page.
Solutions
- Deduplicate ops by seq (keep first occurrence) before calling applyOps.
- Re-pull the page from the cursor instead of replaying a possibly-corrupted push payload.
- If duplicates persist, capture the offending page and report a server-side emit/dedupe bug — the hub is violating the strictly-increasing seq contract.
Example fix
// before
const result = syncApply.applyOps(ops, { epoch });
// after
const seen = new Set<string>();
const deduped = ops.filter(op => {
if (seen.has(op.seq)) return false;
seen.add(op.seq);
return true;
});
const result = syncApply.applyOps(deduped, { epoch }); Defensive patterns
Strategy: validation
Validate before calling
// Deduplicate and order-check a page before non-strict apply.
function sanitizePage(ops: Array<{ seq: string }>): Array<{ seq: string }> {
const bySeq = new Map(ops.map(op => [op.seq, op]));
return [...bySeq.values()].sort((a, b) => compareCanonicalDecimals(a.seq, b.seq));
} Try / catch
try {
result = syncApply.applyOps(ops, { epoch });
} catch (e) {
if (e instanceof Error && e.message.startsWith('SyncApply: ops out of order')) {
const sanitized = sanitizePage(ops);
result = syncApply.applyOps(sanitized, { epoch });
} else throw e;
} Prevention
- Dedupe by seq at the transport boundary if your delivery is at-least-once.
- Sort pages by seq before apply when the producer does not guarantee ordering.
- Treat repeated occurrences as a producer bug to report, not a client condition to permanently mask.
When it happens
Trigger: applyOps receiving a page containing the same seq twice ([10,10]) or a regression ([10,11,10]) with any op above the cursor — typically a server double-emitting an op, a delivery layer stitching overlapping retry pages, or a producer bug re-sending the tail of the previous batch.
Common situations: Message queue redelivery duplicating an op within one delivered page; server retry logic appending the previous page's last op to the next page; a server-side migration replaying ops without dedupe.
Related errors
- cloud sync identity unavailable; refusing an unreplicated…
- cloud sync unavailable; refusing an unreplicated delete
- SyncApply: sequence gap
- Backfill failed
- canonical content
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/9b68d43f0987777c.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/SyncApply.ts:469
const cursor = this.getCursor();
let lastSeq = cursor;
for (const op of ops) {
const seq = assertCanonicalDecimal(op.seq, { positive: true });
assertCanonicalDecimal(op.rev, { positive: true });
// Strict HTTP pages describe the exact raw suffix after our cursor.
// Validate every supplied sequence before the ordinary replay skip;
// otherwise a stale prefix (even an out-of-order one) is silently
// discarded and a malformed page can look contiguous.
if (options.requireContiguous === true && seq !== incrementCanonicalDecimal(lastSeq)) {
throw new Error(`SyncApply: sequence gap (expected ${incrementCanonicalDecimal(lastSeq)}, got ${seq})`);
}
if (compareCanonicalDecimals(seq, cursor) <= 0) {
result.skippedCursor++;
continue;
}
if (compareCanonicalDecimals(seq, lastSeq) <= 0) {
throw new Error(`SyncApply: ops out of order (seq ${op.seq} after ${lastSeq})`);
}
lastSeq = seq;
if (op.origin_device === this.deviceId) {
result.skippedOwn++;
continue;
}
let outcome: 'applied' | 'stale';
if (op.kind === 'mutation') {
outcome = this.applyMutation(op);
} else {
outcome = this.applyCanonicalRowOp(op, chromaJobs);
}
if (outcome === 'applied') result.applied++;
else result.skippedStale++;
}
View on GitHub (pinned to e2d1df569a)