{"record":{"id":"81e3ff3f67a746cb","repo":"thedotmack/claude-mem","slug":"projection-error-epoch-mismatch","errorCode":null,"errorMessage":"projection_error: epoch mismatch","messagePattern":"projection_error: epoch mismatch","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":752,"sourceCode":"\theartbeatProjectionLease(leaseToken: string, now = Date.now()): ProjectionState {\n\t\tthis.assertLease(leaseToken, now);\n\t\tthis.renewProjectionLease(leaseToken, now);\n\t\treturn this.getProjectionState();\n\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\");","sourceCodeStart":734,"sourceCodeEnd":770,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L734-L770","documentation":"advanceProjectionCheckpoint() verifies that the epoch string supplied by the projector matches the hub's current epoch meta value. The epoch is rotated on hub re-initialization to fence off stale projectors; a checkpoint arriving under an old epoch would move projected_seq in the wrong log generation, so the DO throws this projectionError inside the same transaction as the lease check.","triggerScenarios":"Calling advanceProjectionCheckpoint(token, epoch, from, through) with an epoch captured before a DO reset/epoch rotation; a projector that kept running across the hub being recreated; copying an epoch string from a stale ProjectionState.","commonSituations":"Durable Object hibernation/restart with a new epoch; deploy that wipes DO storage and rotates epoch while a projector worker holds in-memory state; multi-region projector where one observed an old epoch.","solutions":["Handle this error by releasing the stale lease, calling getProjectionState() to read the new epoch and projected_seq, re-acquiring the lease, and resuming projection from the new checkpoint.","Never cache epoch across process restarts of the projector; read it from acquireProjectionLease's returned ProjectionLease each cycle.","Compare your cached epoch to the one in each lease response before advancing; on divergence, rebuild local state.","Log and alert on epoch mismatches — they indicate the hub was recreated and downstream projections may need a rebuild."],"exampleFix":"// before\nawait hub.advanceProjectionCheckpoint(token, myEpoch, from, through);\n// after\ntry {\n  await hub.advanceProjectionCheckpoint(token, lease.epoch, from, through);\n} catch (e) {\n  if (String(e).includes('epoch mismatch')) {\n    hub.releaseProjectionLease(token);\n    const st = await hub.getProjectionState();\n    lease = await hub.acquireProjectionLease(st.head_seq); // resume with new epoch\n  }\n}","handlingStrategy":"retry","validationCode":"const st = await hub.getProjectionState();\nif (cachedEpoch !== st.epoch) {\n  cachedEpoch = st.epoch; // rebuild local projector state before continuing\n}","typeGuard":null,"tryCatchPattern":"try {\n  await hub.advanceProjectionCheckpoint(token, epoch, from, through);\n} catch (e) {\n  if (String(e).includes('epoch mismatch')) {\n    hub.releaseProjectionLease(token);\n    await rebuildProjectorState(); // new epoch: re-read head/projected, re-acquire\n    return;\n  }\n  throw e;\n}","preventionTips":["Read epoch fresh from each lease response; never persist across projector restarts","Treat any epoch change as 'hub recreated' and rebuild downstream state","Alert on epoch mismatch — it signals a rollover event"],"tags":["projection","epoch","fencing","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"}