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

  1. Deduplicate ops by seq (keep first occurrence) before calling applyOps.
  2. Re-pull the page from the cursor instead of replaying a possibly-corrupted push payload.
  3. 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

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


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)