Skip to content

fix(gateway): do not auto-TTS A2A replies - #90121

Open
mooserini wants to merge 1 commit into
NousResearch:mainfrom
mooserini:fix/a2a-skip-global-auto-tts
Open

mooserini wants to merge 1 commit into
NousResearch:mainfrom
mooserini:fix/a2a-skip-global-auto-tts

Conversation

@mooserini

Copy link
Copy Markdown

Summary

Desktop Read replies aloud persists voice.auto_tts as a global gateway default. The runner then treats that default as “speak every reply on every platform that has no /voice mode.”

A2A inbound is MessageType.TEXT. The A2A adapter has no native send_voice. Result: a completed text reply is synthesized to tts_reply_*.mp3, then:

[A2A] send_voice fallback: native audio send unavailable
⚠️ Couldn't deliver the audio attachment.

The peer sees a failed audio attachment instead of the text. Observed on v0.20.4 after an update flipped auto_tts true; earlier A2A text deliveries on the same hosts were clean.

Change

  • _should_send_voice_reply returns False for platform a2a.
  • _sync_voice_mode_state_to_adapter never copies the global default onto the A2A adapter.
  • Regression: A2A + adapter_auto_tts=True stays text; Telegram with the same default still voices.

/voice on / /voice tts on Discord/Telegram are unchanged.

Test plan

  • Isolated smoke: before patch, A2A text + global auto-TTS → _should_send_voice_reply True; after → False; Telegram still True.
  • test_should_send_voice_reply_a2a_ignores_global_auto_tts passes.
  • CI gateway voice tests.

Field report + logs: #90103

voice.auto_tts is a global default written by Desktop Read replies aloud.
The runner applied it to every adapter with no /voice mode, including A2A,
which has no send_voice and fails delivery with an audio-attachment error.

Keep A2A text-only. Human platforms still honor the global default and
per-chat /voice.

Fixes NousResearch#90103
@mooserini

Copy link
Copy Markdown
Author

Local verification (official runner)

Isolated worktree on 13ce0c5c6. Live install not modified.

scripts/run_tests.sh tests/gateway/test_auto_voice_reply_format.py
=== Summary: 1 files, 7 tests passed, 0 failed ===

That file includes the new test_should_send_voice_reply_a2a_ignores_global_auto_tts plus the existing Telegram / voice_only / opus cases.

Isolated smoke of _should_send_voice_reply (A2A text + adapter_auto_tts=True, no /voice mode):

A2A Telegram
stock main True (bug) True
this PR False True

Overlap

Triage on #90103 pointed at #80313, which disables auto-TTS inside A2AAdapter._should_auto_tts_for_chat. Same production failure (text swallowed by Couldn't deliver the audio attachment). That PR is older (Aug 6). This one is the runner-side gate so A2A never inherits Desktop/voice.auto_tts even if an adapter forgets the override.

Happy to close this as overlapping if maintainers prefer the adapter-only fix. CI on this fork did not start jobs (No jobs were run) — first contribution from this account; needs a maintainer workflow approve if you want this branch exercised.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery tool/tts Text-to-speech and transcription P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 19, 2026

@mooserini mooserini left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has again been checked, tested and verified locally.

@mooserini

Copy link
Copy Markdown
Author

Sequencing heads-up: this is PR 1 of a 3-PR voice/Photon fix arc — #90121 (this: gateway shouldn't auto-TTS A2A replies) → #91129 (inbound CAF detection + ffmpeg-static dep) → #91215 (migrate sidecar to Spectrum 12.8.0, remove obsolete v8 patch). #91129 is the small, safe wedge ready to land next; #91215 is the larger migration that depends on it. Flagging so the three are reviewed as one campaign rather than three unrelated drops.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Textbook scoped-default fix: the runner-level early return stops the synthesis at the decision point, the adapter-sync override stops a stale True from lingering on a2a adapters, and the test asserts both sides of the boundary (a2a False under global default, telegram still True). Defense in depth without changing any human-platform behavior.

Points:

  1. Stringly-typed platform check: getattr(platform, "value", None) == "a2a" twice. If Platform has an A2A member, comparing platform == Platform.A2A survives renames and typos ("a2a" vs "A2A" wouldn't be caught by tests that construct Platform("a2a") the same way); if the member doesn't exist, constructing it in the helper would be cheaper than the getattr dance at both sites.
  2. Explicit /voice on is now dead on a2a chats: the early return precedes the voice_mode lookup, so a per-chat explicit opt-in — not just the leaked global default — is silently ignored. The PR text says that's intended ("/voice scoped to human platforms"), and with no native send_voice it can't work anyway; consider rejecting /voice on for a2a chats at command level with a clear message so users aren't setting a mode that does nothing.
  3. Test suggestion: one case pinning that an explicit per-chat voice mode on a2a stays text documents the intended precedence for whoever later adds audio transport to the adapter.

@donald2008

Copy link
Copy Markdown

Field reproduction on latest upstream (2026-09-05) — still unfixed, with an extra angle

Confirming the issue is still live on current main. This is the second independent repro alongside #90103, and it adds a resource-impact reason to merge this PR.

Environment

  • Hermes Agent v0.21.0 (2026.8.31), upstream 79445a49 — i.e. after the 09-03 adapter refactors (192058fd / e83816a4)
  • A2A platform enabled, voice.auto_tts: true (set deliberately for desktop read-aloud)
  • Local TTS engine: vLLM server (qwen3-tts, :8091) behind the OpenAI-compatible tts.openai.base_url

Repro (2026-09-05, four hours ago)
Two plain-text A2A tasks from a peer agent. Gateway log on the receiving host:

12:42:22 inbound message: platform=a2a user=home-computer chat=ctx-2afbb026a02c42f6 msg="[A2A inbound ..."
12:43:03 response ready: platform=a2a chat=ctx-2afbb026a02c42f6 time=41.0s api_calls=4 response=695 chars
12:43:46 WARNING [A2A] send_voice fallback: native audio send unavailable for /tmp/hermes_voice/tts_reply_c059abb0f27c.mp3
12:44:00 inbound message: platform=a2a ...
12:44:10 response ready: platform=a2a ... response=998 chars
12:45:18 WARNING [A2A] send_voice fallback: native audio send unavailable for /tmp/hermes_voice/tts_reply_2ab994dd8973.mp3

Exact send_voice fallback per message, as in #90103.

Extra impact beyond text-delivery failure — TTS engine resource burn
The synthesized files are real and wasted:

  • tts_reply_c059abb0f27c.mp3 = 806,520 bytes (~17s audio, took 43 s to synthesize)
  • tts_reply_2ab994dd8973.mp3 = 1,180,848 bytes (~25s audio)

Each inbound A2A message burns a full TTS generation against the shared engine (tools.tts_tool: Generating speech with OpenAI TTS... → vLLM :8091), then the audio is thrown away. On a host where the user's own voice replies share that engine, this steals latency/throughput from real user speech and compounds long-running degradation of the TTS server. It is not only "peer sees an attachment error" — it is also "engine does pointless work on every agent message."

Local patch we are running (adapter-layer, verified)
We patched the adapter directly rather than the runner — functionally equivalent, and a bit tighter in scope since it also covers any future caller of _should_auto_tts_for_chat:

# plugins/platforms/a2a/adapter.py
def _should_auto_tts_for_chat(self, chat_id: str) -> bool:
    """A2A is a text-only agent protocol — never auto-TTS agent-to-agent replies."""
    return False

py_compile clean; after restart, no new send_voice fallback lines for A2A traffic while Telegram etc. keep their /voice behavior.

The PR's two changes (skip in _should_send_voice_reply for a2a + never push the global default onto the A2A adapter in _sync_voice_mode_state_to_adapter) are strictly more thorough than the adapter patch — the sync-side fix matters because _auto_tts_default on the adapter would otherwise still be True for other probe paths. +1 on getting this merged; happy to help with CI if the gateway voice tests need a hand.

cc maintainers: this is worth sweeping against any adapter without a native send_voice (the base-class fallback is the smell), not just A2A.

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 P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

4 participants