Skip to content

fix(media): gate inbound audio on structured media facts#111315

Merged
steipete merged 1 commit into
mainfrom
claude/media-placeholder-audio-gate
Jul 19, 2026
Merged

fix(media): gate inbound audio on structured media facts#111315
steipete merged 1 commit into
mainfrom
claude/media-placeholder-audio-gate

Conversation

@steipete

Copy link
Copy Markdown
Contributor

What Problem This Solves

hasInboundAudio — the gate that decides whether an inbound message carries audio for transcription/voice handling — reverse-engineered presentation text: it matched <media:audio> placeholder bodies, [Audio transcript headers, and audio-looking filenames in addition to the structured MediaType/MediaTypes fields. This is PR1 of the <media:kind> placeholder-elimination program: before channels stop minting placeholder bodies, nothing in core may depend on them for behavior.

Why This Change Was Made

  • hasInboundAudio now consumes only structured facts: normalized MediaType/MediaTypes accepting the structured kind audio or audio/* MIME (case-insensitive, parameters stripped). The placeholder regex, header regex, and filename inference are deleted (net −10 prod LOC in core).
  • A full producer/consumer audit (11 bundled channel plugins mint <media: strings; table with file:line cites in the audit summary below) proved every audio-capable channel populates structured media types — except one hole: Discord, when a download fails, could leave a voice-classified attachment with a usable remote URL but no MIME. Discord now carries its native audio classification as structured kind: "audio" with deliberate precedence: definitive fetched MIME > definitive declared MIME > generic container types (application/octet-stream, binary/octet-stream, application/ogg); filename inference applies only when the effective MIME is absent/non-definitive; definitive non-audio MIME is never dropped or overridden. The rendered placeholder now derives from the same classification, so text and structured facts cannot diverge.
  • Deliberate contract change, frozen by tests: a placeholder body without structured media no longer counts as audio. Such a message has no materialized media and cannot be sent to STT anyway; the old string match only produced a doomed transcription attempt.

User Impact

Voice-message transcription now triggers on the same prepared facts channel ingress already carries, for every bundled channel. Discord voice messages with failed downloads or generic container MIME are now correctly classified as audio (previously possible false negative), and documents misnamed *.ogg are no longer routed into audio-only understanding (previously possible false positive).

Evidence

  • Audit: 11 producers (not ~15 — Matrix and Slack use different conventions), 5 regex consumers plus 6 exact-string consumers enumerated with file:line; per-channel structured audio coverage proof for Discord, Feishu, iMessage, LINE, Mattermost, Signal, Telegram, WhatsApp. Program plan: 4 producer-migration batches, shared formatter API frozen (structured facts only), deletion sweep last.
  • Tests via node scripts/run-vitest.mjs: new inbound-media.test.ts 6/6 (structured detection, placeholder/filename non-inference), get-reply.message-hooks.test.ts 18/18 (filename-inference contract flip), Discord message-utils.test.ts 59/59 (MIME-conflict, generic-container, native-voice, fetched-override, pdf-named-.ogg cases); reran post-rebase onto origin/main.
  • Autoreview (Codex gpt-5.6-sol): first pass found two real P2 precedence defects (generic fetched MIME erasing declared audio; filename hints deleting definitive non-audio MIME) — both fixed; final review clean, "patch is correct (0.87)".

hasInboundAudio previously accepted <media:audio> placeholder bodies,
[Audio transcript headers, and filename sniffing alongside MediaTypes.
It now consumes only the structured facts channels already carry
(MediaType/MediaTypes, kind audio or audio/*), removing the string
reverse-engineering ahead of the placeholder-elimination program.
Discord — the one channel with a coverage hole — now carries its native
audio classification (MIME, voice fields, constrained filename fallback)
as structured kind: "audio", with definitive fetched/declared MIME
precedence so generic container types cannot erase declared audio and
filename hints cannot override definitive non-audio MIME.
@steipete
steipete marked this pull request as ready for review July 19, 2026 08:36
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord size: M maintainer Maintainer-authored PR labels Jul 19, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c6160ba9f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const mime = normalizeMimeType(contentType);
const kind =
mime?.startsWith("audio/") ||
hasDiscordVoiceAttachmentFields(params.attachment) ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Honor definitive MIME before native voice fields

When an attachment has duration_secs or waveform but the downloaded bytes resolve to a definitive non-audio MIME such as image/png, this unconditional branch still sets kind to audio; line 123 then discards the authoritative fetched MIME, causing the image/document to be projected as audio and routed to transcription. This also contradicts resolveEffectiveMediaType, which explicitly gives definitive fetched MIME highest precedence. Apply native voice classification only when the effective MIME is absent or non-definitive, as the filename fallback already does.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit e20d4cd into main Jul 19, 2026
194 of 200 checks passed
@steipete
steipete deleted the claude/media-placeholder-audio-gate branch July 19, 2026 08:55
@steipete

Copy link
Copy Markdown
Contributor Author

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 20, 2026
…1315)

hasInboundAudio previously accepted <media:audio> placeholder bodies,
[Audio transcript headers, and filename sniffing alongside MediaTypes.
It now consumes only the structured facts channels already carry
(MediaType/MediaTypes, kind audio or audio/*), removing the string
reverse-engineering ahead of the placeholder-elimination program.
Discord — the one channel with a coverage hole — now carries its native
audio classification (MIME, voice fields, constrained filename fallback)
as structured kind: "audio", with definitive fetched/declared MIME
precedence so generic container types cannot erase declared audio and
filename hints cannot override definitive non-audio MIME.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant