hermes - ✅(Solved) Fix [Bug]: Telegram /voice off legacy state is dropped by #12542 migration, causing one unwanted voice reply after upgrade [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14025Fetched 2026-04-23 07:47:16
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Participants
Timeline (top)
labeled ×5cross-referenced ×2

Error Message

Relevant Logs / Traceback

Root Cause

Root Cause Analysis (optional)

The regression appears to come from the interaction of these pieces

Fix Action

Fixed

PR fix notes

PR #14035: fix(gateway): preserve legacy /voice off suppression across #12542 migration

Description (problem / solution / changelog)

Summary

Fixes #14025.

  • Preserve legacy unprefixed "<chat_id>": "off" rows in gateway_voice_mode.json on load so auto-TTS suppression survives the #12542 platform-namespacing migration. Legacy voice_only / all rows continue to be dropped — migrating them without knowing the owning platform could fire TTS on a platform the user never configured.
  • In _sync_voice_mode_state_to_adapter, make explicit <platform>:<chat_id> entries take precedence over legacy fallback rows for the same chat id on that platform, regardless of the explicit mode. Without this, a user who ran /voice on on a chat that had a legacy off row would get silently re-suppressed on the next restart — the same two-store drift as #14025 in the opposite direction.

Reproduction (before fix)

  1. On a pre-#12542 build, run /voice off in a Telegram DM so gateway_voice_mode.json contains {"<telegram_chat_id>": "off"}.
  2. Upgrade to a build containing 52a972e9 (#12542). Restart.
  3. Send a voice message in that DM. Hermes sends one unwanted TTS reply because _auto_tts_disabled_chats was never rebuilt from the dropped legacy row, even though /voice status still reports off.

After fix

  • Startup load preserves {"<chat_id>": "off"} and logs one info line explaining the preservation.
  • Adapter sync applies the legacy off as a cross-platform fallback → _auto_tts_disabled_chats contains the chat id → the auto-TTS gate at gateway/platforms/base.py:1879 correctly suppresses.
  • When the user later runs /voice on|tts on any platform, a prefixed row is written. On the next restart the precedence rule ensures the explicit row wins for that platform; the legacy row continues to act as a fallback for platforms that have not yet been explicitly reconfigured.

Test plan

New tests in tests/gateway/test_voice_mode_platform_isolation.py:

  • test_load_voice_modes_preserves_legacy_off_keys — legacy off preserved, legacy all/voice_only dropped.
  • test_sync_legacy_off_applies_to_all_adapters — legacy off suppresses auto-TTS on every adapter that has no explicit prefixed row for the same chat id.
  • test_explicit_prefixed_state_overrides_legacy_off_on_same_platform — explicit prefixed rows (covering voice_only, all, and off) win over legacy fallback for the same chat id on the same platform; legacy fallback still applies to platforms that have no explicit row for that id.
  • test_legacy_off_survives_upgrade_restart_for_telegram — end-to-end regression for the exact repro from the issue.

Renamed and tightened test_load_voice_modes_skips_legacy_keystest_load_voice_modes_skips_legacy_nonoff_keys to reflect the new preservation behavior for off.

New test in tests/gateway/test_voice_command.py:

  • test_voice_on_after_legacy_off_overrides_fallback_after_restart — full lifecycle through the real _handle_voice_command / _save_voice_modes / reload / sync chain, asserts no split-brain where /voice status reports on while auto-TTS stays suppressed.

All voice-mode tests pass: pytest tests/gateway/test_voice_mode_platform_isolation.py tests/gateway/test_voice_command.py — 164 passed, 21 skipped.

Changed files

  • gateway/run.py (modified, +38/-12)
  • tests/gateway/test_voice_command.py (modified, +29/-0)
  • tests/gateway/test_voice_mode_platform_isolation.py (modified, +99/-8)

PR #14412: fix(gateway): preserve legacy voice off suppression

Description (problem / solution / changelog)

Summary

Fixes #14025.

This preserves legacy /voice off auto-TTS suppression after the platform-key migration without reintroducing cross-platform voice-mode collisions.

Root cause

After #12542, _load_voice_modes() skipped all legacy unprefixed voice-mode keys. That was safe for old all / voice_only entries because enabling voice for an unknown platform can collide, but it also dropped old off entries. Since adapter auto-TTS suppression is rebuilt only from persisted off state, a Telegram chat that had /voice off before upgrade could receive one unwanted voice reply until /voice off was run again.

Fix

  • Preserve legacy unprefixed off entries as internal legacy:<chat_id> suppression markers.
  • Continue skipping legacy unprefixed all and voice_only entries.
  • During adapter sync, apply legacy suppression only when that platform does not already have an explicit platform-prefixed state for the chat.
  • Add regressions for legacy off loading, runtime suppression restore, and platform-specific override behavior.

Validation

  • /Users/stephenyu/Documents/hermes-agent/.venv/bin/python -m pytest tests/gateway/test_voice_mode_platform_isolation.py::TestLegacyKeyMigration::test_load_voice_modes_preserves_legacy_off_only tests/gateway/test_voice_mode_platform_isolation.py::TestSyncVoiceModeStateToAdapter::test_sync_restores_legacy_off_for_any_platform tests/gateway/test_voice_mode_platform_isolation.py::TestSyncVoiceModeStateToAdapter::test_sync_platform_state_overrides_legacy_off tests/gateway/test_voice_command.py::TestHandleVoiceCommand::test_restart_restores_voice_off_state -q --tb=short
  • /Users/stephenyu/Documents/hermes-agent/.venv/bin/python -m pytest tests/gateway/test_voice_mode_platform_isolation.py tests/gateway/test_voice_command.py -q --tb=short
  • git diff --check

Changed files

  • gateway/run.py (modified, +29/-3)
  • tests/gateway/test_voice_mode_platform_isolation.py (modified, +37/-4)

Code Example

{"<telegram_chat_id>": "off"}

---

{"<telegram_chat_id>": "off"}

---

Skipping legacy unprefixed voice mode key '<telegram_chat_id>' during migration. Re-enable voice mode on that chat to rebuild the prefixed key.

---

{"telegram:<telegram_chat_id>": "off"}
RAW_BUFFERClick to expand / collapse

Bug Description

After upgrading to a build containing commit 52a972e9273c32a3912fa4a9b6df2ff2be532767 (fix(gateway): namespace voice mode state by platform to prevent cross-platform collision (#12542)), a Telegram DM that previously had /voice off can send one unwanted TTS voice reply on the next inbound voice message

The old persisted key is skipped during migration, so the logical /voice state becomes missing => off, but runtime auto-TTS suppression is not rebuilt for that chat until /voice off is run again

Steps to Reproduce

  1. On a pre-#12542 build, run /voice off in a Telegram DM so gateway_voice_mode.json contains a legacy unprefixed key like
    {"<telegram_chat_id>": "off"}
  2. Upgrade Hermes to a build containing commit 52a972e9273c32a3912fa4a9b6df2ff2be532767
  3. Restart the gateway
  4. Send a Telegram voice message in the same DM without touching /voice
  5. Observe that Hermes sends a TTS voice reply
  6. Then run bare /voice
  7. Observe it responds with Voice mode enabled, which implies the logical state was being treated as off

Expected Behavior

A previously saved /voice off state should survive the upgrade and restart, or at minimum the gateway should remain text-only for voice-input auto-TTS until the user explicitly re-enables voice replies

Actual Behavior

  • the legacy key is skipped during migration
  • /voice status and bare /voice treat missing state as off
  • but base-adapter auto-TTS suppression is not active, so the first inbound Telegram voice message can still receive a TTS voice reply
  • only an explicit /voice off after the upgrade rebuilds the new prefixed key and restores correct runtime behavior

Affected Component

Gateway, Telegram, voice mode migration

Messaging Platform (if gateway-related)

Telegram

Operating System

Linux in Docker

Python Version

3.13.5

Hermes Version

v0.10.0 (2026.4.16)

Relevant Logs / Traceback

Legacy state before migration

{"<telegram_chat_id>": "off"}

Gateway warning after upgrade

Skipping legacy unprefixed voice mode key '<telegram_chat_id>' during migration. Re-enable voice mode on that chat to rebuild the prefixed key.

State after explicit re-disable

{"telegram:<telegram_chat_id>": "off"}

After the explicit /voice off, restarts behave correctly again

Root Cause Analysis (optional)

The regression appears to come from the interaction of these pieces

  • gateway/run.py::_load_voice_modes() now skips legacy unprefixed keys introduced before #12542
  • gateway/run.py::_handle_voice_command(... status/toggle ...) treats missing state as off
  • gateway/platforms/base.py gates voice-input auto-TTS using _auto_tts_disabled_chats
  • _auto_tts_disabled_chats is rebuilt from explicit persisted off entries via _sync_voice_mode_state_to_adapter()

So after upgrade

  • logical or UI state becomes missing => off
  • runtime suppression state becomes missing => not disabled

That mismatch allows one unwanted voice reply until /voice off is run again

Proposed Fix (optional)

Migrate legacy unprefixed off keys instead of skipping them, or otherwise preserve their runtime suppression semantics during the migration window

Are you willing to submit a PR for this?

No response yet

Related

  • #12542 platform-prefixed voice-mode keys
  • #13126 another voice or TTS state mismatch in gateway logic

extent analysis

TL;DR

Migrate legacy unprefixed off keys instead of skipping them to preserve their runtime suppression semantics during the migration window.

Guidance

  • Review the gateway/run.py::_load_voice_modes() function to understand how legacy unprefixed keys are handled and consider modifying it to migrate these keys instead of skipping them.
  • Investigate the _sync_voice_mode_state_to_adapter() function in gateway/platforms/base.py to ensure it correctly rebuilds the _auto_tts_disabled_chats set after migration.
  • Test the proposed fix by running the steps to reproduce the issue and verifying that the unwanted TTS voice reply is no longer sent after the upgrade.
  • Consider adding additional logging or debugging statements to help identify the root cause of the issue and verify the effectiveness of the proposed fix.

Example

# Modified _load_voice_modes() function to migrate legacy unprefixed keys
def _load_voice_modes():
    # ...
    for key, value in legacy_voice_modes.items():
        if not key.startswith('telegram:'):
            # Migrate legacy unprefixed key to new format
            new_key = f'telegram:{key}'
            voice_modes[new_key] = value
    # ...

Notes

The proposed fix assumes that migrating the legacy unprefixed keys will correctly preserve their runtime suppression semantics. However, additional testing and verification may be necessary to ensure that this fix does not introduce any new issues.

Recommendation

Apply the proposed fix to migrate legacy unprefixed off keys instead of skipping them, as this appears to be the most straightforward solution to the issue. This fix should prevent the unwanted TTS voice reply from being sent after the upgrade.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: Telegram /voice off legacy state is dropped by #12542 migration, causing one unwanted voice reply after upgrade [2 pull requests, 1 participants]