…recv-task exception
Closes #411, closes #413. Also documents #405 (closed by-design) in-code.
#411 — the recv-loop stale-sid drop gate fired only for orderbook frame types,
but `order_group_updates` is also sequenced (carries a required `seq`). A stale
order_group_updates frame for an unmapped/recycled sid therefore skipped the
drop and reached `track_sync`, re-arming the watermark for a dead sid and
manufacturing a spurious gap on a later subscription. Renamed `_ORDERBOOK_TYPES`
→ `_SEQUENCED_TYPES` (adds `order_group_updates`) and gate on it.
#413 — prediction `KalshiWebSocket._stop()` skipped its recv-task block when the
task was already done, so a recv loop that exited with an exception (e.g. a
permanent close code) left the exception unretrieved → asyncio "Task exception
was never retrieved" on GC. Added an `elif` that retrieves it. (This fixes the
released prediction WS; the perps `PerpsWebSocket._stop()` already got the same
fix.)
#405 — added an in-code comment at the `_wait_for_response` discard site
explaining the by-design rationale (resubscribe stashes; normal commands rely on
sequence-gap detection), per the issue's "keep by-design + document" resolution.
Tests: stale order_group_updates for an unmapped sid does not arm the watermark;
_stop retrieves a dead recv-task's exception (verified the test fails without the
fix). mypy strict + ruff + 549 WS/drift tests green.
Cluster A of the post-review follow-ups: two WS correctness bugs + the #405 in-code documentation.
#411 — stale-sid drop gate missed
order_group_updatesThe recv-loop stale-sid drop gate in
_process_framefired only for_ORDERBOOK_TYPES, butorder_group_updatesis also sequenced (its envelope carries a requiredseq). So a staleorder_group_updatesframe for an unmapped/recycled sid skipped the drop and reachedtrack_sync(sid, seq), re-arming the watermark for a dead sid and manufacturing a spurious sequence gap on a later subscription to that sid.Fix: renamed
_ORDERBOOK_TYPES→_SEQUENCED_TYPES(=orderbook_snapshot,orderbook_delta,order_group_updates) and gate on it. Non-sequenced channels (ticker/trade/fill/user_orders) carry noseqand are handled at the dispatch layer, so they're unaffected.#413 —
_stop()left a dead recv-task's exception unretrievedPrediction
KalshiWebSocket._stop()skipped its recv-task block when the task was alreadydone(), so a recv loop that exited with an exception (e.g. a permanent close code) left the stored exception unretrieved → asyncio logs "Task exception was never retrieved" on GC.Fix: added an⚠️ This fixes the released prediction WS (the perps
elif self._recv_task is not None: … self._recv_task.exception()branch.PerpsWebSocket._stop()already received the same fix in the merged perps PR).#405 — documented in-code (already closed by-design)
Added a comment at the
_wait_for_responsediscard site explaining the rationale: frames are stashed duringresubscribe_all(rebuilding a book with no baseline) but discarded during normal commands, where a dropped sequenced frame surfaces as a sequence gap and is recovered — not silently lost.Verification
mypy --strict+ruffclean; 549 WS + drift tests pass.order_group_updatesfor an unmapped sid does not arm the watermark (perps: stale-sid drop guard misses order_group_updates (re-arms seq watermark for unmapped sids) #411);_stopretrieves a dead recv-task's exception (ws: KalshiWebSocket._stop() never retrieves a recv-loop exception (asyncio "Task exception was never retrieved") #413) — verified the ws: KalshiWebSocket._stop() never retrieves a recv-loop exception (asyncio "Task exception was never retrieved") #413 test fails without the fix (non-vacuous).🤖 Generated with Claude Code