diff --git a/gateway/session.py b/gateway/session.py index 7a1273e93..605a3602f 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -1193,12 +1193,14 @@ class SessionStore: # end_reason not None -> session ended — prune if row is not None and row.get("end_reason") is not None: recovered_entry = None + recovery_lookup_failed = False if entry.origin is not None: try: recovered_entry = self._recover_session_from_db( session_key=key, source=entry.origin, now=_now(), + raise_on_lookup_error=True, ) except Exception as exc: logger.debug( @@ -1208,6 +1210,10 @@ class SessionStore: entry.session_id, exc, ) + recovery_lookup_failed = True + + if recovery_lookup_failed: + continue # If the stale entry points at a compression-ended parent but # a newer live child session exists for the exact same gateway @@ -1431,6 +1437,7 @@ class SessionStore: session_key: str, source: SessionSource, now: datetime, + raise_on_lookup_error: bool = False, ) -> Optional[SessionEntry]: """Rebuild a missing session-key mapping from durable state.db data.""" if not self._db: @@ -1449,6 +1456,8 @@ class SessionStore: ) except Exception as exc: logger.debug("Gateway session DB recovery failed for %s: %s", session_key, exc) + if raise_on_lookup_error: + raise return None if not recovered: return None diff --git a/tests/gateway/test_session_store_stale_prune.py b/tests/gateway/test_session_store_stale_prune.py index 9b8cf1fe4..c5811adad 100644 --- a/tests/gateway/test_session_store_stale_prune.py +++ b/tests/gateway/test_session_store_stale_prune.py @@ -151,6 +151,25 @@ class TestPruneStaleSessionsLocked: assert key not in store._entries + def test_keeps_stale_entry_when_recovery_lookup_raises(self, tmp_path): + """Indeterminate recovery must not delete the only routing handle. + + Startup pruning sees an ended parent and tries to repoint it to the + latest live gateway child. If that recovery query raises, deleting the + sessions.json entry loses the routing key entirely; keeping it lets the + runtime stale guard retry recovery on the next message. + """ + key = "agent:main:telegram:dm:5140768830" + db = _db_returning({"sid_parent": {"end_reason": "compression", "id": "sid_parent"}}) + db.find_latest_gateway_session_for_peer.side_effect = RuntimeError("db busy") + store = _make_store_with_db(tmp_path, db) + store._entries[key] = _make_entry_with_origin(key, "sid_parent") + + store._prune_stale_sessions_locked() + + assert key in store._entries + assert store._entries[key].session_id == "sid_parent" + def test_noop_when_db_is_none(self, tmp_path): config = GatewayConfig(default_reset_policy=SessionResetPolicy(mode="none")) with patch("gateway.session.SessionStore._ensure_loaded"):