thedotmack/claude-mem · error · Error

projection_error: lease clock must be a safe millisecond…

Error message

projection_error: lease clock must be a safe millisecond integer

What it means

projectionError("lease clock must be a safe millisecond integer") is thrown by SyncHub.leaseNow when the 'now' argument supplied for lease bookkeeping is not a non-negative safe integer (Number.isSafeInteger fails or now < 0). Lease timestamps are stored as canonical decimal millisecond strings, so the input clock must be a valid JS safe integer before conversion.

Solutions

  1. Pass Date.now() (or another integer millisecond source) as now; validate with Number.isSafeInteger(now) && now >= 0 before the call.
  2. If using a custom clock, ensure it returns integer milliseconds.
  3. Check that a timestamp string/number is parsed with Number() or parseInt before reaching leaseNow.
  4. For sub-millisecond or high-precision clocks, truncate with Math.floor and keep units consistent (ms).

Example fix

// before
hub.renewProjectionLease(token, performance.now()); // float ms
// after
const now = Math.floor(Date.now());
if (Number.isSafeInteger(now) && now >= 0) hub.renewProjectionLease(token, now);
Defensive patterns

Strategy: validation

Validate before calling

function assertLeaseClock(now: unknown): number {
  if (typeof now !== "number" || !Number.isSafeInteger(now) || now < 0) {
    throw new TypeError(`lease clock must be a non-negative safe integer (ms), got ${String(now)}`);
  }
  return now;
}
// call as: hub.renewProjectionLease(token, assertLeaseClock(Date.now()))

Type guard

function isMillisTimestamp(v: unknown): v is number {
  return typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= Date.now() + 86400000;
}

Prevention

When it happens

Trigger: Calling acquire/renew/assertLease paths with now = Date.now() replaced by NaN, undefined (becomes NaN), a float, a negative number, or a value above Number.MAX_SAFE_INTEGER (e.g. microsecond timestamps); passing a string that wasn't parsed.

Common situations: Mixing Date objects or microsecond clocks (performance.now()*1000) into a millisecond API; null/undefined 'now' from an unconfigured clock provider in tests; environments where Date.now() is mocked with non-integer values.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

	private assertLease(token: string, now: number): void {
		if (typeof token !== "string" || token.length === 0 || this.metaOptional("projection_lease_token") !== token) {
			throw projectionError("projection lease is not held");
		}
		const expires = this.metaOptional("projection_lease_expires_at");
		if (!expires || compareCanonicalDecimals(expires, this.leaseNow(now)) <= 0) {
			throw projectionError("projection lease expired");
		}
	}

	private renewProjectionLease(token: string, now: number): void {
		this.ctx.storage.transactionSync(() => {
			this.assertLease(token, now);
			this.setMeta("projection_lease_expires_at", this.leaseExpiry(this.leaseNow(now)));
		});
	}

	private leaseNow(now: number): string {
		if (!Number.isSafeInteger(now) || now < 0) throw projectionError("lease clock must be a safe millisecond integer");
		return String(now);
	}

	private leaseExpiry(now: string): string {
		return (BigInt(assertCanonicalDecimal(now)) + BigInt(PROJECTION_LEASE_MS)).toString(10);
	}

	// ---------------------------------------------------------------------
	// Launch safety: physical log compaction is intentionally disabled.
	// ---------------------------------------------------------------------

	async alarm(): Promise<void> {
		// A cursor-0 device has no snapshot/reset bootstrap yet, so every
		// canonical operation remains replayable. Keep the alarm surface for
		// deployed-object compatibility, but deliberately delete zero rows.
		await this.ctx.storage.setAlarm(Date.now() + DAY_MS);
	}

View on GitHub (pinned to d8bc9755e7)