π΄ Required Information
Describe the Bug:
ADK has two v0 migration 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. adk migrate session calls only the pickle implementation (migration runner); the SQLite implementation is the separate module that SqliteSessionService recommends.
- 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:
- 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);
- keep both on UTC (right only for rows written after 1b8ed3d);
- 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
π΄ Required Information
Describe the Bug:
ADK has two v0 migration implementations β
migrate_from_sqlalchemy_pickleandmigrate_from_sqlalchemy_sqliteβ and they read the same v0events.timestampcolumn under two conventions, so the same v0 database migrated through the two paths yields timestamps that differ by the host's UTC offset.adk migrate sessioncalls only the pickle implementation (migration runner); the SQLite implementation is the separate module that SqliteSessionService recommends.StorageEvent.to_event(), which reads the column withutc_datetime_to_timestamp(naive value forced to UTC);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_eventwrote 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)
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):2025-06-15 15:06:40(naive UTC)1750028800.0Every 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:
Model Information:
π‘ 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:
Additional Context:
test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_localpins the local reading on the pickle side, and its docstring gives as premise thatStorageEvent.from_eventuseddatetime.fromtimestampβ true until 1b8ed3d, not true now. I would rather ask than declare one side wrong, so three options:--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?: