Skip to content

fix: interpolate responsePrefix template variables in heartbeat replies#43065

Closed
yweiii wants to merge 1 commit into
openclaw:mainfrom
yweiii:fix/heartbeat-response-prefix-template
Closed

fix: interpolate responsePrefix template variables in heartbeat replies#43065
yweiii wants to merge 1 commit into
openclaw:mainfrom
yweiii:fix/heartbeat-response-prefix-template

Conversation

@yweiii

@yweiii yweiii commented Mar 11, 2026

Copy link
Copy Markdown

Summary

  • Heartbeat replies now correctly interpolate responsePrefix template variables ({model}, {provider}, {thinkingLevel}, etc.) instead of showing them as literal text
  • Wire up createReplyPrefixContext() in runHeartbeatOnce() so onModelSelected populates model/provider context during the LLM call
  • After the LLM responds, call resolveResponsePrefixTemplate() to interpolate the prefix before passing it to normalizeHeartbeatReply()

Root cause

runHeartbeatOnce() resolved responsePrefix from config as the raw template string (e.g. "{model}: ") and passed it directly to normalizeHeartbeatReply() without interpolation. The normal reply path uses createReplyPrefixContext() + resolveResponsePrefixTemplate() — the heartbeat path bypassed both.

Changes

  • src/infra/heartbeat-runner.ts: Replace resolveEffectiveMessagesConfig() with createReplyPrefixContext() to get the raw prefix, onModelSelected callback, and mutable prefixContext. Pass onModelSelected to getReplyFromConfig(). Interpolate the prefix after the LLM call.
  • src/infra/heartbeat-runner.response-prefix-template.test.ts: 3 new tests verifying onModelSelected is passed to getReplyFromConfig in all heartbeat configurations.

Test plan

  • All 63 heartbeat tests pass (60 existing + 3 new)
  • Build passes (pnpm build)
  • Lint/format passes (pnpm check)
  • Manual: set responsePrefix: "{model}: ", trigger heartbeat alert, verify model name appears instead of {model}

Fixes #43064

The heartbeat runner bypassed `resolveResponsePrefixTemplate()`, causing
template variables like `{model}` and `{provider}` to appear as literal
text in heartbeat alert replies.

Wire up `createReplyPrefixContext()` in `runHeartbeatOnce()` so the
`onModelSelected` callback populates model/provider context, then
interpolate the prefix template after the LLM responds.

Fixes openclaw#43064
@greptile-apps

greptile-apps Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where responsePrefix template variables (e.g., {model}, {provider}, {thinkingLevel}) were shown as literal text in heartbeat replies because the heartbeat path bypassed the interpolation machinery used by the normal reply path.

The fix wires up createReplyPrefixContext() in runHeartbeatOnce() to obtain a mutable prefixContext and onModelSelected callback, forwards onModelSelected into both branches of the getReplyFromConfig options object so the context is populated during the LLM call, and then calls resolveResponsePrefixTemplate() post-LLM to produce the final interpolated prefix before passing it to normalizeHeartbeatReply().

Key points:

  • The fix is correct: prefixContext is a mutable object reference, and since onModelSelected mutates it in place, calling resolveResponsePrefixTemplate(rawResponsePrefix, prefixContext) after getReplyFromConfig returns will see the populated model/provider values.
  • heartbeatOkText (the pre-LLM "ping OK" message) still uses the raw template string — this is intentional and documented inline, since the model identity is not yet known before the LLM runs. Users with {model} in their responsePrefix will see the literal placeholder in the "OK" ping, which is a pre-existing UX limitation accepted by the PR.
  • 3 new unit tests confirm onModelSelected is passed through in all replyOpts code paths (with and without heartbeatModelOverride). The first two tests cover the same branch with slightly different assertion shapes, which is minor redundancy but harmless.
  • responsePrefixContextProvider (also returned by createReplyPrefixContext) is unused here; the direct prefixContext reference is semantically equivalent and correct.

Confidence Score: 4/5

  • This PR is safe to merge — it correctly plugs a missing interpolation step in the heartbeat path with no behavioural regressions.
  • The fix is minimal, well-targeted, and mirrors the existing interpolation pattern used in the normal reply path. No API contracts are changed. The pre-LLM heartbeat OK text retaining raw template variables is an intentional and documented trade-off. Tests cover both replyOpts branches. The only minor concern is the UX of heartbeatOkText showing literal {model} to users when showOk is true, but this is pre-existing behaviour and out of scope for this fix.
  • No files require special attention.

Last reviewed commit: 6046c57

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Codex automated review: keeping this open.

Keep this PR open. Current main still reads the heartbeat responsePrefix as a raw config string, does not pass onModelSelected into the heartbeat LLM call, and normalizes heartbeat alert replies with that raw prefix. The PR remains a focused implementation candidate for the same-author open bug report #43064, though it should be rebased onto the current lazy runtime boundary and its test coverage should avoid deep plugin internals if possible.

Best possible solution:

Keep this PR open for maintainer review or a rebased equivalent fix. The best implementation belongs in runHeartbeatOnce: create the reply prefix context for the heartbeat target, pass onModelSelected through getReplyFromConfig, resolve the raw responsePrefix with the populated prefixContext after the LLM call, and add focused heartbeat regression coverage that respects core/plugin boundaries. Keep #43064 open until a fix merges.

What I checked:

  • Current heartbeat path still reads raw prefix: runHeartbeatOnce imports resolveEffectiveMessagesConfig and reads responsePrefix directly for the heartbeat delivery target rather than creating a reply prefix context. (src/infra/heartbeat-runner.ts:14, 5828dcdb05aa)
  • Heartbeat reply options omit model-selection callback: The replyOpts object passed to getReplyFromConfig contains heartbeat flags, model override, warning suppression, timeout, and bootstrap context mode, but no onModelSelected callback to populate model/provider/thinking template values. (src/infra/heartbeat-runner.ts:1069, 5828dcdb05aa)
  • Heartbeat normalization still prepends the unresolved template: normalizeHeartbeatReply strips and prepends the responsePrefix it receives, and runHeartbeatOnce passes the raw responsePrefix into it after the LLM reply. (src/infra/heartbeat-runner.ts:1109, 5828dcdb05aa)
  • Normal reply path has the machinery the PR is trying to reuse: createReplyPrefixContext creates a mutable prefixContext and onModelSelected hook; agent-runner-execution invokes onModelSelected after model selection; normalizeReplyPayload resolves responsePrefix templates when context is provided. (src/channels/reply-prefix.ts:24, 5828dcdb05aa)
  • User-facing config contract includes responsePrefix variables: The agent config docs list responsePrefix template variables including {model}, {modelFull}, {provider}, {thinkingLevel}, {identity.name}, and the {think} alias, so literal placeholders in heartbeat alert replies remain a user-visible gap. Public docs: docs/gateway/config-agents.md. (docs/gateway/config-agents.md:1248, 5828dcdb05aa)
  • No current regression coverage for the heartbeat template path: Current heartbeat tests cover static responsePrefix stripping around HEARTBEAT_OK, but searches found no heartbeat test for createReplyPrefixContext, onModelSelected, or resolveResponsePrefixTemplate in runHeartbeatOnce. (src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts:224, 5828dcdb05aa)

Remaining risk / open question:

  • The PR head predates current main's lazy heartbeat-runner.runtime boundary, so it needs a rebase or maintainer-equivalent patch rather than a direct apply.
  • The proposed test imports bundled Telegram and WhatsApp extension internals from a core infra test, which conflicts with the repo boundary guidance; maintainers may want equivalent coverage through existing heartbeat deps/helpers or public plugin contracts.
  • The proposed patch intentionally leaves pre-LLM HEARTBEAT_OK text using the raw prefix because model selection has not happened yet; maintainer review should decide whether that tradeoff is acceptable or needs follow-up.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 5828dcdb05aa.

@openclaw-clownfish

Copy link
Copy Markdown
Contributor

ProjectClownfish could not safely update this branch, so it opened a narrow replacement PR instead.

Replacement PR: #73996
Source PR: #43065
Contributor credit is preserved in the replacement PR body and changelog plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Heartbeat replies show literal {model} instead of interpolated model name in responsePrefix

1 participant