thedotmack/claude-mem · error · Error
projection_error: epoch mismatch
Error message
projection_error: epoch mismatch
What it means
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.
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.
Example fix
// before
await hub.advanceProjectionCheckpoint(token, myEpoch, from, through);
// after
try {
await hub.advanceProjectionCheckpoint(token, lease.epoch, from, through);
} catch (e) {
if (String(e).includes('epoch mismatch')) {
hub.releaseProjectionLease(token);
const st = await hub.getProjectionState();
lease = await hub.acquireProjectionLease(st.head_seq); // resume with new epoch
}
} Defensive patterns
Strategy: retry
Validate before calling
const st = await hub.getProjectionState();
if (cachedEpoch !== st.epoch) {
cachedEpoch = st.epoch; // rebuild local projector state before continuing
} Try / catch
try {
await hub.advanceProjectionCheckpoint(token, epoch, from, through);
} catch (e) {
if (String(e).includes('epoch mismatch')) {
hub.releaseProjectionLease(token);
await rebuildProjectorState(); // new epoch: re-read head/projected, re-acquire
return;
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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: checkpoint compare-and-set mismatch
- projection_error: invalid projected through_seq
- projection_error: projection lease is not held
- projection_error: target_seq exceeds head_seq
- projection_error: user_id must be non-empty
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/81e3ff3f67a746cb.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:752
heartbeatProjectionLease(leaseToken: string, now = Date.now()): ProjectionState {
this.assertLease(leaseToken, now);
this.renewProjectionLease(leaseToken, now);
return this.getProjectionState();
}
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");View on GitHub (pinned to d8bc9755e7)