Bug Summary
The heartbeat ignores its configured agents.defaults.heartbeat.model when the active session has a modelOverride set (e.g., from a TUI /model switch). The heartbeat ends up using whatever model the user last selected in the TUI instead of the pinned heartbeat model.
Steps to Reproduce
- Set
agents.defaults.heartbeat.model to "openrouter/openrouter/auto" in openclaw.json
- Set
heartbeat.target to "last" (the default)
- Open the TUI and use
/model to switch to a different model (e.g., openrouter/moonshotai/kimi-k2.5)
- Wait for the next heartbeat cycle (or trigger with
openclaw system event --mode now)
- Check gateway logs:
grep -E "(heartbeat|agent model)" ~/.openclaw/logs/gateway.log | tail -10
Expected: Heartbeat uses openrouter/openrouter/auto (from heartbeat.model config)
Actual: Heartbeat uses moonshotai/kimi-k2.5 (from session's modelOverride)
Root Cause
In getReplyFromConfig() (dist/reply-DptDUVRg.js, minified), the heartbeat model is correctly resolved from agentCfg.heartbeat.model when opts.isHeartbeat is true. However, resolveReplyDirectives() is called afterward and returns the session's modelOverride, which then unconditionally overwrites the heartbeat model:
// Heartbeat model correctly resolved here ✓
if (opts?.isHeartbeat) {
const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? "";
const heartbeatRef = heartbeatRaw ? resolveModelRefFromString({ ... }) : null;
if (heartbeatRef) {
provider = heartbeatRef.ref.provider;
model = heartbeatRef.ref.model;
}
}
// ... later, after resolveReplyDirectives():
provider = resolvedProvider; // ← overwrites heartbeat model
model = resolvedModel; // ← with session's modelOverride
Inside resolveReplyDirectives() → createModelSelectionState(), the session's stored modelOverride is applied unconditionally:
if (sessionEntry?.providerOverride) provider = sessionEntry.providerOverride;
if (sessionEntry?.modelOverride) model = sessionEntry.modelOverride;
There is no isHeartbeat guard at either of these override points.
Why It Matters
The heartbeat is meant to be a background process that runs on a cheap/fast model (openrouter/auto). When it inherits the user's chat model (e.g., Claude Opus, Gemini Pro), it:
- Uses more expensive models unnecessarily
- May behave differently due to model-specific quirks
- Defeats the purpose of having a separate
heartbeat.model config
Suggested Fix
Skip session modelOverride application when opts.isHeartbeat is true and heartbeat.model is explicitly configured:
if (!opts?.isHeartbeat && sessionEntry?.modelOverride) {
model = sessionEntry.modelOverride;
}
Or apply the heartbeat model after directive resolution so it takes final precedence.
Current Workaround
Manually edit ~/.openclaw/agents/main/sessions/sessions.json to remove the providerOverride and modelOverride fields, then restart the gateway. This needs to be repeated after every TUI /model switch.
Environment
- OpenClaw version: 2026.2.9
- OS: macOS (Darwin 25.2.0, Apple Silicon)
- Model routing: OpenRouter
- Heartbeat config:
{ every: "30m", model: "openrouter/openrouter/auto", target: "last" }
Bug Summary
The heartbeat ignores its configured
agents.defaults.heartbeat.modelwhen the active session has amodelOverrideset (e.g., from a TUI/modelswitch). The heartbeat ends up using whatever model the user last selected in the TUI instead of the pinned heartbeat model.Steps to Reproduce
agents.defaults.heartbeat.modelto"openrouter/openrouter/auto"inopenclaw.jsonheartbeat.targetto"last"(the default)/modelto switch to a different model (e.g.,openrouter/moonshotai/kimi-k2.5)openclaw system event --mode now)grep -E "(heartbeat|agent model)" ~/.openclaw/logs/gateway.log | tail -10Expected: Heartbeat uses
openrouter/openrouter/auto(fromheartbeat.modelconfig)Actual: Heartbeat uses
moonshotai/kimi-k2.5(from session'smodelOverride)Root Cause
In
getReplyFromConfig()(dist/reply-DptDUVRg.js, minified), the heartbeat model is correctly resolved fromagentCfg.heartbeat.modelwhenopts.isHeartbeatis true. However,resolveReplyDirectives()is called afterward and returns the session'smodelOverride, which then unconditionally overwrites the heartbeat model:Inside
resolveReplyDirectives()→createModelSelectionState(), the session's storedmodelOverrideis applied unconditionally:There is no
isHeartbeatguard at either of these override points.Why It Matters
The heartbeat is meant to be a background process that runs on a cheap/fast model (
openrouter/auto). When it inherits the user's chat model (e.g., Claude Opus, Gemini Pro), it:heartbeat.modelconfigSuggested Fix
Skip session
modelOverrideapplication whenopts.isHeartbeatis true andheartbeat.modelis explicitly configured:Or apply the heartbeat model after directive resolution so it takes final precedence.
Current Workaround
Manually edit
~/.openclaw/agents/main/sessions/sessions.jsonto remove theproviderOverrideandmodelOverridefields, then restart the gateway. This needs to be repeated after every TUI/modelswitch.Environment
{ every: "30m", model: "openrouter/openrouter/auto", target: "last" }