fix(cli): only mark speech-to-text messages as voice input - #65925
Closed
brian717 wants to merge 1 commit into
Closed
fix(cli): only mark speech-to-text messages as voice input#65925brian717 wants to merge 1 commit into
brian717 wants to merge 1 commit into
Conversation
Voice mode stays active while the user types, but the "[Voice input ...]" prefix was selected from `self._voice_mode` alone. Every message sent while voice mode was on therefore reached the model labelled as a transcript, so the model could not tell speech-to-text output from typed text -- and read a typed troubleshooting message as evidence that transcription had started working when it had not. Tag the transcript at the one place it is produced (`_voice_stop_and_transcribe`) with a `VoiceTranscript` str subclass, so it rides the existing `_pending_input` queue and every `isinstance(..., str)` check unchanged. `process_loop` reads the tag off the raw queue item -- before the input sanitizers rebuild it as a plain str -- and passes it to `chat(voice_input=...)`. Provenance and conciseness are now decided separately: the reply is spoken aloud for as long as voice mode is on, so typed messages keep the brevity guidance but are labelled "Typed input (not transcribed)" instead of claiming to be voice. The prefix for a real transcript is unchanged, and `chat` defaults to `voice_input=False` so no caller can claim voice input by accident. Fixes NousResearch#65827
Collaborator
brian717
marked this pull request as ready for review
July 16, 2026 20:55
Contributor
Author
|
Looks like a dupe of #11744. I'll close this one out. |
tonydwb
reviewed
Jul 16, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment
Looks Good
- Adds
VoiceTranscript(str)marker class to distinguish speech-to-text messages from typed input _build_voice_prefixonly applies to actual STT transcripts (not typed text in voice mode), fixing the STT failure masking bug (#65827)- Clean design: subclasses
strso it passesisinstance(..., str)checks unchanged - No security or performance concerns
Reviewed by Hermes Agent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Voice mode stays active while you type, but the
[Voice input ...]prefix inchat()was selected fromself._voice_modealone. Every message sent while voice mode was on therefore reached the model labelled as a speech-to-text transcript, so the model had no way to tell a real transcript from typed text. In the reported case it took a typed troubleshooting message as evidence that transcription had started working, when it hadn't.A transcript is only produced in one place (
_voice_stop_and_transcribe), so that's where I tag it.VoiceTranscriptis astrsubclass, which is what keeps the change small: it rides the existing_pending_inputqueue and satisfies everyisinstance(..., str)check unchanged, so none of the other producers or consumers need to learn a new payload shape.process_loopreads the tag off the raw queue item and passes it tochat(voice_input=...). That read has to happen where it does — the sanitizers immediately after it (paste-wrapper stripping, file-drop rewrites) return plainstrand the subclass doesn't survive them.One thing worth a second opinion: that prefix was doing two jobs at once, provenance and conciseness. The reply is still spoken aloud for as long as voice mode is on, so I kept the brevity instruction for typed messages and only changed the label to
Typed input (not transcribed). The issue suggested either leaving typed messages unmarked or giving each message explicit provenance; I went with explicit provenance, since dropping the prefix entirely would make spoken replies to typed messages long again. The prefix for a real transcript is byte-for-byte what it was before, andchat()defaults tovoice_input=Falseso no caller can claim voice input by accident.Related Issue
Fixes #65827
Type of Change
Changes Made
cli.py: add aVoiceTranscript(str)marker for messages that came from speech-to-text.cli.py(_voice_stop_and_transcribe): tag the transcript when queueing it.cli.py(process_loop): read the tag off the queue item before the sanitizers flatten it, and pass it through tochat().cli.py(chat): newvoice_inputparameter, defaulting toFalse.cli.py(_build_voice_prefix): pulled the prefix construction out ofchat()into its own method so it can be tested directly, and picked the provenance label fromvoice_inputrather than_voice_mode.tests/tools/test_voice_cli_integration.py: newTestVoiceInputProvenancecovering both paths.How to Test
No microphone needed — the STT call is mocked, so the reproduction is deterministic:
To watch it catch the original bug, change the provenance line in
_build_voice_prefixback to the old behaviour:test_typed_message_in_voice_mode_is_not_marked_voice_inputandtest_typed_and_voice_prefixes_are_distinguishablethen fail with the reported symptom, a typed message arriving as[Voice input — respond concisely ...].Manually, against a real session: start
hermes, turn on voice mode, record a message, then type a follow-up without leaving voice mode. The recorded one reaches the agent as[Voice input — ...], the typed one as[Typed input (not transcribed) — ...]. Session history is unaffected either way; the prefix stays API-call-local as before.The existing
TestVoiceMessagePrefixtests re-implement the prefix logic inline rather than calling intocli.py, so they pass either way — the new tests exercise the real methods.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — ran the affected suites rather than the whole tree:tests/cli/,tests/tools/test_voice_cli_integration.py,tests/tools/test_clipboard.pyand the voice/prefix/persist tests intests/run_agent/.tests/cli/has 13 failures on Windows (symlink privileges, and source-reading tests that hitcp1252), but the set is identical with and without this change, and they reproduce on a cleanmain.Documentation & Housekeeping
docs/, docstrings) — docstrings only; no user-facing docs describe this prefixcli-config.yaml.exampleif I added/changed config keys — N/A, no config keysCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A