openclaw - ✅(Solved) Fix ACP block replies not marked as visible text on Discord, causing full message duplication [2 pull requests, 2 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#58892Fetched 2026-04-08 02:31:25
View on GitHub
Comments
2
Participants
2
Timeline
5
Reactions
0
Timeline (top)
cross-referenced ×2commented ×1referenced ×1subscribed ×1

Root Cause

In dispatch-acp.runtime-nq9T9sRU.js, the function shouldTreatDeliveredTextAsVisible only marks a delivery as visible when:

  • kind === "final", or
  • channel === "telegram"

Block deliveries (kind === "block") on non-Telegram channels (e.g. Discord) always return false. This means state.deliveredVisibleText stays false after blocks are streamed.

Then in finalizeAcpTurnOutput, the guard condition:

!params.delivery.hasDeliveredVisibleText()

evaluates to true even after all blocks were posted, so the full accumulatedBlockText is sent again as a final reply — duplicating the entire response.

Telegram works correctly because the channel check catches it. Discord does not.

Fix Action

Fix

Add kind === "block" to the visible-text check:

function shouldTreatDeliveredTextAsVisible(params) {
  if (!params.text?.trim()) return false;
  if (params.kind === "final") return true;
  if (params.kind === "block") return true;  // ← add this
  return normalizeDeliveryChannel(params.channel) === "telegram";
}

PR fix notes

PR #58902: fix(acp): treat block deliveries as visible text to prevent duplicate final reply

Description (problem / solution / changelog)

Summary

Fixes #58892

ACP block replies on Discord (and other non-Telegram channels) get sent twice — once streamed as blocks, then again as a duplicate final reply.

Root cause

shouldTreatDeliveredTextAsVisible() only returns true for kind === "final" or Telegram. Block deliveries (kind === "block") on Discord return false, so state.deliveredVisibleText stays false. Then finalizeAcpTurnOutput sees !hasDeliveredVisibleText() and resends the full text.

Fix

Add kind === "block" check to mark block deliveries as visible text:

  if (params.kind === "final") { return true; }
+ if (params.kind === "block") { return true; }
  return normalizeDeliveryChannel(params.channel) === "telegram";

Test plan

  • ACP session on Discord: streamed blocks should appear once, no duplicate final message
  • Telegram: unchanged behavior (already handled by channel check)
  • Final-only deliveries: unchanged behavior

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) [email protected]

Changed files

  • src/auto-reply/reply/dispatch-acp-delivery.ts (modified, +3/-0)

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)

Code Example

!params.delivery.hasDeliveredVisibleText()

---

function shouldTreatDeliveredTextAsVisible(params) {
  if (!params.text?.trim()) return false;
  if (params.kind === "final") return true;
  if (params.kind === "block") return true;  // ← add this
  return normalizeDeliveryChannel(params.channel) === "telegram";
}
RAW_BUFFERClick to expand / collapse

Bug

When an ACP agent streams output via block replies on Discord, the final message gets sent twice — once as streamed blocks, and again as a full duplicate at the end.

Root Cause

In dispatch-acp.runtime-nq9T9sRU.js, the function shouldTreatDeliveredTextAsVisible only marks a delivery as visible when:

  • kind === "final", or
  • channel === "telegram"

Block deliveries (kind === "block") on non-Telegram channels (e.g. Discord) always return false. This means state.deliveredVisibleText stays false after blocks are streamed.

Then in finalizeAcpTurnOutput, the guard condition:

!params.delivery.hasDeliveredVisibleText()

evaluates to true even after all blocks were posted, so the full accumulatedBlockText is sent again as a final reply — duplicating the entire response.

Telegram works correctly because the channel check catches it. Discord does not.

Fix

Add kind === "block" to the visible-text check:

function shouldTreatDeliveredTextAsVisible(params) {
  if (!params.text?.trim()) return false;
  if (params.kind === "final") return true;
  if (params.kind === "block") return true;  // ← add this
  return normalizeDeliveryChannel(params.channel) === "telegram";
}

Steps to Reproduce

  1. Set up an ACP session (e.g. cursor) bound to a Discord thread
  2. Send a prompt that produces a multi-chunk streamed response
  3. Observe the agent's reply appears twice in the thread (identical content)

Environment

  • Channel: Discord
  • OpenClaw version: current (post-0.4.0 acpx update)
  • Delivery mode: live streaming (block replies)

extent analysis

TL;DR

To fix the issue of duplicate messages on Discord, update the shouldTreatDeliveredTextAsVisible function to include kind === "block" in its conditions.

Guidance

  • Review the shouldTreatDeliveredTextAsVisible function in dispatch-acp.runtime-nq9T9sRU.js to ensure it correctly handles block deliveries on non-Telegram channels.
  • Verify that adding kind === "block" to the function resolves the issue of duplicate messages on Discord.
  • Test the fix by reproducing the steps outlined in the issue, checking for the presence of duplicate messages after applying the change.
  • Consider reviewing other channel-specific logic to ensure consistency across different platforms.

Example

The corrected shouldTreatDeliveredTextAsVisible function should look like this:

function shouldTreatDeliveredTextAsVisible(params) {
  if (!params.text?.trim()) return false;
  if (params.kind === "final") return true;
  if (params.kind === "block") return true;  
  return normalizeDeliveryChannel(params.channel) === "telegram";
}

Notes

This fix assumes that the issue is solely due to the missing condition in the shouldTreatDeliveredTextAsVisible function and that no other factors are contributing to the duplicate messages.

Recommendation

Apply the workaround by updating the shouldTreatDeliveredTextAsVisible function as described, to prevent duplicate messages on Discord.

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