openclaw - ✅(Solved) Fix [Bug]: shouldSuppressMessagingToolReplies drops legitimate final reply when message tool sent media-only to same channel [3 pull requests, 1 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#59743Fetched 2026-04-08 02:41:03
View on GitHub
Comments
0
Participants
1
Timeline
9
Reactions
0
Author
Participants
Timeline (top)
referenced ×6cross-referenced ×3

shouldSuppressMessagingToolReplies() suppresses all post-turn final reply payloads when the message tool was used to send media-only content (e.g., an audio file) to the same channel that originated the message. The legitimate final text reply is silently dropped — no error is logged, no delivery is attempted.

Error Message

shouldSuppressMessagingToolReplies() suppresses all post-turn final reply payloads when the message tool was used to send media-only content (e.g., an audio file) to the same channel that originated the message. The legitimate final text reply is silently dropped — no error is logged, no delivery is attempted.

  • No error in gateway.err.log

Root Cause

In src/auto-reply/reply/agent-runner-reply-payloads.ts (compiled as agent-runner.runtime-*.js):

const suppressMessagingToolReplies = dedupeRuntime?.shouldSuppressMessagingToolReplies({
    messageProvider: ...,
    messagingToolSentTargets,
    originatingTo: ...,
    accountId: ...
}) ?? false;

// ...

return {
    replyPayloads: suppressMessagingToolReplies ? [] : filteredPayloads,  // ← ALL payloads dropped
    didLogHeartbeatStrip
};

shouldSuppressMessagingToolReplies() (in src/auto-reply/reply/reply-payloads-dedupe.ts) checks only whether any messagingToolSentTargets entry matches the originating channel. It does not distinguish between:

  • Sending text to the same channel (legitimate suppression to avoid duplicates)
  • Sending media-only to the same channel (should NOT suppress the subsequent text reply)

When the targets match, suppressMessagingToolReplies becomes true, and all replyPayloads are returned as [] — the text-level dedup in filterMessagingToolDuplicates() is never reached.

Fix Action

Fixed

PR fix notes

PR #59752: fix: use text-level dedup instead of blanket suppression for same-target messaging tool replies

Description (problem / solution / changelog)

Summary

  • Problem: When the message tool sends content to the same channel that originated the message, shouldSuppressMessagingToolReplies() blanket-suppressed all post-turn final reply payloads (replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads). This caused legitimate final text replies to be silently dropped when the message tool had sent media (e.g., an audio file) with a short label to the same channel — even though the final reply text was completely different content.
  • Why it matters: Users lost substantive final replies without any error or warning whenever the message tool had sent any content (including media-only) to the originating channel in the same turn.
  • What changed: Removed the blanket suppressMessagingToolReplies ? [] : filteredPayloads path in both agent-runner-payloads.ts and followup-runner.ts. Same-target sends now go through content-level dedup (filterMessagingToolDuplicates for text, filterMessagingToolMediaDuplicates for media). Added cross-target guard to followup-runner.ts to align with agent-runner-payloads.ts. Updated and added tests in both test files.
  • What did NOT change (scope boundary): shouldSuppressMessagingToolReplies() is still used to enable same-target dedup (vs. cross-target sends which skip dedup entirely). MIN_DUPLICATE_TEXT_LENGTH threshold unchanged — short-text dedup is an intentionally accepted edge case (see comment thread).

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #59743
  • Related #15147 #9281 #9471
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: shouldSuppressMessagingToolReplies() only checks if any messagingToolSentTargets entry matches the originating channel. When targets match, it returns true, and the entire filteredPayloads array is replaced with [] — the text-level dedup in filterMessagingToolDuplicates() is never reached.
  • Missing detection / guardrail: No test case existed for the scenario where the message tool sends media-only (with a short label) to the same target while a substantively different final text reply should still be delivered.
  • Prior context (git blame, prior PR, issue, or refactor if known): The blanket suppression logic was the original design — it worked correctly when the message tool only sent text, but broke when media-with-short-label sends were introduced.
  • Why this regressed now: The message tool gained the ability to send media (audio files, images) with short text labels. The blanket suppression never distinguished between "sent duplicate text" and "sent media with a label".
  • If unknown, what was ruled out: N/A — root cause is clear.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: agent-runner-payloads.test.ts, followup-runner.test.ts
  • Scenario the test should lock in: Message tool sends media-only (e.g., audio file with short label "🟢") to the same target → final text reply with different content must still be delivered.
  • Why this is the smallest reliable guardrail: Unit test directly exercises buildReplyPayloads with the exact payload combination that triggered the bug — no integration setup needed.
  • Existing test that already covers this (if any): None previously. Added "does not suppress final reply when message tool sent media-only to same target" in agent-runner-payloads.test.ts.
  • If no new test is added, why not: New tests were added (see above).

User-visible / Behavior Changes

  • Before: When the message tool sent any content (including media with a short label) to the originating channel, all final text replies in that turn were silently dropped.
  • After: Only exact text duplicates and matching media URLs are suppressed. Non-duplicate final text replies are correctly delivered even when the message tool sent to the same channel.

Diagram (if applicable)

Before:
[message tool sends audio+label to same channel] -> shouldSuppressMessagingToolReplies = true
  -> replyPayloads = [] (ALL replies dropped, including non-duplicate text)

After:
[message tool sends audio+label to same channel] -> messagingToolTargetsMatchOrigin = true
  -> filterMessagingToolDuplicates (text dedup: only drops matching text)
  -> filterMessagingToolMediaDuplicates (media dedup: only strips matching URLs)
  -> replyPayloads = [non-duplicate final text reply delivered ✓]

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Any (server-side logic)
  • Runtime/container: Node.js
  • Model/provider: Any LLM provider
  • Integration/channel (if any): Any messaging channel (Slack, Discord, Telegram, etc.)
  • Relevant config (redacted): Default config, no special settings required

Steps

  1. Send a message to a bot on any messaging channel (e.g., Discord).
  2. The bot's agent run uses the message tool to send an audio file with a short label (e.g., "🟢") back to the same channel.
  3. The agent also generates a substantive final text reply (e.g., "Setup complete! Here is the summary...").

Expected

  • The audio file is sent.
  • The final text reply ("Setup complete! Here is the summary...") is also delivered to the channel.

Actual

  • Before fix: The audio file is sent, but the final text reply is silently dropped (blanket suppression).
  • After fix: Both the audio file and the final text reply are delivered correctly.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Test results after fix:

✓ src/auto-reply/reply/agent-runner-payloads.test.ts (16 tests passed)
✓ src/auto-reply/reply/followup-runner.test.ts (22 tests passed, 3 pre-existing failures unrelated to this PR)

The 3 pre-existing test failures in followup-runner.test.ts are caused by a missing resolveSessionLockMaxHoldFromTimeout mock in the test setup — these fail identically on the base branch.

Human Verification (required)

  • Verified scenarios:
    • Media-only send (audio + short label) to same target → final text reply is delivered
    • Duplicate text send to same target → correctly suppressed
    • Cross-target send → no dedup applied, reply delivered
    • Same-target send with different text → reply delivered (not suppressed)
  • Edge cases checked:
    • Cross-target guard in both agent-runner-payloads.ts and followup-runner.ts
    • Synthetic provider matching (e.g., heartbeattelegram)
    • Account ID mismatch → no suppression
  • What you did not verify: End-to-end verification on a live messaging channel (verified via unit tests only).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.
BotFindingStatus
Greptile P0 ×2Stale followup-runner.test.ts tests relying on removed blanket suppression✅ Fixed in commit 3537e78 — updated 3 tests to use matching sent texts for text-level dedup
Greptile P1Cross-target dedup guard missing in followup-runner.ts✅ Fixed in commit 3537e78 — added shouldSuppressMessagingToolReplies cross-target guard
Codex P2Short same-target duplicate suppression (MIN_DUPLICATE_TEXT_LENGTH)✅ Acknowledged — intentional tradeoff (see comment)
Codex P2Keep short duplicate guard in followup delivery path✅ Acknowledged — same as above
Codex P2Preserve account fallback in same-target dedupe check✅ Addressed — account fallback preserved via resolveOriginAccountId

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Short identical texts (< MIN_DUPLICATE_TEXT_LENGTH chars, e.g., "ok", "done") sent by the message tool to the same target will no longer be suppressed and may result in duplicate short replies.
    • Mitigation: This is a much narrower edge case than the original bug. If it becomes a practical issue, it should be addressed by lowering MIN_DUPLICATE_TEXT_LENGTH or adding an exact-match check in filterMessagingToolDuplicates, not by restoring blanket suppression.

AI-assisted PR Checklist

  • Mark as AI-assisted in the PR title or description — This PR was authored with AI assistance (Claude Code agent for followup commits addressing bot review feedback)
  • Note the degree of testing: Fully tested — all changes covered by updated and new unit tests
  • Include prompts or session logs if possible — Bot review findings were used as prompts for the followup commit
  • Confirm you understand what the code does — Yes, the author understands the dedup pipeline and the distinction between blanket suppression vs. content-level dedup
  • If you have access to Codex, run codex review --base origin/main locally and address the findings before asking for review — Codex review findings addressed (see Review Conversations above)
  • Resolve or reply to bot review conversations after you address them — All Greptile and Codex conversations addressed

Changed files

  • src/auto-reply/reply/agent-runner-payloads.test.ts (modified, +73/-10)
  • src/auto-reply/reply/agent-runner-payloads.ts (modified, +8/-3)
  • src/auto-reply/reply/followup-runner.test.ts (modified, +33/-4)
  • src/auto-reply/reply/followup-runner.ts (modified, +35/-28)

PR #59787: fix: only suppress text reply when messaging tool sent text (not media-only)

Description (problem / solution / changelog)

When the message tool sent media-only content to the same channel, shouldSuppressMessagingToolReplies() was incorrectly suppressing the subsequent text reply. This happened because it returned true whenever any message was sent to the matching target, without checking whether text content was included.

The fix adds a hasText?: boolean field to MessagingToolSend, sets it when text content is present in the tool args, and makes shouldSuppressMessagingToolReplies() skip targets that only sent media.

Fixes #59743

Changed files

  • src/agents/pi-embedded-messaging.ts (modified, +2/-0)
  • src/agents/pi-embedded-subscribe.handlers.tools.ts (modified, +4/-3)
  • src/auto-reply/reply/reply-payloads-dedupe.ts (modified, +5/-0)

PR #59795: fix(messaging): skip reply suppression for media-only messaging tool sends

Description (problem / solution / changelog)

Summary

  • shouldSuppressMessagingToolReplies suppressed all post-turn final replies when the message tool sent anything to the originating channel, including media-only sends (audio files, images). This caused the agent's final text reply to be silently dropped when it had also sent media to the same channel in the same turn.
  • Added a sentText flag to MessagingToolSend that tracks whether a messaging tool call included text content. The suppression logic now only fires when text was actually sent to the same channel, allowing media-only sends to coexist with a final text reply.
  • The fix is scoped to three files: the type definition, the tool commit handler, and the suppression check.

Closes #59743

Test plan

  • New test: "does not suppress when message tool sent media-only to the same channel" (passes)
  • Updated existing suppression tests to include sentText: true for text-sending scenarios
  • agent-runner-payloads.test.ts (14 tests pass)
  • followup-runner.test.ts (updated expectations)
  • agent-runner.misc.runreplyagent.test.ts (updated expectations)
  • Note: 3 pre-existing test failures in reply-payloads.test.ts related to Telegram topic-origin matching are unrelated to this change (also fail on upstream/main)

Changed files

  • src/agents/pi-embedded-messaging.ts (modified, +1/-0)
  • src/agents/pi-embedded-subscribe.handlers.tools.ts (modified, +10/-3)
  • src/auto-reply/reply/agent-runner-payloads.test.ts (modified, +6/-2)
  • src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts (modified, +12/-4)
  • src/auto-reply/reply/followup-runner.test.ts (modified, +9/-3)
  • src/auto-reply/reply/reply-payloads-dedupe.ts (modified, +3/-0)
  • src/auto-reply/reply/reply-payloads.test.ts (modified, +19/-5)

Code Example

User: "Docker版で進めて。"
Agent runs ~30 tool calls (docker pull, build, test, etc.)
Agent calls message tool: send audio file "test.mp3" to thread (✅ delivered)
Agent produces final reply: "セットアップ完了。状況まとめ:..." (❌ silently dropped)
Next user message arrives → Agent replies normally (✅ delivered)

---

const suppressMessagingToolReplies = dedupeRuntime?.shouldSuppressMessagingToolReplies({
    messageProvider: ...,
    messagingToolSentTargets,
    originatingTo: ...,
    accountId: ...
}) ?? false;

// ...

return {
    replyPayloads: suppressMessagingToolReplies ? [] : filteredPayloads,  // ← ALL payloads dropped
    didLogHeartbeatStrip
};
RAW_BUFFERClick to expand / collapse

Summary

shouldSuppressMessagingToolReplies() suppresses all post-turn final reply payloads when the message tool was used to send media-only content (e.g., an audio file) to the same channel that originated the message. The legitimate final text reply is silently dropped — no error is logged, no delivery is attempted.

Environment

  • OpenClaw version: 2026.4.2 (npm, macOS arm64)
  • Node.js: v22.17.0
  • Channel: Discord (guild thread)
  • Model: Claude Opus 4.6 (Anthropic)

Steps to Reproduce

  1. In a Discord thread, send a prompt that causes the agent to: a. Use the message tool to send a media file (audio, image) to the same thread b. Produce additional assistant text as the final reply in the same turn
  2. Observe that the media file arrives in Discord, but the final text reply does not

Example flow (from real session transcript):

User: "Docker版で進めて。"
  → Agent runs ~30 tool calls (docker pull, build, test, etc.)
  → Agent calls message tool: send audio file "test.mp3" to thread (✅ delivered)
  → Agent produces final reply: "セットアップ完了。状況まとめ:..." (❌ silently dropped)
  → Next user message arrives → Agent replies normally (✅ delivered)

Root Cause

In src/auto-reply/reply/agent-runner-reply-payloads.ts (compiled as agent-runner.runtime-*.js):

const suppressMessagingToolReplies = dedupeRuntime?.shouldSuppressMessagingToolReplies({
    messageProvider: ...,
    messagingToolSentTargets,
    originatingTo: ...,
    accountId: ...
}) ?? false;

// ...

return {
    replyPayloads: suppressMessagingToolReplies ? [] : filteredPayloads,  // ← ALL payloads dropped
    didLogHeartbeatStrip
};

shouldSuppressMessagingToolReplies() (in src/auto-reply/reply/reply-payloads-dedupe.ts) checks only whether any messagingToolSentTargets entry matches the originating channel. It does not distinguish between:

  • Sending text to the same channel (legitimate suppression to avoid duplicates)
  • Sending media-only to the same channel (should NOT suppress the subsequent text reply)

When the targets match, suppressMessagingToolReplies becomes true, and all replyPayloads are returned as [] — the text-level dedup in filterMessagingToolDuplicates() is never reached.

Evidence from session transcript

The JSONL session log shows:

LineTypeContentDelivered?
L55assistant (toolUse)message tool: send audio to thread
L56assistant (delivery-mirror)test.mp3
L57toolResult{"ok": true, "messageId": "..."}
L58assistant (stop)"セットアップ完了。状況まとめ:..."❌ Not delivered
  • No error in gateway.err.log
  • No delivery attempt logged
  • WebUI shows the message; Discord does not

Suggested Fix

Option A (minimal): In shouldSuppressMessagingToolReplies(), only suppress when the message tool sent text content to the matching target. Media-only sends should not trigger suppression.

Option B (more robust): Remove the blanket suppressMessagingToolReplies ? [] : filteredPayloads path entirely. Instead, always go through filterMessagingToolDuplicates() which does text-content-level dedup — this correctly handles the case where the message tool sent media but the final reply is unique text.

Related Issues

  • #15147 — Reply ordering race (closed/stale, same root cause family: didSendViaMessagingTool boolean over-suppresses)
  • #9281 — Delivery-mirror breaks Anthropic tool_use ordering (closed/stale)
  • #9471 — Message delivery race condition (closed)

extent analysis

TL;DR

The most likely fix involves modifying the shouldSuppressMessagingToolReplies() function to distinguish between sending text and media-only content to the same channel, ensuring that legitimate final text replies are not suppressed.

Guidance

  1. Review the shouldSuppressMessagingToolReplies() function: Ensure it checks the type of content sent by the message tool to the same channel, suppressing only when text content is sent to avoid duplicates.
  2. Modify the suppression logic: Implement a check to see if the message tool sent media-only content, and if so, do not suppress the subsequent text reply.
  3. Consider removing the blanket suppression path: Instead, always filter through filterMessagingToolDuplicates() to handle text-content-level deduplication correctly.
  4. Test with different content types: Verify the fix by sending various types of content (text, media) and checking that final text replies are delivered as expected.

Example

const suppressMessagingToolReplies = dedupeRuntime?.shouldSuppressMessagingToolReplies({
    messageProvider: ...,
    messagingToolSentTargets,
    originatingTo: ...,
    accountId: ...,
    // Add a check for the type of content sent
    contentSent: ... // e.g., 'text', 'media'
}) ?? false;

// Modify the function to consider the content type
if (contentSent === 'media') {
    // Do not suppress the subsequent text reply
    return {
        replyPayloads: filteredPayloads,
        didLogHeartbeatStrip
    };
} else {
    // Suppress only when text content is sent to the same channel
    return {
        replyPayloads: suppressMessagingToolReplies ? [] : filteredPayloads,
        didLogHeartbeatStrip
    };
}

Notes

The provided fix assumes that the shouldSuppressMessagingToolReplies() function can be modified to consider the type of content sent by the message tool. If this is not possible, alternative solutions may be necessary.

Recommendation

Apply the suggested fix by modifying the shouldSuppressMessagingToolReplies() function to distinguish between sending text and media-only content, ensuring that legitimate final text replies are not suppressed. This approach addresses the root cause of the issue and provides a more robust solution.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING