fix: transcribe voice/audio before the mention gate in requireMention groups - #33
fix: transcribe voice/audio before the mention gate in requireMention groups#33diogofrj wants to merge 1 commit into
Conversation
… groups A voice or audio message carries no caption and WhatsApp gives it no native @-mention slot, so `text` is empty by the time `gate()` runs. In a group with requireMention on, isMentioned() tests that empty text and always fails — the message is dropped before it is ever downloaded or transcribed, no matter what is actually said in the recording (even the wake word itself). Text messages are unaffected; only requireMention groups receiving audio hit this, and only in a group — the same audio delivered to a DM (no mention gate at all) works fine, which is what made the asymmetry visible. Fix: in handleMessage, before the gate call, detect this exact otherwise-certain-drop shape (group, requireMention on, sender allowed, no native mention already) and transcribe once, ahead of gate(), folding the transcript into `text` so isMentioned() can see what was actually said. The existing eager-download path further down reuses that result instead of downloading and transcribing the same note twice. gate() itself, DM handling, and the allowFrom check are unchanged — the new code only ever runs to decide a group audio drop that requireMention would otherwise make unconditionally, and only for live (non-backlog) messages, matching the `!backlog` guard the normal media path already uses. Version bumped 0.22.1 -> 0.22.2 (patch fix) in both plugin.json and marketplace.json per CONTRIBUTING.md.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03b21b7a86
ℹ️ 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".
| let earlyAudioPath: string | undefined; | ||
| let earlyAudioTranscript: string | null | undefined; | ||
| if (isGroup && !backlog && sock) { | ||
| const groupPolicy = loadAccess().groups[remoteJid]; |
There was a problem hiding this comment.
Honor disabled mode before pre-gate transcription
When dmPolicy is disabled and a previously configured mention-gated group receives audio, this path still downloads and transcribes the recording before gate() drops it. With the Groq or OpenAI provider configured, that means media is sent to a third party even though disabled mode promises to drop everything; local transcription also performs unnecessary work and writes the file. Check the global policy before any pre-gate media processing.
AGENTS.md reference: AGENTS.md:L71-L75
Useful? React with 👍 / 👎.
| INBOX_DIR, | ||
| `${Date.now()}-${messageId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}.${ext}`, | ||
| ); | ||
| writeFileSync(earlyAudioPath, buffer); |
There was a problem hiding this comment.
Delete pre-gate audio when the mention gate drops it
When an audio transcript does not match a wake pattern, the later mention gate returns drop, but the raw recording written here is never removed. In a group configured with context: false, this violates the documented guarantee that nothing unaddressed is stored; in other mention-gated groups it also accumulates orphaned inbox files. Use temporary storage and remove the file whenever the gate rejects the message.
AGENTS.md reference: AGENTS.md:L71-L75
Useful? React with 👍 / 👎.
| "displayName": "WhatsApp Channel for Claude Code", | ||
| "description": "WhatsApp channel for Claude Code — linked-device messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /whatsapp-channel:access.", | ||
| "version": "0.22.1", | ||
| "version": "0.22.2", |
There was a problem hiding this comment.
Add a matching 0.22.2 changelog entry
After this manifest bump, scripts/update-notice.ts still starts its changelog at 0.22.1. On the first session using 0.22.2, the hook therefore labels the previous release's notes as 0.22.2 and never announces this voice-message fix; bun test scripts/update-notice.test.ts fails the shipping-version invariant for the same mismatch.
Useful? React with 👍 / 👎.
Rich627
left a comment
There was a problem hiding this comment.
Request changes. The existing inline findings are valid merge blockers:
- The pre-gate path runs before
gate()checksdmPolicy === "disabled", so it can download/transcribe group audio (including via an external provider) despite disabled mode dropping every message. - If the later mention gate rejects the transcript, the pre-gate raw audio file remains in
INBOX_DIR; this contradictscontext: false's no-storage guarantee and leaks orphan files otherwise. - The 0.22.2 manifest bump lacks the matching head entry in
scripts/update-notice.ts;scripts/update-notice.test.tsexplicitly enforces that invariant, so the stated full test pass is not reproducible.
Please address these before merge.
What & why
A voice/audio message carries no caption and WhatsApp gives it no native
@-mention slot, sotextis empty by the timegate()runs. In a groupwith
requireMentionon,isMentioned()tests that empty text and alwaysfails — the message is dropped before it is ever downloaded or transcribed,
no matter what is actually said in the recording (even the wake word
itself).
Text messages are unaffected. Only
requireMentiongroups receivingaudio hit this — the same audio sent to a DM (no mention gate at all)
transcribes fine, which is what made the asymmetry visible: identical
voice note, DM works, group silently drops it.
diag.logconfirmeddropped inboundentries at the exact timestamps of the group sends,with nothing downloaded.
Fix
In
handleMessage, before thegate()call, detect the one shape wherethe drop is otherwise certain (group,
requireMentionon, sender allowedper
allowFrom, no native mention already present) and transcribe once,ahead of
gate(), folding the transcript intotextsoisMentioned()can see what was actually said instead of nothing. The existing eager
download/transcribe path further down (unchanged in its own logic) now
checks for that early result first and reuses it instead of downloading
and transcribing the same note twice.
gate()itself is untouched — same signature, same logic, same DMhandling. The new code only ever runs to decide a group-audio drop that
requireMentionwould otherwise make unconditionally, and only for livemessages (
!backlog), matching the guard the normal media-download pathalready uses. Allowlist/access gating invariant preserved: the early path
replicates the exact same
allowFromcheckgate()uses, so anot-allowed sender's audio is never transcribed just to be dropped anyway.
How I tested it
bun test— 452 pass / 1 skip / 0 fail (full existing suite, unchanged).bunx prettier --check— clean (matches the file's existing style; theoriginal file was already prettier-clean before my change, confirmed by
diffing against a pristine
origin/maincopy).live instance first (group with
requireMention: true, plain voice note→ silently dropped,
diag.logshows the drop at the same timestamp,nothing in the inbox), confirmed the code path (
gate()runs onextractText()'s output, which is""for a bareaudioMessage, beforethe download/transcribe block later in the function), then applied and
reasoned through this fix against that same trace.
trunk check— a fresh clone of this repo has no.trunk/config, so the launcher asks for
trunk initrather than checking. Idon't have access to whatever org-level config CI resolves, so please
let CI's
trunkjob be the real check here; happy to adjust foranything it flags.
gate()/handleMessagearen't exported or exercised by the existing suite (nothing in
lib/*.test.tsorscripts/*.test.tstouches them), and there's noexisting harness for the live-socket-shaped parts of
handleMessage.Open to pointers if there's a preferred way to cover this that I missed.
Checklist
trunk fmtandtrunk checkand it passes. — see note above;could not run locally (no
.trunk/config in a fresh clone), ranbun test+prettier --checkinstead..claude-plugin/plugin.json→version.claude-plugin/marketplace.json→plugins[0].versioninvariant preserved) — did not touch connection lifecycle or the
singleton lock.