Skip to content

Redis append_event stamps last_update_time with the wall clock, not the event timestamp #7292

Description

@feiiiiii5

🔴 Required Information

Describe the Bug:

RedisSessionService.append_event() sets session.last_update_time to time.time() instead of the appended event's own timestamp. The other three session backends store the event's timestamp:

backend what it stores
InMemorySessionService event.timestamp (in_memory_session_service.py:357-364)
SqliteSessionService event_timestamp from event.timestamp (sqlite_session_service.py:433, :515-519)
DatabaseSessionService event_timestamp, reloaded into last_update_time (database_session_service.py:976-1010)
RedisSessionService time.time() (_redis_session_service.py:343)

last_update_time is not cosmetic. It is the sort key list_sessions documents as "Sessions are ordered by last update time, oldest first" (base_session_service.py:102-118), and the Redis backend sorts on (s.last_update_time, s.user_id, s.id) (_redis_session_service.py:316-318).

The repository already records this as a known divergence. tests/unittests/sessions/_conformance.py registers the redis backend with the entry test_session_last_update_time_updates_on_event, and the harness docstring states that a divergence "becomes a defect anyone can pick up, and whoever fixes the backend has to delete the entry in the same change" (_conformance.py:15-31). That is the harness's own statement of the expected behaviour, marked unfixed.

Steps to Reproduce:

  1. Use RedisSessionService as the session backend, then create a session.
  2. Append an Event whose timestamp predates now — produced earlier, then replayed, re-delivered or imported. ADK re-delivers the same event object to a second session reference, which is why InMemorySessionService.append_event dedupes on event equality (in_memory_session_service.py:344-355).
  3. Read session.last_update_time, or call list_sessions.

No Redis server is required; tests/unittests/integrations/redis/_fake_redis.py is an in-process stand-in.

Expected Behavior:

session.last_update_time equals event.timestamp, and the reloaded session agrees. The contract test asserts exactly this (tests/unittests/sessions/test_session_service.py:1573-1600): it builds an event at original_update_time + 10 and asserts both the in-memory and reloaded values equal it.

Observed Behavior:

The stored value is the wall clock at append time. The contract test on main at 9625b06:

XFAIL ...test_session_last_update_time_updates_on_event[redis] - Redis stamps the session with the wall clock instead of the appended event's timestamp.
5 passed, 1 xfailed

The five passing parameters are the other backends. A regression test that pins the clock far from the event's own timestamp fails by exactly the pinned offset:

E   assert 1790353307.6994162 == 1790353207.6994162 ± 1.0e-06

Deterministic consequences:

  • Two sessions that received the identical event report different activity times, breaking the re-delivery semantics the in-memory backend was built around.
  • A session persisted via one backend and read via another (src/google/adk/sessions/migration/) shows the Redis session jumping forward by however long the event sat in a queue, while SQLite does not.
  • list_sessions ordering is wrong for any event that predates its append.

Optional information

Environment Details:

  • ADK Library Version: main at 9625b06c9a1be6b9ecd85225657690ae5c0e9d3e
  • Desktop OS: macOS
  • Python Version: 3.14 locally; CI covers 3.10 through 3.14
  • LiteLLM: N/A (no model call involved)

Regression: Not recent. The line has always read time.time(); the divergence entry documents it as long-standing.

Minimal reproduction:

from unittest import mock

from google.adk.events.event import Event
from google.adk.integrations.redis._config import RedisSessionServiceConfig
from google.adk.integrations.redis._redis_session_service import RedisSessionService
import pytest


@pytest.mark.asyncio
async def test_append_event_stamps_session_with_event_timestamp(session_service):
  session = await session_service.create_session(app_name="app1", user_id="u1")

  event_timestamp = session.last_update_time + 10
  event = Event(author="agent", invocation_id="inv1", timestamp=event_timestamp)

  with mock.patch("google.adk.integrations.redis._redis_session_service.time") as clock:
    clock.time.return_value = event_timestamp + 100
    await session_service.append_event(session, event)

  assert session.last_update_time == pytest.approx(event_timestamp, abs=1e-6)

How often: Always (100%).


Scope note

This is the last_update_time defect only. The divergences map has a second Redis entry, test_append_event_to_unknown_session_raises_session_not_found, which is a separate defect: append_event writes the session key unconditionally (:384-390), so appending to a deleted, expired or never-stored session recreates it instead of raising SessionNotFoundError. That one overlaps open PR #7140, which owns those lines, and is out of scope here.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions