Skip to content

fix(sessions): stop returned sessions aliasing scoped state - #7105

Open
VedanthB wants to merge 1 commit into
google:mainfrom
VedanthB:fix-inmemory-scoped-state-copy
Open

fix(sessions): stop returned sessions aliasing scoped state#7105
VedanthB wants to merge 1 commit into
google:mainfrom
VedanthB:fix-inmemory-scoped-state-copy

Conversation

@VedanthB

Copy link
Copy Markdown

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:

InMemorySessionService copies its stored Session before returning it, but
_merge_state() then inserts app- and user-scoped nested values into the copy
by reference.

With the default deep-copy configuration, mutating a nested app: or user:
value on a session returned by create_session(), get_session(), or
list_sessions() therefore mutates service storage. A later session observes
the change even though no append_event() or state delta occurred.

Steps to Reproduce:

On current main, run:

import asyncio

from google.adk.sessions import InMemorySessionService


async def main() -> None:
  service = InMemorySessionService()
  returned = await service.create_session(
      app_name='app',
      user_id='user',
      session_id='first',
      state={
          'app:config': {'theme': 'light'},
          'user:profile': {'name': 'Alice'},
      },
  )

  returned.state['app:config']['theme'] = 'dark'
  returned.state['user:profile']['name'] = 'Mallory'

  later = await service.create_session(
      app_name='app', user_id='user', session_id='second'
  )
  print(later.state['app:config'])
  print(later.state['user:profile'])


asyncio.run(main())

Save the snippet as /tmp/repro.py and run python /tmp/repro.py.

Expected Behavior:

Mutating a returned deep-copied session does not mutate stored scoped state. A
later session prints:

{'theme': 'light'}
{'name': 'Alice'}

Observed Behavior:

Current main prints:

{'theme': 'dark'}
{'name': 'Mallory'}

Environment Details:

  • ADK: current main at 37aa7308 (2.9.0)
  • OS: macOS 26.6.1
  • Python: 3.11.15

Model Information:

  • LiteLLM: No
  • Model: N/A; no model is used

Frequency: Always (100%)

Solution:

Copy app and user state using the same configured depth as the session itself
before merging it into a returned session. Default mode deep-copies scoped
state; IN_MEMORY_SESSION_SERVICE_LIGHT_COPY retains its intentional shallow
copy behavior.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally. (Two environment-specific failures in the
    full matrix reproduce identically on unmodified main; details below.)

Added parameterized regression coverage for create_session(),
get_session(), and list_sessions(), with nested app- and user-scoped values
under both copy modes.

  • Unmodified main plus the regression test: 3 failed, 3 passed — exactly
    the three default-copy cases failed; all light-copy cases passed.
  • Patched regression test: 6 passed.
  • Focused copy-depth tests: 8 passed.
  • Complete session-service module: 271 passed, 3 xfailed.
  • Complete sessions directory: 484 passed, 3 xfailed.
  • Five-version tox matrix: Python 3.10, 3.11, and 3.13 passed in full
    (14,627, 14,634, and 14,627 passed). Python 3.12 and 3.14 each
    completed 14,625 passing tests with two unrelated import-loading failures:
    this machine's sitecustomize module appeared outside the allowlist. Both
    failures reproduce identically on unmodified main under the same tox
    environments.
  • Changed-file pre-commit hooks: all passed.
  • CI-style mypy comparison: 740 baseline errors, 740 patched errors, zero
    new errors.
  • uv build: source distribution and wheel built successfully.

Manual End-to-End (E2E) Tests:

The keyless public-API reproduction creates a session with nested app: and
user: values, mutates the returned object, and then creates a second session.
On main, the second session exposes the mutations. With this change, default
mode retains the original values, while light-copy mode continues to share
nested objects by design. No model, credentials, or network access is required.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code where the copy-depth contract is not obvious.
  • I have added tests that prove the fix is effective.
  • New and existing unit tests pass locally with my changes. (Two
    environment-specific failures reproduce identically on unmodified main.)
  • I have manually tested the public behavior end to end.
  • Any dependent changes have been merged and published in downstream
    modules. (N/A; this change has no dependencies.)

Additional context

This is an internal copy-isolation fix. It does not change public API
signatures or surface and does not require an adk-docs update.

AI assistance disclosure

OpenAI Codex assisted with implementation, test preparation, and review.
Patched results were run against commit 2234ac92; baseline comparisons were
run against unmodified main at 37aa7308 under matching environments.

App- and user-scoped values were merged into returned in-memory sessions after the session copy, preserving nested references to stored state.

Copy scoped dictionaries at the same configured depth so default reads cannot mutate storage while light-copy mode retains intentional aliasing.
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