Skip to content

fix(sessions): snapshot appended state deltas in in-memory sessions - #7704

Open
kyo-zzz wants to merge 1 commit into
google:mainfrom
kyo-zzz:fix/in-memory-append-state-snapshot
Open

kyo-zzz wants to merge 1 commit into
google:mainfrom
kyo-zzz:fix/in-memory-append-state-snapshot

Conversation

@kyo-zzz

@kyo-zzz kyo-zzz commented Oct 23, 2026

Copy link
Copy Markdown

Link to Issue or Description of Change

Addresses the state-delta half of #7701 (the append boundary counterpart of #7676, which covers create_session).

Problem: InMemorySessionService.append_event() applies event.actions.state_delta to storage with plain .update() calls, so nested values from the caller's delta are stored by reference, even with the default deep-copy setting. Mutating the caller's delta after the append returns silently changes the saved session state — without another event — and the next get_session() returns the changed values. This affects session state as well as the app:/user: scoped state routed by extract_state_delta().

Reproduction (default feature settings, no model/network; from #7701):

service = InMemorySessionService()
session = await service.create_session(app_name="app", user_id="user")
event = Event(author="assistant", content=..., actions=EventActions(state_delta={"cart": {"items": ["coffee"]}}))
await service.append_event(session, event)
event.actions.state_delta["cart"]["items"].append("tea")
stored = await service.get_session(app_name="app", user_id="user", session_id=session.id)
print(stored.state["cart"])   # {'items': ['coffee', 'tea']} — should be {'items': ['coffee']}

The SqliteSessionService control in #7701 returns the unmutated value.

What changed

  • append_event() now routes the state delta through the existing _copy_state() helper before extract_state_delta(), mirroring what fix(sessions): snapshot initial state in in-memory sessions #7676 does for initial state in create_session(): under the default settings the delta values are deep-copied into storage; under IN_MEMORY_SESSION_SERVICE_LIGHT_COPY the top level alone is copied and values stay aliased, matching that mode's documented design. Temporary keys are trimmed before the copy, so uncopyable temp values are never traversed (same reasoning as in fix(sessions): snapshot initial state in in-memory sessions #7676).
  • Two new contract tests in the shared conformance suite:
    • test_append_event_does_not_retain_caller_state — passes on every registered backend with this change;
    • test_append_event_does_not_retain_caller_event — passes on the persistent backends; registered as a written divergence for both in-memory backends, because append deliberately stores the caller's event object (events may hold values that cannot be copied, and append must not pay copy costs — pinned by test_in_memory_list_sessions_omits_events_without_copying_them and test_in_memory_get_session_copies_only_selected_events). The divergence entry documents the remaining half of InMemorySessionService retains appended Event references, allowing later mutations to change saved history and state #7701 as a pickable defect; resolving it needs a design decision about copying events at append time, which I did not want to make unilaterally.
  • test_in_memory_append_event_state_uses_configured_copy_depth pins the new depth behavior in both modes and that the stored event itself stays aliased.

Tests

  • The reproduction above prints {'items': ['coffee']} with this change (and {'items': ['coffee', 'tea']} on main).
  • pytest tests/unittests/sessions/: 650 passed, 3 xfailed (the registered divergences), with two failures that pre-date this change and reproduce on unmodified main in my environment (missing optional agentplatform and spanner-dialect dependencies).
  • pyink and isort pass on the changed files.

AI usage disclosure

Developed with AI assistance (analysis, patch, tests); reviewed line by line, and I take full responsibility for correctness and design.

@google-cla

google-cla Bot commented Oct 23, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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