Files
openclaw/src/cron/isolated-agent/delivery-target.ts
Tyler Yust 8fae55e8e0 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)
2026-02-07 19:46:01 -08:00

108 lines
3.1 KiB
TypeScript

import type { ChannelId } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { OutboundChannel } from "../../infra/outbound/targets.js";
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
import {
loadSessionStore,
resolveAgentMainSessionKey,
resolveStorePath,
} from "../../config/sessions.js";
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
import {
resolveOutboundTarget,
resolveSessionDeliveryTarget,
} from "../../infra/outbound/targets.js";
export async function resolveDeliveryTarget(
cfg: OpenClawConfig,
agentId: string,
jobPayload: {
channel?: "last" | ChannelId;
to?: string;
},
): Promise<{
channel: Exclude<OutboundChannel, "none">;
to?: string;
accountId?: string;
threadId?: string | number;
mode: "explicit" | "implicit";
error?: Error;
}> {
const requestedChannel = typeof jobPayload.channel === "string" ? jobPayload.channel : "last";
const explicitTo = typeof jobPayload.to === "string" ? jobPayload.to : undefined;
const allowMismatchedLastTo = requestedChannel === "last";
const sessionCfg = cfg.session;
const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId });
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
const store = loadSessionStore(storePath);
const main = store[mainSessionKey];
const preliminary = resolveSessionDeliveryTarget({
entry: main,
requestedChannel,
explicitTo,
allowMismatchedLastTo,
});
let fallbackChannel: Exclude<OutboundChannel, "none"> | undefined;
if (!preliminary.channel) {
try {
const selection = await resolveMessageChannelSelection({ cfg });
fallbackChannel = selection.channel;
} catch {
fallbackChannel = preliminary.lastChannel ?? DEFAULT_CHAT_CHANNEL;
}
}
const resolved = fallbackChannel
? resolveSessionDeliveryTarget({
entry: main,
requestedChannel,
explicitTo,
fallbackChannel,
allowMismatchedLastTo,
mode: preliminary.mode,
})
: preliminary;
const channel = resolved.channel ?? fallbackChannel ?? DEFAULT_CHAT_CHANNEL;
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,
mode,
};
}
const docked = resolveOutboundTarget({
channel,
to: toCandidate,
cfg,
accountId: resolved.accountId,
mode,
});
return {
channel,
to: docked.ok ? docked.to : undefined,
accountId: resolved.accountId,
threadId,
mode,
error: docked.ok ? undefined : docked.error,
};
}