fix(session-titles): resolve the canonical Bot Chat by exact title, never a "#N" sibling

`SessionDB.resolve_session_by_title()` prefers a "`<title> #N`" continuation over the
exact match. For the canonical Bot Chat that preference is wrong: Bot Mode identifies
that session BY its exact title, and this lookup is the only one every DM transport
shares (`hermes -p <bot> chat --in ~ -c "Bot Chat"` — used by `message_agent`,
`bot_relay` and cron delivery).

Once a numbered sibling exists (a Desktop branch of the canonical chat,
`title_source='derived'`), every teammate DM resolves to that sibling: a visible,
unmanaged session where the `message_agent` gate is off, so the addressed bot cannot
answer and the sender gets silence instead of an error.

Return the exact match when the requested title IS the canonical Bot Chat title; every
other title keeps the "newest continuation wins" behaviour untouched.

(cherry picked from commit e92638b4c53c97c48db4d9bbb2d3b6b580d289a7)
This commit is contained in:
nicecuprani
2026-09-17 14:28:56 +03:00
committed by Teknium
parent cfa54b1e62
commit 06475d1138
2 changed files with 41 additions and 0 deletions

View File

@@ -167,6 +167,15 @@ class SessionTitlesMixin:
def resolve_session_by_title(self, title: str) -> Optional[str]: def resolve_session_by_title(self, title: str) -> Optional[str]:
"""Resolve a title to a session ID, preferring the latest "title #N" continuation.""" """Resolve a title to a session ID, preferring the latest "title #N" continuation."""
exact = self.get_session_by_title(title) exact = self.get_session_by_title(title)
# Exception to the "#N continuation" preference: the canonical Bot Chat's identity
# IS its exact title (Bot Mode re-resolves it by name on every open, no id pointer).
# A "<title> #N" sibling — a Desktop branch or a client-minted numbered row — is NOT
# a Bot Mode session: it is visible, unmanaged, and the message_agent gate is off in
# it. Every DM transport (``hermes -p <bot> chat --in ~ -c "Bot Chat"``: message_agent,
# bot_relay, cron delivery) resolves through here, so letting the numbered row win
# silently routes teammates' messages into a chat whose bot cannot answer back.
if exact is not None and title == self.CANONICAL_BOT_CHAT_TITLE:
return exact["id"]
# Escape LIKE wildcards so "%"/"_" in titles cannot false-match. # Escape LIKE wildcards so "%"/"_" in titles cannot false-match.
numbered = self._read_all( numbered = self._read_all(
"SELECT id, title, started_at FROM sessions " "SELECT id, title, started_at FROM sessions "

View File

@@ -142,3 +142,35 @@ def test_auto_titler_can_rename_visible_derived_bot_chat(db):
source=SessionDB.TITLE_SOURCE_LLM, source=SessionDB.TITLE_SOURCE_LLM,
) )
assert db.get_session("visible")["title"] == "Renamed by titler" assert db.get_session("visible")["title"] == "Renamed by titler"
def test_numbered_branch_never_shadows_the_canonical_chat(db):
"""Title resolution must land on the canonical Bot Chat, never on a "#N" sibling.
Every DM transport resolves the target bot by that exact name
(``hermes -p <bot> chat --in ~ -c "Bot Chat"``: message_agent, bot_relay, cron
delivery). A numbered sibling — a Desktop branch, which is visible and NOT
Bot-Mode-managed — would otherwise swallow each teammate's message into a
session whose bot has no message_agent and cannot answer.
"""
canonical = _make_canonical(db, "forever")
db.create_session("branch", source="desktop", parent_session_id=canonical)
assert db._set_session_title(
"branch",
f"{SessionDB.CANONICAL_BOT_CHAT_TITLE} #2",
source=SessionDB.TITLE_SOURCE_DERIVED,
)
assert db.set_session_hidden("branch", False)
assert db.resolve_session_by_title(SessionDB.CANONICAL_BOT_CHAT_TITLE) == canonical
def test_numbered_continuation_still_wins_for_ordinary_titles(db):
# Control: the "#N continuation" preference is the point of the lookup — it
# stays intact for every non-Bot-Chat title.
db.create_session("plain", source="cli")
assert db.set_session_title("plain", "My session")
db.create_session("plain2", source="cli")
assert db.set_session_title("plain2", "My session #2")
assert db.resolve_session_by_title("My session") == "plain2"