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
@@ -79,3 +79,20 @@ def test_export_all_includes_timing_evidence(tmp_path):
assert exported[0]["timings"]["wall_clock_ms"] == 1000
assert exported[0]["timings"]["intervals"][0]["gap_ms"] == 1000
def test_corrupt_timestamp_rows_count_as_missing_instead_of_aborting_export(tmp_path):
db = SessionDB(db_path=tmp_path / "state.db")
try:
db.create_session(session_id="s1", source="cli", model="test-model")
db.append_message("s1", "user", "hello", timestamp=10.0)
db.append_message("s1", "assistant", "hi", timestamp=11.0)
# Writers refuse bad stamps; emulate a pre-existing corrupt row directly.
db._conn.execute("UPDATE messages SET timestamp = 8.4e252 WHERE id = 2")
db._conn.commit()
timings = db.export_session("s1")["timings"]
finally:
db.close()
assert timings["message_timestamps"] == {"available": 1, "missing": 1}
assert timings["wall_clock_ms"] == 0 and timings["intervals"] == []