fix(gateway): unify reset boundaries vs recovery — promote accidental ends, honor mode=none, adapter-aware resume guidance
Unifies the two gateway subsystems that were fighting each other: the 'never lose a session' recovery machinery (#54878 stale-route self-heal, find_latest_gateway_session_for_peer reopening agent_close/ws_orphan_reap rows) and the session reset/expiry machinery (expiry watcher, /new, /resume, resume_pending freshness gate). The unified contract: - INTENTIONAL boundaries (expiry finalization, auto-reset, /new, /resume switch) are recorded durably via promote_to_session_reset(), which upgrades accidental recoverable end_reasons (agent_close, ws_orphan_reap) to the explicit boundary while preserving other explicit reasons (compression, etc.). Recovery then correctly refuses to resurrect them. - ACCIDENTAL ends (crash, cleanup bug, mistaken reaper) stay recoverable — genuine crash recovery is untouched. On top of the cherry-picked contributor commits: - promote_to_session_reset widened to ws_orphan_reap + parameterized reason so auto-reset paths stay auditable (idle/daily/suspended/ resume_pending_expired) (#61220, #61993, #63539) - get_or_create_session auto-reset, reset_session (/new), and switch_session (/resume) all write through the promote path — the first-reason-wins end_session no-op could previously leave a reset session resurrectable behind a stale agent_close row (#61993) - resume_pending freshness gate now honors session_reset.mode=none: explicit opt-out of automatic resets also opts out of the zombie gate (#61052) - resume recovery note extracted to build_resume_recovery_note() and made adapter-aware via a new interactive_resume capability flag: webhook/api_server auto-resume turns now CONTINUE the interrupted task instead of emitting an unanswerable 'session restored' acknowledgement that abandoned the work (#57056) - tests updated to call the real note builder instead of mirroring it E2E-validated against a real SessionDB + SessionStore in a temp HERMES_HOME: expiry->agent_close->no-resurrection, /new promote, crash recovery preserved, mode=none opt-out, routing-table flag sync.
This commit is contained in:
+27
-8
@@ -2286,15 +2286,33 @@ class SessionDB:
|
||||
)
|
||||
self._execute_write(_do)
|
||||
|
||||
def promote_to_session_reset(self, session_id: str) -> bool:
|
||||
"""Mark a session as ended by session_reset — but only when safe.
|
||||
def promote_to_session_reset(
|
||||
self, session_id: str, reason: str = "session_reset"
|
||||
) -> bool:
|
||||
"""Durably mark a session as ended by an intentional reset boundary.
|
||||
|
||||
Promotes *only* live rows (``ended_at IS NULL``) or rows ended with
|
||||
``agent_close``. Explicit conversation boundaries such as
|
||||
``compression``, ``session_reset``, ``new_command``, etc. are
|
||||
Promotes *only* live rows (``ended_at IS NULL``) or rows carrying an
|
||||
accidental end_reason that the recovery query
|
||||
(``find_latest_gateway_session_for_peer``) treats as recoverable:
|
||||
``agent_close`` (older gateway cleanup bug) and ``ws_orphan_reap``
|
||||
(mistaken TUI reaper). Explicit conversation boundaries such as
|
||||
``compression``, ``session_reset``, ``session_switch``, etc. are
|
||||
preserved — the first writer wins for those, and a later expiry
|
||||
finalization must not silently overwrite them.
|
||||
|
||||
Plain ``end_session()`` is NOT sufficient for reset boundaries: it
|
||||
no-ops on an already-ended row, so a row that agent cleanup already
|
||||
closed as ``agent_close`` would stay recoverable and stale-route
|
||||
recovery would resurrect the reset session with its full history
|
||||
(#61220, #61993, #63539).
|
||||
|
||||
Keep this promotion set in sync with the recoverable set in
|
||||
``find_latest_gateway_session_for_peer`` — any reason recovery would
|
||||
reopen must be promotable here.
|
||||
|
||||
``reason`` lets reset paths keep their auditable specific reasons
|
||||
(``idle``, ``daily``, ``suspended``, ``resume_pending_expired``).
|
||||
|
||||
Returns ``True`` when the row was promoted, ``False`` when skipped
|
||||
(already has a different explicit end_reason, or row not found).
|
||||
"""
|
||||
@@ -2304,9 +2322,10 @@ class SessionDB:
|
||||
|
||||
def _do(conn):
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET ended_at = ?, end_reason = 'session_reset' "
|
||||
"WHERE id = ? AND (ended_at IS NULL OR end_reason = 'agent_close')",
|
||||
(now, session_id),
|
||||
"UPDATE sessions SET ended_at = ?, end_reason = ? "
|
||||
"WHERE id = ? AND (ended_at IS NULL "
|
||||
"OR end_reason IN ('agent_close', 'ws_orphan_reap'))",
|
||||
(now, reason, session_id),
|
||||
)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
Reference in New Issue
Block a user