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

  1. Fetch the current head via getProjectionState()/getMetadata() and request min(desiredTarget, head_seq) as the target.
  2. Re-read head_seq immediately before acquiring; if ops may still be in flight, retry acquire after the append completes.
  3. Verify the target is a canonical decimal string (no leading zeros, no '+' sign) since assertCanonicalDecimal runs first and non-canonical values fail earlier.
  4. 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

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


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)