Repository navigation
InMemorySessionService retains appended Event references, allowing later mutations to change saved history and state聽#7701
Description
馃敶 Required Information
Describe the Bug:
With the default deep-copy configuration, InMemorySessionService.append_event() retains the caller's mutable Event and nested state-delta values. Mutating that event after the append returns changes the stored conversation history and session state without another append. A subsequent get_session() returns those changed values.
Steps to Reproduce:
- Check out
mainat48d18a6aea65ed399bf036f29e383219ec167b3b(ADK 2.12.0) with runtime dependencies installed. - Save the code below as
repro.pyand runPYTHONPATH=src python repro.py. - Compare the in-memory result with the SQLite control. No model, credentials, or network are used.
Expected Behavior:
In the default deep-copy mode, a completed append should preserve the saved event and state. Changing the caller's retained event should leave the saved answer as original answer and the saved cart as {'items': ['coffee']}.
Observed Behavior:
The in-memory service reloads changed after append and {'items': ['coffee', 'tea']}. The SQLite control reloads the original answer and cart.
Environment Details:
- ADK Library Version: source 2.12.0 at the commit above.
- Desktop OS: macOS 27.0.1, arm64.
- Python Version: 3.13.16; dependencies installed from cache offline.
Model Information:
- LiteLLM: No.
- Model: N/A; session-service calls only.
馃煛 Optional Information
Regression: Unknown; no earlier release was tested.
Logs:
InMemorySessionService: text='changed after append', cart={'items': ['coffee', 'tea']}
SqliteSessionService: text='original answer', cart={'items': ['coffee']}
Screenshots / Video: N/A.
Additional Context:
This is the append boundary, separate from #7676's initial create_session(state=...) snapshot and #7105's copies of scoped state returned to callers. It also reproduces on #7676's head 110262ad33f2c82b6eaa47faca9d6931e0566782. #7454 concerns duplicate delivery to stale callers; this reproduction appends one event once. No fix is included.
Minimal Reproduction Code:
import asyncio
from google.adk.events import Event
from google.adk.events.event_actions import EventActions
from google.adk.features import FeatureName, override_feature_enabled
from google.adk.sessions import InMemorySessionService
from google.adk.sessions.sqlite_session_service import SqliteSessionService
from google.genai import types
async def check(service):
try:
session = await service.create_session(app_name="app", user_id="user")
event = Event(
author="assistant",
content=types.Content(
role="model", parts=[types.Part(text="original answer")]
),
actions=EventActions(state_delta={"cart": {"items": ["coffee"]}}),
)
await service.append_event(session, event)
event.content.parts[0].text = "changed after append"
event.actions.state_delta["cart"]["items"].append("tea")
stored = await service.get_session(
app_name="app", user_id="user", session_id=session.id
)
text = stored.events[0].content.parts[0].text
print(f"{type(service).__name__}: text={text!r}, cart={stored.state['cart']}")
finally:
if isinstance(service, SqliteSessionService):
await service.close()
async def main():
override_feature_enabled(FeatureName.IN_MEMORY_SESSION_SERVICE_LIGHT_COPY, False)
await check(InMemorySessionService())
await check(SqliteSessionService(":memory:"))
asyncio.run(main())How often has this issue occurred?: Always with the reproduction above.
Activity
widechaos commentedon Oct 23, 2026
May I take this on if nobody is already working on it? I reproduced the append-boundary mutation and have a local candidate that snapshots the event after temporary state is trimmed, then uses that snapshot for internal history and persistent scoped state. The caller鈥檚 event/return value and light-copy reference sharing stay unchanged.
An independent source-revert check gives four failures on main and 22 passes with the candidate, covering nested session/user/app state, SQLite controls, partial events, temporary non-copyable values, and the canonical-session append path. The related session/Runner suite passes 1041 tests (one skipped, three xfailed).
One compatibility point needs confirmation: persistent payloads that cannot be deep-copied would fail in default mode at append rather than later at get/list. Light-copy still accepts them. Is that boundary consistent with the intended default snapshot contract? I have not submitted a PR; full supported-Python tox validation remains outstanding.