openclaw - 💡(How to fix) Fix Delivery layer concatenates multiple text content items — pick-one policy needed (split from #69737) [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#74674Fetched 2026-04-30 06:21:30
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
2
Timeline (top)
commented ×1cross-referenced ×1mentioned ×1subscribed ×1

Splitting Bug 2 out of #69737 per @martingarramon's recommendation in the source-trace comment (different blast radius from Bug 1, which stays scoped to #69737).

The shared visible-text extractor concatenates all matching text content items in an assistant message with joinWith: "\n". When a turn produces multiple text blocks (e.g., one per branch after parallel tool calls), the user-facing delivery layer posts the concatenation — visible as a near-duplicate paragraph with slight wording variation. The extractor is also used by non-delivery surfaces, so the fix is a policy call about scope.

Root Cause

Splitting Bug 2 out of #69737 per @martingarramon's recommendation in the source-trace comment (different blast radius from Bug 1, which stays scoped to #69737).

The shared visible-text extractor concatenates all matching text content items in an assistant message with joinWith: "\n". When a turn produces multiple text blocks (e.g., one per branch after parallel tool calls), the user-facing delivery layer posts the concatenation — visible as a near-duplicate paragraph with slight wording variation. The extractor is also used by non-delivery surfaces, so the fix is a policy call about scope.

Code Example

content:
  - type: text
    text: "<single paragraph answer to the user's question, variant 1>"  (~120 chars)
  - type: text
    text: "<same paragraph, slightly different wording>"                  (~136 chars)
stopReason: stop

---

const rawVisibleText = coerceChatContentText(extractAssistantVisibleText(assistantMessage));
RAW_BUFFERClick to expand / collapse

Summary

Splitting Bug 2 out of #69737 per @martingarramon's recommendation in the source-trace comment (different blast radius from Bug 1, which stays scoped to #69737).

The shared visible-text extractor concatenates all matching text content items in an assistant message with joinWith: "\n". When a turn produces multiple text blocks (e.g., one per branch after parallel tool calls), the user-facing delivery layer posts the concatenation — visible as a near-duplicate paragraph with slight wording variation. The extractor is also used by non-delivery surfaces, so the fix is a policy call about scope.

Environment

  • OpenClaw: 2026.4.14 (originally observed); upstream/main confirmed by community trace at cfda375bb6 (2026-04-22) and 7ddd815e469e (2026-04-28)
  • Provider: openai-codex (api: openai-codex-responses)
  • Model: gpt-5.4
  • Delivery channel verified: Slack

Evidence

An interactive session, preceded by an assistant turn with two parallel tool calls, produced one assistant message containing two text content items:

content:
  - type: text
    text: "<single paragraph answer to the user's question, variant 1>"  (~120 chars)
  - type: text
    text: "<same paragraph, slightly different wording>"                  (~136 chars)
stopReason: stop

Slack delivery concatenated both items into one post, visible to the end user as a near-duplicate paragraph with slight wording variation between the two halves. Treat the model's multi-text emission as something we can't rely on not happening.

Source trace (per #69737 community trace, 2026-04-22)

Delivery path:

  • src/agents/pi-embedded-subscribe.handlers.messages.ts:546:
    const rawVisibleText = coerceChatContentText(extractAssistantVisibleText(assistantMessage));
  • extractAssistantVisibleText at src/agents/pi-embedded-utils.ts:116
  • extractAssistantTextForPhase(msg) at src/agents/pi-embedded-utils.ts:47-115, which concatenates all matching-phase text blocks with joinWith: "\n" (line ~106)
  • Parallel function (same shape) at src/shared/chat-message-content.ts:155
  • No "pick-one" logic in either.

Codex re-check on 2026-04-28 against 7ddd815e469e confirmed handleMessageEnd at src/agents/pi-embedded-subscribe.handlers.messages.ts:675 still emits joined text via the same path.

Cross-surface blast radius

The shared extractor has at least four callers beyond Slack/Telegram delivery:

  • TUI: src/tui/tui-formatters.ts:300
  • Gateway session dump: src/gateway/session-utils.fs.ts:637
  • Chat-history tool: src/agents/tools/chat-history-text.ts
  • Slack/Telegram delivery: src/agents/pi-embedded-subscribe.handlers.messages.ts:546 / :675

Changing the default from joinWith: "\n" to pick-last would flip behavior across all four surfaces. TUI / gateway dumps / chat-history may legitimately want concatenated blocks (full transcript fidelity); only the delivery boundary is unambiguously "post one thing to the user."

Proposed fix — two viable shapes

Both scope cleanly; preference is a maintainer call.

Option A — delivery-only override. Keep extractAssistantTextForPhase joining (preserves TUI / gateway / history). Apply a delivery-boundary selector in pi-embedded-subscribe.handlers.messages.ts that picks one text item (last non-empty by default).

Option B — config flag on the shared extractor. Add a selection: 'concat' | 'pick-last' | 'pick-longest' (or similar) parameter, with concat remaining the default for non-delivery callers and pick-last set on delivery sites.

Option A has a tighter blast radius. Option B is more flexible but adds config surface that other callers may need to reason about.

Acceptance / regression

  • Repro: send the agent a request that triggers parallel tool calls; confirm the user-facing channel receives a single text block rather than the concatenation.
  • Regression check: TUI rendering, gateway session dump, and chat-history tool output unchanged for messages with multiple text items (or behavior change is explicit / documented if Option B is chosen).

Relationship to #69737

Bug 1 in #69737 (raw errorMessage surfacing — single-file fallback chain in lifecycle.ts) stays scoped to that issue. This issue is Bug 2 only. Per @martingarramon's recommendation: different blast radius, different reviewer pool, likely different timelines.

Offer

Happy to test a PR against our production deployment and provide additional evidence (scrubbed of user data) if useful.

Drafted by Claude Code (my coding agent) and reviewed by me before posting.

extent analysis

TL;DR

The most likely fix is to modify the extractAssistantTextForPhase function to pick one text item instead of concatenating all matching-phase text blocks.

Guidance

  • Identify the extractAssistantTextForPhase function at src/agents/pi-embedded-utils.ts:47-115 and consider modifying it to pick one text item, such as the last non-empty one, instead of concatenating all matching-phase text blocks.
  • Evaluate the two proposed fix options: delivery-only override (Option A) and config flag on the shared extractor (Option B), considering the trade-offs between blast radius and flexibility.
  • Test the fix by sending a request that triggers parallel tool calls and verifying that the user-facing channel receives a single text block rather than the concatenation.
  • Perform regression checks to ensure that TUI rendering, gateway session dump, and chat-history tool output remain unchanged for messages with multiple text items.

Example

// Modified extractAssistantTextForPhase function
function extractAssistantTextForPhase(msg) {
  const textBlocks = msg.content.filter(item => item.type === 'text');
  return textBlocks.length > 0 ? textBlocks[textBlocks.length - 1].text : '';
}

Notes

The fix should be carefully evaluated to ensure that it does not introduce unintended changes to the behavior of other components that rely on the shared extractor.

Recommendation

Apply a delivery-only override (Option A) to minimize the blast radius and avoid introducing additional config surface that other callers may need to reason about.

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

openclaw - 💡(How to fix) Fix Delivery layer concatenates multiple text content items — pick-one policy needed (split from #69737) [1 comments, 2 participants]