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
- Pass Date.now() (or another integer millisecond source) as now; validate with Number.isSafeInteger(now) && now >= 0 before the call.
- If using a custom clock, ensure it returns integer milliseconds.
- Check that a timestamp string/number is parsed with Number() or parseInt before reaching leaseNow.
- 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
- Always source 'now' from Date.now() or an integer-ms clock abstraction.
- Never pass performance.now() (float) or microsecond timestamps to ms-based APIs.
- Wrap custom clocks to return Math.floor(integer ms).
- Unit-test clock providers to assert safe-integer output.
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
- Adapter rejected input
- agent_event source_id must belong to project_id and team_id
- canonical content
- canonical content
- CLAUDE_MEM_REDIS_URL must use redis:// or rediss://
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)