Repository navigation
Conversation
…r bot's turn _is_duplicate_voice_transcript keeps one runner-wide store keyed by (guild_id, user_id). When one gateway serves several Discord bots (multiplexed profiles) listening in the same guild, each bot transcribes its own copy of an utterance; the first callback records it and the second returns before building its event, so that bot never answers. A `/voice join` from another text channel also inherited the previous binding's 12-second history. The key is now the receiving bot's profile, the guild, the bound text channel and the speaker. A repeated capture inside one binding is still suppressed, as NousResearch#19428 intended. Reported by andrexibiza (F5) on NousResearch#128788.
There was a problem hiding this comment.
Batch review at 0b889d0.
Please fold this into #128788 with #129303 and #130303. This directly repairs that review's F5: dedup state belongs to the receiving bot and bound conversation. The supplied regression fails on the earlier Discord version with bot B dispatch count 0, and passes with this change; the broader selection passed 57 tests with 4 skips.
Keep the same-capture suppression, two-bot separation and text-channel rebind checks in the consolidated PR. The key proves separation across owners and different bound channels. It does not prove capture-generation ownership across leave/rejoin, reconnect, or delayed transcription; those are separate lifecycle acceptance conditions from the earlier F6 thread. I found no additional reproduced defect in this key change.
%%{init: {'theme':'dark','themeVariables':{'background':'#0d1117','primaryTextColor':'#e6edf3','lineColor':'#94a3b8'}}}%%
flowchart TD
subgraph CORE[" "]
direction TB
A["Bot A transcript"] --> K["Key: owner, guild, bound chat, speaker"]
B["Bot B transcript"] --> K
K --> H{"Seen within this key?"}
H -->|same capture| S["Suppress repeat"]
H -->|new bot or chat| D["Dispatch its own turn"]
end
CORE ~~~ LEGEND
LEGEND["Legend<br/>Blue: entry / incoming<br/>Purple: ownership / policy<br/>Green: success"]
style CORE fill:#0d1117,stroke:#30363d,color:#e6edf3
style LEGEND fill:#1e293b,stroke:#94a3b8,color:#f8fafc,font-size:11px
class A,B input
class K,H step
class S,D good
classDef input fill:#172554,stroke:#60a5fa,color:#eff6ff,stroke-width:2px
classDef step fill:#3b0764,stroke:#c084fc,color:#faf5ff,stroke-width:2px
classDef good fill:#052e16,stroke:#4ade80,color:#f0fdf4,stroke-width:2px
classDef bad fill:#450a0a,stroke:#f87171,color:#fef2f2,stroke-width:2px
click A "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L275-L279"
click B "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L275-L279"
click K "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L275-L279"
click H "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L275-L279"
click S "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L275-L279"
click D "https://github.com/NousResearch/hermes-agent/blob/0b889d02455584315913cf29076bb9952f8489e5/gateway/run_voice.py#L293-L299"
Diagram source: gateway/run_voice.py:275. Batch consolidation map: published set review.
I reviewed the 13 open PRs in this burst as one set, including existing review threads, changed producers, downstream consumers, and a local composition of all 13 full PR diffs. The composition applied without conflicts and passed 419 tests in 18 files, with 6 skips, using the repository's canonical per-file test runner. This is targeted regression evidence, not a full repository test run or a live Slack, Discord, Relay or provider test. Five additional local consumer/lifecycle mechanisms still fail (6 failing checks, including positive and negative peer caches): native Discord role handoff, Slack middle-cancellation ordering, Slack reply-context ownership, lazy provider-stream permit reentry, and Relay stale peer label caches. Their owning reviews contain runnable checks and precise boundaries. GitHub returned no check runs or commit status entries for the reviewed heads; local results do not establish CI success.
Individual pinned-head check: === Summary: 3 files, 57 tests passed, 0 failed, 4 skipped (100% complete) in 6.7s (2 workers) ===.
scripts/run_tests.sh -j 2 tests/gateway/test_voice_transcript_dedup_scope.py tests/gateway/test_voice_command.py tests/gateway/test_voice_mode_platform_isolation.py| logger.debug("Unauthorized voice input from user %d, ignoring", user_id) | ||
| return | ||
| if self._is_duplicate_voice_transcript(guild_id, user_id, transcript): | ||
| binding = (getattr(adapter, "_owner_profile", None), guild_id, text_ch_id) |
There was a problem hiding this comment.
Fold this into #128788 as its dedup-scope repair. The owner/guild/bound-chat key prevents another bot or a different text binding from inheriting the speaker's recent transcript history; the supplied regression fails on the earlier version and passes with this patch. Preserve the same-capture suppression and both separation controls in the consolidated tests. Do not infer a capture-generation guarantee from this tuple: a fresh join to the same chat retains the same key, and delayed transcription ownership needs a separately specified lifecycle contract. This is an acceptance-boundary distinction, not a new reproduced generation defect.
|
Thanks. We're keeping this separate from #128788 on purpose.
|
There was a problem hiding this comment.
Thank you @jonpol01: your fix landed in #133470, cherry-picked with your authorship kept. We shortened one comment so gateway/run.py stays within its code-health size cap and added type hints to the test. Thanks also to @andrexibiza for the review. Closing in favour of #133470.
What does this PR do?
When one gateway serves several Discord bots (multiplexed profiles) listening in the same guild, each bot now answers its own copy of an utterance. Before, the voice-transcript dedup was one runner-wide store keyed by
(guild_id, user_id): the first bot's callback recorded the utterance and the second bot's returned as a "duplicate" before building its event. A/voice joinfrom another text channel also inherited the previous binding's 12-second history.The key is now (receiving bot's profile, guild, bound text channel, speaker). A capture emitted twice inside one binding is still suppressed, which is what #19428 added the dedup for.
Reported by @andrexibiza (F5) in the review of #128788.
Related Issue
Fixes #130307
Type of Change
Changes Made
gateway/run_voice.py:_is_duplicate_voice_transcripttakes abindingtuple instead of the bare guild id;_handle_voice_channel_inputpasses(adapter._owner_profile, guild_id, text_ch_id). Normalization, the 12 s window, the fuzzy match and the 5-entry cap are unchanged.gateway/run.py: the store's comment and type follow the new key.tests/gateway/test_voice_transcript_dedup_scope.py: one invariant test through the real callback: two bots each dispatch the same utterance once, a repeat on one bot is suppressed, and a rebind to another text channel dispatches again.Docs: N/A. No page describes voice-transcript dedup.
How to Test
scripts/run_tests.sh tests/gateway/test_voice_transcript_dedup_scope.py: 1 passed.main(233434b414):assert (1, 0) == (1, 1)(bot B dropped).scripts/run_tests.sh tests/gateway/test_voice_command.py tests/gateway/test_voice_mode_platform_isolation.py tests/gateway/test_discord_race_polish.py tests/gateway/test_discord_voice_mixer.py tests/gateway/test_platform_reconnect.py: all pass. Ruff clean. Python 3.14.Checked while building:
_handle_voice_channel_inputis the only one.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass. I ran only the files listed above viascripts/run_tests.sh, not the full suite.Documentation & Housekeeping
docs/, docstrings) — or N/A (see "Docs: N/A" above)cli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A