openclaw - ✅(Solved) Fix [Bug]: MEDIA: directive delivers voice/audio messages twice on all channels (4.15) [3 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70085Fetched 2026-04-23 07:29:30
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×4closed ×1commented ×1

Voice messages delivered via MEDIA:path + [[audio_as_voice]] are sent twice to Telegram, Discord, and WhatsApp. Confirmed on 2026.4.15, not fixed in 4.20.

Root Cause

Root Cause (confirmed via stack traces)

Fix Action

Workaround

Use the message tool with action=send + filePath + asVoice=true then reply with NO_REPLY. This bypasses the reply-media normalization pipeline entirely.

Alternatively, using the built-in TTS system (messages.tts) also avoids this since it attaches audio directly to the delivery payload.

PR fix notes

PR #27: fix(reply): accumulate media blocks instead of sending immediately when streaming disabled

Description (problem / solution / changelog)

Fixes #70085 - MEDIA directive sends duplicate messages on all channels.

When block streaming is disabled, media blocks were sent immediately via onBlockReply, invoking normalizeMediaPaths twice (once here, once in buildReplyPayloads), producing different UUID outbound paths each time and defeating the deduplication key match — causing duplicate delivery.

Now media blocks are accumulated and sent via buildReplyPayloads at end of run, ensuring consistent UUID paths and correct deduplication.

Changed files

  • .agents/maintainers.md (removed, +0/-1)
  • .agents/skills/openclaw-parallels-smoke/SKILL.md (modified, +15/-0)
  • .agents/skills/openclaw-qa-testing/SKILL.md (modified, +10/-10)
  • .agents/skills/openclaw-release-maintainer/SKILL.md (modified, +12/-0)
  • .agents/skills/openclaw-secret-scanning-maintainer/SKILL.md (modified, +31/-12)
  • .agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs (modified, +274/-15)
  • .agents/skills/openclaw-test-performance/SKILL.md (added, +134/-0)
  • .agents/skills/openclaw-test-performance/agents/openai.yaml (added, +6/-0)
  • .github/actionlint.yaml (modified, +2/-0)
  • .github/actions/setup-node-env/action.yml (modified, +6/-6)
  • .github/actions/setup-pnpm-store-cache/action.yml (modified, +3/-16)
  • .github/instructions/copilot.instructions.md (modified, +3/-3)
  • .github/workflows/ci.yml (modified, +1247/-353)
  • .github/workflows/codeql.yml (modified, +8/-7)
  • .github/workflows/control-ui-locale-refresh.yml (modified, +2/-2)
  • .github/workflows/docker-release.yml (modified, +23/-15)
  • .github/workflows/docs-sync-publish.yml (modified, +2/-2)
  • .github/workflows/install-smoke.yml (modified, +14/-11)
  • .github/workflows/macos-release.yml (modified, +0/-1)
  • .github/workflows/openclaw-cross-os-release-checks-reusable.yml (added, +472/-0)
  • .github/workflows/openclaw-live-and-e2e-checks-reusable.yml (added, +658/-0)
  • .github/workflows/openclaw-npm-release.yml (modified, +12/-3)
  • .github/workflows/openclaw-release-checks.yml (modified, +113/-37)
  • .github/workflows/openclaw-scheduled-live-checks.yml (added, +74/-0)
  • .github/workflows/parity-gate.yml (modified, +11/-2)
  • .github/workflows/plugin-clawhub-release.yml (modified, +0/-3)
  • .github/workflows/plugin-npm-release.yml (modified, +0/-3)
  • .github/workflows/sandbox-common-smoke.yml (modified, +4/-1)
  • .github/workflows/workflow-sanity.yml (modified, +3/-1)
  • .oxlintrc.json (modified, +29/-2)
  • .pre-commit-config.yaml (modified, +2/-2)
  • .vscode/settings.json (modified, +2/-1)
  • AGENTS.md (modified, +199/-318)
  • CHANGELOG.md (modified, +283/-1)
  • CONTRIBUTING.md (modified, +5/-2)
  • Dockerfile (modified, +7/-1)
  • Dockerfile.sandbox (modified, +1/-1)
  • Dockerfile.sandbox-browser (modified, +1/-1)
  • README.md (modified, +252/-384)
  • SECURITY.md (modified, +5/-0)
  • appcast.xml (modified, +116/-0)
  • apps/android/app/build.gradle.kts (modified, +2/-2)
  • apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayDiscovery.kt (modified, +36/-35)
  • apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt (modified, +2/-8)
  • apps/ios/CHANGELOG.md (modified, +16/-0)
  • apps/ios/Config/Version.xcconfig (modified, +2/-2)
  • apps/ios/Sources/Gateway/GatewayConnectionController.swift (modified, +2/-1)
  • apps/ios/Sources/Gateway/GatewaySettingsStore.swift (modified, +3/-1)
  • apps/ios/Sources/HomeToolbar.swift (modified, +4/-1)
  • apps/ios/Sources/Model/NodeAppModel.swift (modified, +79/-48)
  • apps/ios/Sources/Onboarding/OnboardingWizardView.swift (modified, +3/-1)
  • apps/ios/Sources/Services/WatchConnectivityTransport.swift (modified, +7/-4)
  • apps/ios/Sources/Services/WatchMessagingService.swift (modified, +11/-3)
  • apps/ios/Sources/Voice/TalkModeManager.swift (modified, +6/-2)
  • apps/ios/fastlane/metadata/en-US/release_notes.txt (modified, +1/-1)
  • apps/ios/version.json (modified, +1/-1)
  • apps/macos/Sources/OpenClaw/AppState.swift (modified, +52/-66)
  • apps/macos/Sources/OpenClaw/ChannelsStore+Lifecycle.swift (modified, +2/-1)
  • apps/macos/Sources/OpenClaw/CommandResolver.swift (modified, +7/-3)
  • apps/macos/Sources/OpenClaw/ExecApprovalCommandDisplaySanitizer.swift (modified, +15/-1)
  • apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift (modified, +2/-4)
  • apps/macos/Sources/OpenClaw/GeneralSettings.swift (modified, +3/-1)
  • apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift (modified, +1/-0)
  • apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift (modified, +32/-4)
  • apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntimeMainActorServices.swift (modified, +22/-0)
  • apps/macos/Sources/OpenClaw/NodeMode/MacNodeScreenCommands.swift (modified, +9/-0)
  • apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift (modified, +1/-2)
  • apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift (modified, +3/-1)
  • apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift (modified, +21/-12)
  • apps/macos/Sources/OpenClaw/RemotePortTunnel.swift (modified, +1/-3)
  • apps/macos/Sources/OpenClaw/Resources/Info.plist (modified, +2/-2)
  • apps/macos/Sources/OpenClaw/ScreenSnapshotService.swift (added, +109/-0)
  • apps/macos/Sources/OpenClawProtocol/GatewayModels.swift (modified, +18/-0)
  • apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift (modified, +56/-49)
  • apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift (modified, +3/-0)
  • apps/macos/Tests/OpenClawIPCTests/ExecApprovalCommandDisplaySanitizerTests.swift (modified, +33/-0)
  • apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift (modified, +101/-0)
  • apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift (modified, +37/-25)
  • apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift (modified, +25/-0)
  • apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift (modified, +18/-0)
  • apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatComposerTextViewTests.swift (added, +15/-0)
  • docs/.generated/config-baseline.sha256 (modified, +4/-4)
  • docs/.generated/plugin-sdk-api-baseline.sha256 (modified, +2/-2)
  • docs/.i18n/glossary.zh-CN.json (modified, +24/-0)
  • docs/automation/cron-jobs.md (modified, +7/-1)
  • docs/automation/hooks.md (modified, +3/-1)
  • docs/automation/tasks.md (modified, +1/-1)
  • docs/channels/bluebubbles.md (modified, +49/-0)
  • docs/channels/groups.md (modified, +2/-2)
  • docs/channels/index.md (modified, +1/-1)
  • docs/channels/matrix.md (modified, +5/-3)
  • docs/channels/pairing.md (modified, +6/-0)
  • docs/channels/telegram.md (modified, +6/-2)
  • docs/channels/troubleshooting.md (modified, +10/-8)
  • docs/channels/wechat.md (added, +168/-0)
  • docs/ci.md (modified, +42/-28)
  • docs/cli/config.md (modified, +28/-0)
  • docs/cli/devices.md (modified, +9/-2)
  • docs/cli/gateway.md (modified, +21/-7)
  • docs/cli/hooks.md (modified, +1/-0)

PR #70129: fix(reply): accumulate media blocks instead of sending immediately when streaming disabled

Description (problem / solution / changelog)

Fixes #70085 - MEDIA directive sends duplicate messages on all channels.

When block streaming is disabled, media blocks were sent immediately via onBlockReply, invoking normalizeMediaPaths twice (once here, once in buildReplyPayloads), producing different UUID outbound paths each time and defeating the deduplication key match — causing duplicate delivery.

Now media blocks are accumulated and sent via buildReplyPayloads at end of run, ensuring consistent UUID paths and correct deduplication.

Changed files

  • extensions/browser/src/browser/pw-session.test.ts (modified, +75/-0)
  • extensions/browser/src/browser/pw-session.ts (modified, +65/-0)
  • extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts (modified, +1/-0)
  • extensions/browser/src/browser/pw-tools-core.snapshot.ts (modified, +10/-1)
  • extensions/feishu/package.json (modified, +3/-0)
  • extensions/telegram/package.json (modified, +4/-1)
  • extensions/telegram/src/bot-message-context.body.ts (modified, +10/-1)
  • extensions/whatsapp/src/auto-reply.web-auto-reply.last-route.test.ts (modified, +109/-0)
  • extensions/whatsapp/src/auto-reply/monitor/on-message.ts (modified, +6/-0)
  • src/agents/pi-embedded-subscribe.handlers.tools.media.test.ts (modified, +27/-0)
  • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.custom-provider-payloads.test.ts (added, +113/-0)
  • src/agents/pi-embedded-subscribe.ts (modified, +22/-3)
  • src/agents/sandbox/remote-fs-bridge.test.ts (modified, +57/-0)
  • src/agents/sandbox/remote-fs-bridge.ts (modified, +16/-2)
  • src/auto-reply/reply/reply-delivery.test.ts (modified, +12/-19)
  • src/auto-reply/reply/reply-delivery.ts (modified, +5/-11)
  • src/infra/system-events.test.ts (modified, +31/-0)
  • src/infra/system-events.ts (modified, +6/-1)
  • src/plugins/bundled-capability-runtime.ts (modified, +1/-1)
  • src/plugins/bundled-channel-config-metadata.ts (modified, +1/-1)
  • src/plugins/loader.ts (modified, +1/-1)
  • src/plugins/public-surface-loader.ts (modified, +2/-2)
  • src/plugins/source-loader.ts (modified, +1/-1)
  • src/process/supervisor/supervisor.ts (modified, +1/-1)
  • src/tasks/task-registry.audit.test.ts (modified, +77/-0)
  • src/tasks/task-registry.ts (modified, +7/-4)

PR #70394: fix(plugins): degrade gracefully instead of crashing worker on invalid config

Description (problem / solution / changelog)

Fix #70371

Problem: A single invalid plugin config schema causes the entire worker/boot path to crash instead of degrading gracefully.

Root Cause: Both loadPluginMetadataRegistrySnapshot and ensurePluginRegistryLoaded set throwOnLoadError: true. These functions run in mode: "validate" paths where the goal is to check and report — not crash the worker.

Fix: Remove throwOnLoadError: true from both functions. Error status and diagnostics are recorded correctly; only the fatal throw is removed.

Changes:

  • src/plugins/runtime/metadata-registry-loader.ts: throwOnLoadError: truefalse
  • src/plugins/runtime/runtime-registry-loader.ts: throwOnLoadError: truefalse
  • src/plugins/runtime/runtime-registry-loader.test.ts: update expected value
  • src/plugins/loader.cli-metadata.test.ts: add regression test

Changed files

  • extensions/qqbot/src/utils/text-parsing.test.ts (modified, +4/-0)
  • extensions/qqbot/src/utils/text-parsing.ts (modified, +2/-2)
  • src/agents/anthropic-vertex-stream.test.ts (modified, +22/-0)
  • src/agents/anthropic-vertex-stream.ts (modified, +9/-1)
  • src/auto-reply/reply/reply-delivery.test.ts (modified, +12/-20)
  • src/auto-reply/reply/reply-delivery.ts (modified, +31/-10)
  • src/plugins/loader.cli-metadata.test.ts (modified, +29/-0)
  • src/plugins/runtime/metadata-registry-loader.ts (modified, +1/-1)
  • src/plugins/runtime/runtime-registry-loader.test.ts (modified, +1/-1)
  • src/plugins/runtime/runtime-registry-loader.ts (modified, +1/-1)

Code Example

Send 1 (streaming handler):
dispatch.js:908 → bot:4888 → sendPayload → deliverReplies → sendVoice

Send 2 (buffered block dispatcher):
provider-dispatcher.js:4 → dispatch.js:979 (dispatchInboundMessage)
  → dispatch.js:39 (withReplyDispatcher) → dispatch.js:908
  → bot:4888 → sendPayload → deliverReplies → sendVoice
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Summary

Voice messages delivered via MEDIA:path + [[audio_as_voice]] are sent twice to Telegram, Discord, and WhatsApp. Confirmed on 2026.4.15, not fixed in 4.20.

Root Cause (confirmed via stack traces)

Two independent normalizeMediaPaths() instances fire for the same reply payload:

  1. onBlockReply (streaming handler) creates normalizer instance A with its own persistedMediaBySource cache
  2. buildReplyPayloads (final reply builder) creates normalizer instance B with a separate cache

Same MEDIA path → two UUID outbound files → two channel sends ~190ms apart.

Critically, onBlockReply fires for media content even with blockStreaming: false (the default).

Stack Traces

Send 1 (streaming handler):
dispatch.js:908 → bot:4888 → sendPayload → deliverReplies → sendVoice

Send 2 (buffered block dispatcher):
provider-dispatcher.js:4 → dispatch.js:979 (dispatchInboundMessage)
  → dispatch.js:39 (withReplyDispatcher) → dispatch.js:908
  → bot:4888 → sendPayload → deliverReplies → sendVoice

Steps to reproduce

  1. Configure Telegram or Discord channel
  2. Have the agent reply with MEDIA:/path/to/audio.mp3 + [[audio_as_voice]]
  3. Observe two identical voice messages delivered

Expected behavior

Single voice message delivery.

Actual behavior

Two voice messages delivered ~190ms apart from the same audio file. Both are audible. Different Telegram message IDs confirm two separate API calls.

Workaround

Use the message tool with action=send + filePath + asVoice=true then reply with NO_REPLY. This bypasses the reply-media normalization pipeline entirely.

Alternatively, using the built-in TTS system (messages.tts) also avoids this since it attaches audio directly to the delivery payload.

Related Issues

This appears to be the same root cause across multiple reports:

  • #68056 (WhatsApp media sent twice in 4.15)
  • #30316 (Telegram text and audio sent twice)
  • #33592 (Telegram duplicate replies)
  • #37844 (Discord duplicate replies)
  • #65468, #3549, #17991

Environment

  • OpenClaw: 2026.4.15 (041266a)
  • OS: Ubuntu 24.04, Azure VM
  • Channels: Telegram + Discord (both affected)
  • blockStreamingDefault: off (default)
  • Node: v22.22.2

Suggested Fix Direction

The normalizeMediaPaths duplication could be resolved by either:

  1. Sharing a single normalizer cache between the streaming and final reply paths
  2. Deduplicating media payloads by source path before channel delivery
  3. Skipping the onBlockReply media path when blockStreaming is disabled

extent analysis

TL;DR

The most likely fix involves resolving the duplication of normalizeMediaPaths instances to prevent sending the same voice message twice to Telegram, Discord, and WhatsApp.

Guidance

  • Identify and merge the two independent normalizeMediaPaths() instances to share a single cache, ensuring that only one instance processes each media path.
  • Consider deduplicating media payloads by source path before channel delivery to prevent duplicate sends.
  • Evaluate skipping the onBlockReply media path when blockStreaming is disabled to prevent unnecessary processing.
  • Review the provided workaround using the message tool with action=send + filePath + asVoice=true and replying with NO_REPLY as a temporary solution.

Example

No code snippet is provided due to the complexity of the issue and the need for a thorough understanding of the normalizeMediaPaths function and its integration with the streaming and final reply paths.

Notes

The suggested fix direction involves resolving the normalizeMediaPaths duplication, which could be achieved through one of the proposed methods. However, the exact implementation details are not provided, and further investigation is required to determine the most suitable solution.

Recommendation

Apply the workaround using the message tool until a permanent fix is implemented, as it bypasses the reply-media normalization pipeline entirely and avoids the duplication issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Single voice message delivery.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING