← Back to PRs

#11647: fix(webchat): filter HEARTBEAT_OK messages from chat.history response

by liuxiaopai-ai open 2026-02-08 03:37 View on GitHub →
app: web-ui gateway stale
## Summary Fixes #11630 — `chat.history` returns raw transcript data without heartbeat filtering, causing `HEARTBEAT_OK` messages to reappear in the webchat UI after every page refresh or tab switch. The **live broadcast** path already correctly suppresses heartbeat messages via `shouldSuppressHeartbeatBroadcast()` in `server-chat.ts`, but the **`chat.history`** handler had no equivalent filter. ## Root Cause `chat.history` reads messages from the session transcript and returns them directly. The live broadcast path checks `resolveHeartbeatVisibility({ cfg, channel: "webchat" })` and suppresses broadcasts when `showOk` is `false` (the default). But `chat.history` skipped this check entirely, creating an inconsistency: heartbeat messages vanish during live sessions but reappear on every page load. ## Fix Added `filterHeartbeatMessages()` in a new module `src/gateway/chat-history-heartbeat.ts` that removes: 1. **Assistant messages** whose text is solely `HEARTBEAT_OK` (including markup-wrapped variants like `**HEARTBEAT_OK**`, `<b>HEARTBEAT_OK</b>`) 2. **The immediately preceding user message** when it contains `HEARTBEAT_OK` (the standard heartbeat prompt asks the agent to "reply HEARTBEAT_OK") The filter is applied in the `chat.history` handler **only when** `resolveHeartbeatVisibility` returns `showOk: false` (the webchat default), matching the existing live broadcast behavior. When `showOk` is explicitly set to `true`, heartbeat messages are preserved as before. ## Changes | File | Change | |------|--------| | `src/gateway/chat-history-heartbeat.ts` | New module: `isHeartbeatOkMessage()` + `filterHeartbeatMessages()` | | `src/gateway/chat-history-heartbeat.test.ts` | 18 unit tests | | `src/gateway/server-methods/chat.ts` | Wire filter into `chat.history` handler (4 lines) | ## Tests 18 unit tests covering: - Plain / whitespace / markdown-bold / HTML-bold wrapped tokens - Content arrays vs string content format - Prompt+reply pair removal (heartbeat prompt preceding HEARTBEAT_OK) - Custom prompts without HEARTBEAT_OK token (kept, not removed) - Multiple heartbeat exchanges interleaved with real messages - Edge cases: empty arrays, reply at index 0, alert messages with real content beyond token, consecutive replies without prompts - Reference identity (returns same array reference when no filtering needed) ``` ✓ src/gateway/chat-history-heartbeat.test.ts (18 tests) 15ms Test Files 1 passed (1) Tests 18 passed (18) ``` <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR makes `chat.history` consistent with the live webchat broadcast path by filtering out heartbeat exchanges (assistant `HEARTBEAT_OK` acknowledgements and their paired prompt) when `resolveHeartbeatVisibility({ channel: "webchat" }).showOk` is `false`. It does this by introducing `src/gateway/chat-history-heartbeat.ts` with helpers to recognize markup-wrapped `HEARTBEAT_OK` responses and then wiring that filter into the `chat.history` handler in `src/gateway/server-methods/chat.ts`. Unit tests were added to cover the detection and filtering behavior across string/array content formats and several edge cases. <h3>Confidence Score: 4/5</h3> - This PR is mostly safe to merge, with one behavior bug around `chat.history` limits when heartbeats are present. - The change is localized and covered by unit tests, and it reuses existing heartbeat visibility config. The main concern is that filtering happens after slicing to `limit`, so sessions with many heartbeat exchanges can return fewer messages than requested even though more non-heartbeat history exists earlier. - src/gateway/server-methods/chat.ts <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs