#16373: fix: suppress leaked heartbeat poll prompts in reply delivery
size: S
Cluster:
Heartbeat Functionality Improvements
## Problem
When the agent is busy and a heartbeat poll prompt leaks into the reply pipeline, the raw heartbeat prompt text can be delivered to users.
The original suppression logic only matched hardcoded/default phrasing, so custom prompts from `config.agents.defaults.heartbeat.prompt` could bypass filtering.
Additionally, after `normalizeReplyPayload` gained `heartbeatPrompt`, production callers were not wiring it through, so custom prompts were still not consistently applied in real delivery paths.
## Solution
Make heartbeat-poll leak suppression config-aware in `normalizeReplyPayload` and wire the resolved prompt through production call paths.
- Added `heartbeatPrompt?: string` to `NormalizeReplyOptions`
- Resolve prompt via `resolveHeartbeatPrompt(opts.heartbeatPrompt)`
- Compare using lowercase + whitespace-collapsed full-text matching
- Suppress repeated/stacked prompt leaks
- Preserve media/channel-data payloads
- Threaded heartbeat prompt through dispatcher path via `createReplyPrefixOptions` -> `ReplyDispatcherOptions` -> `normalizeReplyPayload`
- Wired direct `routeReply` normalization call to pass resolved heartbeat prompt from config
## Files changed
- `src/auto-reply/reply/normalize-reply.ts`
- `src/auto-reply/reply/reply-utils.test.ts`
- `src/channels/reply-prefix.ts`
- `src/auto-reply/reply/reply-dispatcher.ts`
- `src/auto-reply/reply/route-reply.ts`
## Validation
- `./node_modules/.bin/tsc --noEmit`
## Notes
- Rebased on latest `main`
- Tests were moved from deleted `normalize-reply.test.ts` into current consolidated `reply-utils.test.ts`
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR fixes heartbeat poll prompt leaks by making suppression config-aware. Previously, only hardcoded default prompts were detected and filtered; custom prompts from `config.agents.defaults.heartbeat.prompt` would leak through to users.
The fix threads the resolved heartbeat prompt through two main delivery paths:
- **Dispatcher path**: `createReplyPrefixOptions` → `ReplyDispatcherOptions` → `normalizeReplyPayload`
- **Direct routing**: `routeReply` resolves prompt from config and passes to `normalizeReplyPayload`
The suppression logic uses case-insensitive, whitespace-collapsed comparison and handles stacked/repeated prompts. Payloads with media or channel data are preserved even if text matches the prompt. Prompts with additional content (e.g. "PROMPT Thanks, done.") are correctly treated as real replies.
Test coverage is comprehensive, including default prompts, custom prompts, stacked prompts, prompts with extra content, legitimate messages mentioning heartbeat, and media preservation.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The implementation is straightforward, well-tested, and addresses a real bug. The heartbeat prompt is threaded through all normalization paths correctly. Type safety ensures all callers must provide the prompt. The logic correctly handles edge cases (stacked prompts, prompts with extra content, media preservation). Previous review concerns have been addressed with clarifying comments and regression tests. No breaking changes or risky refactoring.
- No files require special attention
<sub>Last reviewed commit: ee23af8</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#20563: fix: skip default HEARTBEAT_OK instructions when custom prompt is set
by clawalpha · 2026-02-19
81.3%
#15575: fix(heartbeat): suppress prefixed HEARTBEAT_OK ack replies (#15505)
by TsekaLuk · 2026-02-13
80.6%
#19339: fix(heartbeat): skip isError payloads when resolving heartbeat reply
by aldoeliacim · 2026-02-17
79.7%
#16321: Fix #12767: suppress HEARTBEAT_OK leakage in Telegram DM replies
by tdjackey · 2026-02-14
79.6%
#19406: fix(heartbeat): filter error payloads from heartbeat reply selection
by namabile · 2026-02-17
79.5%
#17371: fix(heartbeat): always strip HEARTBEAT_OK token from reply text
by BinHPdev · 2026-02-15
78.1%
#21014: fix(cron): suppress main-session summary for HEARTBEAT_OK responses
by nickjlamb · 2026-02-19
78.0%
#12240: fix: suppress heartbeat agent events from webchat broadcast
by Yida-Dev · 2026-02-09
76.4%
#11647: fix(webchat): filter HEARTBEAT_OK messages from chat.history response
by liuxiaopai-ai · 2026-02-08
76.4%
#11859: fix: filter HEARTBEAT_OK messages from chat.history when showOk is ...
by Zjianru · 2026-02-08
76.2%