Skip to content

DatabaseSessionService.get_session returns all events when num_recent_events=0 #5730

Closed

Description

@tiino

🔴 Required Information

Describe the Bug:

DatabaseSessionService.get_session(..., config=GetSessionConfig(num_recent_events=0)) is documented to return no events (see GetSessionConfig docstring in src/google/adk/sessions/base_session_service.py: "if 0, no events are returned").

However, when num_recent_events=0 is passed, all events of the session are returned instead of an empty list.

This appears to be caused by a truthy check on num_recent_events in src/google/adk/sessions/database_session_service.py (around line 523):

if config and config.num_recent_events:          # ← `0` is falsy, so this branch is skipped
    stmt = stmt.limit(config.num_recent_events)

Because 0 is falsy in Python, the LIMIT clause is never appended when num_recent_events=0, and the query returns every event for the session.

The other in-tree implementations appear to handle 0 correctly:

  • InMemorySessionService (src/google/adk/sessions/in_memory_session_service.py:204-211): uses is not None and explicitly assigns events = [] when num_recent_events == 0.
  • SqliteSessionService (src/google/adk/sessions/sqlite_session_service.py:264-271): uses is not None for the LIMIT clause, plus an explicit early return for == 0.

So this issue appears to be specific to DatabaseSessionService (SQLAlchemy backend).

Steps to Reproduce:

  1. Install google-adk (reproduced on 1.33.0 / current main) and aiosqlite.

  2. Run the following script (uses SQLite in-memory, no external DB needed):

import asyncio
from google.genai import types
from google.adk.events.event import Event
from google.adk.sessions.base_session_service import GetSessionConfig
from google.adk.sessions.database_session_service import DatabaseSessionService

async def main():
    svc = DatabaseSessionService(db_url="sqlite+aiosqlite:///:memory:")
    app, user, sid = "app", "user_1", "session_1"

    session = await svc.create_session(app_name=app, user_id=user, session_id=sid)

    for i in range(5):
        ev = Event(
            invocation_id=f"inv_{i}",
            author="user" if i % 2 == 0 else "agent",
            content=types.Content(
                role="user" if i % 2 == 0 else "model",
                parts=[types.Part(text=f"event #{i}")],
            ),
        )
        await svc.append_event(session=session, event=ev)

    got = await svc.get_session(
        app_name=app,
        user_id=user,
        session_id=sid,
        config=GetSessionConfig(num_recent_events=0),
    )

    print("num_recent_events=0 -> events len =", len(got.events))

asyncio.run(main())

Expected Behavior:

num_recent_events=0 -> events len = 0

This matches the behavior of InMemorySessionService for the equivalent call.

Observed Behavior:

num_recent_events=0 -> events len = 5

All 5 events are returned.

Testing across multiple num_recent_events values suggests that 0 is the only problematic case:

num_recent_events= None -> events len = 5   # OK (no filter)
num_recent_events=    0 -> events len = 5   # Incorrect behavior (should be 0)
num_recent_events=    2 -> events len = 2   # OK
num_recent_events=    5 -> events len = 5   # OK

The issue is reproducible regardless of the underlying database, because the affected branch is in Python (the LIMIT clause is simply never appended to the SQLAlchemy statement).

SQLite is used here only for convenience; PostgreSQL, MySQL, and MariaDB appear to be affected in the same way.

Environment Details:

  • ADK Library Version (pip show google-adk): 1.33.0
  • Desktop OS: Linux (WSL2, Ubuntu)
  • Python Version (python -V): Python 3.12.3

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-3.1-flash-lite)

Thanks for taking a look.

Activity

  1. added 2 commits that reference this issue on May 17, 2026
    f8dff86
    7e67699
  2. added
    services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc
    on May 18, 2026
  3. surajksharma07 commented on May 18, 2026

    @surajksharma07
    Collaborator

    @tiino the falsy check on num_recent_events at line 523 is exactly the problem, and swapping it to is not None paired with an explicit == 0 early return (the same pattern SqliteSessionService already uses) is the right fix.

    Since you've already got PR #5731 open, could you verify it covers both parts of that change and run your repro script across num_recent_events=0, 2, and None to confirm the behavior holds end-to-end?
    Thorough testing across those three cases would really help move the PR forward for review!

  4. tiino commented on May 19, 2026

    @tiino
    Author

    Thanks for the review, @surajksharma07!

    Pushed a follow-up commit (edce3f54) that adds the early return you suggested. The structure now mirrors SqliteSessionService.get_session directly:

    if config and config.num_recent_events is not None:
        stmt = stmt.limit(config.num_recent_events)
    
    if config and config.num_recent_events == 0:
        storage_events = []
    else:
        result = await sql_session.execute(stmt)
        storage_events = result.scalars().all()

    The LIMIT 0 path alone would already return zero rows on any major SQL backend, but skipping the execute() call entirely avoids an unnecessary round-trip — same reasoning as SqliteSessionService (skips execute_fetchall) and VertexAiSessionService (skips the events.list API call). That matters more in this service's typical remote-DB deployments (PostgreSQL / MySQL).

    On testing, I extended test_get_session_with_config to cover num_recent_events=2. The parametrized test now runs None, 0, 2, and 3 against all four session backends (InMemorySessionService, InMemorySessionService with light-copy, DatabaseSessionService, SqliteSessionService).

    I also ran the end-to-end repro across the three values you called out, using SQLite in-memory backing the DatabaseSessionService:

    import asyncio
    from google.adk.events.event import Event
    from google.adk.sessions.base_session_service import GetSessionConfig
    from google.adk.sessions.database_session_service import DatabaseSessionService
    from google.genai import types
    
    async def main():
        svc = DatabaseSessionService(db_url="sqlite+aiosqlite:///:memory:")
        app, user, sid = "app", "user_1", "session_1"
        session = await svc.create_session(app_name=app, user_id=user, session_id=sid)
        for i in range(5):
            await svc.append_event(
                session=session,
                event=Event(
                    invocation_id=f"inv_{i}",
                    author="user" if i % 2 == 0 else "agent",
                    content=types.Content(
                        role="user" if i % 2 == 0 else "model",
                        parts=[types.Part(text=f"event #{i}")],
                    ),
                ),
            )
        for value in [None, 0, 2]:
            cfg = None if value is None else GetSessionConfig(num_recent_events=value)
            got = await svc.get_session(app_name=app, user_id=user, session_id=sid, config=cfg)
            print(f"num_recent_events={value!r:>5} -> events len = {len(got.events)}")
    
    asyncio.run(main())

    Output on current main (before fix):

    num_recent_events= None -> events len = 5
    num_recent_events=    0 -> events len = 5   # BUG
    num_recent_events=    2 -> events len = 2
    

    Output on this branch (after fix + early return):

    num_recent_events= None -> events len = 5
    num_recent_events=    0 -> events len = 0
    num_recent_events=    2 -> events len = 2
    
  5. surajksharma07 commented on May 25, 2026

    @surajksharma07
    Collaborator

    Thanks for the thorough follow-up @tiino — the two-block structure (is not None for the LIMIT + == 0 early return to skip execute()) is exactly right and mirrors SqliteSessionService line-for-line.

    The reasoning around avoiding the unnecessary DB round-trip is especially valid for the typical remote PostgreSQL/MySQL deployment scenario.

    One thing worth noting: there are now two PRs open for the same issue (#5731 yours and #5736 from @mangod12), so it's worth taking a quick look at the other to see if the approaches differ meaningfully — maintainers will likely need to pick one or ask for a consolidation.

    The parametrized test coverage across all four backends (None, 0, 2, 3) is exactly what reviewers will want to see, and the before/after repro output makes the fix straightforward to validate end-to-end.

    Team is looking more into it.

  6. self-assigned this
    on Jun 17, 2026
  7. reopened this on Jun 17, 2026
  8. GWeale commented on Jun 17, 2026

    @GWeale
    Collaborator

    Fixed on main in commit d9a672e (via #5965, also #5731): get_session now uses an is-not-None check, so num_recent_events=0 returns zero events instead of the full history. Thanks for the report and the fix. Closing as landed.

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

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authorservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions