diff --git a/hermes_state_sessions.py b/hermes_state_sessions.py index a7039236bc..82e59eded0 100644 --- a/hermes_state_sessions.py +++ b/hermes_state_sessions.py @@ -967,6 +967,11 @@ class SessionSessionsMixin: ): if key in tip_row: merged[key] = tip_row[key] + if merged.get("title") is None: + # The title is carried root->tip AFTER the publish transaction; a rotation cut off in + # between leaves it on the ended root, and exact-title lookups (`hermes peer dm` -> + # canonical "Bot Chat") must still see the lineage under its name (#106165). + merged["title"] = s.get("title") merged["_lineage_root_id"] = s["id"] merged["_lineage_ids"] = chain projected.append(merged) @@ -1089,7 +1094,7 @@ class SessionSessionsMixin: tip.id, tip.source, tip.model, - tip.title, + COALESCE(tip.title, s.title) AS title, s.started_at AS started_at, tip.ended_at, tip.end_reason, diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index bb676562b9..99e6438e9f 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -1708,6 +1708,21 @@ class TestSessionTitleLineage: # The unrelated holder keeps its title. assert db.get_session("a")["title"] == "shared" + def test_projected_tip_inherits_root_title_when_untitled(self, db): + """A rotation that ended the root before the title carry ran leaves the name on the + root only; the projected lineage row must still surface it (exact-title lookups such as + `hermes peer dm` -> canonical "Bot Chat", #106165). A titled tip keeps its own title.""" + import time as _time + self._make_compression_chain(db, _time.time() - 3600) + db.set_session_title("root", "Bot Chat") + + rows = db.list_sessions_rich(limit=50, order_by_last_active=True, search_query="Bot Chat") + assert [(r["id"], r["title"], r["_lineage_root_id"]) for r in rows] == [("tip", "Bot Chat", "root")] + + db.set_session_title("tip", "renamed tip") + rows = db.list_sessions_rich(limit=50, order_by_last_active=True) + assert [(r["id"], r["title"]) for r in rows] == [("tip", "renamed tip")] + class TestSanitizeTitle: