π΄ 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:
- 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 migrate sessionhas two 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.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?: