mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-01 07:01:41 +00:00
feat(telegram): add disable_topic_auto_rename gateway flag
When Hermes auto-titles a session in a Telegram DM topic it currently renames the topic itself to the generated title. That works for operator-managed lanes (extra.dm_topics) but is disruptive for ad-hoc Threaded-Mode topics that users name by hand — every first exchange overwrites their chosen title. Add gateway.platforms.telegram.extra.disable_topic_auto_rename (default False, preserving prior behaviour). When set, both _schedule_telegram_topic_title_rename and the underlying _rename_telegram_topic_for_session_title short-circuit before touching the Telegram API. Internal session titles (sessions list, TUI) keep working unchanged. Also bridge the legacy top-level telegram.disable_topic_auto_rename key through to gateway.platforms.telegram.extra so users on the older config layout don't have to migrate to enable it. - Tests cover the runtime flag, the scheduling entry-point, and string truthiness coercion for YAML-loaded values. - Docs updated in messaging/telegram.md with an example block.
This commit is contained in:
parent
3ec28f34ca
commit
9d789f3a5b
4 changed files with 134 additions and 0 deletions
|
|
@ -1002,6 +1002,16 @@ def load_gateway_config() -> GatewayConfig:
|
|||
# Telegram settings → env vars (env vars take precedence)
|
||||
telegram_cfg = yaml_cfg.get("telegram", {})
|
||||
if isinstance(telegram_cfg, dict):
|
||||
# Bridge top-level legacy `telegram.disable_topic_auto_rename` into
|
||||
# gateway.platforms.telegram.extra so the runtime config sees it.
|
||||
# Read as a runtime-config flag, not env-var (no need for env override).
|
||||
if "disable_topic_auto_rename" in telegram_cfg:
|
||||
_tg_plat = platforms_data.setdefault(Platform.TELEGRAM.value, {})
|
||||
_tg_extra = _tg_plat.setdefault("extra", {})
|
||||
_tg_extra.setdefault(
|
||||
"disable_topic_auto_rename",
|
||||
telegram_cfg["disable_topic_auto_rename"],
|
||||
)
|
||||
# Prefer telegram.require_mention; fall back to the top-level shorthand.
|
||||
_effective_rm = telegram_cfg.get("require_mention", yaml_cfg.get("require_mention"))
|
||||
if _effective_rm is not None and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
|
||||
|
|
|
|||
|
|
@ -11808,6 +11808,13 @@ class GatewayRunner:
|
|||
if not self._is_telegram_topic_lane(source) or not source.chat_id or not source.thread_id:
|
||||
return
|
||||
|
||||
# Operator can fully disable per-topic auto-rename via
|
||||
# extra.disable_topic_auto_rename. Useful when topics are managed
|
||||
# by the user (ad-hoc Threaded Mode) and auto-rename would
|
||||
# overwrite their chosen names every time the auto-title fires.
|
||||
if self._telegram_topic_auto_rename_disabled(source):
|
||||
return
|
||||
|
||||
# Skip rename when the topic is operator-declared via
|
||||
# extra.dm_topics. Those topics have fixed names chosen by the
|
||||
# operator (plus optional skill binding); auto-renaming would
|
||||
|
|
@ -11876,6 +11883,29 @@ class GatewayRunner:
|
|||
except Exception:
|
||||
logger.debug("Failed to rename Telegram topic for auto-generated title", exc_info=True)
|
||||
|
||||
def _telegram_topic_auto_rename_disabled(self, source: SessionSource) -> bool:
|
||||
"""Return True when operator disabled per-topic auto-rename for this Telegram chat.
|
||||
|
||||
Controlled via ``gateway.platforms.telegram.extra.disable_topic_auto_rename``.
|
||||
Default is False (auto-rename enabled, preserves prior behaviour).
|
||||
"""
|
||||
platform_cfg = (
|
||||
self.config.platforms.get(source.platform)
|
||||
if getattr(self, "config", None) and getattr(self.config, "platforms", None)
|
||||
else None
|
||||
)
|
||||
if platform_cfg is None:
|
||||
return False
|
||||
extra = getattr(platform_cfg, "extra", None) or {}
|
||||
value = extra.get("disable_topic_auto_rename")
|
||||
if value is None:
|
||||
return False
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
def _schedule_telegram_topic_title_rename(
|
||||
self,
|
||||
source: SessionSource,
|
||||
|
|
@ -11885,6 +11915,8 @@ class GatewayRunner:
|
|||
"""Schedule a topic rename from the auto-title background thread."""
|
||||
if not title or not self._is_telegram_topic_lane(source):
|
||||
return
|
||||
if self._telegram_topic_auto_rename_disabled(source):
|
||||
return
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
|
|
|
|||
|
|
@ -840,6 +840,85 @@ async def test_operator_declared_topic_is_not_auto_renamed(tmp_path):
|
|||
fake.rename_dm_topic.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_topic_auto_rename_extra_skips_rename(tmp_path):
|
||||
"""extra.disable_topic_auto_rename=True must short-circuit auto-rename."""
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.apply_telegram_topic_migration()
|
||||
db.create_session("sess-topic", source="telegram", user_id="208214988")
|
||||
db.bind_telegram_topic(
|
||||
chat_id="208214988",
|
||||
thread_id="42",
|
||||
user_id="208214988",
|
||||
session_key="agent:main:telegram:dm:208214988:42",
|
||||
session_id="sess-topic",
|
||||
)
|
||||
runner = _make_runner(session_db=db)
|
||||
runner._telegram_topic_mode_enabled = lambda source: True
|
||||
# Flip the operator switch.
|
||||
runner.config.platforms[Platform.TELEGRAM].extra["disable_topic_auto_rename"] = True
|
||||
|
||||
await runner._rename_telegram_topic_for_session_title(
|
||||
_make_source(thread_id="42"),
|
||||
"sess-topic",
|
||||
"Auto-generated title",
|
||||
)
|
||||
|
||||
runner.adapters[Platform.TELEGRAM].rename_dm_topic.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_topic_rename_respects_disable_flag(tmp_path):
|
||||
"""The scheduling entry-point must also honour disable_topic_auto_rename."""
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
runner = _make_runner(session_db=db)
|
||||
runner._telegram_topic_mode_enabled = lambda source: True
|
||||
runner.config.platforms[Platform.TELEGRAM].extra["disable_topic_auto_rename"] = "yes"
|
||||
|
||||
# If the flag is honoured we never schedule the coroutine, so
|
||||
# _rename_telegram_topic_for_session_title is never invoked.
|
||||
called = False
|
||||
|
||||
async def _spy(*args, **kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
runner._rename_telegram_topic_for_session_title = _spy
|
||||
|
||||
runner._schedule_telegram_topic_title_rename(
|
||||
_make_source(thread_id="42"),
|
||||
"sess-topic",
|
||||
"Auto-generated title",
|
||||
)
|
||||
|
||||
# Give any (incorrectly scheduled) coroutine a chance to run.
|
||||
import asyncio
|
||||
await asyncio.sleep(0)
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_telegram_topic_auto_rename_disabled_string_truthy(tmp_path):
|
||||
"""Common truthy string forms ('1', 'true', 'on', 'yes') must disable rename."""
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
runner = _make_runner(session_db=db)
|
||||
source = _make_source(thread_id="42")
|
||||
|
||||
cfg_extra = runner.config.platforms[Platform.TELEGRAM].extra
|
||||
for value in ("1", "true", "TRUE", "yes", "on"):
|
||||
cfg_extra["disable_topic_auto_rename"] = value
|
||||
assert runner._telegram_topic_auto_rename_disabled(source) is True, value
|
||||
|
||||
for value in ("0", "false", "no", "off", "", None):
|
||||
cfg_extra["disable_topic_auto_rename"] = value
|
||||
assert runner._telegram_topic_auto_rename_disabled(source) is False, value
|
||||
|
||||
# Explicit bools still work.
|
||||
cfg_extra["disable_topic_auto_rename"] = True
|
||||
assert runner._telegram_topic_auto_rename_disabled(source) is True
|
||||
cfg_extra["disable_topic_auto_rename"] = False
|
||||
assert runner._telegram_topic_auto_rename_disabled(source) is False
|
||||
|
||||
|
||||
def test_general_topic_is_treated_as_root_lobby(tmp_path):
|
||||
"""Messages in the Telegram General topic (thread_id=1) route to the lobby, not a lane."""
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
|
|
|
|||
|
|
@ -497,6 +497,18 @@ Every topic gets its own conversation history, model state, tool execution, and
|
|||
|
||||
When Hermes generates a session title for a topic (via the auto-title pipeline, after the first exchange), the Telegram topic itself is renamed to match — e.g. "New Topic" becomes "Database migration plan". The rename is best-effort: failures are logged but don't break the session.
|
||||
|
||||
To disable this and keep your manually-chosen topic names untouched, set:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
disable_topic_auto_rename: true
|
||||
```
|
||||
|
||||
When this flag is on, Hermes still generates an internal session title (used by `hermes sessions`, the TUI, etc.) but never edits the Telegram topic name. Useful when you organise topics by hand under BotFather Threaded Mode and don't want every first reply to overwrite the title.
|
||||
|
||||
### `/new` inside a topic
|
||||
|
||||
Resets the current topic's session (new session ID, fresh history) without touching other topics. Hermes replies with a reminder that for parallel work, creating another topic (via **All Messages**) is usually what you want.
|
||||
|
|
@ -530,6 +542,7 @@ Shows the current topic's binding: session title, session ID, and hints for `/ne
|
|||
- Each inbound DM message looks up its `(chat_id, thread_id)` binding. If present, the lookup routes the message to the bound session via `SessionStore.switch_session()` so the session-key-to-session-id mapping stays consistent on disk
|
||||
- `/new` inside a topic rewrites the binding row to point at the new session ID, so the next message stays on the fresh session
|
||||
- Topics declared in `extra.dm_topics` are **never auto-renamed** — the operator-chosen name is preserved even when multi-session mode is enabled
|
||||
- Set `extra.disable_topic_auto_rename: true` to turn off auto-rename for **all** topics in the chat (ad-hoc topics created via Threaded Mode included)
|
||||
- The General (pinned top) topic in a forum-enabled DM is treated as the root lobby, regardless of whether Telegram delivers its messages with `message_thread_id=1` or with no thread_id
|
||||
- Root-lobby reminders are rate-limited to one message per 30 seconds per chat — a user who forgets topic mode is on and types ten prompts in the root won't get ten replies
|
||||
- BotFather setup screenshots are rate-limited to one send per 5 minutes per chat — repeated `/topic` attempts while Threads Settings are still disabled won't re-upload the same image
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue