diff --git a/hermes_state.py b/hermes_state.py index b1e558d438..a1624dc28a 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -10979,13 +10979,13 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin) """Write a title, enforcing provenance precedence. ``source`` is one of ``TITLE_SOURCE_{DERIVED,LLM,USER}``. A ``user`` - write is authoritative except for a canonical Bot Chat identity. An - automatic write (``derived``/``llm``) lands only when the row is - untitled or the stored title has strictly lower authority, so the - instant ``derived`` title upgrades to ``llm`` exactly once and neither - can ever overwrite a name the user typed. Re-running the titler on an - already-``llm`` row is a no-op, which is what stops a session renaming - itself. + write always lands — an explicit rename is authoritative. An automatic + write (``derived``/``llm``) lands only when the row is untitled or the + stored title has strictly lower authority, so the instant ``derived`` + title upgrades to ``llm`` exactly once and neither can ever overwrite a + name the user typed. Re-running the titler on an already-``llm`` row is + a no-op, which is what stops a session renaming itself. The one thing + no writer may do is move a hidden canonical Bot Chat off its title. The read and the write are one compare-and-swap inside a single transaction, so a manual ``/title`` racing an in-flight generation @@ -11010,7 +11010,9 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin) # surface funnels through (gateway session.title, /title, CLI # rename, REST). Hidden is the discriminator: canonical chats are # born hidden; an ordinary visible session a user happens to call - # "Bot Chat" stays freely renameable. + # "Bot Chat" stays freely renameable. Provenance-blind: an + # automatic llm write outranks a derived title, so the auto-titler + # would otherwise rename the row too (#99517) — it no-ops instead. if ( (current["title"] or "") == self.CANONICAL_BOT_CHAT_TITLE and bool(current["hidden"]) diff --git a/tests/hermes_state/test_canonical_title_guard.py b/tests/hermes_state/test_canonical_title_guard.py index 6ab7f88812..784825c25e 100644 --- a/tests/hermes_state/test_canonical_title_guard.py +++ b/tests/hermes_state/test_canonical_title_guard.py @@ -72,6 +72,9 @@ def test_auto_titler_still_cannot_touch_the_canonical_row(db): def test_auto_titler_cannot_rename_derived_canonical_bot_chat(db): + # #99517: the guard must be provenance-blind. A derived (rank 0) canonical + # title loses to an llm (rank 1) auto-title on precedence alone, so the + # identity check — not precedence — has to stop the write. db.create_session("derived", source="desktop") assert db._set_session_title( "derived", @@ -91,6 +94,8 @@ def test_auto_titler_cannot_rename_derived_canonical_bot_chat(db): def test_auto_titler_can_rename_visible_derived_bot_chat(db): + # Control: hidden is still the discriminator — a visible session that + # merely carries the text "Bot Chat" upgrades derived -> llm as usual. db.create_session("visible", source="desktop") assert db._set_session_title( "visible",