GmailWatchStateStore.claimReconcileLease returns the rendered claimed_until::text as its lease token. Two claims that land within one clock tick mint the same token, so a stale holder's token compares equal to the live holder's.
Evidence this is real, not theoretical
HT-101 (PR #165) hit exactly this. Its IMAP lease was written to mirror the Gmail one statement-for-statement, and a test forcing a same-tick reclaim produced two identical tokens. The IMAP side was fixed there by adding a per-claim lease_token uuid (gen_random_uuid()), keeping claimed_until for expiry and the token for ownership.
Notably, src/store/imap-watch-state.test.ts already carried a waitForClockTick() helper — the original author knew the collision was possible and worked around it in the tests rather than fixing the production code.
Why this is lower severity than the IMAP case was
Gmail's token guards only releaseReconcileLease. It never fences a cursor advance. Worst case is a stale holder clearing a live successor's lease, allowing a redundant concurrent reconcile — wasteful, and the ingest ledger still dedupes. The IMAP case was worse because the same token fenced the cursor write, so a collision could let a stale holder escape a UIDVALIDITY-reset pause and corrupt the cursor.
Acceptance
gmail_watch_state gains a per-claim lease_token uuid; claimReconcileLease returns it, releaseReconcileLease matches on it.
- A test forces a same-tick reclaim and asserts the two tokens differ.
- Any
waitForClockTick-style workaround that existed only to dodge the collision is removed, not kept.
Unlike migration 027, gmail_watch_state is live in production, so this needs a real additive migration — not an in-place amendment.
Found while fixing the IMAP equivalent in PR #165, 2026-07-31.
GmailWatchStateStore.claimReconcileLeasereturns the renderedclaimed_until::textas its lease token. Two claims that land within one clock tick mint the same token, so a stale holder's token compares equal to the live holder's.Evidence this is real, not theoretical
HT-101 (PR #165) hit exactly this. Its IMAP lease was written to mirror the Gmail one statement-for-statement, and a test forcing a same-tick reclaim produced two identical tokens. The IMAP side was fixed there by adding a per-claim
lease_token uuid(gen_random_uuid()), keepingclaimed_untilfor expiry and the token for ownership.Notably,
src/store/imap-watch-state.test.tsalready carried awaitForClockTick()helper — the original author knew the collision was possible and worked around it in the tests rather than fixing the production code.Why this is lower severity than the IMAP case was
Gmail's token guards only
releaseReconcileLease. It never fences a cursor advance. Worst case is a stale holder clearing a live successor's lease, allowing a redundant concurrent reconcile — wasteful, and the ingest ledger still dedupes. The IMAP case was worse because the same token fenced the cursor write, so a collision could let a stale holder escape a UIDVALIDITY-reset pause and corrupt the cursor.Acceptance
gmail_watch_stategains a per-claimlease_token uuid;claimReconcileLeasereturns it,releaseReconcileLeasematches on it.waitForClockTick-style workaround that existed only to dodge the collision is removed, not kept.Unlike migration 027,
gmail_watch_stateis live in production, so this needs a real additive migration — not an in-place amendment.Found while fixing the IMAP equivalent in PR #165, 2026-07-31.