Skip to content

[Bug]: Heartbeat delivers reasoning payload as main reply when includeReasoning is false (resolveHeartbeatReplyPayload ignores isReasoning) #92260

Description

@jmpei

Version

OpenClaw 2026.6.5 (5181e4f), macOS, Telegram channel.

Setup

  • Agent model: xai/grok-4.3 (reasoning model, openai-responses API)
  • heartbeat configured with target: telegram; includeReasoning not set (defaults to false)
  • Heartbeat prompt instructs the agent to reply HEARTBEAT_OK when there is nothing to say

What happens

Occasionally a heartbeat poll delivers the model's reasoning/thinking text to the Telegram chat as a user-visible message (English meta-deliberation like "The message is an OpenClaw heartbeat poll. I'm in a Heartbeat direct conversation… Check if recent 3 hours chat happened, or dice roll…"), even though the assistant's final text block is a clean HEARTBEAT_OK and includeReasoning is false.

Root cause

resolveHeartbeatReplyPayload (src/auto-reply/heartbeat-reply-payload.ts) picks the last payload with outbound content and never checks the isReasoning flag:

for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
    const payload = replyResult[idx];
    if (!payload) continue;
    if (hasOutboundReplyContent(payload)) return payload; // reasoning payloads pass this check
}

hasOutboundReplyContent only checks text/media presence. With a reasoning model, the reply array contains a reasoning payload alongside the text payload. Whenever the reasoning payload ends up after the final text payload (e.g. a turn that ends with a tool call, or reasoning summary arriving last), it is selected as the main heartbeat reply. The subsequent stripHeartbeatToken finds no HEARTBEAT_OK in it, so the run is treated as non-silent and the thinking text is delivered.

The heartbeat runner appears aware that the main payload can be a reasoning payload — it filters payload !== replyPayload when building reasoningPayloads for includeReasoning: true — but when includeReasoning is false, a reasoning payload selected as replyPayload is still delivered as the main message.

Related but distinct from #66888 (model wrote reasoning into the text block itself; closed via Telegram-path hardening) and #84319 (Slack non-streaming delivery). This one is the heartbeat payload-selection layer.

Suggested fix

Skip reasoning payloads in resolveHeartbeatReplyPayload unless heartbeat.includeReasoning is true:

if (payload.isReasoning === true) continue;

(possibly also skipping payloads whose text matches the formatted Reasoning: / Thinking… _…_ prefixes, mirroring resolveHeartbeatReasoningPayloads).

I patched this locally in dist/heartbeat-reply-payload-*.js and it resolves the leak for my setup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.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

    Fields

    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