#11647: fix(webchat): filter HEARTBEAT_OK messages from chat.history response
app: web-ui
gateway
stale
Cluster:
Heartbeat Message Filtering
## 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
#11889: fix(chat): filter HEARTBEAT_OK messages in chat.history when showOk...
by bendyclaw · 2026-02-08
94.8%
#11661: fix: Filter HEARTBEAT_OK from chat.history when showOk is false
by veast · 2026-02-08
94.1%
#11859: fix: filter HEARTBEAT_OK messages from chat.history when showOk is ...
by Zjianru · 2026-02-08
93.6%
#12240: fix: suppress heartbeat agent events from webchat broadcast
by Yida-Dev · 2026-02-09
87.4%
#12774: fix: webchat heartbeat should respect showAlerts config
by a2093930 · 2026-02-09
86.5%
#8334: fix(webchat): Filter NO_REPLY messages from chat history
by vishaltandale00 · 2026-02-03
83.2%
#14993: fix(webchat): add heartbeat detection to prevent zombie WebSocket c...
by BenediktSchackenberg · 2026-02-12
79.1%
#8742: fix(webchat): hide internal system messages from UI (#7440)
by revenuestack · 2026-02-04
79.0%
#14966: fix(webchat): preserve user message visibility after chat.send
by BenediktSchackenberg · 2026-02-12
78.8%
#21014: fix(cron): suppress main-session summary for HEARTBEAT_OK responses
by nickjlamb · 2026-02-19
78.5%