Heartbeat: allow suppressing tool warnings (#18497)

* Heartbeat: allow suppressing tool warnings

* Changelog: note heartbeat tool-warning suppression
This commit is contained in:
Shadow
2026-02-16 13:29:24 -06:00
committed by GitHub
parent 3238bd78d9
commit 72e228e14b
14 changed files with 36 additions and 2 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus.
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
- Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x.
- Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow.
- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope.
- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091)
- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky.

View File

@@ -718,6 +718,7 @@ Periodic heartbeat runs.
target: "last", // last | whatsapp | telegram | discord | ... | none
prompt: "Read HEARTBEAT.md if it exists...",
ackMaxChars: 300,
suppressToolErrorWarnings: false,
},
},
},
@@ -725,6 +726,7 @@ Periodic heartbeat runs.
```
- `every`: duration string (ms/s/m/h). Default: `30m`.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
- Heartbeats run full agent turns — shorter intervals burn more tokens.

View File

@@ -209,6 +209,7 @@ Use `accountId` to target a specific account on multi-account channels like Tele
- `accountId`: optional account id for multi-account channels. When `target: "last"`, the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.
- `prompt`: overrides the default prompt body (not merged).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
- Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
- `"local"`: always uses the host system timezone.

View File

@@ -922,6 +922,7 @@ export async function runEmbeddedPiAgent(
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: resolvedToolResultFormat,
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
inlineToolResultsAllowed: false,
});

View File

@@ -74,6 +74,8 @@ export type RunEmbeddedPiAgentParams = {
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;
/** If true, suppress tool error warning payloads for this run (including mutating tools). */
suppressToolErrorWarnings?: boolean;
execOverrides?: Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
bashElevated?: ExecElevatedDefaults;
timeoutMs: number;

View File

@@ -252,6 +252,15 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads[0]?.text).toContain("connection timeout");
});
it("suppresses mutating tool errors when suppressToolErrorWarnings is enabled", () => {
const payloads = buildPayloads({
lastToolError: { toolName: "exec", error: "command not found" },
suppressToolErrorWarnings: true,
});
expect(payloads).toHaveLength(0);
});
it("shows recoverable tool errors for mutating tools", () => {
const payloads = buildPayloads({
lastToolError: { toolName: "message", meta: "reply", error: "text required" },

View File

@@ -48,7 +48,11 @@ function shouldShowToolErrorWarning(params: {
lastToolError: LastToolError;
hasUserFacingReply: boolean;
suppressToolErrors: boolean;
suppressToolErrorWarnings?: boolean;
}): boolean {
if (params.suppressToolErrorWarnings) {
return false;
}
const isMutatingToolError =
params.lastToolError.mutatingAction ?? isLikelyMutatingToolName(params.lastToolError.toolName);
if (isMutatingToolError) {
@@ -71,6 +75,7 @@ export function buildEmbeddedRunPayloads(params: {
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;
suppressToolErrorWarnings?: boolean;
inlineToolResultsAllowed: boolean;
}): Array<{
text?: string;
@@ -247,6 +252,7 @@ export function buildEmbeddedRunPayloads(params: {
lastToolError: params.lastToolError,
hasUserFacingReply: hasUserFacingAssistantReply,
suppressToolErrors: Boolean(params.config?.messages?.suppressToolErrors),
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
});
// Always surface mutating tool failures so we do not silently confirm actions that did not happen.

View File

@@ -312,6 +312,7 @@ export async function runAgentTurnWithFallback(params: {
}
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
})(),
suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings,
bashElevated: params.followupRun.run.bashElevated,
timeoutMs: params.followupRun.run.timeoutMs,
runId,

View File

@@ -166,6 +166,7 @@ export function createFollowupRunner(params: {
thinkLevel: queued.run.thinkLevel,
verboseLevel: queued.run.verboseLevel,
reasoningLevel: queued.run.reasoningLevel,
suppressToolErrorWarnings: opts?.suppressToolErrorWarnings,
execOverrides: queued.run.execOverrides,
bashElevated: queued.run.bashElevated,
timeoutMs: queued.run.timeoutMs,

View File

@@ -29,6 +29,8 @@ export type GetReplyOptions = {
isHeartbeat?: boolean;
/** Resolved heartbeat model override (provider/model string from merged per-agent config). */
heartbeatModelOverride?: string;
/** If true, suppress tool error warning payloads for this run. */
suppressToolErrorWarnings?: boolean;
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
onReasoningStream?: (payload: ReplyPayload) => Promise<void> | void;
/** Called when a thinking/reasoning block ends. */

View File

@@ -17,6 +17,10 @@ export const FIELD_HELP: Record<string, string> = {
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].identity.avatar":
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
"agents.defaults.heartbeat.suppressToolErrorWarnings":
"Suppress tool error warning payloads during heartbeat runs.",
"agents.list[].heartbeat.suppressToolErrorWarnings":
"Suppress tool error warning payloads during heartbeat runs.",
"discovery.mdns.mode":
'mDNS broadcast mode ("minimal" default, "full" includes cliPath/sshPort, "off" disables mDNS).',
"gateway.auth.token":

View File

@@ -220,6 +220,8 @@ export type AgentDefaultsConfig = {
prompt?: string;
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
ackMaxChars?: number;
/** Suppress tool error warning payloads during heartbeat runs. */
suppressToolErrorWarnings?: boolean;
/**
* When enabled, deliver the model's reasoning payload for heartbeat runs (when available)
* as a separate message prefixed with `Reasoning:` (same as `/reasoning on`).

View File

@@ -29,6 +29,7 @@ export const HeartbeatSchema = z
accountId: z.string().optional(),
prompt: z.string().optional(),
ackMaxChars: z.number().int().nonnegative().optional(),
suppressToolErrorWarnings: z.boolean().optional(),
})
.strict()
.superRefine((val, ctx) => {

View File

@@ -540,9 +540,10 @@ export async function runHeartbeatOnce(opts: {
try {
const heartbeatModelOverride = heartbeat?.model?.trim() || undefined;
const suppressToolErrorWarnings = heartbeat?.suppressToolErrorWarnings === true;
const replyOpts = heartbeatModelOverride
? { isHeartbeat: true, heartbeatModelOverride }
: { isHeartbeat: true };
? { isHeartbeat: true, heartbeatModelOverride, suppressToolErrorWarnings }
: { isHeartbeat: true, suppressToolErrorWarnings };
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
const includeReasoning = heartbeat?.includeReasoning === true;