Three RED-blessed non-blocking follow-ups on the KV-optional work.
1. Drop the non-atomic getdel() shim in lib/redis.ts. It shadowed BOTH backends'
native atomic GETDEL with a get->del pipeline (other commands interleave
between GET and DEL), reintroducing the one-time login-code double-use race
that fetchAndDeleteLoginCodeData relies on GETDEL to prevent. ioredis maps
getdel to the single-round-trip Redis GETDEL command; MemoryKV.getdel now
reads+deletes in one un-yielded step (was `await this.get()` then delete — the
await yielded the event loop, letting two racers both observe the value).
2. Bound MemoryKV growth. Lazy on-read eviction never fires for write-once-
never-read keys (rl:<ip> rate-limit counters, one-shot tokens), so the map
grew unbounded until the pod OOMKilled. Add sweepExpired() (one O(n) active-
expire pass) run on an unref'd 60s timer, started by lib/redis.ts for the
server singleton only (edge-gated; no test imports that module, so unit runs
stay timer-free). MemoryKV stays pure (zero imports).
3. Document in .env.example that re-enabling multi-replica REQUIRES KV_URL
(single-replica-by-construction otherwise: SQLite-on-RWO + in-process KV).
The one-time non-silent in-process warning already exists in lib/kv/select.ts;
no metrics surface exists, so log + doc is sufficient.
Tests: +3 cases (getdel atomicity under concurrent race; 100k-key sweep reclaims
to 0; sweep retains live/no-TTL keys). 28 pass, 3 skip (select.ts skips without
ioredis in a bare checkout), 0 fail.
Co-authored-by: Hanzo AI <ai@hanzo.ai>