{"record":{"id":"8150d782cc1d7d24","repo":"thedotmack/claude-mem","slug":"projection-error-checkpoint-compare-and-set-mismatch","errorCode":null,"errorMessage":"projection_error: checkpoint compare-and-set mismatch","messagePattern":"projection_error: checkpoint compare-and-set mismatch","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":756,"sourceCode":"\t}\n\n\tadvanceProjectionCheckpoint(\n\t\tleaseToken: string,\n\t\tepoch: string,\n\t\tfromSeqExclusive: string,\n\t\tthroughSeq: string,\n\t\tnow = Date.now(),\n\t): ProjectionState {\n\t\tthis.ctx.storage.transactionSync(() => {\n\t\t\t// Token check and checkpoint compare-and-set are fenced in the same\n\t\t\t// synchronous transaction. A timed-out predecessor can never replay\n\t\t\t// after a successor has acquired a fresh token.\n\t\t\tthis.assertLease(leaseToken, now);\n\t\t\tif (epoch !== this.meta(\"epoch\")) throw projectionError(\"epoch mismatch\");\n\t\t\tconst expected = assertCanonicalDecimal(fromSeqExclusive);\n\t\t\tconst through = assertCanonicalDecimal(throughSeq);\n\t\t\tconst current = this.projectedSeq();\n\t\t\tif (current !== expected) throw projectionError(\"checkpoint compare-and-set mismatch\");\n\t\t\tif (compareCanonicalDecimals(through, current) < 0 || compareCanonicalDecimals(through, this.headSeq()) > 0) {\n\t\t\t\tthrow projectionError(\"invalid projected through_seq\");\n\t\t\t}\n\t\t\tthis.setMeta(\"projected_seq\", through);\n\t\t\tthis.setMeta(\"projection_lease_expires_at\", this.leaseExpiry(this.leaseNow(now)));\n\t\t});\n\t\treturn this.getProjectionState();\n\t}\n\n\treleaseProjectionLease(leaseToken: string): void {\n\t\tif (this.metaOptional(\"projection_lease_token\") !== leaseToken) return;\n\t\tthis.ctx.storage.transactionSync(() => {\n\t\t\tthis.deleteMeta(\"projection_lease_token\");\n\t\t\tthis.deleteMeta(\"projection_lease_expires_at\");\n\t\t});\n\t}\n\n\tgetProjectionState(): ProjectionState {","sourceCodeStart":738,"sourceCodeEnd":774,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L738-L774","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait hub.advanceProjectionCheckpoint(token, epoch, page.from_seq_exclusive, page.through_seq);\n// after\ntry {\n  await hub.advanceProjectionCheckpoint(token, epoch, page.from_seq_exclusive, page.through_seq);\n} catch (e) {\n  const st = await hub.getProjectionState();\n  if (st.projected_seq >= page.through_seq) return; // already applied\n  throw e; // otherwise re-page from st.projected_seq\n}","handlingStrategy":"retry","validationCode":"const st = await hub.getProjectionState();\nif (st.projected_seq !== page.from_seq_exclusive) {\n  // page is stale: skip if already applied, else re-page from st.projected_seq\n}","typeGuard":null,"tryCatchPattern":"try {\n  await hub.advanceProjectionCheckpoint(token, epoch, from, through);\n} catch (e) {\n  if (String(e).includes('compare-and-set mismatch')) {\n    const st = await hub.getProjectionState();\n    if (st.projected_seq >= through) return; // idempotent success\n    return reprojectFrom(st.projected_seq);\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["projection","concurrency","compare-and-set","sync"],"backgroundTag":"invalid-state-transition","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}