Skip to content

fix(live): stop a live run from writing session state onto the RunConfig - #6774

Closed
LHMQ878 wants to merge 1 commit into
google:mainfrom
LHMQ878:fix/live-run-config-not-mutated
Closed

fix(live): stop a live run from writing session state onto the RunConfig#6774
LHMQ878 wants to merge 1 commit into
google:mainfrom
LHMQ878:fix/live-run-config-not-mutated

Conversation

@LHMQ878

@LHMQ878 LHMQ878 commented Aug 17, 2026

Copy link
Copy Markdown

Link to Issue or Description of Change

Related: #6765 is unrelated; this one has no issue, so the problem and solution are described below. The code path is the one added in eac32c3 (feat(live): use RunConfig.session_resumption.handle when opening a live session).

Problem:

_build_basic_request forwards two caller-owned objects to the live connect config by reference:

llm_request.live_connect_config.session_resumption = run_config.session_resumption
llm_request.live_connect_config.history_config = run_config.history_config

The connect loop in BaseLlmFlow.run_live then writes onto whatever object it finds there:

  • live_connect_config.session_resumption.handle = invocation_context.live_session_resumption_handle — the newest server-issued handle, on every reconnect;
  • session_resumption.transparent = True — the Vertex AI backend default;
  • history_config.initial_history_in_client_content = True — when it replays history.

All three land on the caller's own RunConfig. So a caller that reuses its RunConfig — the natural thing to do when the point of RunConfig.session_resumption.handle is to resume across runs — starts the next run resuming the session that just ended, with a handle it never set. Concretely, after one run that reconnected once:

run_config = RunConfig(
    session_resumption=types.SessionResumptionConfig(handle='caller_handle')
)
# ... one run_live over this config, server issued 'server_handle' ...
run_config.session_resumption
# SessionResumptionConfig(handle='server_handle', transparent=True)   <-- was handle='caller_handle'

transparent=True is the same kind of leak in the other direction: it is set only for the Vertex AI backend, and the Gemini API backend "explicitly rejects it" (per the comment at that site), so it sticks to a config that is later used against a backend that refuses it.

Solution:

Forward a copy of each instead of the caller's object. This is exactly what http_options already does a few lines above, for the same reason — see _merge_run_config_http_options: "The RunConfig's options are copied in rather than aliased, so request assembly cannot write back into the RunConfig."

model_copy() (shallow) is enough: both objects hold only scalars, and the flow only sets top-level fields on them.

Nothing reads these back off the RunConfig during a run — the seed added in eac32c3 deliberately reads llm_request.live_connect_config.session_resumption, which is now the copy the reconnect path writes, so the seed and the writes stay on the same object. Agent transfer is unaffected: it clears the handle on a deep=True copy of the run config, and the child's own request assembly copies from there.

Testing Plan

Unit Tests:

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

Three tests, each failing on main:

  • test_basic_processor.py::test_run_config_live_session_objects_are_not_aliased — assembly level: after the processor runs, writing the handle / transparent / initial_history_in_client_content on the request must not show up on the RunConfig objects.
  • test_base_llm_flow.py::test_run_live_does_not_write_the_session_handle_onto_the_run_config — end to end: caller supplies handle='caller_handle', the server issues 'server_handle', the connection drops, and the reconnect uses 'server_handle' while run_config.session_resumption stays SessionResumptionConfig(handle='caller_handle'). Runs with the Vertex AI backend so it covers the transparent write too.
  • test_base_llm_flow.py::test_run_live_does_not_write_initial_history_onto_the_run_config — end to end: the connect request declares initial_history_in_client_content=True while run_config.history_config stays HistoryConfig().

On main:

FAILED tests/unittests/flows/llm_flows/test_base_llm_flow.py::test_run_live_does_not_write_the_session_handle_onto_the_run_config
  assert SessionResump...sparent=True\n) == SessionResump...ller_handle'\n)
FAILED tests/unittests/flows/llm_flows/test_base_llm_flow.py::test_run_live_does_not_write_initial_history_onto_the_run_config
  assert HistoryConfig...content=True\n) == HistoryConfig()
FAILED tests/unittests/flows/llm_flows/test_basic_processor.py::TestBasicLlmRequestProcessor::test_run_config_live_session_objects_are_not_aliased

With the change:

$ pytest tests/unittests/flows/ -q -p no:randomly
572 passed, 1 xfailed in 100.78s

$ pytest tests/unittests/flows/llm_flows/test_base_llm_flow.py -q -p no:randomly
85 passed

$ pytest tests/unittests/flows/llm_flows/test_basic_processor.py -q -p no:randomly
21 passed

$ pytest tests/unittests/streaming/ tests/unittests/cli/test_adk_web_server_run_live.py \
         tests/unittests/models/test_gemini_llm_connection.py -q -p no:randomly
119 passed

pre-commit run --files <changed files> passes (isort, pyink, ruff, addlicense, codespell, ADK compliance checks).

Manual End-to-End (E2E) Tests:

Not run against a live model — reproducing the leak needs a server-issued session_resumption_update followed by a dropped socket, which is what the second test simulates. The observable symptom is the RunConfig printed above: handle='server_handle', transparent=True on an object the caller created with handle='caller_handle'.

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. (see above — covered by simulation instead)
  • Any dependent changes have been merged and published in downstream modules.

The basic request processor forwards `RunConfig.session_resumption` and
`RunConfig.history_config` to `LiveConnectConfig` by reference, and the connect
loop in `BaseLlmFlow.run_live` then writes onto whatever object it finds there:
the newest server-issued resumption handle on every reconnect, `transparent`
on the Vertex AI backend, and `initial_history_in_client_content` when it
replays history. All of that lands on the caller's own RunConfig, so a caller
that reuses it for the next run resumes the session that just ended instead of
starting a new one.

Forward a copy of both, which is what `http_options` already does for the same
reason. Agent transfer is unaffected: it clears the handle on a deep-copied
run config, and the child's request assembly copies from there.
@wuliang229

Copy link
Copy Markdown
Collaborator

This is already handled by a pending commit. Thanks.

@wuliang229 wuliang229 closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants