Skip to content

The two migrate session paths disagree on v0 event timestampsΒ #7300

Description

@feiiiiii5

πŸ”΄ Required Information

Describe the Bug:

adk migrate session has two implementations β€” migrate_from_sqlalchemy_pickle and migrate_from_sqlalchemy_sqlite β€” and they read the same v0 events.timestamp column under two conventions, so the same v0 database migrated through the two paths yields timestamps that differ by the host's UTC offset.

  • the sqlite path calls StorageEvent.to_event(), which reads the column with utc_datetime_to_timestamp (naive value forced to UTC);
  • the pickle path reads the raw row and calls timestamp.timestamp() on the naive value, i.e. local time, with a comment saying v0 wrote it that way.

Each is right for one era of v0 writer: before 1b8ed3d (2026-08-13) StorageEvent.from_event wrote naive local time, and 1b8ed3d changed the write path to naive UTC (and the read path to match) without touching the pickle migration. A naive value carries no marker of which convention wrote it.

Steps to Reproduce:

Build a v0 database whose event row holds a naive local datetime (what pre-1b8ed3d9 v0 stored), run each migration on its own copy, compare the results:

Reproduction script (self-contained, no network or model)
import tempfile
from datetime import datetime, timezone
from pathlib import Path

from google.adk.events.event_actions import EventActions
from google.adk.sessions.migration import migrate_from_sqlalchemy_pickle as mfsp
from google.adk.sessions.migration import migrate_from_sqlalchemy_sqlite as mfss
from google.adk.sessions.schemas import v0
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

EPOCH = 1750000000.0
local_naive = datetime.fromtimestamp(EPOCH)  # what pre-1b8ed3d9 v0 stored
tmp = Path(tempfile.mkdtemp())


def build(path):
    engine = create_engine(f"sqlite:///{path}")
    v0.Base.metadata.create_all(engine)
    s = sessionmaker(bind=engine)()
    now = datetime.now(timezone.utc)
    s.add(v0.StorageSession(app_name="a", user_id="u", id="s", state={},
                            create_time=now, update_time=now))
    s.add(v0.StorageEvent(id="e1", app_name="a", user_id="u", session_id="s",
                          invocation_id="i", author="user",
                          actions=EventActions(), timestamp=local_naive))
    s.commit()
    s.close()
    return f"sqlite:///{path}"


mfsp.migrate(build(tmp / "src.db"), f"sqlite:///{tmp / 'dest_pickle.db'}")
mfss.migrate(build(tmp / "src_sqlite.db"), str(tmp / "dest_sqlite.db"))

Expected Behavior:

Both paths migrate the event to the same instant, and the convention is documented once.

Observed Behavior:

On a UTC+8 host, for EPOCH = 1750000000.0 (2025-06-15 15:06:40 UTC):

path migrated value vs. correct instant
pickle 2025-06-15 15:06:40 (naive UTC) 0
sqlite 1750028800.0 +28800 s (+8 h)

Every event migrated from a pre-1b8ed3d9 database lands 8 hours in the future, so ordering, turn durations and time-based filters are wrong afterwards. The shift is the host's offset at that local time, so it varies per row across DST boundaries.

Environment Details:

  • ADK Library Version: main @ 044a1ec
  • Desktop OS: macOS
  • Python Version: 3.14.5
  • Host timezone: Asia/Shanghai (UTC+8)

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A (offline migration script)

🟑 Optional Information

Regression:

The sqlite path became wrong for old rows on 2026-08-13 (1b8ed3d); the two paths have disagreed since the pickle migration was written.

Logs:

dest_pickle.db: id=e1 column_timestamp='2025-06-15 15:06:40.000000'   # correct
dest_sqlite.db: id=e1 column_timestamp=1750028800.0                  # +8h

Additional Context:

test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local pins the local reading on the pickle side, and its docstring gives as premise that StorageEvent.from_event used datetime.fromtimestamp β€” true until 1b8ed3d, not true now. I would rather ask than declare one side wrong, so three options:

  1. treat the v0 column as naive local in both paths (right for every database written before 1b8ed3d, which I assume is the realistic input, and per-row DST-correct);
  2. keep both on UTC (right only for rows written after 1b8ed3d);
  3. add --v0-timestamps {local,utc}, default local, and document it.

I lean to 1 plus a note that post-1b8ed3d9 v0 rows will be shifted, since a silently 8-hour-wrong history is harder to notice than a documented conversion. Whichever you pick, I would also add a test that runs one fixture through both paths and asserts they agree.

Screenshots / Video: N/A.

How often has this issue occurred?:

  • Always (100%) for pre-1b8ed3d9 v0 databases migrated with the sqlite path on a non-UTC host

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