fix: prevent handoff leg data loss + surface state.db corruption to users
Two data-loss bugs reported by users: 1. /handoff CLI→gateway race (#88234): After /handoff completed, CLI cleanup called finalize_session on the session the gateway just reopened. This set end_reason on a row the gateway was actively writing to, causing the handoff leg to vanish from session history and breaking session_search recall. Fix: add _handed_off_session_ids module-level set (mirrors _single_query_finalize_attempted_session_ids pattern). _handle_handoff_command registers the session_id on completion; _should_emit_cleanup_session_finalize and _emit_interrupted_session_end check it before firing. 2. state.db corruption silent failure (#88235): When SessionDB init failed at gateway startup, the error stayed in logs — messages flowed but nothing was persisted, with no user-visible indication. Fix: store _session_db_init_error on GatewayRunner, broadcast a recovery-guidance message to all home channels via _send_session_db_warning_notifications() after the gateway connects. Also improved the 'corrupt' persistence cause wording in _format_turn_completion_explanation to include the full recovery path (hermes doctor --fix, sqlite3 .recover, backups). Tests: 6 new tests for handoff cleanup race, 3 for corruption wording. All existing CLI/turn-completion tests pass.
This commit is contained in:
@@ -990,6 +990,13 @@ _cli_wake_owner = None
|
||||
# the session boundary while the agent is still attached. If a signal lands in
|
||||
# that narrow window, atexit cleanup must not emit that session finalization again.
|
||||
_single_query_finalize_attempted_session_ids: set[str | None] = set()
|
||||
# Session IDs that were handed off to the gateway via /handoff. The CLI
|
||||
# process exits after a successful handoff, but the gateway now owns the
|
||||
# session lifecycle — _run_cleanup must NOT call finalize_session on these,
|
||||
# because doing so sets end_reason on a row the gateway just reopened and is
|
||||
# actively writing to (#88234). The race made the handoff leg vanish from
|
||||
# session history and broke session_search recall for the handed-off session.
|
||||
_handed_off_session_ids: set[str | None] = set()
|
||||
# Weak reference to the active AIAgent for memory provider shutdown at exit
|
||||
_active_agent_ref = None
|
||||
_deferred_agent_startup_done = False
|
||||
@@ -1279,11 +1286,19 @@ def _run_cleanup(*, notify_session_finalize: bool = True):
|
||||
|
||||
|
||||
def _should_emit_cleanup_session_finalize(session_id: str | None) -> bool:
|
||||
# A session that was handed off to the gateway is now owned by the
|
||||
# gateway process. The CLI must not finalize it on exit — that sets
|
||||
# end_reason on a row the gateway reopened and is actively writing
|
||||
# to, causing the handoff leg to vanish from session history (#88234).
|
||||
if session_id is not None and session_id in _handed_off_session_ids:
|
||||
return False
|
||||
if not _single_query_finalize_attempted_session_ids:
|
||||
return True
|
||||
if session_id is None:
|
||||
return False
|
||||
return session_id not in _single_query_finalize_attempted_session_ids
|
||||
if session_id in _single_query_finalize_attempted_session_ids:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _notify_session_finalize(
|
||||
@@ -1315,6 +1330,10 @@ def _emit_interrupted_session_end(cli, *, reason: str = "keyboard_interrupt") ->
|
||||
pass
|
||||
|
||||
session_id = getattr(agent, "session_id", None) or getattr(cli, "session_id", None)
|
||||
# Don't emit session-end for a session that was handed off to the
|
||||
# gateway — the gateway owns the lifecycle now (#88234).
|
||||
if session_id in _handed_off_session_ids:
|
||||
return
|
||||
if session_id:
|
||||
try:
|
||||
cli.session_id = session_id
|
||||
|
||||
Reference in New Issue
Block a user