session/inmemory: Create persists temp: keys from the initial state into the session record
Describe the bug
inMemoryService.Create splits the caller's initial CreateRequest.State with
sessionutils.ExtractStateDeltas — which already separates app:, user:,
temp: and session-scoped keys — but then discards the session-scoped delta
(_) and merges the raw state map into the stored session:
appDelta, userDelta, _ := sessionutils.ExtractStateDeltas(req.State)
appState := s.updateAppState(appDelta, req.AppName)
userState := s.updateUserState(userDelta, req.AppName, req.UserID)
val.state = sessionutils.MergeStates(appState, userState, state) // raw stateA temp: key in the initial state therefore lands in the canonical session
record and is returned by every later Get, even though temp: keys are
documented as invocation-scoped ("Discarded after the invocation completes",
session/session.go KeyPrefixTemp) and AppendEvent deliberately strips them
from the canonical event record (#1356) and from what it persists (#1514).
This also makes the same CreateRequest behave differently across backends:
session/database stores only sessionState (the extracted session-scoped
delta), and adk-python's _create_session_impl stores
state_deltas['session'] for the same reason. temp: is the only observable
difference, since app:/user: keys re-enter the record through the same
merge either way.
Where
session/inmemory.go, (*inMemoryService).Create — the MergeStates call at
~line 84 and the discarded third return of ExtractStateDeltas at ~line 76.
Impact
Any caller that seeds a session with temp: keys in CreateRequest.State
(e.g. restoring a saved state snapshot that contains invocation-scoped keys)
gets those keys back on every read for the life of the session on the
in-memory service — in State().Get, State().All, and anything that renders
state into prompts (instruction templating iterates session state). Switching
to session/database silently drops them instead, so behavior differs by
backend.
How to reproduce
svc := session.InMemoryService()
created, _ := svc.Create(ctx, &session.CreateRequest{
AppName: "app", UserID: "u",
State: map[string]any{"temp:scratch": "x", "keep": "y"},
})
got, _ := svc.Get(ctx, &session.GetRequest{
AppName: "app", UserID: "u", SessionID: created.Session.ID(),
})
v, err := got.Session.State().Get("temp:scratch")
// got: v == "x", err == nil — the temp key persisted
// want: err == session.ErrStateKeyNotExistThe regression test TestInMemoryService_Create_StripsTempKeysFromInitialState
in session/inmemory_test.go fails on the unfixed code with
temp key "temp:scratch" leaked into the stored session state.
Suggested fix
Use the session-scoped delta that Create already computes:
appDelta, userDelta, sessionDelta := sessionutils.ExtractStateDeltas(req.State)
...
val.state = sessionutils.MergeStates(appState, userState, sessionDelta)MergeStates re-adds app:/user: keys with their prefixes, so the merged
state is identical except that temp: keys no longer persist — matching
session/database and adk-python.
Source: google/adk-go