Skip to content

fix(sessions): migrate v0 event timestamps as local time, not UTC#6396

Open
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/migration-event-timestamp-utc
Open

fix(sessions): migrate v0 event timestamps as local time, not UTC#6396
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/migration-event-timestamp-utc

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • N/A (no existing issue; the bug is described below following the bug report template)

2. Or, if no issue exists, describe the change:

Problem:

The pickle-to-JSON session migration tool
(src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py)
corrupts every migrated event's timestamp on any host whose local timezone is
not UTC.

When rebuilding each Event, _row_to_event reads the old row's naive
timestamp and forces it to UTC:

timestamp=timestamp.replace(tzinfo=timezone.utc).timestamp(),

But the v0 schema persisted that column as a naive datetime in local time,
and read it back the same way:

  • write, sessions/schemas/v0.py StorageEvent.from_event:
    timestamp=datetime.fromtimestamp(event.timestamp) (naive, local)
  • read, sessions/schemas/v0.py StorageEvent.to_event:
    timestamp=self.timestamp.timestamp() (naive .timestamp() = local)
  • column type sessions/schemas/shared.py PreciseTimestamp:
    impl = DateTime (not timezone=True), so drivers hand back naive datetimes.

Relabeling that naive local wall-clock value as UTC shifts every migrated
timestamp by the host's UTC offset. On a machine in Asia/Kolkata (+5:30) a
stored epoch of 1000000.0 migrates to 1019800.0 (off by 19800 seconds).
Because the shift is applied to every event, it silently corrupts event
timestamps and their ordering in the destination database.

Steps to Reproduce:

Run the migration on a host configured to a non-UTC timezone (or simulate it):

import os, time
os.environ["TZ"] = "Asia/Kolkata"  # +5:30
time.tzset()

from datetime import datetime
from google.adk.events import EventActions
from google.adk.sessions.migration import migrate_from_sqlalchemy_pickle as m

original_epoch = 1000000.0
# Exactly what v0.StorageEvent.from_event persisted: naive datetime in local time.
naive_local = datetime.fromtimestamp(original_epoch)

event = m._row_to_event({
    "id": "e1",
    "invocation_id": "inv1",
    "author": "user",
    "actions": EventActions(),
    "timestamp": naive_local,
})

print(event.timestamp)  # before fix: 1019800.0  (should be 1000000.0)

Expected Behavior:

The migrated event's timestamp equals the original epoch (1000000.0),
regardless of the host's timezone.

Observed Behavior:

On a non-UTC host the migrated timestamp is shifted by the host's UTC offset
(for example 1019800.0 under Asia/Kolkata), corrupting event timestamps and
ordering.

Solution:

Interpret the naive value as local time to match how v0 both wrote and read it:

timestamp=timestamp.timestamp(),

datetime.timestamp() treats a naive datetime as local time (matching
StorageEvent.to_event), and still honors tzinfo if a driver ever returns a
timezone-aware value (the previous .replace(tzinfo=timezone.utc) would have
overwritten a real offset). The destination write remains UTC
(datetime.fromtimestamp(event_obj.timestamp, timezone.utc)), which is correct
once the epoch itself is right; that line and the timezone import are
unchanged.

Environment Details:

  • ADK Library Version: 2.3.0 (behavior present on current main)
  • Desktop OS: reproducible on any non-UTC host (verified on macOS)
  • Python Version: 3.11

Regression: Present since the migration tool was introduced; independent of
ADK version.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local in
tests/unittests/sessions/migration/test_migration.py. It pins a fixed non-UTC
zone (Asia/Kolkata) via monkeypatch + time.tzset(), feeds a naive-local v0
timestamp (built with datetime.fromtimestamp, exactly as v0 persisted it)
through _row_to_event, and asserts the migrated epoch round-trips exactly. It
fails on the old code (1019800.0 != 1000000.0) and passes with the fix. The
zone is restored in a finally block.

pytest results:

uv run pytest tests/unittests/sessions/migration -q  ->  31 passed
uv run pytest tests/unittests/sessions -q            ->  229 passed

Manual End-to-End (E2E) Tests:

The runnable snippet under "Steps to Reproduce" is the manual repro: it prints
1019800.0 before the fix and 1000000.0 after, on a non-UTC host.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (N/A)

The pickle-to-JSON migration read each v0 event's naive `timestamp`
column and forced it to UTC via
`timestamp.replace(tzinfo=timezone.utc).timestamp()`. But the v0 schema
persisted that column as a naive datetime in local time
(`StorageEvent.from_event` uses `datetime.fromtimestamp(event.timestamp)`)
and read it back as local (`StorageEvent.to_event` uses a naive
`.timestamp()`). Reinterpreting the naive value as UTC shifted every
migrated event timestamp by the host's UTC offset on any non-UTC host
(for example +19800s under Asia/Kolkata), corrupting event timestamps and
ordering in the destination database.

Interpret the naive value as local time to match how v0 both wrote and
read it. `datetime.timestamp()` treats naive datetimes as local and still
honors tzinfo when a driver returns a timezone-aware value.

Add a regression test that pins a fixed non-UTC zone and asserts the
migrated epoch round-trips exactly.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@adk-bot adk-bot added the services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants