Skip to content

fix(matrix): restore voice bubbles — send_voice rejected is_voice, dr… - #99712

Open
simonamsharp wants to merge 1 commit into
NousResearch:mainfrom
simonamsharp:fix/matrix-voice-bubble-msc3245
Open

simonamsharp wants to merge 1 commit into
NousResearch:mainfrom
simonamsharp:fix/matrix-voice-bubble-msc3245

Conversation

@simonamsharp

Copy link
Copy Markdown

…opping MSC3245

Matrix voice notes are currently broken in two independent ways.

  1. BasePlatformAdapter media delivery calls self.send_voice(..., is_voice=is_voice) (gateway/platforms/base.py), but MatrixAdapter.send_voice() accepts no such parameter and has no **kwargs. Every audio attachment therefore raises

    TypeError: MatrixAdapter.send_voice() got an unexpected keyword
    argument 'is_voice'
    

    and is logged as "[Matrix] Error sending media". _send_local_file() already accepts is_voice, so the parameter is added and forwarded.

  2. Element only draws an inline player for an m.audio event carrying the MSC3245 org.matrix.msc3245.voice flag, which is set only when is_voice is true. is_voice comes from an [[audio_as_voice]] directive in the response text, which GatewayRunner auto-appends from tool results — but only if "MEDIA:" not in final_response. An agent that synthesises its own audio and emits its own MEDIA: tag suppresses that block entirely and loses the directive, so its voice notes arrive as file cards.

Fixes:

  • MatrixAdapter.send_voice() takes is_voice (default True) plus **kwargs and forwards it, and forces it True for .ogg/.oga/.opus — that is precisely the container MSC3245 voice messages use, so the flag should not depend on the caller having inferred intent. Non-Ogg input still honours the caller, so a plain .mp3 attachment is not turned into a voice note.
  • gateway/run.py recovers the [[audio_as_voice]] directive on its own when the agent wrote its own MEDIA: tag, without re-appending the MEDIA tags (which would deliver the file twice).

Deciding this in code rather than relying on the model to emit a marker also makes delivery deterministic, which matters on smaller local models that follow such instructions unreliably.

Tests: 11 new cases in tests/gateway/test_matrix_voice_bubble_msc3245.py, covering the TypeError, the flag in both directions (present when is_voice, absent otherwise, so ordinary audio is not converted to voice notes), and the directive-to-is_voice path. All 7 send_voice cases fail on main and pass with this change.

Claude-Session: https://claude.ai/code/session_0196UNhqc3eiwuW7y4xaRHnx

What does this PR do?

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

How to Test

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

…opping MSC3245

Matrix voice notes are currently broken in two independent ways.

1. `BasePlatformAdapter` media delivery calls
   `self.send_voice(..., is_voice=is_voice)` (gateway/platforms/base.py), but
   `MatrixAdapter.send_voice()` accepts no such parameter and has no
   `**kwargs`. Every audio attachment therefore raises

       TypeError: MatrixAdapter.send_voice() got an unexpected keyword
       argument 'is_voice'

   and is logged as "[Matrix] Error sending media". `_send_local_file()`
   already accepts `is_voice`, so the parameter is added and forwarded.

2. Element only draws an inline player for an `m.audio` event carrying the
   MSC3245 `org.matrix.msc3245.voice` flag, which is set only when
   `is_voice` is true. `is_voice` comes from an `[[audio_as_voice]]`
   directive in the response text, which `GatewayRunner` auto-appends from
   tool results — but only `if "MEDIA:" not in final_response`. An agent that
   synthesises its own audio and emits its own `MEDIA:` tag suppresses that
   block entirely and loses the directive, so its voice notes arrive as file
   cards.

Fixes:

- `MatrixAdapter.send_voice()` takes `is_voice` (default True) plus
  `**kwargs` and forwards it, and forces it True for `.ogg`/`.oga`/`.opus` —
  that is precisely the container MSC3245 voice messages use, so the flag
  should not depend on the caller having inferred intent. Non-Ogg input still
  honours the caller, so a plain `.mp3` attachment is not turned into a voice
  note.
- `gateway/run.py` recovers the `[[audio_as_voice]]` directive on its own when
  the agent wrote its own `MEDIA:` tag, without re-appending the MEDIA tags
  (which would deliver the file twice).

Deciding this in code rather than relying on the model to emit a marker also
makes delivery deterministic, which matters on smaller local models that
follow such instructions unreliably.

Tests: 11 new cases in tests/gateway/test_matrix_voice_bubble_msc3245.py,
covering the TypeError, the flag in both directions (present when is_voice,
absent otherwise, so ordinary audio is not converted to voice notes), and the
directive-to-is_voice path. All 7 send_voice cases fail on main and pass with
this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196UNhqc3eiwuW7y4xaRHnx
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/matrix Matrix adapter (E2EE) tool/tts Text-to-speech and transcription sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 31, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

The two layers of this fix are coherent. In the gateway, when the agent wrote its own MEDIA: tag the auto-append block is skipped entirely, losing the [[audio_as_voice]] directive the TTS tool requested; the new elif recovers just the directive without duplicating the MEDIA tags (gateway/run.py:7224-7236). In the Matrix adapter, Ogg/Opus input — the exact MSC3245 container — now forces is_voice=True regardless of caller inference (plugins/platforms/matrix/adapter.py:2602-2604), while non-Ogg input still honors the caller so plain attachments aren't converted to voice notes. The parametrized tests pin the flag both directions and cover case-insensitivity and the end-to-end extract_media derivation.

Non-blocking: forcing is_voice=True for Ogg means a caller deliberately passing is_voice=False for an Ogg file can no longer opt out; the docstring justifies this (Element renders a file card otherwise), but it is a silent override of explicit intent.

Verdict: LGTM

@kvnloo

kvnloo commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Consolidation map for send_voice is_voice kwargs vs BasePlatformAdapter (exact heads at comment time):

Same contract miss class. Prefer landing one Matrix fix + shared adapter **kwargs/is_voice acceptance pattern, then close overlaps as duplicate, rather than merging the spray independently. Happy to draft the shared helper if maintainers want consolidation over land-as-is.

No competing PR from me.

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Voice / STT KEEP cluster map (comment-only)

Preferred vehicle for the Matrix send_voice(..., is_voice=) contract miss remains this PR (#99712). Sibling sprays should close as duplicate after one shared adapter pattern lands — not merge independently.

Cluster (gateway / TTS / platform voice)

PR Slice Ask
#99712 (this) Matrix MSC3245 voice bubbles / is_voice Preferred Matrix land + shared **kwargs/is_voice acceptance
#107289 / #104248 / #100679 Matrix is_voice overlaps Dup/overlap — fold into #99712 pattern
#100021 Mattermost + LINE Same contract class after Matrix
#101381 Weixin Same contract class after Matrix
#107738 Telegram STT silent failures KEEP surface/log (no hang on typing…)
#100454 Global auto-TTS needs voice-in KEEP voice-in→voice-out; /voice tts for speak-all
#95740 Telegram replied voice-note STT KEEP AUDIO→VOICE remap for native replies
#98419 Telegram STT echo HTML quote KEEP expandable quote when stt.echo_transcripts
#103790 Discord Opus + strip reasoning KEEP OPUS_VOICE_PLATFORMS; share strip helper w/ #107047
#99212 Desktop auto-speak ≠ gateway auto_tts KEEP separate knobs (no double-speak)
#100120 TTS voice-memo vs cache sweep KEEP durable memos outside 24h gateway cache

Recommended order

  1. Land one Matrix is_voice fix (fix(matrix): restore voice bubbles — send_voice rejected is_voice, dr… #99712) + shared adapter acceptance; close Matrix overlaps as duplicate.
  2. Telegram STT honesty trio (fix(gateway): surface and log voice auto-transcription failures instead of stalling #107738fix(telegram): preserve replied voice-note semantics for STT #95740feat(gateway): collapse Telegram STT echoes into expandable HTML quotes #98419) — failures first, then reply semantics, then echo UX.
  3. Cross-surface TTS hygiene (fix(gateway): require voice input for global auto-TTS fallback (#100431) #100454, fix(desktop): separate auto-speak from gateway voice.auto_tts to prevent double-speech #99212, fix(tts): stop the hourly cache sweep from deleting TTS voice memos #100120, fix(tts): Discord voice replies — Opus routing + strip reasoning from speech #103790) — independent once platform contracts stop fighting.

No new mega-issue / Wave from this comment. No competing PR from me.

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 comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/matrix Matrix adapter (E2EE) 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