hermes - ✅(Solved) Fix gateway: /voice state collides across platforms when chat IDs match [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

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#12542Fetched 2026-04-20 12:18:27
View on GitHub
Comments
0
Participants
1
Timeline
8
Reactions
0
Participants
Timeline (top)
referenced ×5cross-referenced ×2closed ×1

/voice mode is persisted and looked up by bare chat_id only, so chats from different platforms collide whenever they share the same ID string. Toggling voice mode in one platform silently overwrites the other platform's state.

Root Cause

/voice mode is persisted and looked up by bare chat_id only, so chats from different platforms collide whenever they share the same ID string. Toggling voice mode in one platform silently overwrites the other platform's state.

Fix Action

Fixed

PR fix notes

PR #12824: fix(gateway): namespace voice mode state by platform to prevent cross-platform collision

Description (problem / solution / changelog)

What changed

Voice mode state keys in gateway/run.py are now namespaced as platform:chat_id instead of bare chat_id. This prevents cross-platform state collisions when the same chat ID exists on multiple platforms (e.g., Discord channel 12345 and Telegram chat 12345).

Before: Voice mode state was keyed by chat_id alone. If a Discord channel and Telegram chat shared the same numeric ID, enabling voice mode on one platform would bleed into the other.

After: State keys include the platform identifier (e.g., telegram:12345, discord:12345), providing full isolation. Legacy unprefixed keys in persisted storage are gracefully skipped with a warning during load.

Changes:

  • Added _voice_key(platform, chat_id) helper to construct namespaced keys
  • All 15+ _voice_mode dict operations now use the helper
  • _load_voice_modes skips legacy unprefixed keys with a warning
  • _sync_voice_mode_state_to_adapter filters by platform prefix

How to test

  1. Configure two platforms (e.g., Telegram and Discord) with overlapping chat IDs
  2. Enable voice mode on one platform and verify it does not affect the other
  3. Run the test suite:
    pytest tests/gateway/test_voice_mode_platform_isolation.py -v

Platforms tested

  • Linux (Docker container, Python 3.11)

Closes #12542

Changed files

  • gateway/run.py (modified, +39/-18)
  • tests/gateway/test_voice_mode_platform_isolation.py (added, +242/-0)

PR #12844: fix(gateway): namespace voice mode state by platform (#12542)

Description (problem / solution / changelog)

Salvage of #12824 by @Tranquil-Flow onto current main.

Summary

Voice mode state was keyed by bare chat_id, so Telegram chat "123" and Slack chat "123" collided — toggling voice mode on one platform silently overwrote the other. Keys are now namespaced as platform:chat_id.

Closes #12542. Original PR #12824.

Changes

  • gateway/run.py: Added _voice_key(platform, chat_id) helper; updated all 15+ _voice_mode call sites; _sync_voice_mode_state_to_adapter filters by platform prefix; _load_voice_modes skips legacy unprefixed keys with a warning.
  • _handle_voice_channel_join/leave now use event.source.platform instead of hardcoded Platform.DISCORD (consistent with other voice handlers).
  • tests/gateway/test_voice_command.py: updated existing tests to match the new key format (wasn't done in the original PR, caused 21 regressions); adapter mocks in sync tests now set adapter.platform = Platform.* (required by new isinstance check).
  • Added platform isolation regression test + dedicated tests/gateway/test_voice_mode_platform_isolation.py from the original PR (minus the decorative test_legacy_key_collision_bug which mutated a single key twice, not a real collision).

Validation

BeforeAfter
test_voice_command.py (180 tests)21 failed, 159 passed180 passed
test_voice_mode_platform_isolation.py (12 tests)n/a12 passed
Broader tests/gateway/ suite2 pre-existing whatsapp failures on mainsame 2 (unrelated)

Migration

Existing users' persisted voice_mode state is dropped on first load after upgrade (warning logged per key). They need to re-run /voice on|tts|off once.

Changed files

  • gateway/run.py (modified, +39/-18)
  • tests/gateway/test_voice_command.py (modified, +51/-30)
  • tests/gateway/test_voice_mode_platform_isolation.py (added, +218/-0)

Code Example

from types import SimpleNamespace
import asyncio
import gateway.run as runmod

runner = runmod.GatewayRunner()
runner._voice_mode = {}

telegram_event = SimpleNamespace(
    source=SimpleNamespace(chat_id='123', platform=runmod.Platform.TELEGRAM),
    get_command_args=lambda: 'on',
)
slack_event = SimpleNamespace(
    source=SimpleNamespace(chat_id='123', platform=runmod.Platform.SLACK),
    get_command_args=lambda: 'off',
)

asyncio.run(runner._handle_voice_command(telegram_event))
asyncio.run(runner._handle_voice_command(slack_event))
print(runner._voice_mode)
# Actual: {'123': 'off'}
RAW_BUFFERClick to expand / collapse

Summary

/voice mode is persisted and looked up by bare chat_id only, so chats from different platforms collide whenever they share the same ID string. Toggling voice mode in one platform silently overwrites the other platform's state.

Affected code

  • gateway/run.py:653-666
  • gateway/run.py:669-674
  • gateway/run.py:4818-4887
  • gateway/run.py:4934-4973
  • gateway/run.py:5059

Why this is a bug

Voice mode state is stored as {chat_id: mode} with no platform namespace. Runtime reads and writes use event.source.chat_id directly, so Telegram chat "123" and Slack chat "123" share the same persisted key.

Minimal reproduction

from types import SimpleNamespace
import asyncio
import gateway.run as runmod

runner = runmod.GatewayRunner()
runner._voice_mode = {}

telegram_event = SimpleNamespace(
    source=SimpleNamespace(chat_id='123', platform=runmod.Platform.TELEGRAM),
    get_command_args=lambda: 'on',
)
slack_event = SimpleNamespace(
    source=SimpleNamespace(chat_id='123', platform=runmod.Platform.SLACK),
    get_command_args=lambda: 'off',
)

asyncio.run(runner._handle_voice_command(telegram_event))
asyncio.run(runner._handle_voice_command(slack_event))
print(runner._voice_mode)
# Actual: {'123': 'off'}

Expected behavior

  • Voice mode should be isolated per platform/chat pair.
  • Persisted state should not let one platform's /voice command modify another platform's chat.

Actual behavior

  • A later /voice command on a different platform overwrites the earlier platform's state when the chat IDs match.

Suggested investigation

  • Key persisted voice state by a stable composite such as <platform>:<chat_id>.
  • Add a regression test covering two platforms that share the same chat ID string.

extent analysis

TL;DR

Modify the voice mode state storage to use a composite key that includes the platform, such as <platform>:<chat_id>, to prevent collisions between different platforms.

Guidance

  • Investigate modifying the runner._voice_mode dictionary to store voice mode state with a composite key, such as <platform>:<chat_id>, to isolate state per platform/chat pair.
  • Update the _handle_voice_command method to use the composite key when reading and writing voice mode state.
  • Add a regression test to cover the scenario where two platforms share the same chat ID string, to ensure the fix prevents state overwrites.
  • Review the affected code lines in gateway/run.py to ensure the composite key is used consistently throughout the voice mode state management.

Example

runner._voice_mode = {}
# ...
async def _handle_voice_command(self, event):
    composite_key = f"{event.source.platform}:{event.source.chat_id}"
    # Use the composite key to read and write voice mode state
    if event.get_command_args() == 'on':
        self._voice_mode[composite_key] = 'on'
    else:
        self._voice_mode[composite_key] = 'off'

Notes

This fix assumes that the event.source.platform value is a stable and unique identifier for each platform. If this is not the case, an alternative platform identifier may need to be used.

Recommendation

Apply the workaround by modifying the voice mode state storage to use a composite key, as this will prevent collisions between different platforms and ensure that voice mode state is isolated per platform/chat pair.

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…

FAQ

Expected behavior

  • Voice mode should be isolated per platform/chat pair.
  • Persisted state should not let one platform's /voice command modify another platform's chat.

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 gateway: /voice state collides across platforms when chat IDs match [2 pull requests, 1 participants]