thedotmack/claude-mem · error

SyncApply: sequence gap

Error message

SyncApply: sequence gap (expected ${incrementCanonicalDecimal(lastSeq)}, got ${seq})

What it means

Thrown by SyncApply.applyOps during a strict pull (options.requireContiguous === true, which the SyncClient HTTP path always sets) when an incoming op's seq is not exactly incrementCanonicalDecimal(lastSeq). Strict pages must describe the exact raw suffix after the cursor, so the gap check runs before the stale-prefix skip: even an out-of-order stale op fails it, preventing a malformed page from looking contiguous. Halt reason: ops could have been lost between the cursor and this page, so replaying silently would diverge local state from the hub.

Solutions

  1. Trigger a full resync: reset the local cursor to 0 (apply is idempotent by design) so the next pull re-reads the whole suffix and re-establishes contiguity.
  2. Verify the hub op log around the expected seq to confirm whether ops were actually lost server-side or only reordered.
  3. Confirm you are hitting a single hub head (one instance or a shared log), not alternating servers with different heads.
  4. If the server legitimately reset sequences, make sure it also bumped epoch so clients take the epochReset re-pull path instead of gap errors.

Example fix

// before
try {
  const result = syncApply.applyOps(page.ops, { epoch: page.epoch, requireContiguous: true });
} catch (e) {
  throw e; // gap aborts sync forever
}

// after
try {
  const result = syncApply.applyOps(page.ops, { epoch: page.epoch, requireContiguous: true });
} catch (e) {
  if (e instanceof Error && e.message.includes('sequence gap')) {
    syncApply.resetCursorToZero(); // re-pull from start; apply is idempotent
    return;
  }
  throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// Before applying a strict page, verify its first op continues the cursor exactly.
import { incrementCanonicalDecimal, compareCanonicalDecimals } from './canonical-decimal.js';
function pageIsContiguousSuffix(cursor: string, ops: Array<{ seq: string }>): boolean {
  let last = cursor;
  for (const op of ops) {
    if (op.seq !== incrementCanonicalDecimal(last)) return false;
    last = op.seq;
  }
  return true;
}

Try / catch

try {
  result = syncApply.applyOps(ops, { epoch, requireContiguous: true });
} catch (e) {
  if (e instanceof Error && e.message.startsWith('SyncApply: sequence gap')) {
    // Ops may be missing server-side: do NOT skip ahead. Reset and re-pull.
    syncApply.resetCursor();
    return pullAgain();
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling applyOps(ops, { requireContiguous: true }) where the first op is not cursor+1, or any later op skips a seq: hub returned a truncated page (server data loss), local cursor is ahead of the server head after a server restore/reset, ops re-ordered in transit, or server restarted its sequence without bumping epoch (so the epochReset re-pull path never triggers).

Common situations: Sync hub redeployed from a backup so its op log no longer contains the seqs after your cursor; two hub instances behind a load balancer with divergent heads serving alternating pages; a server-side bug pruning ops; client DB restored from an older snapshot so its cursor points past the server head.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/20a4120e8b007ac6. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/SyncApply.ts:462

      epochReset: false,
    };
    if (ops.length === 0) return result;

    const chromaJobs: ChromaJob[] = [];

    const tx = this.db.transaction(() => {
      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);

View on GitHub (pinned to e2d1df569a)