fix(heartbeat): suppress reasoning-only acknowledgements (#114454)

This commit is contained in:
Peter Steinberger
2026-08-09 05:21:02 -07:00
committed by GitHub
parent 2d33a15e3e
commit bec06affef
4 changed files with 56 additions and 32 deletions
@@ -216,7 +216,14 @@ describe("embedded attempt context injection", () => {
{ role: "user", content: "real question", timestamp: 1 } as AgentMessage,
{ role: "assistant", content: "real answer", timestamp: 2 } as unknown as AgentMessage,
{ role: "user", content: HEARTBEAT_PROMPT, timestamp: 3 } as AgentMessage,
{ role: "assistant", content: "HEARTBEAT_OK", timestamp: 4 } as unknown as AgentMessage,
{
role: "assistant",
content: [
{ type: "reasoning", text: "Checking the heartbeat." },
{ type: "text", text: "HEARTBEAT_OK" },
],
timestamp: 4,
} as unknown as AgentMessage,
];
const heartbeatFiltered = filterHeartbeatTranscriptArtifacts(
+34 -28
View File
@@ -13,6 +13,12 @@ import {
} from "./heartbeat.js";
import { MESSAGE_TOOL_DELIVERY_HINTS } from "./reply/delivery-hints.js";
const HIDDEN_REASONING_BLOCKS = [
["thinking", { type: "thinking", thinking: "Checking the heartbeat." }],
["reasoning", { type: "reasoning", text: "Checking the heartbeat." }],
["redacted thinking", { type: "redacted_thinking", data: "opaque-reasoning" }],
] as const;
describe("isHeartbeatUserMessage", () => {
it("matches heartbeat prompts", () => {
expect(
@@ -93,17 +99,17 @@ describe("isHeartbeatOkResponse", () => {
).toBe(true);
});
it.each([
["thinking", { type: "thinking", thinking: "Checking the heartbeat." }],
["redacted thinking", { type: "redacted_thinking", data: "opaque-reasoning" }],
])("matches acknowledgements with hidden %s blocks", (_label, thinkingBlock) => {
expect(
isHeartbeatOkResponse({
role: "assistant",
content: [thinkingBlock, { type: "text", text: "HEARTBEAT_OK" }],
}),
).toBe(true);
});
it.each(HIDDEN_REASONING_BLOCKS)(
"matches acknowledgements with hidden %s blocks",
(_label, reasoningBlock) => {
expect(
isHeartbeatOkResponse({
role: "assistant",
content: [reasoningBlock, { type: "text", text: "HEARTBEAT_OK" }],
}),
).toBe(true);
},
);
it("preserves meaningful or non-text responses", () => {
expect(
@@ -170,24 +176,24 @@ describe("filterHeartbeatTranscriptArtifacts", () => {
]);
});
it.each([
["thinking", { type: "thinking", thinking: "Checking the heartbeat." }],
["redacted thinking", { type: "redacted_thinking", data: "opaque-reasoning" }],
])("removes no-op heartbeat pairs with hidden %s blocks", (_label, thinkingBlock) => {
const nextUserMessage = { role: "user", content: "What time is it?" };
const messages = [
{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT },
{
role: "assistant",
content: [thinkingBlock, { type: "text", text: "HEARTBEAT_OK" }],
},
nextUserMessage,
];
it.each(HIDDEN_REASONING_BLOCKS)(
"removes no-op heartbeat pairs with hidden %s blocks",
(_label, reasoningBlock) => {
const nextUserMessage = { role: "user", content: "What time is it?" };
const messages = [
{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT },
{
role: "assistant",
content: [reasoningBlock, { type: "text", text: "HEARTBEAT_OK" }],
},
nextUserMessage,
];
expect(filterHeartbeatTranscriptArtifacts(messages, undefined, HEARTBEAT_PROMPT)).toEqual([
nextUserMessage,
]);
});
expect(filterHeartbeatTranscriptArtifacts(messages, undefined, HEARTBEAT_PROMPT)).toEqual([
nextUserMessage,
]);
},
);
it("removes OpenAI Responses input/output text heartbeat pairs", () => {
for (const deliveryHint of MESSAGE_TOOL_DELIVERY_HINTS) {
+6 -2
View File
@@ -269,9 +269,13 @@ function resolveMessageText(content: unknown): { text: string; hasNonTextContent
hasNonTextContent = true;
continue;
}
// Provider thinking is not user-visible output; it must not keep a
// Provider thinking/reasoning is not user-visible output; it must not keep a
// no-op heartbeat acknowledgement in future model request history.
if (block.type === "thinking" || block.type === "redacted_thinking") {
if (
block.type === "thinking" ||
block.type === "reasoning" ||
block.type === "redacted_thinking"
) {
continue;
}
if (block.type !== "text" && block.type !== "input_text" && block.type !== "output_text") {
+8 -1
View File
@@ -688,7 +688,14 @@ describe("SessionHistorySseState", () => {
content: `${HEARTBEAT_PROMPT}\nWhen reading HEARTBEAT.md, use workspace file /tmp/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.`,
__openclaw: { seq: 1 },
},
assistantTextMessage("HEARTBEAT_OK", 2),
{
role: "assistant",
content: [
{ type: "reasoning", text: "Checking the heartbeat." },
{ type: "text", text: "HEARTBEAT_OK" },
],
__openclaw: { seq: 2 },
},
{
role: "user",
content: HEARTBEAT_PROMPT,