fix: session-export timings tolerate corrupt timestamp cells

`_export_timings` parsed message timestamps with a bare `float()`, so a
pre-existing out-of-range cell (e.g. 8.4e252) reached
`datetime.fromtimestamp` and raised OverflowError, killing the whole
`export_session`/`export_all` call — a regression caught by
tests/hermes_state/test_corrupt_row_robustness.py. Route the cell through
`coerce_epoch`, the reader every other timestamp surface already uses, so a
bad row degrades to a `missing` count (with the usual warning naming the
session) and the export still lands.
This commit is contained in:
teknium1
2026-09-14 19:01:30 -07:00
committed by Teknium
parent b2a3c7ac0c
commit 2a92d6be4d
2 changed files with 24 additions and 9 deletions
+7 -9
View File
@@ -81,7 +81,7 @@ def _rich_select(select_cols: str, where: str, tail: str = "", prompt_select: Op
_PROMPT_RESOLVED_SQL = "COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved"
def _export_timings(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
def _export_timings(messages: List[Dict[str, Any]], session_id: Optional[str] = None) -> Dict[str, Any]:
"""Text-free timing evidence for a session export (port of nearai/ironclaw#7735).
Exports get attached to bug reports; a reader should not have to infer from raw
@@ -89,13 +89,11 @@ def _export_timings(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
intervals. Hermes persists no model/tool stopwatch samples, so message
timestamps are the durable floor (``complete`` is therefore always False).
Ids, roles, counts and durations only — never prompt text, arguments or results.
Corrupt timestamp cells go through ``coerce_epoch`` like every other reader: they
count as ``missing`` and never abort the export.
"""
timestamped = []
for msg in messages:
try:
timestamped.append((msg, float(msg.get("timestamp"))))
except (TypeError, ValueError):
continue
timestamped = [(msg, ts) for msg in messages
if (ts := coerce_epoch(msg.get("timestamp"), session_id=session_id)) is not None]
role_counts = Counter(str(msg.get("role") or "unknown") for msg in messages)
tool_calls_emitted = sum(
len(tc) if isinstance(tc, list) else 1 for tc in (msg.get("tool_calls") for msg in messages) if tc)
@@ -280,7 +278,7 @@ class SessionPortabilityMixin:
def _with_messages(self, session: Dict[str, Any]) -> Dict[str, Any]:
messages = self.get_messages(session["id"])
return {**session, "messages": messages, "timings": _export_timings(messages)}
return {**session, "messages": messages, "timings": _export_timings(messages, session["id"])}
def export_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Export a single session with all its messages as a dict."""
@@ -320,7 +318,7 @@ class SessionPortabilityMixin:
self._row_to_message_dict(row, warn_context="get_messages", summary_flag=True)
)
return [{**session, "messages": messages_by_session[session["id"]],
"timings": _export_timings(messages_by_session[session["id"]])} for session in sessions]
"timings": _export_timings(messages_by_session[session["id"]], session["id"])} for session in sessions]
def adopt_session_lineage_from(self, donor_db: Any, session_id: str, *, retire_donor: bool = True) -> Dict[str, Any]:
"""Adopt *session_id*'s full compression lineage from *donor_db* (stranded-bot-session