openclaw - ✅(Solved) Fix [Bug]: Single WhatsApp media reply is sent twice in 2026.4.15 [2 pull requests, 3 comments, 3 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#68056Fetched 2026-04-18 05:54:11
View on GitHub
Comments
3
Participants
3
Timeline
7
Reactions
0
Timeline (top)
commented ×3cross-referenced ×2labeled ×1referenced ×1

On OpenClaw 2026.4.15, one WhatsApp request that yields one media reply results in two media replies being sent to the same conversation.

Error Message

Additional evidence: a temporary instrumentation of resolveOutboundAttachmentFromUrl (logging new Error().stack on each call, subsequently reverted) captured two distinct call stacks for the same mediaUrl on a single reply.

Root Cause

On OpenClaw 2026.4.15, one WhatsApp request that yields one media reply results in two media replies being sent to the same conversation.

Fix Action

Fixed

PR fix notes

PR #68096: fix(whatsapp): deduplicate media in final reply when block streaming is disabled

Description (problem / solution / changelog)

Fixes #68056

Problem

When blockStreaming: false, a single media response is sent twice:

  1. Block path: Media is sent immediately via sendDirectBlockReply (media-only, text stripped) because media cannot be reconstructed in the final response.
  2. Final path: The final payload also carries the same media URL, goes through a separate createReplyMediaPathNormalizer instance, and the deduplication filter (directlySentBlockKeys) fails to match because the two normalizer instances produce different persisted file paths.

Root Cause

agent-runner-execution.ts and agent-runner.ts each create their own createReplyMediaPathNormalizer instance. Each instance has its own persistedMediaBySource cache. When the same source media URL is persisted by both instances, they produce different output paths (unique filenames from saveMediaBuffer). The createBlockReplyContentKey function compares normalized paths, so the keys don't match and the final payload leaks through.

Regression from d21f07a39e (fix: allow workspace-rooted absolute media paths in auto-reply, 2026-04-14), which moved media persistence into normalizeMediaSource — before this change, only volatile paths were persisted, and remote URLs passed through unchanged (matching between instances).

Fix

Return the block-delivery normalizer from runAgentTurnWithFallback and reuse it in buildReplyPayloads. When the execution normalizer is available (i.e., media was sent during block delivery), it takes precedence over the separate instance, ensuring the same persistence cache is used for both block and final payloads → identical content keys → proper deduplication.

Tests

Added 3 test cases to agent-runner-payloads.test.ts:

  • ✅ Existing replyToId dedup test (unchanged)
  • ✅ Shared normalizer correctly deduplicates media payloads
  • ✅ Separate normalizers fail to deduplicate (demonstrates pre-fix bug)

Changed files

  • src/auto-reply/reply/agent-runner-execution.ts (modified, +3/-0)
  • src/auto-reply/reply/agent-runner-payloads.test.ts (modified, +65/-0)
  • src/auto-reply/reply/agent-runner.ts (modified, +2/-1)

PR #68111: fix(agent-runner): share media-path normalizer to prevent duplicate WhatsApp media reply (#68056)

Description (problem / solution / changelog)

Summary

Fixes #68056 — a single WhatsApp media reply is sent twice when blockStreaming is disabled.

Root Cause

createReplyMediaPathNormalizer was called in two separate places, each producing an independent closure with its own persistedMediaBySource cache:

  • agent-runner-execution.ts — inside runAgentTurnWithFallback, used by the block-reply delivery handler
  • agent-runner.ts — at the call site, used by buildReplyPayloads

When block streaming is off but onBlockReply is provided, reply-delivery.ts sends media immediately via sendDirectBlockReply (by design — media cannot be reconstructed from final text). Both delivery paths then called resolveOutboundAttachmentFromUrl for the same source, each cache-missing and producing a separate UUID outbound file and a separate WhatsApp send.

This matches the reporter's instrumentation finding: "The two call sites create separate normalizer instances whose persistedMediaBySource caches are not shared."

Changes

  • agent-runner-execution.tsrunAgentTurnWithFallback now accepts an optional normalizeMediaPaths param. When supplied it is used directly; otherwise a local normalizer is created (preserves backward-compat for any callers that do not supply one).
  • agent-runner.ts — passes normalizeReplyMediaPaths (already created at the call site) into runAgentTurnWithFallback, so both the block-reply delivery handler and buildReplyPayloads share one closure and one persistedMediaBySource cache.

Why This Is Safe

Purely additive — an optional parameter with a ?? fallback. All callers that do not pass normalizeMediaPaths behave identically to before. The only behavioural change is that the two delivery paths now share the same normalizer cache, which is the intended behaviour.

Testing

Added regression test in agent-runner.media-paths.test.ts: mocks the .runtime import path used exclusively by agent-runner-execution.ts and asserts that createReplyMediaPathNormalizer from that path is never called after the fix (because the injected normalizer from agent-runner.ts is used instead).

Local CI results:

  • auto-reply-reply test suite: 1073/1073 passed
  • pnpm test:bundled: 103/103 passed
  • pnpm check / pnpm build / pnpm test:contracts: pre-existing failures present identically on main — not introduced by this PR

Fixes #68056

Changed files

  • src/auto-reply/reply/agent-runner-execution.ts (modified, +17/-14)
  • src/auto-reply/reply/agent-runner.media-paths.test.ts (modified, +79/-0)
  • src/auto-reply/reply/agent-runner.ts (modified, +1/-0)

Code Example

Additional evidence: a temporary instrumentation of `resolveOutboundAttachmentFromUrl` (logging `new Error().stack` on each call, subsequently reverted) captured two distinct call stacks for the same mediaUrl on a single reply.

Stack 1:

resolveOutboundAttachmentFromUrl (outbound-attachment-Dc2tDWj8.js:6:54)
persistLocalReplyMedia            (reply-media-paths.runtime-B671n_FS.js:84:26)
normalizeMediaSource              (reply-media-paths.runtime-B671n_FS.js:117:16)
Object.normalizeMediaPaths        (reply-media-paths.runtime-B671n_FS.js:127:18)
Object.onBlockReply               (agent-runner.runtime-CTlghBhJ.js:217:63)


Stack 2 (~190 ms later, same request, same mediaUrl):

resolveOutboundAttachmentFromUrl (outbound-attachment-Dc2tDWj8.js:6:54)
persistLocalReplyMedia            (reply-media-paths.runtime-B671n_FS.js:84:26)
normalizeMediaSource              (reply-media-paths.runtime-B671n_FS.js:117:16)
Object.normalizeMediaPaths        (reply-media-paths.runtime-B671n_FS.js:127:18)
normalizeReplyPayloadMedia        (agent-runner.runtime-CTlghBhJ.js:1631:10)
buildReplyPayloads                (agent-runner.runtime-CTlghBhJ.js:1704:10)
async Promise.all (index 0)


Both stacks terminate at `resolveOutboundAttachmentFromUrl`, which calls `saveMediaBuffer(..., "outbound", ...)` and generates a fresh UUID per call. The two call sites create separate normalizer instances whose `persistedMediaBySource` caches are not shared; the identical input path therefore does not deduplicate across them, and each persists a separate outbound copy.

Stack 1 originates from `onBlockReply` despite `channels.whatsapp.blockStreaming: false` and `agents.defaults.blockStreamingDefault: "off"` in the active config.
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

On OpenClaw 2026.4.15, one WhatsApp request that yields one media reply results in two media replies being sent to the same conversation.

Steps to reproduce

  1. Start OpenClaw 2026.4.15 with WhatsApp enabled and channels.whatsapp.blockStreaming: false.
  2. Send one WhatsApp request that produces a single media response, for example a chart image.
  3. Observe that the agent sends two media replies for that one request to the same DM or group conversation.
  4. Confirm two outbound files are staged under ~/.openclaw/media/outbound/ within the same second for the single request.
  5. Confirm gateway logs contain two Sent media reply lines for the same conversation within the same second.

Expected behavior

One request that produces one media response should result in exactly one outbound media file and exactly one WhatsApp media reply.

Actual behavior

One WhatsApp request that yields one media response produces two outbound media files and two WhatsApp media replies to the same conversation a few hundred milliseconds apart.

OpenClaw version

2026.4.15 (041266a)

Operating system

Ubuntu Server 24.04

Install method

npm global

Model

openai-codex/gpt-5.4

Provider / routing chain

openclaw -> openai

Additional provider/model setup details

The affected agent config sets `agents.defaults.model.primary` to `openai-codex/gpt-5.4` and uses the `openai-codex` auth profile in OAuth mode. No additional provider routing relevant to the reproduction was identified in the local agent config.

Logs, screenshots, and evidence

Additional evidence: a temporary instrumentation of `resolveOutboundAttachmentFromUrl` (logging `new Error().stack` on each call, subsequently reverted) captured two distinct call stacks for the same mediaUrl on a single reply.

Stack 1:

resolveOutboundAttachmentFromUrl (outbound-attachment-Dc2tDWj8.js:6:54)
persistLocalReplyMedia            (reply-media-paths.runtime-B671n_FS.js:84:26)
normalizeMediaSource              (reply-media-paths.runtime-B671n_FS.js:117:16)
Object.normalizeMediaPaths        (reply-media-paths.runtime-B671n_FS.js:127:18)
Object.onBlockReply               (agent-runner.runtime-CTlghBhJ.js:217:63)


Stack 2 (~190 ms later, same request, same mediaUrl):

resolveOutboundAttachmentFromUrl (outbound-attachment-Dc2tDWj8.js:6:54)
persistLocalReplyMedia            (reply-media-paths.runtime-B671n_FS.js:84:26)
normalizeMediaSource              (reply-media-paths.runtime-B671n_FS.js:117:16)
Object.normalizeMediaPaths        (reply-media-paths.runtime-B671n_FS.js:127:18)
normalizeReplyPayloadMedia        (agent-runner.runtime-CTlghBhJ.js:1631:10)
buildReplyPayloads                (agent-runner.runtime-CTlghBhJ.js:1704:10)
async Promise.all (index 0)


Both stacks terminate at `resolveOutboundAttachmentFromUrl`, which calls `saveMediaBuffer(..., "outbound", ...)` and generates a fresh UUID per call. The two call sites create separate normalizer instances whose `persistedMediaBySource` caches are not shared; the identical input path therefore does not deduplicate across them, and each persists a separate outbound copy.

Stack 1 originates from `onBlockReply` despite `channels.whatsapp.blockStreaming: false` and `agents.defaults.blockStreamingDefault: "off"` in the active config.

Impact and severity

Affected users/systems/channels: WhatsApp DM and WhatsApp group conversations on OpenClaw 2026.4.15.

Severity: Medium. The agent responds, but every single-media reply is duplicated to the end user.

Frequency: Observed repeatedly for the affected media-reply workflow in both DM and group tests.

Consequence: Users receive duplicated chart/media messages for one request, which creates visible noise and reduces trust in the agent's output.

Additional information

Local agent-side checks did not identify a repo-level cause. The affected config still used tools.alsoAllow: ["message"], did not deny group:media or group:openclaw, and had channels.whatsapp.blockStreaming: false.

Additional debugging against the 2026.4.15 bundle suggests the duplicate is upstream in reply-media handling. For the affected reply, the final assistant output contained one MEDIA:<path> token once, but reply-media normalization appears to run through two paths for the same reply. Each path can stage the same media into outbound separately, producing two UUID files and two WhatsApp sends a few hundred milliseconds apart.

extent analysis

TL;DR

The most likely fix is to modify the resolveOutboundAttachmentFromUrl function to deduplicate media files based on their source URL.

Guidance

  1. Investigate the resolveOutboundAttachmentFromUrl function: Review the code to understand how it generates UUIDs for media files and how it can be modified to prevent duplicate files from being created.
  2. Implement a cache or deduplication mechanism: Consider adding a cache or a deduplication mechanism to the resolveOutboundAttachmentFromUrl function to prevent duplicate media files from being created for the same source URL.
  3. Verify the fix: Test the modified function with the provided steps to reproduce the issue and confirm that only one media reply is sent to the conversation.
  4. Review the onBlockReply function: Investigate why the onBlockReply function is being called despite channels.whatsapp.blockStreaming: false and agents.defaults.blockStreamingDefault: "off" in the active config.

Example

// Pseudo-code example of how to deduplicate media files
const mediaCache = new Map();

function resolveOutboundAttachmentFromUrl(mediaUrl) {
  if (mediaCache.has(mediaUrl)) {
    return mediaCache.get(mediaUrl);
  }
  const uuid = generateUUID();
  mediaCache.set(mediaUrl, uuid);
  // Save media buffer with the generated UUID
  saveMediaBuffer(..., uuid, ...);
  return uuid;
}

Notes

The provided stacks suggest that the issue is related to the resolveOutboundAttachmentFromUrl function and the lack of deduplication for media files. However, the exact implementation details are not provided, and the fix may require additional modifications to the surrounding code.

Recommendation

Apply a workaround by modifying the resolveOutboundAttachmentFromUrl function to deduplicate media files based on their source URL. This should prevent duplicate media replies from being sent to the conversation.

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

One request that produces one media response should result in exactly one outbound media file and exactly one WhatsApp media reply.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Single WhatsApp media reply is sent twice in 2026.4.15 [2 pull requests, 3 comments, 3 participants]