Fixed in #149, merged to main as 55f79b5. CI green on main.
Change: 0041_sweep_expired_sessions.sql replaces the sweep function so a session goes when its own expiry has passed:
AND (expires_at < now() OR created_at < now() - INTERVAL '3 years')
Decisions on the open questions in this ticket:
- The three-year age rule stays as the backstop for sessions with no
expires_at—expires_at < now()is NULL for those rows, so they fall through to it, and nothing else would ever clear them. - Swept rows stay revoked rather than being deleted. That is a retention decision, not a correctness one, and nothing currently reads revoked rows in a way that suffers. Worth a separate ticket if the table growth ever matters.
uiPartitionAuthTokenskeeps its expiry filter. The sweep is lazy — at most hourly, and only on the back of a token refresh — so the database does not guarantee the invariant at render time. Its comment claimed sessions "are only swept long after" expiry, which this change makes false, so it now describes the laziness instead.
Unchanged: API tokens are never touched, the refreshing token is spared, the LIMIT 1000 bounds one refresh's work, and the hourly claim still rate-limits the sweep. A partial index on (kind, expires_at) WHERE revoked_at IS NULL keeps the new predicate off a full scan. The Down migration restores the age-only body and is exercised by the existing goose.DownTo(..., 28) test.
Coverage: TestSessionSweepRevokesExpiredSessions covers expired session swept, unexpired survives, expired API token survives, young no-expiry session survives on the age rule, refreshing token spared. Verified non-vacuous — with 0041 removed it fails on "an expired session survived the sweep".