Description
When responsePrefix uses template variables like {model}, heartbeat replies output the literal placeholder text (e.g. {model}: The heartbeat just came in...) instead of the interpolated model name (e.g. gemini-2.5-flash: The heartbeat just came in...).
Normal user-initiated replies correctly interpolate {model}, {provider}, {thinkingLevel}, etc. via resolveResponsePrefixTemplate(). The heartbeat code path bypasses this interpolation.
Steps to reproduce
- Set
responsePrefix: "{model}: " in a channel config (e.g. Telegram)
- Wait for a heartbeat alert reply (not HEARTBEAT_OK, an actual alert)
- Observe the reply starts with
{model}: literally
Root cause
In src/infra/heartbeat-runner.ts, runHeartbeatOnce():
- Resolves
responsePrefix from config as the raw template string ("{model}: ")
- Calls
getReplyFromConfig() without passing onModelSelected — so no model context is captured
- Passes the raw un-interpolated template to
normalizeHeartbeatReply() which prepends it literally
The normal reply path (src/channels/reply-prefix.ts) creates a prefixContext via createReplyPrefixContext(), wires up onModelSelected to capture model/provider info, and later calls resolveResponsePrefixTemplate() to interpolate. The heartbeat path skips all of this.
Expected behavior
Heartbeat replies should interpolate responsePrefix template variables the same way normal replies do.
Environment
- OpenClaw v2026.2.17
- Channel: Telegram (but affects all channels with template responsePrefix)
Description
When
responsePrefixuses template variables like{model}, heartbeat replies output the literal placeholder text (e.g.{model}: The heartbeat just came in...) instead of the interpolated model name (e.g.gemini-2.5-flash: The heartbeat just came in...).Normal user-initiated replies correctly interpolate
{model},{provider},{thinkingLevel}, etc. viaresolveResponsePrefixTemplate(). The heartbeat code path bypasses this interpolation.Steps to reproduce
responsePrefix: "{model}: "in a channel config (e.g. Telegram){model}:literallyRoot cause
In
src/infra/heartbeat-runner.ts,runHeartbeatOnce():responsePrefixfrom config as the raw template string ("{model}: ")getReplyFromConfig()without passingonModelSelected— so no model context is capturednormalizeHeartbeatReply()which prepends it literallyThe normal reply path (
src/channels/reply-prefix.ts) creates aprefixContextviacreateReplyPrefixContext(), wires uponModelSelectedto capture model/provider info, and later callsresolveResponsePrefixTemplate()to interpolate. The heartbeat path skips all of this.Expected behavior
Heartbeat replies should interpolate
responsePrefixtemplate variables the same way normal replies do.Environment