mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:48:27 +00:00
fix(cron): share isolated announce flow + harden cron scheduling/delivery (#11641)
* fix(cron): comprehensive cron scheduling and delivery fixes - Fix delivery target resolution for isolated agent cron jobs - Improve schedule parsing and validation - Add job retry logic and error handling - Enhance cron ops with better state management - Add timer improvements for more reliable cron execution - Add cron event type to protocol schema - Support cron events in heartbeat runner (skip empty-heartbeat check, use dedicated CRON_EVENT_PROMPT for relay) * fix: remove cron debug test and add changelog/docs notes (#11641) (thanks @tyler6204)
This commit is contained in:
@@ -70,12 +70,21 @@ export async function resolveDeliveryTarget(
|
||||
const mode = resolved.mode as "explicit" | "implicit";
|
||||
const toCandidate = resolved.to;
|
||||
|
||||
// Only carry threadId when delivering to the same recipient as the session's
|
||||
// last conversation. This prevents stale thread IDs (e.g. from a Telegram
|
||||
// supergroup topic) from being sent to a different target (e.g. a private
|
||||
// chat) where they would cause API errors.
|
||||
const threadId =
|
||||
resolved.threadId && resolved.to && resolved.to === resolved.lastTo
|
||||
? resolved.threadId
|
||||
: undefined;
|
||||
|
||||
if (!toCandidate) {
|
||||
return {
|
||||
channel,
|
||||
to: undefined,
|
||||
accountId: resolved.accountId,
|
||||
threadId: resolved.threadId,
|
||||
threadId,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
@@ -91,7 +100,7 @@ export async function resolveDeliveryTarget(
|
||||
channel,
|
||||
to: docked.ok ? docked.to : undefined,
|
||||
accountId: resolved.accountId,
|
||||
threadId: resolved.threadId,
|
||||
threadId,
|
||||
mode,
|
||||
error: docked.ok ? undefined : docked.error,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
import { runSubagentAnnounceFlow } from "../../agents/subagent-announce.js";
|
||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import { ensureAgentWorkspace } from "../../agents/workspace.js";
|
||||
@@ -40,7 +41,11 @@ import {
|
||||
supportsXHighThinking,
|
||||
} from "../../auto-reply/thinking.js";
|
||||
import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js";
|
||||
import { resolveSessionTranscriptPath, updateSessionStore } from "../../config/sessions.js";
|
||||
import {
|
||||
resolveAgentMainSessionKey,
|
||||
resolveSessionTranscriptPath,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
|
||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||
@@ -358,6 +363,8 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||
let fallbackProvider = provider;
|
||||
let fallbackModel = model;
|
||||
const runStartedAt = Date.now();
|
||||
let runEndedAt = runStartedAt;
|
||||
try {
|
||||
const sessionFile = resolveSessionTranscriptPath(cronSession.sessionEntry.sessionId, agentId);
|
||||
const resolvedVerboseLevel =
|
||||
@@ -420,6 +427,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
runResult = fallbackResult.result;
|
||||
fallbackProvider = fallbackResult.provider;
|
||||
fallbackModel = fallbackResult.model;
|
||||
runEndedAt = Date.now();
|
||||
} catch (err) {
|
||||
return withRunSession({ status: "error", error: String(err) });
|
||||
}
|
||||
@@ -465,6 +473,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
: synthesizedText
|
||||
? [{ text: synthesizedText }]
|
||||
: [];
|
||||
const deliveryPayloadHasStructuredContent =
|
||||
Boolean(deliveryPayload?.mediaUrl) ||
|
||||
(deliveryPayload?.mediaUrls?.length ?? 0) > 0 ||
|
||||
Object.keys(deliveryPayload?.channelData ?? {}).length > 0;
|
||||
const deliveryBestEffort = resolveCronDeliveryBestEffort(params.job);
|
||||
|
||||
// Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content).
|
||||
@@ -507,20 +519,73 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
logWarn(`[cron:${params.job.id}] ${message}`);
|
||||
return withRunSession({ status: "ok", summary, outputText });
|
||||
}
|
||||
try {
|
||||
await deliverOutboundPayloads({
|
||||
cfg: cfgWithAgentDefaults,
|
||||
channel: resolvedDelivery.channel,
|
||||
to: resolvedDelivery.to,
|
||||
accountId: resolvedDelivery.accountId,
|
||||
threadId: resolvedDelivery.threadId,
|
||||
payloads: deliveryPayloads,
|
||||
bestEffort: deliveryBestEffort,
|
||||
deps: createOutboundSendDeps(params.deps),
|
||||
// Shared subagent announce flow is text-based; keep direct outbound delivery
|
||||
// for media/channel payloads so structured content is preserved.
|
||||
if (deliveryPayloadHasStructuredContent) {
|
||||
try {
|
||||
await deliverOutboundPayloads({
|
||||
cfg: cfgWithAgentDefaults,
|
||||
channel: resolvedDelivery.channel,
|
||||
to: resolvedDelivery.to,
|
||||
accountId: resolvedDelivery.accountId,
|
||||
threadId: resolvedDelivery.threadId,
|
||||
payloads: deliveryPayloads,
|
||||
bestEffort: deliveryBestEffort,
|
||||
deps: createOutboundSendDeps(params.deps),
|
||||
});
|
||||
} catch (err) {
|
||||
if (!deliveryBestEffort) {
|
||||
return withRunSession({ status: "error", summary, outputText, error: String(err) });
|
||||
}
|
||||
}
|
||||
} else if (synthesizedText) {
|
||||
const announceSessionKey = resolveAgentMainSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!deliveryBestEffort) {
|
||||
return withRunSession({ status: "error", summary, outputText, error: String(err) });
|
||||
const taskLabel =
|
||||
typeof params.job.name === "string" && params.job.name.trim()
|
||||
? params.job.name.trim()
|
||||
: `cron:${params.job.id}`;
|
||||
try {
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: runSessionKey,
|
||||
childRunId: `${params.job.id}:${runSessionId}`,
|
||||
requesterSessionKey: announceSessionKey,
|
||||
requesterOrigin: {
|
||||
channel: resolvedDelivery.channel,
|
||||
to: resolvedDelivery.to,
|
||||
accountId: resolvedDelivery.accountId,
|
||||
threadId: resolvedDelivery.threadId,
|
||||
},
|
||||
requesterDisplayKey: announceSessionKey,
|
||||
task: taskLabel,
|
||||
timeoutMs,
|
||||
cleanup: "keep",
|
||||
roundOneReply: synthesizedText,
|
||||
waitForCompletion: false,
|
||||
startedAt: runStartedAt,
|
||||
endedAt: runEndedAt,
|
||||
outcome: { status: "ok" },
|
||||
announceType: "cron job",
|
||||
});
|
||||
if (!didAnnounce) {
|
||||
const message = "cron announce delivery failed";
|
||||
if (!deliveryBestEffort) {
|
||||
return withRunSession({
|
||||
status: "error",
|
||||
summary,
|
||||
outputText,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
logWarn(`[cron:${params.job.id}] ${message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!deliveryBestEffort) {
|
||||
return withRunSession({ status: "error", summary, outputText, error: String(err) });
|
||||
}
|
||||
logWarn(`[cron:${params.job.id}] ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user