{"record":{"id":"b20884a197010e5d","repo":"thedotmack/claude-mem","slug":"projection-error-projection-lease-expired","errorCode":null,"errorMessage":"projection_error: projection lease expired","messagePattern":"projection_error: projection lease expired","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":789,"sourceCode":"\t\t});\n\t}\n\n\tgetProjectionState(): ProjectionState {\n\t\treturn {\n\t\t\tprotocol_version: 1,\n\t\t\tepoch: this.meta(\"epoch\"),\n\t\t\thead_seq: this.headSeq(),\n\t\t\tprojected_seq: this.projectedSeq(),\n\t\t};\n\t}\n\n\tprivate assertLease(token: string, now: number): void {\n\t\tif (typeof token !== \"string\" || token.length === 0 || this.metaOptional(\"projection_lease_token\") !== token) {\n\t\t\tthrow projectionError(\"projection lease is not held\");\n\t\t}\n\t\tconst expires = this.metaOptional(\"projection_lease_expires_at\");\n\t\tif (!expires || compareCanonicalDecimals(expires, this.leaseNow(now)) <= 0) {\n\t\t\tthrow projectionError(\"projection lease expired\");\n\t\t}\n\t}\n\n\tprivate renewProjectionLease(token: string, now: number): void {\n\t\tthis.ctx.storage.transactionSync(() => {\n\t\t\tthis.assertLease(token, now);\n\t\t\tthis.setMeta(\"projection_lease_expires_at\", this.leaseExpiry(this.leaseNow(now)));\n\t\t});\n\t}\n\n\tprivate leaseNow(now: number): string {\n\t\tif (!Number.isSafeInteger(now) || now < 0) throw projectionError(\"lease clock must be a safe millisecond integer\");\n\t\treturn String(now);\n\t}\n\n\tprivate leaseExpiry(now: string): string {\n\t\treturn (BigInt(assertCanonicalDecimal(now)) + BigInt(PROJECTION_LEASE_MS)).toString(10);\n\t}","sourceCodeStart":771,"sourceCodeEnd":807,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L771-L807","documentation":"projectionError(\"projection lease expired\") is thrown by SyncHub.assertLease when the caller holds a projection lease whose expiry timestamp (meta key projection_lease_expires_at) is at or before the current lease clock. The lease is a timed mutual-exclusion token: only the holder of the matching token may perform projection work, and holding it is not enough — it must not have expired. This guards concurrent DO instances or retried requests from double-applying projections.","triggerScenarios":"Calling any projection mutation while the lease's projection_lease_expires_at <= leaseNow(now); the lease was acquired earlier and never renewed (renewProjectionLease) before its PROJECTION_LEASE_MS window elapsed; a long-running operation overran the lease TTL; or a clock value was stored manually that is already in the past.","commonSituations":"Batch jobs or handlers that hold a lease across an await that takes longer than the lease TTL; retry storms where the original lease expired before the retry runs; two workers contending where one lets its lease lapse mid-operation; testing with a fixed 'now' in the past relative to a stored expiry.","solutions":["Renew the lease before it lapses: call renewProjectionLease(token, now) inside the same storage transaction before the expiry passes.","Re-acquire a fresh lease and retry the operation if the work can safely be restarted idempotently.","Increase the lease window (PROJECTION_LEASE_MS) if legitimate work regularly outlives it.","Treat the error as a lost-lock signal: abort the projection attempt rather than proceeding with a stale token.","Verify the stored projection_lease_expires_at value is a canonical decimal and in the future; fix any manual meta writes."],"exampleFix":"// before\nthis.assertLease(token, now);\ndoExpensiveProjection(); // lease may expire mid-way\n// after\nthis.renewProjectionLease(token, now);\nthis.assertLease(token, now);\ndoExpensiveProjection();","handlingStrategy":"try-catch","validationCode":"const expires = /* read stored projection_lease_expires_at */;\nconst token = /* held lease token */;\nif (!token) throw new Error(\"no lease held\");\nif (!expires || BigInt(expires) <= BigInt(Date.now())) throw new Error(\"lease already expired; renew first\");","typeGuard":"function hasLiveLease(expiresAt: string | undefined, now: number): boolean {\n  return typeof expiresAt === \"string\" && /^\\d+$/.test(expiresAt) && BigInt(expiresAt) > BigInt(now);\n}","tryCatchPattern":"try {\n  await doProjection();\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"projection lease expired\")) {\n    await reacquireLease(); // re-acquire and retry idempotently\n  } else throw e;\n}","preventionTips":["Renew the lease inside the same storage transaction as the work, not before an await boundary.","Keep lease-held work short relative to PROJECTION_LEASE_MS.","Renew proactively on a timer at ~half the lease TTL during long operations.","Never cache a lease token across request retries without re-validating expiry.","In tests, advance the mock clock consistently with stored expiries."],"tags":["concurrency","lease","distributed-lock","sync"],"backgroundTag":"lease-expired","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}