Skip to content

FirestoreSessionService ignores app: / user: state prefixes, so app and user state are never shared across sessions聽#1645

Description

@innoprej

馃敶 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:

  1. appendEvent only routes keys starting with _app_ / _user_ to the app-state / user-state collections. Keys written through the normal ADK API (app:theme, user:name) fall through to the per-session state field, so they are only visible in that one session.
  2. The app-state and user-state collections are write-only. getSession, createSession and listSessions never read them, so even a value stored under _app_ / _user_ never reaches another session.
  3. temp: keys are persisted in the session document. BaseSessionService.appendEvent skips them.
  4. Removal checks value == null, but ADK marks removals with the State.REMOVED sentinel, so a removed key is kept and the sentinel object is written to the session's state map instead of the key being deleted.

Code references (main @ 189d463):

  • contrib/firestore-session-service/.../FirestoreSessionService.java appendEvent (prefix split around L657-L689, removal check L666)
  • getSession builds the session only from the session document's state field (around L262-L272)
  • Compare core/.../InMemorySessionService.java appendEvent (L316-L338) and mergeWithGlobalState (L398)

Steps to Reproduce:

  1. Configure a runner with FirestoreSessionService.
  2. In session A, a tool or callback sets context.state().put("user:name", "Jae").
  3. 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

  1. self-assigned this
    on Oct 12, 2026
  2. added theissue type on Oct 12, 2026
  3. hemasekhar-p commented on Oct 12, 2026

    @hemasekhar-p
    Contributor

    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?

  4. added
    waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.
    on Oct 12, 2026
  5. innoprej commented on Oct 12, 2026

    @innoprej
    ContributorAuthor

    Thanks for the quick fix, @hemasekhar-p!

    I checked out #1650 (1329546d) and ran FirestoreSessionServiceTest. The write side looks right to me: app:/user: keys now go to the app-state/user-state documents, temp: keys in the delta are skipped, and State.REMOVED deletes the key (FieldValue.delete() in the shared documents). Your four new or updated appendEvent tests pass on the PR and fail on main (189d463a).

    It doesn't resolve this issue yet, though, because nothing reads those documents back. getSession, createSession and listSessions never look at app-state/user-state (getSession builds the state from the session document only, listSessions returns an empty state), so the steps in the issue still end with user:name missing in session B. And with #1650 alone, an event that only sets app:/user: keys no longer updates the session document's state. So if a tool sets user:name and nothing later in that turn changes a session key, the next getSession of the same session (the runner's next turn) doesn't return it. On main that case worked, because the key fell through to the session state.

    What I ran, with mocked Firestore in the same test class (no emulator):

    Behaves like InMemorySessionService? main #1650
    Session B, a new session and listSessions see app:theme / user:name stored in the shared documents no no
    Session A, loaded again after a tool set user:name (no other state change in that turn), still has it yes no

    To fix the read side, getSession, createSession and listSessions would need to merge the two documents back with the prefixes, like InMemorySessionService.mergeWithGlobalState (here) or _merge_state in the Python Firestore service (here). appendEvent should probably also apply app:/user: delta values to the in-memory session.state(), as BaseSessionService.appendEvent does. #1650 now only sends them to the shared documents, so a value that arrives only through EventActions.stateDelta() is no longer visible later in the same invocation.

    One smaller gap the table doesn't cover: Context.state() writes through to session.state() (Context), and appendEvent still writes the whole map with update("state", session.state()) (here). So temp: keys (and app:/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-memory session.state (here), as suggested above, but filters these prefixes out before it writes the session document (here). InMemorySessionService keeps 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions