You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(live): prevent zombie WebSocket session after LiveRequestQueue.close()
Calling `LiveRequestQueue.close()` is the documented way to shut down a live
session from the client side. However, `run_live()`'s `while True:` reconnect
loop had no awareness of this intentional shutdown: when the resulting
APIError(1000) / ConnectionClosed event arrived it would either reconnect (if a
session-resumption handle was present) or raise a spurious error (if no handle
was present), in both cases creating a long-lived zombie WebSocket connection
that Gemini eventually terminates after ~2 hours with a 1006 error.
Fix
---
* Add `is_closed: bool` property to `LiveRequestQueue` backed by a simple
boolean flag that is set synchronously in `close()` *before* the sentinel is
enqueued. The synchronous flag avoids any asyncio scheduling race: by the
time any connection-close exception reaches `run_live()`'s handlers, the
flag is already True.
* In `run_live()`, check `live_request_queue.is_closed` in both the
`ConnectionClosed` and `APIError(1000)` exception handlers. When the queue
is closed, log an info message and `return` instead of reconnecting or
raising. A trailing guard at the bottom of the loop body covers the (less
common) case where the receive generator returns normally without raising.
Behaviour after this fix
------------------------
| Scenario | Before | After |
|--------------------------------------------|---------------|-----------|
| `close()` called, no session handle | raises error | terminates cleanly |
| `close()` called, session handle present | reconnects | terminates cleanly |
| Network drop, session handle present | reconnects | reconnects (unchanged) |
| Network drop, no session handle | raises | raises (unchanged) |
Tests
-----
* `test_is_closed_initially_false` — property starts False
* `test_is_closed_true_after_close` — property becomes True after close()
* `test_is_closed_not_affected_by_other_sends` — other sends don't set it
* `test_run_live_no_reconnect_after_queue_close_api_error_1000` — APIError(1000)
after close() → terminates, connect called once
* `test_run_live_no_reconnect_after_queue_close_connection_closed` — same for
ConnectionClosed variant
* `test_run_live_still_reconnects_on_unintentional_drop_with_handle` — genuine
network drop without close() still reconnects (regression guard)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 commit comments