thedotmack/claude-mem · critical · Error

projection_error: unprojected log gap

Error message

projection_error: unprojected log gap

What it means

After paging, getProjectionPage() checks the invariant that if the caller asked for a target beyond the projected checkpoint, at least one op must have been returned. If zero ops came back while projected_seq < target_seq, the canonical log has a hole relative to expectations — the expected contiguous ops are missing — so the DO throws this projectionError to signal a broken invariant rather than silently returning an empty page.

Solutions

  1. Inspect the canonical_ops table for the (projected, target] range; if rows were deleted, restore them or reset projected_seq to the true earliest remaining seq via advanceProjectionCheckpoint under a lease.
  2. Recover the DO from a snapshot taken before the gap, or trigger epoch rotation so state rebuilds cleanly.
  3. Re-acquire the lease and retry once in case of transient read timing; persistent gaps indicate data loss, not a retryable condition.
  4. Halt the projection loop on this error instead of spinning — it's an invariant violation requiring data repair.

Example fix

// before
while (projected < target) { page = hub.getProjectionPage(token, target, userId); }
// after
try {
  page = hub.getProjectionPage(token, target, userId);
} catch (e) {
  if (String(e).includes('unprojected log gap')) {
    // stop and alert: canonical log hole needs repair, not retry
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

const st = await hub.getProjectionState();
if (st.projected_seq >= targetSeq) return; // nothing to project; avoid gap path

Try / catch

try {
  page = hub.getProjectionPage(token, target, userId);
} catch (e) {
  if (String(e).includes('unprojected log gap')) {
    alertOps('canonical log gap — manual repair required', { projected, target });
    return; // do NOT retry; invariant violation
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getProjectionPage(token, target, userId) where projected_seq < target but the SQL range (projected, target] yields no rows: e.g. target_seq equals projected (no-op request should return empty, but if target > projected and the log gap exists), or ops between projected and target were deleted/compacted, or a non-canonical target confusing the lexicographic length-aware comparison.

Common situations: Manual DB maintenance/compaction removed canonical_ops rows without advancing projected_seq; restoring a DO snapshot from an older backup while meta says projected_seq is ahead; projector pointed at a target from a divergent epoch.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/c138aa6c8981987f. Report an issue: GitHub.

Appendix: source

Thrown at workers/sync-hub/src/do/SyncHub.ts:721

		const ops: ChangeOp[] = [];
		for (const row of rows) {
			const op = toChange(row);
			const candidate = [...ops, op];
			const bytes = projectionRequestBytes({
				userId,
				epoch,
				fromSeqExclusive: projected,
				throughSeq: op.seq,
				ops: candidate,
			});
			if (bytes > byteLimit) {
				if (ops.length === 0) throw projectionError("one operation exceeds projection request byte budget");
				break;
			}
			ops.push(op);
		}
		if (ops.length === 0 && compareCanonicalDecimals(projected, target) < 0) {
			throw projectionError("unprojected log gap");
		}
		this.renewProjectionLease(leaseToken, now);
		return {
			protocol_version: 1,
			epoch,
			from_seq_exclusive: projected,
			through_seq: ops.length > 0 ? ops[ops.length - 1].seq : projected,
			target_seq: target,
			ops,
		};
	}

	heartbeatProjectionLease(leaseToken: string, now = Date.now()): ProjectionState {
		this.assertLease(leaseToken, now);
		this.renewProjectionLease(leaseToken, now);
		return this.getProjectionState();
	}

View on GitHub (pinned to d8bc9755e7)