From 416095a7f122f5559a0910e2c4c3be65dc1fa9f5 Mon Sep 17 00:00:00 2001 From: Pheby Date: Thu, 10 Sep 2026 05:44:02 +0000 Subject: [PATCH] [verified] fix: emit UTC-offset last_active so the app can sort by recency Session-store updated_at values are naive local datetimes; passing them through without an offset broke Instant.parse() on the client, dropping last_active and leaving the chat list unsorted after restart. --- plugin/pheby/hermes_bridge.py | 10 ++++++++++ tests/test_pheby.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/plugin/pheby/hermes_bridge.py b/plugin/pheby/hermes_bridge.py index f7b1fc5..79a1207 100644 --- a/plugin/pheby/hermes_bridge.py +++ b/plugin/pheby/hermes_bridge.py @@ -210,6 +210,16 @@ def _iso(value: Any) -> Optional[str]: try: if isinstance(value, (int, float)): return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).isoformat() + if isinstance(value, dt.datetime): + # Session-store timestamps are naive LOCAL datetimes (see + # gateway.session_lifecycle._now). The client parses with + # Instant.parse(), which requires an offset — a bare + # "2026-09-10T05:41:48" fails and last_active is lost, so the + # app can no longer sort by recency. Attach the local offset + # and normalize to UTC. + if value.tzinfo is None: + value = value.astimezone() + return value.astimezone(dt.timezone.utc).isoformat() return value.isoformat() if value else None except (AttributeError, OSError, OverflowError, TypeError, ValueError): return None diff --git a/tests/test_pheby.py b/tests/test_pheby.py index 1b4e620..583cd06 100644 --- a/tests/test_pheby.py +++ b/tests/test_pheby.py @@ -12,6 +12,7 @@ by ``conftest.py``). from __future__ import annotations import asyncio +import datetime as dt import json import threading import time @@ -459,6 +460,24 @@ class TestBridge: assert history[0]["ts"] == "2026-09-10T00:26:40.250000+00:00" assert history[1]["ts"] == "2026-09-10T00:26:41.500000+00:00" + def test_iso_parses_by_instant_for_naive_local_datetimes(self): + # gateway session-store timestamps are naive LOCAL datetimes + # (gateway.session_lifecycle._now). last_active must come back with a + # UTC offset, or the Kotlin client's Instant.parse() fails and the + # chat list loses its recency ordering. + from pheby import hermes_bridge as hb + + naive = dt.datetime(2026, 9, 10, 5, 41, 48, 401094) # naive local + got = hb._iso(naive) + assert got is not None + assert got.endswith("+00:00") + # Java-style Instant.parse equivalent: parse with offset required. + parsed = dt.datetime.fromisoformat(got) + assert parsed.tzinfo is not None + # Aware datetimes pass through normalized to UTC unchanged. + aware = dt.datetime(2026, 9, 10, 12, 0, tzinfo=dt.timezone(dt.timedelta(hours=-5))) + assert hb._iso(aware) == "2026-09-10T17:00:00+00:00" + @pytest.mark.asyncio async def test_tool_start_event_is_structured_not_text(self, tmp_path, monkeypatch):