Review round 3 advisory (gpt-6-astra): ws._dispatch_extension_message still
called a synchronous in-process handler directly on the event loop, so one
skill's blocking callback (its barrier wait included) stalled unrelated HTTP
and WebSocket work. The WS dispatcher now mirrors the HTTP one: a coroutine
handler is awaited, a synchronous one runs through asyncio.to_thread.
Regression tests/test_ws_dispatch_off_loop.py: a heartbeat coroutine keeps
ticking while the handler blocks, and the handler observes the release the
loop sets afterwards (fails on the previous tree).