fix: lazy session creation — defer DB row until first message (#18370)
Prevents ghost sessions from accumulating in state.db when the TUI/web dashboard is opened and closed without sending a message. Changes: - run_agent.py: Add _ensure_db_session() gate method, called at run_conversation() entry. Remove eager create_session() from __init__. Handle compression rotation flag correctly. - tui_gateway/server.py: Remove eager db.create_session() in _start_agent_build(). Add post-first-message pending_title re-apply. - hermes_state.py: Extract _insert_session_row() shared helper (DRY). Add prune_empty_ghost_sessions() for one-time migration. - cli.py: One-time ghost session prune on startup. Fix _pending_title to call _ensure_db_session() before set_session_title(). - hermes_cli/main.py: Guard TUI exit summary on message_count > 0. - tests: Update test_860_dedup to call _ensure_db_session() before direct _flush_messages_to_session_db() calls. Closes: ghost session clutter in hermes sessions list and web dashboard.
This commit is contained in:
@@ -280,7 +280,7 @@ def _notify_session_boundary(event_type: str, session_id: str | None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _finalize_session(session: dict | None) -> None:
|
||||
def _finalize_session(session: dict | None, end_reason: str = "tui_close") -> None:
|
||||
"""Best-effort finalize hook + memory commit for a session."""
|
||||
if not session or session.get("_finalized"):
|
||||
return
|
||||
@@ -299,13 +299,24 @@ def _finalize_session(session: dict | None) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
session_id = getattr(agent, "session_id", None) or session.get("session_key")
|
||||
session_key = session.get("session_key")
|
||||
session_id = getattr(agent, "session_id", None) or session_key
|
||||
_notify_session_boundary("on_session_finalize", session_id)
|
||||
|
||||
# Mark session ended in DB so it doesn't linger as a ghost row in /resume.
|
||||
# Adapted from #18283 (luyao618) and #18299 (Bartok9).
|
||||
if session_key:
|
||||
try:
|
||||
db = _get_db()
|
||||
if db is not None:
|
||||
db.end_session(session_key, end_reason)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _shutdown_sessions() -> None:
|
||||
for session in list(_sessions.values()):
|
||||
_finalize_session(session)
|
||||
_finalize_session(session, end_reason="tui_shutdown")
|
||||
try:
|
||||
worker = session.get("slash_worker")
|
||||
if worker:
|
||||
@@ -539,32 +550,8 @@ def _start_agent_build(sid: str, session: dict) -> None:
|
||||
finally:
|
||||
_clear_session_context(tokens)
|
||||
|
||||
db = _get_db()
|
||||
if db is not None:
|
||||
db.create_session(key, source="tui", model=_resolve_model())
|
||||
pending_title = (current.get("pending_title") or "").strip()
|
||||
if pending_title:
|
||||
try:
|
||||
title_applied = db.set_session_title(key, pending_title)
|
||||
if title_applied:
|
||||
current["pending_title"] = None
|
||||
else:
|
||||
existing_row = db.get_session(key)
|
||||
existing_title = ((existing_row or {}).get("title") or "").strip()
|
||||
if existing_title == pending_title:
|
||||
current["pending_title"] = None
|
||||
else:
|
||||
logger.info(
|
||||
"Pending title still queued for session %s (wanted=%r, current=%r)",
|
||||
sid,
|
||||
pending_title,
|
||||
existing_title,
|
||||
)
|
||||
except ValueError as e:
|
||||
current["pending_title"] = None
|
||||
logger.info("Dropping pending title for session %s: %s", sid, e)
|
||||
except Exception:
|
||||
logger.warning("Failed to apply pending title for session %s", sid, exc_info=True)
|
||||
# Session DB row deferred to first run_conversation() call.
|
||||
# pending_title applied post-first-message (see cli.exec handler).
|
||||
current["agent"] = agent
|
||||
|
||||
try:
|
||||
@@ -2994,6 +2981,17 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
|
||||
payload["rendered"] = rendered
|
||||
_emit("message.complete", sid, payload)
|
||||
|
||||
# Apply pending_title now that the DB row exists.
|
||||
_pending = session.get("pending_title")
|
||||
if _pending and status == "complete":
|
||||
_pdb = _get_db()
|
||||
if _pdb:
|
||||
try:
|
||||
if _pdb.set_session_title(session.get("session_key") or sid, _pending):
|
||||
session["pending_title"] = None
|
||||
except Exception:
|
||||
pass # Best effort — auto-title will handle it below
|
||||
|
||||
if (
|
||||
status == "complete"
|
||||
and isinstance(raw, str)
|
||||
|
||||
Reference in New Issue
Block a user