diff --git a/agent/system_prompt.py b/agent/system_prompt.py index d9a4ce8086..3b2290e6d8 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -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: diff --git a/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py b/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py index be2a95d5f9..59b228435a 100644 --- a/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +++ b/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py @@ -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: diff --git a/tests/agent/test_system_prompt.py b/tests/agent/test_system_prompt.py index a81d95febb..d65583ce99 100644 --- a/tests/agent/test_system_prompt.py +++ b/tests/agent/test_system_prompt.py @@ -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 diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index 55babcb088..50b6e754a3 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -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: diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 715fa02533..9b87971212 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -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)