Skip to content

Bug: Text before tool calls is lost in Feishu streaming card reply mode #84486

@kentuscn

Description

@kentuscn

Bug: Text before tool calls is lost in Feishu streaming card reply mode

Summary

When the model outputs text before invoking a tool call (stopReason=toolUse), the pre-tool text is silently discarded in Feishu streaming card reply mode. Only the final assistant reply (stopReason=stop) is visible to the user. This results in missing content in Feishu messages.

Environment

  • OpenClaw version: 2026.5.18 (installed via npm)
  • Channel: Feishu (primary affected channel)
  • Model: any model that outputs text before tool calls
  • Reply mode: streaming (default for Feishu p2p)

Steps to Reproduce

  1. Ask the assistant a question that requires tool use AND where the model outputs explanatory text before calling the tool, e.g.: "Query the weather, and detail the execution steps."
  2. The model produces an assistant reply with text like "Step 1... Step 2..." then stops with stopReason=toolUse to invoke a tool
  3. After the tool returns, the model produces a second assistant reply with the results, stopping with stopReason=stop
  4. Only the second reply text is visible in the Feishu message; the first reply text is lost

Expected Behavior

All text produced by the model across the entire turn (including text before tool calls) should be accumulated and delivered together in the final reply, or the streaming card should preserve previously rendered content when a new model invocation starts within the same turn.

Actual Behavior

The pre-tool text is rendered into the Feishu streaming card during the first model invocation. When the model stops with toolUse and a new invocation begins, the streaming card is overwritten with the new invocation content, discarding the previously rendered text. Only the post-tool text remains visible.

Root Cause Analysis

Two independent dispatch systems

OpenClaw has two reply dispatch systems:

  1. ACP dispatch (dispatch-acp): Used for ACP sessions/subagents. Controlled by acp.stream.deliveryMode config ("live" | "final_only").
  2. Feishu card reply dispatcher: Used for Feishu DM/group messages. Uses replyMode=streaming with Feishu interactive cards. Not controlled by acp.stream.deliveryMode.

Why acp.stream.deliveryMode=live does not fix this

Setting acp.stream.deliveryMode to "live" only affects the ACP dispatch path. Feishu messages go through the Feishu card reply dispatcher, which is a completely separate system. The acp.stream.deliveryMode config has no effect on Feishu streaming card behavior.

Evidence from gateway logs

Every Feishu message shows:

feishu[default][msg:om_xxx]: reply mode resolved (effectiveReplyMode=auto, replyMode=streaming, chatType=p2p)

This confirms Feishu uses replyMode=streaming, not ACP dispatch.

Evidence from session history

Session history shows two assistant messages in a single turn:

Seq Role Content stopReason
153 assistant "Step 1: Identify the task… Step 2: Select the tool… Step 3: Execute the query" toolUse
155 assistant "Step 4: Analyze the data and organize the results…" + weather tables stop

Only seq 155 content was visible in the Feishu card. Seq 153 text was rendered into the streaming card but then overwritten when the new model invocation started.

The overwrite mechanism

The Feishu streaming card reply dispatcher updates a single interactive card in-place as the model streams text. When the model stops with toolUse and a new invocation begins within the same turn, the dispatcher starts updating the same card with the new invocation output, replacing the previous content instead of appending to it.

Previously Incorrect Analysis

The initial version of this issue incorrectly attributed the bug to ACP dispatch final_only mode and resetTurnState() clearing finalOnlyOutputText. While that mechanism exists, it is not the cause for Feishu because Feishu does not use ACP dispatch for message delivery. The actual cause is the Feishu streaming card reply dispatcher overwriting previously rendered content when a new model invocation starts.

Suggested Fix

The Feishu streaming card reply dispatcher should preserve previously rendered content across model invocations within the same turn. Options:

  1. Accumulate text across invocations: Before starting a new streaming update for a subsequent model invocation, prepend the previously rendered text from earlier invocations in the same turn.
  2. Use a new card for each invocation: Instead of overwriting the same card, send a new card for each model invocation so all intermediate text is preserved as separate messages.
  3. Buffer until turn completion: Similar to final_only mode, buffer all text across invocations and only render the complete card when the turn finishes (stopReason=stop).

Workarounds

  • Model-side: Avoid outputting text before tool calls; only output all text in the final reply after all tool calls are complete. This is a behavioral workaround and depends on model compliance.
  • Disable streaming cards: If Feishu reply mode can be configured to non-streaming, this may preserve content (untested).

Impact

Any scenario where the model outputs explanatory text before invoking a tool will lose that text in Feishu. This includes:

  • Step-by-step explanations followed by tool calls
  • "Let me look that up…" style preambles before search/query tools
  • Any multi-step workflow with interleaved text and tool calls

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions