5b63c6174b
`create_app` registers `handle_proxy` as an `async def`, and it called `adapter.get_credential()` directly on the aiohttp event loop. `UpstreamAdapter` is a synchronous contract (`adapters/base.py` — every method is a plain `def`), and the shipped adapters implement it with blocking I/O. `NousPortalAdapter.get_credential` takes `_auth_store_lock()` — a cross-process advisory lock with `AUTH_LOCK_TIMEOUT_SECONDS = 15.0` (`hermes_cli/auth.py:110`) — reads `auth.json` off disk, and may issue a token-refresh POST; on a terminal `AuthError` it takes that lock a second time to persist the quarantined state. `XAIGrokAdapter.get_credential` reads its key pool off disk. The proxy is one process with one loop, and `handle_proxy` streams with `sock_read=300`, so long-lived completions are the normal case. Blocking inside credential resolution therefore freezes *every* concurrent in-flight stream mid-token for the duration — a concurrent `hermes auth` command holding the auth-store lock is enough to do it. Every proxied request goes through this path. Dispatch through `asyncio.to_thread` instead. This is a pure scheduling change: `to_thread` re-raises the worker's exception in the awaiting frame, so the existing `except Exception` -> 401 `upstream_auth_failed` mapping is unchanged, and the adapters' own `self._lock` still serialises concurrent resolutions exactly as before. Fixing it at the handler also leaves the synchronous `UpstreamAdapter` ABC untouched, so it covers every adapter without conflicting with in-flight work that subclasses it.