From f1e0292517c15be09f9f1fb6a61046993b562586 Mon Sep 17 00:00:00 2001 From: millerc79 Date: Sat, 2 May 2026 19:19:24 -0500 Subject: [PATCH] fix(gateway): resume sessions after crash/restart instead of blanket suspend suspend_recently_active() was unconditionally setting suspended=True on startup, causing get_or_create_session() to wipe conversation history on every restart. Change to set resume_pending=True instead, so sessions auto-resume while still allowing stuck-loop escalation after 3 failures. --- gateway/run.py | 2 +- gateway/session.py | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 97f72121b..aadb067dc 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2755,7 +2755,7 @@ class GatewayRunner: try: suspended = self.session_store.suspend_recently_active() if suspended: - logger.info("Suspended %d in-flight session(s) from previous run", suspended) + logger.info("Marked %d in-flight session(s) as resumable from previous run", suspended) except Exception as e: logger.warning("Session suspension on startup failed: %s", e) diff --git a/gateway/session.py b/gateway/session.py index fcff336af..3129f7a32 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -1086,19 +1086,22 @@ class SessionStore: return len(removed_keys) def suspend_recently_active(self, max_age_seconds: int = 120) -> int: - """Mark recently-active sessions as suspended. + """Mark recently-active sessions as resumable after an unexpected exit. - Called on gateway startup to prevent sessions that were likely - in-flight when the gateway last exited from being blindly resumed - (#7536). Only suspends sessions updated within *max_age_seconds* - to avoid resetting long-idle sessions that are harmless to resume. - Returns the number of sessions that were suspended. + Called on gateway startup after a crash or fast restart to preserve + in-flight sessions instead of destroying their conversation history + (#7536). Only marks sessions updated within *max_age_seconds* to + avoid touching long-idle sessions. Sets ``resume_pending=True`` so + the next incoming message on the same session_key auto-resumes from + the existing transcript. - Entries flagged ``resume_pending=True`` are skipped — those were - marked intentionally by the drain-timeout path as recoverable. - Terminal escalation for genuinely stuck ``resume_pending`` sessions - is handled by the existing ``.restart_failure_counts`` stuck-loop - counter, which runs after this method on startup. + Entries already flagged ``resume_pending=True`` are skipped. Entries + explicitly ``suspended=True`` (from /stop or stuck-loop escalation) + are also skipped. Terminal escalation for genuinely stuck sessions + is still handled by the existing ``.restart_failure_counts`` counter + (threshold 3), which runs after this method and sets ``suspended=True``. + + Returns the number of sessions marked resumable. """ from datetime import timedelta @@ -1110,7 +1113,9 @@ class SessionStore: if entry.resume_pending: continue if not entry.suspended and entry.updated_at >= cutoff: - entry.suspended = True + entry.resume_pending = True + entry.resume_reason = "restart_interrupted" + entry.last_resume_marked_at = _now() count += 1 if count: self._save()