Skip to content

fix(gateway): transcribe voice replies before clarify resolution and busy steering - #73518

Merged
teknium1 merged 5 commits into
mainfrom
fix/voice-clarify-steer
Jul 28, 2026
Merged

teknium1 merged 5 commits into
mainfrom
fix/voice-clarify-steer

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

fix(gateway): voice messages during agent activity are first-class — transcribe before clarify + steer, gate audio_as_voice, keep the echo ledger

Consolidated salvage of four contributor PRs. Class-level framing: out-of-band voice (a voice note that arrives while the agent is busy or waiting on a clarify) now routes through one transcription choke point_transcribe_and_echo_pending_voice / _transcribe_pending_audio_event_once — so each platform message pays at most one STT call, and the transcript echo respects a count-based ledger that survives pending-media merges.

What was broken on main (all premises re-verified on f228e14)

  1. Clarify ignored voice replies — the clarify intercept in gateway/run.py read event.text raw. A voice reply arrives with the filename as text (e.g. voice_message_1.ogg), so the clarify was "answered" with a filename, or dropped. Fixes Voice message replies to pending clarify are sent as filename, not transcript #52998, Voice messages ignored when Telegram clarify tool is waiting for user response #56739.
  2. Steer mode silently degraded voice to queuecan_steer required message_type == TEXT with zero media, so with display.busy_input_mode: steer a voice follow-up quietly fell back to queue. Fixes the steer half of Telegram: out-of-band messages (text and voice) interrupt current task instead of being queued #58780.
  3. [[audio_as_voice]] tainted non-audio files — the directive is message-global; an image sharing a message with a voice note got is_voice=True, was excluded from the embedded-photo batch, and arrived as a document attachment.
  4. STT echo ledger lost on merge_invalidate_pending_stt_cache() deleted _gateway_pending_stt_echo_sent when a follow-up merged into a pending event, so the re-run transcription re-echoed (and re-paid for) the earlier voice note.

Commits (contributor authorship preserved via cherry-pick)

Commit Author Salvaged from What it does
ebe57ca7fe @izumi0uu #53020 _prepare_clarify_reply_text: transcribe pending voice audio before resolving a clarify; retain the pending clarify when STT yields no usable text (a failure marker is not an answer).
97f2a91f53 @canorionen #65023 _prepare_busy_steer_text: transcribe busy voice follow-ups before the steer decision.
6f8ccd5136 @chefboyrdave21 #44826 Gate [[audio_as_voice]] on audio file extensions in extract_media; wrap expanduser in try/except so a crafted ~\x00 path is skipped, not fatal.
92818ae028 @Frowtek #67281 Keep the echo ledger out of _invalidate_pending_stt_cache; track echoed transcripts as a count so only the unsent tail is echoed after a merge (two identical transcripts stay two deliveries).
a3f970c16c us (follow-up) Unify: _prepare_busy_steer_text now routes through the shared _transcribe_and_echo_pending_voice choke point (event-cached STT + count-ledger echo, same as interrupt/drain); can_steer accepts all-voice-media events so the transcript actually steers; extension gating also applied to extensionless MEDIA-tag resolution; adds extract_media gating tests + contributor mapping.

Duplicate PRs to close with credit (earliest submitter first)

Clarify-voice trio — earliest was #50925 by @Adridot (June 22); #53020 (@izumi0uu, June 26) won on diff quality (reuses _pending_event_audio_paths, retains the clarify on empty STT); #67014 (@dpowrepo, July 18) same idea later.

Steer-voice pair — earliest was #49949 by @the3asic (June 21); #65023 (@canorionen, July 15) won on diff quality (mirrors the inbound STT contract instead of inlining media classification).

Issues

Tests

  • tests/gateway/test_unknown_command.py: clarify-with-voice — transcript resolves the clarify (incl. numeric choice coercion), failed STT leaves the clarify pending (2 new tests, parametrized).
  • tests/gateway/test_busy_session_ack.py: steer-with-voice — voice follow-up is transcribed and steered, never queued.
  • tests/gateway/test_platform_base.py: [[audio_as_voice]] gating — image+voice message keeps the image inline; video/pdf never flagged (2 new tests; verified they FAIL when the gate is reverted).
  • tests/gateway/test_telegram_voice_v0_regressions.py: 4 new ledger tests incl. test_pending_stt_merge_echoes_two_identical_transcripts and no-re-echo across text-only merges.
  • Full tests/gateway suite: 11297 passed; the only failures (test_voice_command.py::TestVoiceReception DAVE decrypt pair + env-dependent errors) reproduce identically on clean origin/main — pre-existing, unrelated.

Merge method: rebase (preserves contributor authorship per-commit).

Infographic

fix(gateway): transcribe voice replies before clarify resolution and busy steering

izumi0uu and others added 5 commits July 28, 2026 09:21
… sent as documents

[[audio_as_voice]] is message-global but was applied to every media file in a
message. A non-audio file flagged is_voice is excluded from the embedded-photo
batch and falls through to send_document, so an image in a message that also
carries a voice note arrives as a file attachment instead of an inline photo.
Gate the voice flag on the file extension so one message can carry an inline
image AND a voice bubble. Also wrap the path append in try/except so a crafted
~\x00 path is skipped rather than aborting extraction of all attachments.
_invalidate_pending_stt_cache() clears the gateway-side transcription
cache when merge_pending_message_event() folds a follow-up message into a
still-pending event, so the next transcription picks up the merged text
and attachments.  It also cleared _gateway_pending_stt_echo_sent, but that
flag is not derived state — it records that the transcript was already
delivered to the user.

Dropping it makes the re-run transcription echo the earlier notes a second
time.  Both merge branches are affected, including the text-only follow-up
case where no new audio arrived at all: there the cache is invalidated,
the same voice note is transcribed again (a second paid STT call) and the
same line is echoed again.

Sequence:

  1. voice note arrives, interrupt monitor transcribes it and echoes
     '🎙️ "hello"'
  2. user sends a follow-up while the turn is still pending, so it merges
  3. drain path re-transcribes and echoes '🎙️ "hello"' a second time

Keep the ledger out of the invalidation set and track it as a count of
already-echoed transcripts instead of a single boolean.  A count is what
the merge case actually needs: re-running transcription over the extended
media list returns the earlier transcripts as a prefix of the new one, so
echoing only the unsent tail suppresses the repeat while still surfacing a
newly merged voice note.  A count rather than a set of seen values, so two
separate notes that transcribe identically stay two distinct deliveries —
covered by test_pending_stt_merge_echoes_two_identical_transcripts.

The guard stays within the 12-line window that
test_all_gateway_transcript_echo_sends_are_gated enforces over run.py.
…point

Follow-up for salvaged #65023/#53020: _prepare_busy_steer_text now calls
_transcribe_and_echo_pending_voice (the same helper the interrupt monitor
and pending-drain paths use) instead of a private transcription+echo copy,
so out-of-band voice pays one STT call per platform message and the echo
respects the count-based ledger from #67281. can_steer now accepts events
whose attachments are all STT-eligible voice media, completing the steer
half of #58780. Adds extract_media gating tests for #44826 and the
contributor mapping for chefboyrdave21.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on a3f970c

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Voice message replies to pending clarify are sent as filename, not transcript

5 participants