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
- 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.
- Verify the hub op log around the expected seq to confirm whether ops were actually lost server-side or only reordered.
- Confirm you are hitting a single hub head (one instance or a shared log), not alternating servers with different heads.
- 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
- Never pass requireContiguous for delivery layers that may reorder or dedupe; reserve it for pull-from-cursor pages that guarantee the exact raw suffix.
- Keep hub epoch bumps paired with any server-side sequence reset so clients take the epochReset path instead of hitting gaps.
- Monitor the first-op seq of each page against your stored cursor during development of transport changes.
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
- cloud sync identity unavailable; refusing an unreplicated…
- cloud sync unavailable; refusing an unreplicated delete
- SyncApply: ops out of order
- Backfill failed
- canonical content
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)