mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
fix: normalize imported session timestamps
OpenClaw session imports can leave ISO-8601 timestamps in SessionDB rows. Normalize legacy string timestamps before rendering or comparing session activity so session listing and resume surfaces do not crash on imported history.
This commit is contained in:
parent
88b6eb9ad1
commit
8d922ddadd
4 changed files with 81 additions and 0 deletions
|
|
@ -198,8 +198,30 @@ from hermes_constants import AI_GATEWAY_BASE_URL, OPENROUTER_BASE_URL
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_timestamp(ts):
|
||||||
|
"""Best-effort parse for epoch or ISO-8601 timestamps."""
|
||||||
|
if ts is None or ts == "":
|
||||||
|
return None
|
||||||
|
if isinstance(ts, (int, float)):
|
||||||
|
return float(ts)
|
||||||
|
if isinstance(ts, str):
|
||||||
|
raw = ts.strip()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(raw.replace("Z", "+00:00")).timestamp()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _relative_time(ts) -> str:
|
def _relative_time(ts) -> str:
|
||||||
"""Format a timestamp as relative time (e.g., '2h ago', 'yesterday')."""
|
"""Format a timestamp as relative time (e.g., '2h ago', 'yesterday')."""
|
||||||
|
ts = _coerce_timestamp(ts)
|
||||||
if not ts:
|
if not ts:
|
||||||
return "?"
|
return "?"
|
||||||
delta = _time.time() - ts
|
delta = _time.time() - ts
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
||||||
|
|
@ -29,6 +30,32 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_timestamp(value: Any) -> Any:
|
||||||
|
"""Best-effort normalization for legacy/imported timestamp values.
|
||||||
|
|
||||||
|
Session rows are supposed to store REAL epoch seconds, but imported data may
|
||||||
|
contain ISO-8601 strings. Normalize those on read so downstream callers can
|
||||||
|
safely compare/subtract timestamps without exploding.
|
||||||
|
"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
raw = value.strip()
|
||||||
|
if not raw:
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(raw.replace("Z", "+00:00")).timestamp()
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
|
||||||
DEFAULT_DB_PATH = get_hermes_home() / "state.db"
|
DEFAULT_DB_PATH = get_hermes_home() / "state.db"
|
||||||
|
|
||||||
SCHEMA_VERSION = 8
|
SCHEMA_VERSION = 8
|
||||||
|
|
@ -858,6 +885,9 @@ class SessionDB:
|
||||||
sessions = []
|
sessions = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
s = dict(row)
|
s = dict(row)
|
||||||
|
for key in ("started_at", "ended_at", "last_active"):
|
||||||
|
if key in s:
|
||||||
|
s[key] = _normalize_timestamp(s.get(key))
|
||||||
# Build the preview from the raw substring
|
# Build the preview from the raw substring
|
||||||
raw = s.pop("_preview_raw", "").strip()
|
raw = s.pop("_preview_raw", "").strip()
|
||||||
if raw:
|
if raw:
|
||||||
|
|
@ -930,6 +960,9 @@ class SessionDB:
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
s = dict(row)
|
s = dict(row)
|
||||||
|
for key in ("started_at", "ended_at", "last_active"):
|
||||||
|
if key in s:
|
||||||
|
s[key] = _normalize_timestamp(s.get(key))
|
||||||
raw = s.pop("_preview_raw", "").strip()
|
raw = s.pop("_preview_raw", "").strip()
|
||||||
if raw:
|
if raw:
|
||||||
text = raw[:60]
|
text = raw[:60]
|
||||||
|
|
|
||||||
10
tests/hermes_cli/test_relative_time.py
Normal file
10
tests/hermes_cli/test_relative_time.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from hermes_cli.main import _relative_time
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_time_accepts_iso_timestamps():
|
||||||
|
ts = time.time() - 7200
|
||||||
|
iso = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(ts))
|
||||||
|
|
||||||
|
assert _relative_time(iso) == "2h ago"
|
||||||
|
|
@ -1438,6 +1438,22 @@ class TestListSessionsRich:
|
||||||
# No messages, so last_active falls back to started_at
|
# No messages, so last_active falls back to started_at
|
||||||
assert sessions[0]["last_active"] == sessions[0]["started_at"]
|
assert sessions[0]["last_active"] == sessions[0]["started_at"]
|
||||||
|
|
||||||
|
def test_last_active_normalizes_legacy_iso_started_at(self, db):
|
||||||
|
db.create_session("legacy", "cli")
|
||||||
|
db._conn.execute(
|
||||||
|
"UPDATE sessions SET started_at=?, ended_at=? WHERE id=?",
|
||||||
|
("2026-04-09T12:00:00", "2026-04-09T13:00:00", "legacy"),
|
||||||
|
)
|
||||||
|
db._conn.commit()
|
||||||
|
|
||||||
|
sessions = db.list_sessions_rich()
|
||||||
|
legacy = next(s for s in sessions if s["id"] == "legacy")
|
||||||
|
|
||||||
|
assert isinstance(legacy["started_at"], float)
|
||||||
|
assert isinstance(legacy["ended_at"], float)
|
||||||
|
assert isinstance(legacy["last_active"], float)
|
||||||
|
assert legacy["last_active"] == legacy["started_at"]
|
||||||
|
|
||||||
def test_rich_list_includes_title(self, db):
|
def test_rich_list_includes_title(self, db):
|
||||||
db.create_session("s1", "cli")
|
db.create_session("s1", "cli")
|
||||||
db.set_session_title("s1", "refactoring auth")
|
db.set_session_title("s1", "refactoring auth")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue