thedotmack/claude-mem · error · Error

projection_error: checkpoint compare-and-set mismatch

Error message

projection_error: checkpoint compare-and-set mismatch

What it means

advanceProjectionCheckpoint() performs a compare-and-set: the caller's fromSeqExclusive must exactly equal the hub's current projected_seq. If the stored checkpoint has moved since the projector read it — another writer advanced it, or the projector is replaying an old page — the DO throws this projectionError to prevent lost updates or regressed checkpoints.

Solutions

  1. On this error, re-read getProjectionState(); if projected_seq has already passed your throughSeq, treat the batch as done (idempotent success); otherwise re-page from the new projected_seq.
  2. Ensure only one projector loop per user advances checkpoints — rely on the lease token and never share it across workers.
  3. Make checkpoint advancement strictly sequential: page → apply → advance → page, with no speculative concurrent batches.
  4. If the CAS mismatch keeps occurring, check for a second projector instance (duplicate deployment) holding/farming the same lease.

Example fix

// before
await hub.advanceProjectionCheckpoint(token, epoch, page.from_seq_exclusive, page.through_seq);
// after
try {
  await hub.advanceProjectionCheckpoint(token, epoch, page.from_seq_exclusive, page.through_seq);
} catch (e) {
  const st = await hub.getProjectionState();
  if (st.projected_seq >= page.through_seq) return; // already applied
  throw e; // otherwise re-page from st.projected_seq
}
Defensive patterns

Strategy: retry

Validate before calling

const st = await hub.getProjectionState();
if (st.projected_seq !== page.from_seq_exclusive) {
  // page is stale: skip if already applied, else re-page from st.projected_seq
}

Try / catch

try {
  await hub.advanceProjectionCheckpoint(token, epoch, from, through);
} catch (e) {
  if (String(e).includes('compare-and-set mismatch')) {
    const st = await hub.getProjectionState();
    if (st.projected_seq >= through) return; // idempotent success
    return reprojectFrom(st.projected_seq);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling advanceProjectionCheckpoint with fromSeqExclusive different from projectedSeq(): two projectors alternating on one lease, replaying an already-advanced page after a lease timeout and re-acquire, or advancing out of order after an error mid-batch.

Common situations: Projector crashed after paging but before checkpointing, then restarted and re-applied an old page; duplicate queue delivery of the same projection job; manual tooling advancing the checkpoint concurrently.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/8150d782cc1d7d24. Report an issue: GitHub.

Appendix: source

Thrown at workers/sync-hub/src/do/SyncHub.ts:756

	}

	advanceProjectionCheckpoint(
		leaseToken: string,
		epoch: string,
		fromSeqExclusive: string,
		throughSeq: string,
		now = Date.now(),
	): ProjectionState {
		this.ctx.storage.transactionSync(() => {
			// Token check and checkpoint compare-and-set are fenced in the same
			// synchronous transaction. A timed-out predecessor can never replay
			// after a successor has acquired a fresh token.
			this.assertLease(leaseToken, now);
			if (epoch !== this.meta("epoch")) throw projectionError("epoch mismatch");
			const expected = assertCanonicalDecimal(fromSeqExclusive);
			const through = assertCanonicalDecimal(throughSeq);
			const current = this.projectedSeq();
			if (current !== expected) throw projectionError("checkpoint compare-and-set mismatch");
			if (compareCanonicalDecimals(through, current) < 0 || compareCanonicalDecimals(through, this.headSeq()) > 0) {
				throw projectionError("invalid projected through_seq");
			}
			this.setMeta("projected_seq", through);
			this.setMeta("projection_lease_expires_at", this.leaseExpiry(this.leaseNow(now)));
		});
		return this.getProjectionState();
	}

	releaseProjectionLease(leaseToken: string): void {
		if (this.metaOptional("projection_lease_token") !== leaseToken) return;
		this.ctx.storage.transactionSync(() => {
			this.deleteMeta("projection_lease_token");
			this.deleteMeta("projection_lease_expires_at");
		});
	}

	getProjectionState(): ProjectionState {

View on GitHub (pinned to d8bc9755e7)