fix(time): session start in the system prompt uses its own date's UTC offset

_session_start_like localised the naive session-id stamp with
datetime.now().astimezone().tzinfo, which is a fixed offset for today.
A stamp from the other DST half got the wrong offset: under
Europe/London on a summer day, session 20260115_003000_* rendered as
"Conversation started: Wednesday, January 14, 2026". dt.astimezone()
applies the offset in force at the stamp itself, as
gateway/message_timestamps.py already does.

Also replace the two remaining datetime.utcfromtimestamp() calls
(deprecated since Python 3.12) with fromtimestamp(..., timezone.utc).
The output strings are unchanged. tui_gateway/methods_session.py runs
with tui_gateway/server.py's globals, so server.py now imports timezone.
This commit is contained in:
alt-glitch
2026-09-28 15:27:21 +05:30
committed by Siddharth Balyan
parent 57d7a15c28
commit 8a2fb31fbc
5 changed files with 32 additions and 4 deletions

View File

@@ -195,7 +195,8 @@ def _session_start_like(agent: Any, now: Any) -> Any:
def _to_display_tz(dt: Any) -> Any:
if dt.tzinfo is None:
try:
dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
# The offset in force at dt, not today's: a stamp from the other DST half differs by an hour.
dt = dt.astimezone()
except (ValueError, OSError):
pass
if getattr(now, "tzinfo", None) is not None and dt.tzinfo is not None:

View File

@@ -171,7 +171,7 @@ def _format_timestamp_ms(value: Any) -> str:
ts_ms = int(value)
except (TypeError, ValueError):
return "-"
return dt.datetime.utcfromtimestamp(ts_ms / 1000).strftime("%Y-%m-%d %H:%M:%S UTC")
return dt.datetime.fromtimestamp(ts_ms / 1000, dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
def _compact_number(value: Any, decimals: int = 2) -> str:

View File

@@ -1,6 +1,8 @@
"""Tests for agent/system_prompt.py — context-file cwd wiring."""
import json
import os
import time
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
@@ -824,6 +826,31 @@ class TestSessionStartLike:
start = _session_start_like(agent, now)
assert start.strftime("%Y-%m-%d") == "2026-01-01"
@pytest.mark.skipif(not hasattr(time, "tzset"), reason="needs a POSIX process timezone switch")
def test_stamp_from_other_dst_half_keeps_its_own_offset(self):
"""A naive stamp takes the UTC offset in force at that stamp, not today's:
a January 00:30 London session read on a summer day rendered as January 14.
Both halves are checked so the test bites whichever season it runs in."""
from agent.system_prompt import _session_start_like
london = ZoneInfo("Europe/London")
original_tz = os.environ.get("TZ")
try:
os.environ["TZ"] = "Europe/London"
time.tzset()
for sid, now, expected in (
("20260115_003000_jan", datetime(2026, 7, 16, 9, 0, tzinfo=london), "2026-01-15T00:30:00+00:00"),
("20260715_003000_jul", datetime(2026, 12, 16, 9, 0, tzinfo=london), "2026-07-15T00:30:00+01:00"),
):
start = _session_start_like(SimpleNamespace(session_id=sid), now)
assert start.isoformat() == expected
finally:
if original_tz is None:
os.environ.pop("TZ", None)
else:
os.environ["TZ"] = original_tz
time.tzset()
def test_conversation_start_uses_session_start_not_build_time(monkeypatch):
"""Regression: a session that started on Jan 1 must still read

View File

@@ -2381,7 +2381,7 @@ def _(rid, params: dict) -> dict:
started_at, label = params.get("started_at"), str(params.get("label") or "")
finished_at = float(params.get("finished_at") or time.time())
d = _spawn_tree_session_dir(session_id or "default")
path = d / f"{datetime.utcfromtimestamp(finished_at).strftime('%Y%m%dT%H%M%S')}.json"
path = d / f"{datetime.fromtimestamp(finished_at, timezone.utc).strftime('%Y%m%dT%H%M%S')}.json"
meta = {"session_id": session_id, "started_at": float(started_at) if started_at else None,
"finished_at": finished_at, "label": label}
try:

View File

@@ -15,7 +15,7 @@ import sys
import threading
import time
import uuid
from datetime import datetime
from datetime import datetime, timezone # noqa: F401 (timezone: split modules)
from pathlib import Path
from typing import Any, Callable, NamedTuple, Optional # noqa: F401 (Callable: split modules)