thedotmack/claude-mem · error · Error
projection_error: target_seq exceeds head_seq
Error message
projection_error: target_seq exceeds head_seq
What it means
acquireProjectionLease(targetSeq) first canonicalizes the requested target sequence and compares it to the log's head sequence. If the projection target is ahead of everything currently committed to the canonical log, the DO throws this projectionError because there is nothing to project up to that sequence yet — a lease for a nonexistent frontier would let the projector checkpoint past real data.
Solutions
- Fetch the current head via getProjectionState()/getMetadata() and request min(desiredTarget, head_seq) as the target.
- Re-read head_seq immediately before acquiring; if ops may still be in flight, retry acquire after the append completes.
- Verify the target is a canonical decimal string (no leading zeros, no '+' sign) since assertCanonicalDecimal runs first and non-canonical values fail earlier.
- After an epoch change, discard old targets and re-read state from the hub.
Example fix
// before const lease = await hub.acquireProjectionLease(myCachedTargetSeq); // after const state = await hub.getProjectionState(); const target = myCachedTargetSeq <= state.head_seq ? myCachedTargetSeq : state.head_seq; const lease = await hub.acquireProjectionLease(target);
Defensive patterns
Strategy: validation
Validate before calling
const head = (await hub.getProjectionState()).head_seq; if (compareDecimals(targetSeq, head) > 0) targetSeq = head; // clamp before acquire
Type guard
function isCanonicalDecimal(s: unknown): s is string {
return typeof s === 'string' && /^[0-9]+$/.test(s) && (s === '0' || s[0] !== '0');
} Try / catch
try {
lease = await hub.acquireProjectionLease(target);
} catch (e) {
if (String(e).includes('target_seq exceeds head_seq')) {
const st = await hub.getProjectionState();
lease = await hub.acquireProjectionLease(st.head_seq);
} else throw e;
} Prevention
- Always derive target_seq from hub state (head_seq), never from local counters
- Validate canonical decimal format before calling
- Re-read head after any append burst before acquiring
When it happens
Trigger: Calling acquireProjectionLease with a target_seq larger than headSeq(): e.g. a target string copied from a stale config, a padded/zero-prefixed decimal that fails canonical comparison semantics the caller didn't expect, or the caller computing a target from its own optimistic view of the log.
Common situations: Projector worker restarted with a cached target from a previous epoch; operator manually passing a sequence number instead of reading head_seq from getProjectionState(); clock/ordering issues where the caller assumed ops were committed that weren't yet.
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: invalid projected through_seq
- projection_error: checkpoint compare-and-set mismatch
- projection_error: epoch mismatch
- projection_error: unprojected log gap
- projection_error: user_id must be non-empty
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/c2b5dcde2f355132.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:639
const normalizedId = deviceId.trim();
const normalizedName = normalizeDeviceName(name);
if (normalizedId.length === 0 || normalizedId.length > 128) throw invalid("device_id must be 1-128 characters");
if (normalizedName === null) throw invalid("name must be 1-80 characters");
return this.ctx.storage.sql.exec(
"UPDATE devices SET name = ? WHERE device_id = ?",
normalizedName,
normalizedId,
).rowsWritten > 0;
}
// ---------------------------------------------------------------------
// Authoritative projection checkpoint and short per-user lease.
// ---------------------------------------------------------------------
acquireProjectionLease(targetSeq: string, now = Date.now()): ProjectionLease {
const target = assertCanonicalDecimal(targetSeq);
const head = this.headSeq();
if (compareCanonicalDecimals(target, head) > 0) throw projectionError("target_seq exceeds head_seq");
const projected = this.projectedSeq();
const existingToken = this.metaOptional("projection_lease_token");
const nowValue = this.leaseNow(now);
const existingExpiry = this.metaOptional("projection_lease_expires_at");
if (existingToken && existingExpiry && compareCanonicalDecimals(existingExpiry, nowValue) > 0) {
return {
acquired: false,
epoch: this.meta("epoch"),
head_seq: head,
projected_seq: projected,
target_seq: target,
};
}
const token = crypto.randomUUID();
this.ctx.storage.transactionSync(() => {
this.setMeta("projection_lease_token", token);
this.setMeta("projection_lease_expires_at", this.leaseExpiry(nowValue));
});View on GitHub (pinned to d8bc9755e7)