Skip to content

fix(live): keep the resumption handle through non-resumable updates - #7939

Closed
AtulJoshi1206 wants to merge 1 commit into
google:mainfrom
AtulJoshi1206:fix/live-keep-resumable-handle
Closed

AtulJoshi1206 wants to merge 1 commit into
google:mainfrom
AtulJoshi1206:fix/live-keep-resumable-handle

Conversation

@AtulJoshi1206

@AtulJoshi1206 AtulJoshi1206 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

No existing issue; described below following the bug-report structure.

Describe the Bug:

With session resumption on, the Live API sends session_resumption_update messages throughout the session. Some of them say the session can't be resumed at that moment. That happens while the model is generating or a tool call is running, and those updates carry no handle. The genai type documents it: "In that case we send update empty new_handle and resumable=false."

receive_from_model in live/_live_llm_flow.py stores new_handle from every update, so the first handle-less one replaces the good handle with None. Every reconnect path in run_live_flow depends on that stored handle, so for much of each turn the run behaves as if resumption were never configured. For comparison, the Live API session management guide only keeps the handle if update.resumable and update.new_handle.

Impact:

This affects every live app that turns on session resumption, which is the documented way to get past the ~10-minute connection limit and to survive network drops:

  • RunConfig(session_resumption=...);
  • /run_live with enable_session_resumption;
  • the LiveKit integration, which turns resumption on for every call by default.

A GoAway or a drop that lands while the agent is talking or running a tool loses the session. In a voice conversation the model is speaking or running tools a big share of the time, so long sessions hit this regularly.

Observed Behavior:

I scripted a live model behind Runner.run_live. It sends a resumable update with handle-1, then a non-resumable one (as happens once the model starts answering), and then the connection goes. On main:

Interruption What happens
GoAway It reconnects with no handle: a brand-new server session, with the pre-run history replayed into it. The live conversation is gone.
APIError 1006 / 1011, ConnectionClosed The run raises instead of resuming.
APIError 1000 The run ends as if the model had closed the session.

Without the non-resumable update in between, all four resume with handle-1, so the lost handle is the only difference.

Expected Behavior:

The run reconnects with handle-1, the last handle the server offered, and doesn't replay history into a session that already has it.

Solution:

The stored handle is now only replaced when the update actually carries one.

  • Resuming from the previous handle loses just the turn in flight, which is what the API documents for this case. That's a lot better than ending the run or starting over without the conversation.
  • The update events are still yielded and persisted as before, so apps that track handles themselves see no change.
  • This is the only place a server handle gets stored. The transfer and restart paths still clear it on purpose, since they start new sessions.

I check new_handle alone rather than resumable and new_handle like the docs sample, because resumable=false is the proto default and gets left off the wire, so the handle itself is the reliable signal.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

I added two tests to tests/unittests/live/test_live_llm_flow.py:

  • test_run_live_resumes_a_connection_lost_while_not_resumable goes through Runner.run_live with a scripted live model. It's parametrized over GoAway, 1006, 1011 and ConnectionClosed, and checks that the reconnect uses the last handle and skips the history replay.
  • test_receive_from_model_keeps_the_last_handle_the_server_offered covers the bookkeeping directly, for both shapes a handle-less update takes: resumable=False, and an update with only last_consumed_client_message_index.

All 6 cases fail on main and pass with the fix.

pytest tests/unittests/live tests/unittests/streaming tests/unittests/flows \
  tests/unittests/models tests/unittests/integrations/livekit tests/unittests/agents \
  tests/unittests/test_runners.py tests/unittests/workflow/test_workflow_live.py
main: 6 failed, 4777 passed, 2 xfailed   (the 6 new cases)
fix:  4783 passed, 2 xfailed

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (none)

Additional context

Two related things I noticed and left out to keep this focused:

  • With resumption off there's no handle at all, and a GoAway still reconnects into a fresh session with the pre-run history. Fixing that needs a decision on what should happen instead: end when the server closes, or restart from the session history.
  • The GoAway path doesn't check whether the client already closed the queue, while the two exception paths do.

Happy to send either as a follow-up.

🤖 Generated with Claude Code

The Live API also sends a session resumption update while the session
cannot be resumed, for example while the model is generating or a tool
call is in flight. That update carries no handle, and the live flow
stored it anyway, so for much of every turn the run held no handle at
all.

A GoAway or a dropped connection in that window then lost the session
even though resumption was configured: a 1006 or 1011 close, or a
ConnectionClosed, failed the run; a 1000 close ended it as if the model
had finished; and a GoAway reconnected without a handle, which opened a
new session seeded only with the history from before the run.

Replace the stored handle only when an update carries one. Resuming from
the last handle the server offered loses only the turn in flight, which
is what the Live API documents for resuming from an earlier handle.
copybara-service Bot pushed a commit that referenced this pull request Nov 4, 2026
Merge #7939

The Live API sends session_resumption_update messages with no handle while
the session cannot be resumed (the model is generating or a tool call is
running). The live flow stored new_handle from every update, so those wiped
the last good handle and a GoAway or dropped connection in that window
reconnected without resumption or ended the run. The stored handle is now
replaced only when an update carries one; the update events are still
yielded as before.

PiperOrigin-RevId: 1014061479
@adk-bot

adk-bot commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Thank you @AtulJoshi1206 for your contribution! 🎉

Your changes have been successfully imported and merged via Copybara in commit 02db0ff.

Closing this PR as the changes are now in the main branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged [Status] This PR is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants