openclaw - ✅(Solved) Fix message.send triggers duplicate delivery-mirror replies [1 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#44467Fetched 2026-04-08 00:46:34
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Author
Timeline (top)
cross-referenced ×2commented ×1subscribed ×1

If the agent successfully sends an image/file/text via message.send(...), OpenClaw should suppress the normal main reply / delivery-mirror path for that turn.

Right now both paths can fire:

  1. The agent calls message.send(...)
  2. The attachment / visible content is delivered successfully
  3. The system then also runs a follow-up layer with:
    • provider=openclaw
    • model=delivery-mirror
  4. A second extra message appears, often just mirroring text like the filename (for example cat-fresh.png)

Error Message

This causes duplicate sends across channels:

Root Cause

If the agent successfully sends an image/file/text via message.send(...), OpenClaw should suppress the normal main reply / delivery-mirror path for that turn.

Right now both paths can fire:

  1. The agent calls message.send(...)
  2. The attachment / visible content is delivered successfully
  3. The system then also runs a follow-up layer with:
    • provider=openclaw
    • model=delivery-mirror
  4. A second extra message appears, often just mirroring text like the filename (for example cat-fresh.png)

Fix Action

Fixed

PR fix notes

PR #59643: fix(agents): preserve commentary/final_answer phase separation

Description (problem / solution / changelog)

Summary

  • Problem: assistant turns that contain both commentary and final_answer text can be flattened into one visible output, which leaks commentary into user-facing replies and can produce duplicate or malformed final delivery.
  • Why it matters: this breaks the expected final-only user experience, causes duplicate replies after tool/send paths, and corrupts replay/context because mixed-phase text is persisted and replayed ambiguously.
  • What changed: preserved phase separation end-to-end across stored-message conversion, replay/input-item rebuilding, WebSocket partial phase propagation, and visible extraction/delivery so user-visible output prefers final_answer while still falling back safely when no final text exists.
  • What did NOT change (scope boundary): this does not globally redefine every text extractor in the repo, does not change tool-call semantics, and does not attempt a broader phase-aware audit outside the main OpenAI WS -> embedded subscribe -> visible delivery path.

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 #59150
  • Related #56198
  • Related #58892
  • Related #52084
  • Related #25592
  • Related #44467
  • Related PR #30479
  • Related PR #57484
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: assistant text phase truth already existed at block level, but several layers still flattened mixed-phase text. Stored assistant messages could carry both commentary and final-answer blocks under one misleading top-level phase; replay collapsed them back together; stream partials could lose item-phase attribution; and visible extraction/delivery then consumed flattened text instead of phase-aware text.
  • Missing detection / guardrail: there was no focused regression coverage for mixed-phase stored messages, phase-aware replay splitting, signature-only phased partials, or text_end / message_end delivery interactions where commentary previews must be suppressed/replaced by final output.
  • Prior context (git blame, prior PR, issue, or refactor if known): this bug family overlaps longstanding leakage/duplication reports in #59150, #56198, #58892, #52084, #25592, and #44467. Adjacent prior art includes #30479 (stripping raw user-facing protocol leakage) and #57484 (commentary-delivery semantics on a channel path), but those did not fix mixed-phase persistence/replay/delivery end-to-end.
  • Why this regressed now: the issue is not a single recent regression; it is an accumulated phase-separation gap that became more visible once commentary, block replies, tool sends, and final-answer delivery all coexisted on the same assistant turn path.
  • If unknown, what was ruled out: ruled out “display-only” root cause. Investigation confirmed the problem begins upstream in stored-message conversion/replay semantics, not only in final rendering.

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:
    • src/agents/openai-ws-stream.test.ts
    • src/agents/pi-embedded-utils.test.ts
    • src/agents/pi-embedded-subscribe.handlers.messages.test.ts
    • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts
    • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts
    • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts
  • Scenario the test should lock in: mixed commentary/final stored messages stay phase-separated on replay; stream partials preserve phase; visible extraction prefers final_answer -> commentary -> legacy/unphased; commentary text_end block replies are suppressed until final delivery; final replacement at message_end works; and duplicate/prefix-extension regressions remain fixed.
  • Why this is the smallest reliable guardrail: the bug spans stored-message conversion, stream partial attribution, and delivery seams. Unit-only coverage at one layer would miss the cross-layer collapse that caused the visible duplication/leak.
  • Existing test that already covers this (if any): none before this branch for the mixed-phase end-to-end path.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Visible assistant output now prefers final_answer text when both commentary and final-answer phases exist in one turn.
  • Commentary-only previews are no longer allowed to leak through as the final visible reply in the main embedded delivery path.
  • When commentary streamed first and final text arrives later, the final visible reply replaces the preview instead of duplicating it.
  • If no final-answer text exists, commentary/unphased fallback still works instead of producing an empty reply.

Diagram (if applicable)

Before:
[mixed commentary + final_answer blocks]
  -> [stored/replayed as flattened assistant text]
  -> [visible extractor concatenates all text]
  -> [commentary leak and/or duplicate final reply]

After:
[mixed commentary + final_answer blocks]
  -> [phase preserved in storage/replay/partials]
  -> [visible extractor prefers final_answer]
  -> [commentary preview suppressed/replaced]
  -> [single intended final visible reply]

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
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Ubuntu (local dev host)
  • Runtime/container: local Node/pnpm repo checkout
  • Model/provider: OpenAI WS / embedded subscribe path
  • Integration/channel (if any): embedded delivery path with Telegram/Discord-adjacent visible-output semantics
  • Relevant config (redacted): default OpenAI WS / embedded subscribe test harnesses; no special secrets required

Steps

  1. Produce or replay an assistant turn containing both commentary and final_answer text blocks.
  2. Observe stored/replayed assistant content and the user-visible delivery path.
  3. Confirm whether visible output leaks commentary, duplicates final delivery, or collapses mixed phases.

Expected

  • Stored and replayed assistant content preserves phase boundaries.
  • User-visible extraction prefers final_answer when present.
  • Commentary previews are suppressed or replaced rather than duplicated.

Actual

  • Before this fix: mixed-phase turns could flatten into one visible assistant reply, leak commentary, or produce duplicate final delivery.
  • On this branch: phase separation is preserved through replay and delivery, and the targeted duplicate/leak regressions are covered by tests.

Evidence

Attach at least one:

  • Failing test/log before + passing after

  • Trace/log snippets

  • Screenshot/recording

  • Perf numbers (if relevant)

  • local transcript evidence showed assistant turns with both commentary and final_answer blocks in a single message

  • targeted regression slice passed: 8 suites / 164 tests

  • fresh independent review loop passed for storage/replay, stream-phase, visible-delivery, and holistic closure before branch submission

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • inspected mixed-phase transcript evidence and confirmed commentary + final-answer coexistence in one assistant turn
    • verified stored-message/replay, stream partial propagation, and visible delivery changes in the touched files
    • ran the targeted regression slice and confirmed 8 suites / 164 tests passed
    • confirmed the branch diff remains scoped to the intended 10 files
  • Edge cases checked:
    • commentary leaking through text_end block replies
    • final replacement at message_end after commentary streamed first
    • legacy/unphased + phased replay collapse
    • signature-only phased partials without top-level partial.phase
    • prefix-extension and duplicate text_end regressions
  • What you did not verify:
    • full-repo tsc --noEmit on this host (prior typecheck attempts were memory-constrained)
    • a broader follow-up audit of other phase-blind helper paths such as src/agents/tools/sessions-helpers.ts

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.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: other assistant-text consumers outside the main embedded delivery path may still use phase-blind flattening and show adjacent inconsistencies.
    • Mitigation: this PR keeps scope tight to the verified main issue path and leaves sessions-helpers parity as explicit follow-up watchpoint rather than silently broadening behavior.
  • Risk: replay/delivery edge cases could regress around partial/final transitions.
    • Mitigation: regression coverage now locks in mixed-phase replay splitting, phase-aware partial attribution, commentary suppression, final replacement, and duplicate/text-end edge cases.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/openai-ws-message-conversion.ts (modified, +35/-13)
  • src/agents/openai-ws-stream.test.ts (modified, +490/-30)
  • src/agents/openai-ws-stream.ts (modified, +145/-12)
  • src/agents/pi-embedded-subscribe.handlers.messages.test.ts (modified, +169/-0)
  • src/agents/pi-embedded-subscribe.handlers.messages.ts (modified, +84/-32)
  • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts (modified, +1/-0)
  • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts (modified, +4/-1)
  • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts (modified, +468/-1)
  • src/agents/pi-embedded-utils.test.ts (modified, +76/-1)
  • src/agents/pi-embedded-utils.ts (modified, +108/-6)
RAW_BUFFERClick to expand / collapse

Now all channels can duplicate outbound replies when the agent proactively sends visible content via message(action=send).

Summary

If the agent successfully sends an image/file/text via message.send(...), OpenClaw should suppress the normal main reply / delivery-mirror path for that turn.

Right now both paths can fire:

  1. The agent calls message.send(...)
  2. The attachment / visible content is delivered successfully
  3. The system then also runs a follow-up layer with:
    • provider=openclaw
    • model=delivery-mirror
  4. A second extra message appears, often just mirroring text like the filename (for example cat-fresh.png)

Observed behavior

This causes duplicate sends across channels:

  • first: attachment/image/file is sent by message.send
  • then: an extra text message is posted afterward by delivery-mirror

From session history, the chain looks like:

  1. message.send(...) succeeds
  2. the system later emits another outbound message through provider=openclaw, model=delivery-mirror

Expected behavior

If the current assistant turn has already successfully delivered user-visible content via message(action=send), then OpenClaw should fully suppress the normal mirrored/main reply path for that same turn.

Suggested fix:

  • mark the turn as externally_delivered = true after successful message.send
  • skip delivery-mirror for that turn
  • or discard any non-NO_REPLY main reply output once external delivery already happened

One-line version

message.send and delivery-mirror are currently both active for the same turn, so proactive sends get mirrored and users see duplicate messages.

extent analysis

Fix Plan

To fix the issue of duplicate outbound replies, we need to suppress the normal main reply / delivery-mirror path when the agent proactively sends visible content via message(action=send). Here are the steps:

  • Mark the turn as externally_delivered = true after a successful message.send:
if message.action == 'send' and message.send_status == 'success':
    turn.externally_delivered = True
  • Skip delivery-mirror for turns marked as externally_delivered:
if turn.externally_delivered:
    # Skip delivery-mirror for this turn
    return 'NO_REPLY'
  • Discard any non-NO_REPLY main reply output once external delivery has already happened:
if turn.externally_delivered and reply != 'NO_REPLY':
    # Discard reply to prevent duplicate messages
    return 'NO_REPLY'

Verification

To verify that the fix worked, test the following scenarios:

  • Send an image/file/text via message.send and check that only one message is delivered.
  • Check the session history to ensure that the delivery-mirror path is skipped for turns marked as externally_delivered.

Extra Tips

  • Make sure to update the turn object with the externally_delivered flag after a successful message.send.
  • Consider adding logging to track when delivery-mirror is skipped to ensure the fix is working as expected.

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

If the current assistant turn has already successfully delivered user-visible content via message(action=send), then OpenClaw should fully suppress the normal mirrored/main reply path for that same turn.

Suggested fix:

  • mark the turn as externally_delivered = true after successful message.send
  • skip delivery-mirror for that turn
  • or discard any non-NO_REPLY main reply output once external delivery already happened

Still need to ship something?

×6

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

Back to top recommendations

TRENDING