Skip to content

Session resumption reconnection loop in run_live() never iterates #4996

Closed

Description

@fparga

🔴 Required Information

Describe the Bug:

The session resumption reconnection loop in base_llm_flow.py's run_live() never iterates. The while True: loop at line 481 and the handle-injection code at lines 484–496 are unreachable because both exception handlers (lines 595 and 600) re-raise unconditionally. Additionally, goAway messages from the server are silently dropped at the connection layer, preventing proactive reconnection before the server terminates the connection.

The net effect is that session resumption handles are received and stored (via _receive_from_model → invocation_context.live_session_resumption_handle) but never used. After the ~10-minute connection lifetime expires, run_live() terminates with an error instead of reconnecting with the saved handle.

Steps to Reproduce:

  1. Configure a live agent with RunConfig(session_resumption=types.SessionResumptionConfig(transparent=True))
  2. Start a run_live() session with a live-only model (e.g. gemini-2.5-flash-live)
  3. Verify that SessionResumptionUpdate messages are received (visible at DEBUG level: "Update session resumption handle: ...")
  4. Trigger any WebSocket disconnect, either:
    • Wait ~10 minutes for the server's connection lifetime limit, or
    • Simulate a network interruption
  5. Observe that run_live() raises an error instead of reconnecting with the saved handle
  6. The log message "Attempting to reconnect (Attempt N)..." (line 485) is never emitted

Expected Behavior:

When the WebSocket connection closes (after goAway or session timeout):

  1. The while True: loop should catch the exception and continue
  2. On the next iteration, the saved live_session_resumption_handle should be injected into the setup config (the code at lines 484–496 already does this)
  3. A new connection should be opened with the handle, restoring the session without replaying history
  4. run_live() should continue yielding events seamlessly

Observed Behavior:

The connection dies and run_live() raises an exception. The reconnection loop never gets a second iteration. There are two disconnect paths, both broken:

Path A: Disconnect during receive (most common):
The genai SDK's _receive() (live.py:538) catches ConnectionClosed from the websockets library and converts it to APIError:

# google/genai/live.py:538-545
except ConnectionClosed as e:
    if e.rcvd:
        code = e.rcvd.code
        reason = e.rcvd.reason
    else:
        code = 1006
        reason = ...
    errors.APIError.raise_error(code, reason, None)

This APIError propagates through _receive_from_model and hits except Exception at line 600, which re-raises. The except (ConnectionClosed, ConnectionClosedOK) at line 595 never fires on this path because the exception is APIError, not ConnectionClosed.

Path B: Disconnect during send:
The genai SDK's send methods (send_client_content, send_tool_response, etc.) call self._ws.send() without catching ConnectionClosed. The raw ConnectionClosed propagates through the send_task's finally cleanup and hits except (ConnectionClosed, ConnectionClosedOK) at line 595, which also re-raises.

Separately, goAway is unhandled:
gemini_llm_connection.py's receive() method processes message.server_content, message.tool_call, message.usage_metadata, and message.session_resumption_update but has no if message.go_away: check. The LiveServerGoAway event (sent ~60s before server termination) is silently dropped, preventing proactive reconnection.

Environment Details:

  • ADK Library Version: 1.27.2
  • Desktop OS: macOS (Darwin 24.6.0)
  • Python Version: 3.12

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-live-2.5-flash-native-audio

🟡 Optional Information

Regression:

No, the reconnection plumbing was added in PR #2270 (fixing #2179) but the exception handling was never updated to actually loop. The handle injection code at lines 484–496 has been unreachable since it was introduced.

Logs:

When the connection times out, the following is logged:

ERROR    An unexpected error occurred in live flow: 1000 None. connection closed

The "Attempting to reconnect" log message at line 485 is never reached.

Additional Context:

The code structure suggests the intent was for clean WebSocket closes to end the _receive_from_model generator naturally (the comment at line 596 says "when the session timeout, it will just close and not throw exception"), allowing the while True: loop to iterate. However, the genai SDK's _receive() converts ALL ConnectionClosed exceptions (including ConnectionClosedOK / code 1000) into APIError, so the "clean end" path never occurs.

_receive_from_model does have an except ConnectionClosedOK: pass at line 744, which would enable the clean-end path but APIError is not ConnectionClosedOK, so it doesn't fire for connection closes that come through _receive().

Suggested fix:

There are three changes needed:

  1. Fix the reconnection loop In run_live(), catch the genai SDK's APIError wrapping ConnectionClosed and continue instead of raise when a session resumption handle is available:
except (ConnectionClosed, ConnectionClosedOK) as e:
    if invocation_context.live_session_resumption_handle:
        logger.info('Connection closed (%s), reconnecting with session handle.', e)
        continue
    raise
except Exception as e:
    if (
        invocation_context.live_session_resumption_handle
        and isinstance(e, APIError)
    ):
        logger.info('Connection lost (%s), reconnecting with session handle.', e)
        continue
    raise
  1. Skip send_history on reconnection When reconnecting with a handle, the server already has the session context. Add a guard at line 503:
if llm_request.contents and not invocation_context.live_session_resumption_handle:
    await llm_connection.send_history(llm_request.contents)
  1. Handle goAway In gemini_llm_connection.py's receive(), surface message.go_away so the flow layer can proactively reconnect before the server terminates the connection.

Related issues / PRs:

How often has this issue occurred?:

  • Always (100%) the reconnection loop has never successfully iterated.

Activity

  1. added
    live[Component] This issue is related to live, voice and video chat
    on Mar 25, 2026
  2. added theissue type on Mar 25, 2026
  3. surajksharma07 commented on Mar 26, 2026

    @surajksharma07
    Collaborator

    @fparga Thanks for the thorough breakdown — the root cause is clearly the unconditional raise in both exception handlers at lines 595 and 600 of base_llm_flow.py.

    As a workaround, patching the except Exception block in run_live() to continue instead of raise when live_session_resumption_handle is set should restore the reconnection loop — could you try this against your session timeout or network interruption scenario and confirm it holds up on your end?

    Once you've verified the workaround is functional and stable, it'd be worth also covering:

    1. The send_history guard (to skip history replay on reconnect)
    2. The go_away handler

    ...before raising a PR, so the fix is complete.

    PR #5007 is already tracking this — but your confirmation on the workaround first would really help validate the approach before it moves forward.

  4. fparga commented on Mar 27, 2026

    @fparga
    Author

    @surajksharma07 PR #5007 covers all three parts verified locally with end-to-end testing. Session resumption handles arrive correctly, send_history is skipped on reconnection, and the model retains full context across WebSocket reconnections.

    One related gap not in #5007: _postprocess_live silently drops events that carry only live_session_resumption_update. The skip guard doesn't check for it. This means applications consuming events from run_live() can never see resumption handles externally. PR #4358 addresses this separately, but it's worth noting the two PRs are complementary: #5007 fixes internal reconnection, #4358 enables external handle access for cross-connection resumption.

  5. surajksharma07 commented on Mar 31, 2026

    @surajksharma07
    Collaborator

    @fparga all three parts covered and e2e verified is exactly the right bar for this, so that's good to hear.

    Nice catch on _postprocess_live silently dropping live_session_resumption_update-only events too — that's a real gap for any app that needs to manage handles externally across connections, so it's worth having on the radar.

    The pairing of #5007 (internal reconnection) + #4358 (external handle exposure) does look like the complete picture.

    If it isn't already called out in #5007's description, it'd be worth adding a note so reviewers know the two PRs are complementary and what's still pending on the other side.

  6. surajksharma07 commented on Mar 31, 2026

    @surajksharma07
    Collaborator

    @seanzhou1023 Please have a look into it once.

  7. wuliang229 commented on Apr 8, 2026

    @wuliang229
    Collaborator

    @fparga please try out with the commit above and let me know if you see any other issues.

  8. added a commit that references this issue on Apr 8, 2026
    6b1600f
  9. added a commit that references this issue on Apr 21, 2026
    2144038
  10. added a commit that references this issue on Aug 11, 2026
    2dd35e0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

live[Component] This issue is related to live, voice and video chatneeds review[Status] The PR/issue is awaiting review from the maintainer

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions