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
- 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.
- Ensure only one projector loop per user advances checkpoints — rely on the lease token and never share it across workers.
- Make checkpoint advancement strictly sequential: page → apply → advance → page, with no speculative concurrent batches.
- 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
- Run exactly one projector per hub; never share lease tokens across workers
- Keep page→apply→advance strictly sequential, no speculative batches
- Make application idempotent so CAS conflicts resolve as no-ops
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
- projection_error: epoch mismatch
- projection_error: invalid projected through_seq
- projection_error: projection lease expired
- projection_error: projection lease is not held
- projection_error: target_seq exceeds head_seq
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)