thedotmack/claude-mem · error

SyncApply: ops out of order (seq ${op.seq} after ${lastSeq})

Error message

SyncApply: ops out of order (seq ${op.seq} after ${lastSeq})

What it means

Thrown by SyncApply when an op's seq is <= lastSeq but > cursor — i.e. it is past the cursor (so not a replay-skip) yet not greater than the highest seq already seen in this same batch. Ops within a page must be strictly monotonically increasing; a duplicate or regression triggers this.

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 d768ba3643)

Solutions

  1. Inspect the page for the seq that regressed relative to lastSeq (the message names both seq and lastSeq).
  2. Ensure the hub returns ops sorted by ascending seq within a page.
  3. Re-request the page from the current cursor; do not merge overlapping pages client-side.
  4. If duplicates are expected from the hub, dedup by seq before calling applyOps.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-sort/dedup a page by seq before applying (only if hub ordering is unreliable):
function normalizePage(ops: SyncOp[]): SyncOp[] {
  return ops
    .filter((op, i, arr) => arr.findIndex(o => o.seq === op.seq) === i) // dedup by seq
    .sort((a, b) => compareCanonicalDecimals(a.seq, b.seq));
}

Try / catch

try { apply.applyOps(page, opts); }
catch (e) {
  if (e instanceof Error && e.message.startsWith('SyncApply: ops out of order')) {
    // hub returned non-monotonic page; sort+dedup then retry without strict contiguity
    apply.applyOps(normalizePage(page), { requireContiguous: false }); return;
  }
  throw e;
}

Prevention

When it happens

Trigger: In applyOps, after the contiguous check: if compareCanonicalDecimals(seq, cursor) <= 0 it is skipped as already-applied; otherwise if compareCanonicalDecimals(seq, lastSeq) <= 0 it throws. Fires when a page contains a seq that goes backward relative to an earlier op in the same page (and is not behind the cursor).

Common situations: Hub returned a page with non-monotonic seq ordering, a duplicate op within one page, or a page stitched from two overlapping fetches. The contiguous check [131] only runs when requireContiguous is true, but this out-of-order check always runs, so it catches unordered pages even in non-strict mode.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/9b68d43f0987777c. Report an issue: GitHub.