## Summary
- Replaces `psycopg2-binary` with `psycopg[binary,pool]>=3.1.8,<4` for
native async connection pooling
- Sets `conn_max_age=0` (persistent connections don't work correctly
under ASGI) and configures psycopg3's `ConnectionPool` with `min_size=2,
max_size=10`
- Changes `log_features` to use `@sync_to_async(thread_sensitive=False)`
so concurrent calls dispatch to different threads, each getting a
separate connection from the pool
## Why
With psycopg2, all `sync_to_async` calls from the same coroutine shared
a single thread and Postgres connection. Parallel DB calls via
`TaskGroup` were serialized at the connection level — zero improvement.
Benchmarks confirmed that with psycopg3's pool, 5 parallel calls now get
5 different threads and 5 different `pg_backend_pid`s. Parallel
`aexists` operations showed **15.6% improvement** (208ms → 175ms).
## Test plan
- [x] 538 tests pass, 0 failures, 12 skipped (pre-existing)
- [x] Ruff lint clean
- [x] Benchmark confirms multiple connections: 5 unique threads, 5
unique pg_pids
- [ ] Verify in staging that connection pool behaves correctly under
sustained load