Context
Surfaced by the claude[bot] review of PR #403. The perps PerpsWebSocket._stop() had this exact bug and was fixed on the perps branch (feat/perps-api); the prediction-API KalshiWebSocket._stop() has the identical structure and should be fixed in tandem. Kept out of the perps PR to preserve its scope.
Problem
kalshi/ws/client.py _stop() (lines ~209-217):
if self._recv_task and not self._recv_task.done():
with contextlib.suppress(asyncio.CancelledError, Exception):
await asyncio.wait_for(self._recv_task, timeout=2.0)
if not self._recv_task.done():
self._recv_task.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await self._recv_task
When _recv_loop exits with an unhandled exception (e.g. a permanent-close-code KalshiConnectionError, or the re-raise in the catchall handler), the task is already done by the time _stop() runs. The not self._recv_task.done() guard is False, so the whole block is skipped and the stored exception is never retrieved. asyncio then logs "Task exception was never retrieved" when the task is garbage-collected.
Proposed fix
Add an elif to retrieve the exception when the task is already finished (mirrors the perps fix on feat/perps-api):
elif self._recv_task is not None:
# Task already finished (e.g. recv loop raised on a permanent close);
# retrieve the stored exception so asyncio doesn't log
# "Task exception was never retrieved" when it is GC'd.
with contextlib.suppress(asyncio.CancelledError, Exception):
self._recv_task.exception()
Acceptance criteria
Notes
Identical one-line-class fix as the perps PerpsWebSocket._stop() change (commit on feat/perps-api). Low risk; defensive only.
Context
Surfaced by the
claude[bot]review of PR #403. The perpsPerpsWebSocket._stop()had this exact bug and was fixed on the perps branch (feat/perps-api); the prediction-APIKalshiWebSocket._stop()has the identical structure and should be fixed in tandem. Kept out of the perps PR to preserve its scope.Problem
kalshi/ws/client.py_stop()(lines ~209-217):When
_recv_loopexits with an unhandled exception (e.g. a permanent-close-codeKalshiConnectionError, or the re-raise in the catchall handler), the task is already done by the time_stop()runs. Thenot self._recv_task.done()guard isFalse, so the whole block is skipped and the stored exception is never retrieved. asyncio then logs "Task exception was never retrieved" when the task is garbage-collected.Proposed fix
Add an
elifto retrieve the exception when the task is already finished (mirrors the perps fix onfeat/perps-api):Acceptance criteria
KalshiWebSocket._stop()retrieves the exception of an already-done recv task._stop()(assert viacaplog/warnings or a stored-exception check).Notes
Identical one-line-class fix as the perps
PerpsWebSocket._stop()change (commit onfeat/perps-api). Low risk; defensive only.