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.
Version
OpenClaw 2026.6.5 (5181e4f), macOS, Telegram channel.
Setup
xai/grok-4.3(reasoning model,openai-responsesAPI)heartbeatconfigured withtarget: telegram;includeReasoningnot set (defaults to false)HEARTBEAT_OKwhen there is nothing to sayWhat 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_OKandincludeReasoningis false.Root cause
resolveHeartbeatReplyPayload(src/auto-reply/heartbeat-reply-payload.ts) picks the last payload with outbound content and never checks theisReasoningflag:hasOutboundReplyContentonly 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 subsequentstripHeartbeatTokenfinds noHEARTBEAT_OKin 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 !== replyPayloadwhen buildingreasoningPayloadsforincludeReasoning: true— but whenincludeReasoningis false, a reasoning payload selected asreplyPayloadis 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
resolveHeartbeatReplyPayloadunlessheartbeat.includeReasoningis true:(possibly also skipping payloads whose text matches the formatted
Reasoning:/Thinking… _…_prefixes, mirroringresolveHeartbeatReasoningPayloads).I patched this locally in
dist/heartbeat-reply-payload-*.jsand it resolves the leak for my setup.