fix(state): bound legacy transcript hydration memory

Co-authored-by: Benjamin Brumbaugh <benbrumbaugh@gmail.com>
This commit is contained in:
brooklyn!
2026-09-15 09:29:03 -07:00
parent d84ece48b8
commit 43bd5db429
5 changed files with 363 additions and 31 deletions
+4 -1
View File
@@ -752,7 +752,10 @@ def _resume_deferred(ctx: _Resume) -> dict:
resume_message_count=int(ctx.found.get("message_count") or 0))
if (reused := ctx.claim(sid, record)) is not None:
return reused
_schedule_resume_hydration(sid, ctx.target, ctx.db, close_db=ctx.owns_db)
# Desktop owns the visible transcript through bounded REST pages, not this model-history restore.
_schedule_resume_hydration(
sid, ctx.target, ctx.db, close_db=ctx.owns_db,
model_history_only=source == "desktop" and ctx.omit_messages)
ctx.owns_db = False # the hydration worker now owns (and closes) the profile-scoped handle
_schedule_session_cap_enforcement()
return _resume_response(ctx, sid, record, info=ctx.info(cwd, overrides), messages=[],
+13 -6
View File
@@ -2575,10 +2575,14 @@ def _schedule_agent_build(sid: str, delay: float = 0.05) -> None:
timer.start()
def _load_resume_transcript(db, stored_id: str) -> tuple[list, list, list]:
def _load_resume_transcript(db, stored_id: str, *, model_history_only: bool = False) -> tuple[list, list, list]:
"""(raw_history, display_history, ancestor_prefix) for a cold resume. The full lineage is materialized
only while it fits sessions.max_resume_messages (the transcript is REST-paginated), else the tip alone."""
from hermes_state import SessionResumeTooLargeError
if model_history_only:
raw_history = db.get_messages_as_conversation(
stored_id, repair_alternation=True, include_row_ids=True)
return raw_history, [], []
prefix_fits = True
guard = getattr(db, "assert_resume_safe", None)
if callable(guard):
@@ -2597,7 +2601,8 @@ def _load_resume_transcript(db, stored_id: str) -> tuple[list, list, list]:
return raw_history, raw_history, []
def _schedule_resume_hydration(sid: str, stored_id: str, db, *, close_db: bool = False) -> None:
def _schedule_resume_hydration(sid: str, stored_id: str, db, *, close_db: bool = False,
model_history_only: bool = False) -> None:
"""Load a cold resume's transcript off the JSON-RPC response path."""
def _run() -> None:
@@ -2607,22 +2612,24 @@ def _schedule_resume_hydration(sid: str, stored_id: str, db, *, close_db: bool =
return
_emit("session.resume_progress", sid, {"phase": "history", "status": "loading"})
db.reopen_session(stored_id)
raw_history, display_history, prefix = _load_resume_transcript(db, stored_id)
raw_history, display_history, prefix = _load_resume_transcript(
db, stored_id, model_history_only=model_history_only)
# Display keeps the full transcript; the model-fed history uses the
# same canonicalization as gateway resume and the send path.
history = canonicalize_replay_history(raw_history)
if _sessions.get(sid) is not session:
return
with session["history_lock"]:
session.update(history=history, display_history_prefix=prefix, resume_hydrating=False,
resume_message_count=len(display_history))
session.update(history=history, display_history_prefix=prefix, resume_hydrating=False)
if not model_history_only:
session["resume_message_count"] = len(display_history)
# Deferred resumes answered before the transcript existed; cache the derived todo snapshot now.
todo_state = _todo_state_from_history(history)
if todo_state is not None and session.get("todo_state") is None:
session["todo_state"] = todo_state
session["resume_history_ready"].set()
_emit("session.resume_progress", sid,
{"message_count": len(display_history), "phase": "history", "status": "complete"})
{"message_count": session["resume_message_count"], "phase": "history", "status": "complete"})
_maybe_schedule_auto_continue(sid, session, stored_id)
_start_agent_build(sid, session)
except Exception as exc: