Repository navigation
FirestoreSessionService ignores app: / user: state prefixes, so app and user state are never shared across sessions聽#1645
Description
馃敶 Required Information
Describe the Bug:
FirestoreSessionService does not implement ADK's scoped state. ADK's State class defines app:, user: and temp: prefixes (State.APP_PREFIX, USER_PREFIX, TEMP_PREFIX), and InMemorySessionService stores app:/user: keys in shared app/user stores and merges them back into every session. FirestoreSessionService behaves differently:
appendEventonly routes keys starting with_app_/_user_to theapp-state/user-statecollections. Keys written through the normal ADK API (app:theme,user:name) fall through to the per-sessionstatefield, so they are only visible in that one session.- The
app-stateanduser-statecollections are write-only.getSession,createSessionandlistSessionsnever read them, so even a value stored under_app_/_user_never reaches another session. temp:keys are persisted in the session document.BaseSessionService.appendEventskips them.- Removal checks
value == null, but ADK marks removals with theState.REMOVEDsentinel, so a removed key is kept and the sentinel object is written to the session'sstatemap instead of the key being deleted.
Code references (main @ 189d463):
contrib/firestore-session-service/.../FirestoreSessionService.javaappendEvent(prefix split around L657-L689, removal check L666)getSessionbuilds the session only from the session document'sstatefield (around L262-L272)- Compare
core/.../InMemorySessionService.javaappendEvent(L316-L338) andmergeWithGlobalState(L398)
Steps to Reproduce:
- Configure a runner with
FirestoreSessionService. - In session A, a tool or callback sets
context.state().put("user:name", "Jae"). - Create session B for the same app and user, and read
user:name.
The same can be shown with the module's existing mocked tests (no Firestore needed): appending an event with state delta {"app:theme": "dark", "user:name": "Jae"} never calls set on the app-state / user-state documents, and the session document is updated with state = {app:theme=dark, user:name=Jae}. Appending {"temp:scratch": "x", "gone": State.REMOVED} writes both temp:scratch and the REMOVED sentinel into the session's state.
Expected Behavior:
Same semantics as InMemorySessionService (and the Python ADK database session services): app: keys shared by all sessions of the app, user: keys shared by all sessions of the app and user, merged back into Session.state() on getSession / createSession / listSessions, temp: keys not persisted, and State.REMOVED deleting the key.
Observed Behavior:
In session B, user:name is missing. App/user state behaves like session state. temp: values persist, and removed keys are not deleted.
Environment Details:
- ADK Library Version: main @ 189d463 (1.11.1-SNAPSHOT),
google-adk-firestore-session-service - OS: Linux
- TS Version: N/A
Model Information:
- N/A (session service bug, model independent)
馃煛 Optional Information
Regression: No. The _app_ / _user_ handling has been there since the Firestore service was added.
Additional Context:
A fix would (a) split on State.APP_PREFIX / State.USER_PREFIX, (b) merge the app-state / user-state documents back into the session state with the prefixes on read, (c) skip State.TEMP_PREFIX, and (d) treat State.REMOVED as a delete (FieldValue.delete() in the shared stores). Existing data stored under _app_ / _user_ keys (if anyone relied on that convention) may need a note in the changelog. I'm happy to send a PR if this direction looks right.
Activity
hemasekhar-p commentedon Oct 12, 2026
Hi @innoprej, thank you for reporting this issue. I have been able to reproduce this behavior using your inputs and have provided a fix in this PR. I hope this will resolves your problem?
innoprej commentedon Oct 12, 2026
Thanks for the quick fix, @hemasekhar-p!
I checked out #1650 (
1329546d) and ranFirestoreSessionServiceTest. The write side looks right to me:app:/user:keys now go to theapp-state/user-statedocuments,temp:keys in the delta are skipped, andState.REMOVEDdeletes the key (FieldValue.delete()in the shared documents). Your four new or updatedappendEventtests pass on the PR and fail onmain(189d463a).It doesn't resolve this issue yet, though, because nothing reads those documents back.
getSession,createSessionandlistSessionsnever look atapp-state/user-state(getSessionbuilds the state from the session document only,listSessionsreturns an empty state), so the steps in the issue still end withuser:namemissing in session B. And with #1650 alone, an event that only setsapp:/user:keys no longer updates the session document'sstate. So if a tool setsuser:nameand nothing later in that turn changes a session key, the nextgetSessionof the same session (the runner's next turn) doesn't return it. Onmainthat case worked, because the key fell through to the session state.What I ran, with mocked Firestore in the same test class (no emulator):
InMemorySessionService?mainlistSessionsseeapp:theme/user:namestored in the shared documentsuser:name(no other state change in that turn), still has itTo fix the read side,
getSession,createSessionandlistSessionswould need to merge the two documents back with the prefixes, likeInMemorySessionService.mergeWithGlobalState(here) or_merge_statein the Python Firestore service (here).appendEventshould probably also applyapp:/user:delta values to the in-memorysession.state(), asBaseSessionService.appendEventdoes. #1650 now only sends them to the shared documents, so a value that arrives only throughEventActions.stateDelta()is no longer visible later in the same invocation.One smaller gap the table doesn't cover:
Context.state()writes through tosession.state()(Context), andappendEventstill writes the whole map withupdate("state", session.state())(here). Sotemp:keys (andapp:/user:keys) set through the context state end up in the session document whenever that event or a later one in the same invocation changes a session key. The Python Firestore service keeps the two apart: it applies the delta to the in-memorysession.state(here), as suggested above, but filters these prefixes out before it writes the session document (here).InMemorySessionServicekeeps them too, since it stores the runner's session object, so this may be better handled separately.Would you like to add the read side to #1650, or should I send a follow-up PR after it's merged? Since #1650 says
Closes: #1645, could this issue stay open until the read side lands? I'm happy to share the mocked tests I used.