fix(cron): prevent announce dedupe from counting disabled message-tool sends

When a cron job runs with `delivery.mode: "announce"`, the message tool
is disabled so the runner can handle delivery. However, if the
`didSendViaMessagingTool` flag carried stale state from a prior turn or
subagent, the `skipMessagingToolDelivery` check could treat it as a
completed delivery and skip announce — resulting in zero delivery.

Fix: add a `messageToolDisabled` flag to the tool policy and exclude
disabled-tool runs from the `skipMessagingToolDelivery` check. When the
message tool was unavailable, any send-state is not a valid delivery
signal.

Also improve `appendCronDeliveryInstruction` to explicitly tell the
agent the message tool is unavailable when announce delivery is active,
so skills that reference the message tool do not confuse the agent into
producing empty or error output.

Closes #42244

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio 2026-03-14 09:31:59 -05:00
parent 392ddb56e2
commit 7eb3766456
1 changed files with 21 additions and 2 deletions

View File

@ -192,11 +192,19 @@ async function resolveCronDeliveryContext(params: {
function appendCronDeliveryInstruction(params: {
commandBody: string;
deliveryRequested: boolean;
messageToolDisabled: boolean;
}) {
if (!params.deliveryRequested) {
return params.commandBody;
}
return `${params.commandBody}\n\nReturn your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.`.trim();
// When the message tool is disabled (announce delivery handles it), be
// explicit so skills that reference the message tool do not confuse the
// agent into producing empty or error output.
// See: https://github.com/openclaw/openclaw/issues/42244
const instruction = params.messageToolDisabled
? "IMPORTANT: The message/send tool is not available for this run. Do NOT attempt to send messages directly. Return your complete output as plain text — it will be delivered to the user automatically."
: "Return your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.";
return `${params.commandBody}\n\n${instruction}`.trim();
}
export async function runCronIsolatedAgentTurn(params: {
@ -478,7 +486,11 @@ export async function runCronIsolatedAgentTurn(params: {
// Internal/trusted source - use original format
commandBody = `${base}\n${timeLine}`.trim();
}
commandBody = appendCronDeliveryInstruction({ commandBody, deliveryRequested });
commandBody = appendCronDeliveryInstruction({
commandBody,
deliveryRequested,
messageToolDisabled: toolPolicy.messageToolDisabled,
});
const existingSkillsSnapshot = cronSession.sessionEntry.skillsSnapshot;
const skillsSnapshot = resolveCronSkillsSnapshot({
@ -825,9 +837,16 @@ export async function runCronIsolatedAgentTurn(params: {
// Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content).
const ackMaxChars = resolveHeartbeatAckMaxChars(agentCfg);
const skipHeartbeatDelivery = deliveryRequested && isHeartbeatOnlyResponse(payloads, ackMaxChars);
// Only treat a matching message-tool send as the delivery path when the
// message tool was actually available. When the tool was disabled (announce
// mode), any `didSendViaMessagingTool` state is stale from a prior turn or
// subagent — counting it would suppress announce delivery, causing zero
// delivery to the user.
// See: https://github.com/openclaw/openclaw/issues/42244
const skipMessagingToolDelivery =
deliveryContract === "shared" &&
deliveryRequested &&
!toolPolicy.messageToolDisabled &&
finalRunResult.didSendViaMessagingTool === true &&
(finalRunResult.messagingToolSentTargets ?? []).some((target) =>
matchesMessagingToolDeliveryTarget(target, {