diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index c5978ebfff4..5645f91ddf7 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -9306,7 +9306,7 @@ def _session_latest_descendant(session_id: str, db): """ WITH RECURSIVE descendants(id, parent_session_id, started_at) AS ( SELECT id, parent_session_id, started_at FROM sessions WHERE id = ? - UNION ALL + UNION SELECT s.id, s.parent_session_id, s.started_at FROM sessions s JOIN descendants d ON s.parent_session_id = d.id diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 748800ca1b2..c67371b4e33 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1138,6 +1138,29 @@ class TestWebServerEndpoints: assert worker_resp.status_code == 200 assert worker_resp.json()["session_id"] == "worker-tip" + def test_latest_descendant_survives_parent_cycle(self): + """Regression for the #39140 CTE salvage: a corrupted parent chain + that loops (a -> b -> a) must terminate (UNION dedup) instead of + recursing forever like UNION ALL would.""" + from hermes_state import SessionDB + + db = SessionDB() + try: + db.create_session(session_id="cyc-a", source="cli") + db.create_session( + session_id="cyc-b", source="cli", parent_session_id="cyc-a" + ) + db._conn.execute( + "UPDATE sessions SET parent_session_id='cyc-b' WHERE id='cyc-a'" + ) + db._conn.commit() + finally: + db.close() + + resp = self.client.get("/api/sessions/cyc-a/latest-descendant") + assert resp.status_code == 200 + assert resp.json()["session_id"] == "cyc-b" + def test_analytics_endpoints_read_requested_profile(self): from hermes_state import SessionDB from hermes_cli import profiles as profiles_mod