fix(session-export): lineage timings span the merged messages; import ignores derived size

export_session_lineage spread segments[-1] over the merged dict, so the top-level `timings`
described only the last compression segment while `messages` spanned the whole lineage — a
reader would see a 500 ms wall clock over a lineage that ran for hours. Compute the block over
the merged message list (segments keep their own).

_validate_import_session measured the raw session JSON, so the derived `timings.intervals`
(one entry per message pair) counted toward the 5 MiB per-session limit and could reject a
long lineage whose actual content fits. Strip `timings` before measuring; it is rebuilt from
the messages on the next export anyway.
This commit is contained in:
teknium1
2026-09-14 21:06:14 -07:00
committed by Teknium
parent a4b3ba5e40
commit d566442b56

View File

@@ -82,7 +82,7 @@ _PROMPT_RESOLVED_SQL = "COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_r
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).
"""Text-free timing evidence for a session export.
Exports get attached to bug reports; a reader should not have to infer from raw
timestamps whether a slow turn was one long model gap or many small tool
@@ -297,7 +297,7 @@ class SessionPortabilityMixin:
return {
**segments[-1], "segments": segments,
"lineage_session_ids": [seg["id"] for seg in segments], "message_count": len(messages),
"messages": messages,
"messages": messages, "timings": _export_timings(messages, session_id),
}
def export_all(self, source: str = None) -> List[Dict[str, Any]]:
@@ -494,7 +494,10 @@ class SessionPortabilityMixin:
if any(not isinstance(msg, dict) for msg in messages):
raise ValueError("messages must contain only objects")
try:
session_bytes = len(json.dumps(raw, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
# `timings` is derived from the messages at export time and rebuilt on the next export;
# it must not eat into the size budget of the content it merely describes.
measured = {k: v for k, v in raw.items() if k != "timings"}
session_bytes = len(json.dumps(measured, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
except (TypeError, ValueError):
raise ValueError("session must be JSON serializable") from None
if session_bytes > self._IMPORT_MAX_SESSION_BYTES: