openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,070 lines (1,069 loc) • 47.2 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { M as parseThreadSessionSuffix, O as parseAgentSessionKey, w as isCronSessionKey } from "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { Bt as resolveAdmittedCronCompletionStatus } from "./openclaw-state-db-BRTnL-D8.js";
import { o as isSilentReplyText } from "./tokens-DbeAFqg7.js";
import { o as resolveSessionStorePathCore } from "./paths-CXdaYWF_.js";
import { o as isAudioFileName } from "./mime-CVpcq9ju.js";
import { i as copyReplyPayloadMetadata } from "./reply-payload-DahKk-7n.js";
import { f as stringifyRouteThreadId } from "./channel-route--2in3Ckg.js";
import { h as withSystemEventOwner } from "./system-events-C03jsM0j.js";
import "./session-accessor-YsytfDtG.js";
import { u as loadSessionEntryReadOnly } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { h as runExclusiveSessionLifecycleMutation, l as getSessionWorkAdmissionRelease, n as beginSessionWorkAdmission } from "./session-lifecycle-admission-CS8v45tk.js";
import { i as OPENCLAW_TRANSCRIPT_ARTIFACT_PROVIDER, r as OPENCLAW_TRANSCRIPT_ARTIFACT_API, t as CRON_DIRECT_DELIVERY_CONTEXT_KIND } from "./transcript-only-openclaw-assistant-CVgy4bjA.js";
import { n as canonicalizeMainSessionAlias, r as resolveAgentMainSessionKey } from "./main-session-Br0F9dzh.js";
import { n as stripOutboundTargetKindPrefix, r as stripTargetProviderPrefix } from "./channel-target-prefix-D7Ut6oKe.js";
import { f as resolveSessionWorkStartError } from "./lifecycle-BaroCBMc.js";
import { n as appendExactAssistantMessageToSessionTranscript } from "./transcript-Dv8-hmIo.js";
import { t as resolveMirroredTranscriptText } from "./transcript-mirror-BTvMHa-8.js";
import { o as hasReplyPayloadContent } from "./payload-DYE7bQ2k.js";
import { o as projectOutboundPayloadPlanForMirror, t as createOutboundPayloadPlan } from "./payloads-DC1mf-yF.js";
import { i as resolveControlUiSessionUrl } from "./control-ui-link-base-D3CcSxxk.js";
import { i as normalizeCronRunErrorText } from "./execution-errors-BmgfSlqy.js";
import "./inbound.runtime-th3B0GYh.js";
import { t as createCronExecutionId } from "./run-id-kGde0n7U.js";
import { t as loadCronSessionEntryLatest } from "./session-ByAcYXZI.js";
import { c as resolveCronLifecycleRevisionIdentity, f as pickLastNonEmptyTextFromPayloads, p as pickSummaryFromOutput } from "./run-session-state-ATuxVdKg.js";
import { t as cleanupCronRunSessionAfterRun } from "./session-cleanup-DNjYw7ip.js";
import { n as resolvedDeliveryTargetsExternalChannel, t as resolveDeliveryTarget } from "./delivery-target-D8sif7VA.js";
import { n as isLikelyInterimCronMessage } from "./subagent-followup-hints-CdPqyvGp.js";
import { a as logCronDeliveryError, c as maybeApplyTtsToCronPayloads, d as resolveCronDeliveryBestEffort, f as resolveCronDeliveryScheduledAtMs, g as waitForCompletedDirectCronDelivery, h as retryTransientDirectCronDelivery, i as isStaleCronDelivery, l as normalizeDeliveryTarget, m as resolveDescendantSubagentFollowup, n as buildDirectCronDeliveryIdempotencyKey, o as logCronDeliveryErrorDeferred, p as resolveCronDeliveryStartDelayMs, r as isCompletedDirectCronDelivery, s as logCronDeliveryWarn, t as DIRECT_CRON_DELIVERY_COMPLETION_RETENTION, u as normalizeSilentReplyText } from "./delivery-dispatch-policy-BUvZPQjV.js";
//#region src/sessions/background-session-result.ts
const AUTOMATION_RESULT_MODEL = "automation-result";
/** Serializes a background assistant result behind active work on its target conversation. */
async function commitBackgroundResultToSession(params) {
const sessionKey = normalizeOptionalString(params.sessionKey);
const text = normalizeOptionalString(params.text);
const idempotencyKey = normalizeOptionalString(params.idempotencyKey);
if (!sessionKey || !text || !idempotencyKey) return {
ok: false,
reason: "background session result is missing required data"
};
const storePath = resolveSessionStorePathCore(params.config.session?.store, { agentId: params.agentId });
const expectedSessionId = normalizeOptionalString(params.expectedGeneration.sessionId);
if (!expectedSessionId) return {
ok: false,
reason: "background session result has an invalid expected generation"
};
const expectedLifecycleRevision = normalizeOptionalString(params.expectedGeneration.lifecycleRevision);
const identities = [sessionKey, expectedSessionId];
return await runExclusiveSessionLifecycleMutation({
scope: storePath,
identities,
signal: params.signal,
prepare: async () => {
await getSessionWorkAdmissionRelease({
scope: storePath,
identities
});
},
run: async () => {
const current = loadSessionEntryReadOnly({
agentId: params.agentId,
sessionKey,
storePath,
readConsistency: "latest"
});
if (current?.sessionId !== expectedSessionId || normalizeOptionalString(current.lifecycleRevision) !== expectedLifecycleRevision) return {
ok: false,
reason: `session rebound for sessionKey: ${sessionKey}`
};
const unavailable = resolveSessionWorkStartError(sessionKey, current, { expectedSessionId });
if (unavailable) return {
ok: false,
reason: unavailable
};
const message = {
role: "assistant",
content: [{
type: "text",
text
}],
api: OPENCLAW_TRANSCRIPT_ARTIFACT_API,
provider: OPENCLAW_TRANSCRIPT_ARTIFACT_PROVIDER,
model: AUTOMATION_RESULT_MODEL,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
},
stopReason: "stop",
timestamp: Date.now(),
openclawAutomation: params.provenance
};
const appended = await appendExactAssistantMessageToSessionTranscript({
agentId: params.agentId,
sessionKey,
expectedSessionId,
expectedLifecycleRevision: expectedLifecycleRevision ?? null,
idempotencyKey,
message,
storePath,
updateMode: "inline",
config: params.config
});
return appended.ok ? {
ok: true,
messageId: appended.messageId
} : {
ok: false,
reason: appended.reason
};
}
});
}
//#endregion
//#region src/cron/isolated-agent/delivery-route-session-key.ts
/**
* Picks the session-key identity used to resolve a cron delivery's outbound route.
*
* An isolated run does not carry its bound source conversation's namespace.
* Reuse only a canonical conversation belonging to the same agent, actual
* delivery provider, and destination; otherwise jobs can adopt another peer's
* conversation or thread.
*/
function selectCronRouteCurrentSessionKey(job, agentSessionKey, deliveryProvider, deliveryTarget) {
const bound = (job.sessionKey ?? "").trim();
const parsedBound = parseAgentSessionKey(bound);
const parsedRun = parseAgentSessionKey(agentSessionKey);
if (!parsedBound || !parsedRun || parsedBound.agentId !== parsedRun.agentId) return agentSessionKey;
const conversation = /^([^:]+):(direct|group|channel):([^:]+)(?::thread:[^:]+)?$/i.exec(parsedBound.rest);
const targetPeerId = stripOutboundTargetKindPrefix(stripTargetProviderPrefix(deliveryTarget, deliveryProvider));
if (conversation?.[1]?.toLowerCase() !== deliveryProvider.trim().toLowerCase() || conversation[3] !== targetPeerId) return agentSessionKey;
return bound;
}
//#endregion
//#region src/cron/isolated-agent/delivery-dispatch-awareness.ts
/** Session awareness and transcript mirroring for direct cron delivery. */
const deliveryOutboundRuntimeLoader$1 = createLazyImportLoader(() => import("./delivery-outbound.runtime.js"));
const outboundSessionRuntimeLoader = createLazyImportLoader(() => import("./outbound-session-DKs3ffS7.js"));
const transcriptRuntimeLoader = createLazyImportLoader(() => import("./transcript.runtime.js"));
function shouldQueueCronAwareness(params) {
return params.job.sessionTarget === "isolated" && !params.deliveryBestEffort && params.delivery.mode === "explicit";
}
function resolveCronAwarenessMainSessionKey(params) {
return params.cfg.session?.scope === "global" ? "global" : resolveAgentMainSessionKey({
cfg: params.cfg,
agentId: params.agentId
});
}
function isSameSessionKey(left, right) {
const normalizedLeft = normalizeOptionalString(left);
const normalizedRight = normalizeOptionalString(right);
return normalizedLeft != null && normalizedLeft === normalizedRight;
}
function resolveCronAwarenessText(params) {
if (params.outboundPayloads?.length) {
const projectedText = resolveDirectCronTranscriptMirrorText(projectDeliveredDirectCronPayloadsForMirror(params.outboundPayloads));
if (projectedText) return projectedText;
}
return params.deliveryPayloads ? pickLastNonEmptyTextFromPayloads(params.deliveryPayloads) : normalizeOptionalString(params.outputText) ?? normalizeOptionalString(params.synthesizedText);
}
function resolveDirectCronSummaryFallbackText(params) {
return normalizeOptionalString(params.outputText) ?? normalizeOptionalString(params.summary) ?? normalizeOptionalString(params.synthesizedText);
}
function shouldAttachDirectCronFallbackText(payload) {
return Boolean(payload.channelData) && !hasReplyPayloadContent(payload, {
trimText: true,
hasChannelData: false
});
}
function resolveDirectCronFallbackSourceIndex(payloads, fallbackText) {
if (!fallbackText) return;
const index = payloads.findLastIndex((payload) => normalizeOptionalString(payload.text) === fallbackText);
return index >= 0 ? index : void 0;
}
function formatTargetCronDeliveryAwarenessText(text) {
return `A scheduled automation delivered this message to this channel:\n${text}`;
}
function formatTargetCronDeliveryFailureAwarenessText(params) {
const targetParts = [`${params.channel}:${params.to}`];
if (params.threadId) targetParts.push(`thread ${params.threadId}`);
return [
"A scheduled automation attempted to deliver to this channel, but delivery failed.",
`Job: ${params.job.name || params.job.id}`,
`Target: ${targetParts.join(" ")}`,
"Check automation history for delivery error details.",
params.partialDelivered ? "One or more scheduled message payloads may already have been delivered." : "No scheduled message was delivered."
].join("\n");
}
async function queueCronAwarenessSystemEvent(params) {
try {
const { enqueueSystemEvent } = await deliveryOutboundRuntimeLoader$1.load();
const mainSessionKey = resolveCronAwarenessMainSessionKey({
cfg: params.cfg,
agentId: params.agentId
});
if (params.queueMainSession) enqueueSystemEvent(params.text, withSystemEventOwner({
sessionKey: mainSessionKey,
contextKey: params.deliveryIdempotencyKey
}, params.agentId));
const targetSessionKey = params.targetSessionKey;
if (targetSessionKey && (!isSameSessionKey(targetSessionKey, mainSessionKey) || !params.queueMainSession)) enqueueSystemEvent(params.targetText ?? formatTargetCronDeliveryAwarenessText(params.text), withSystemEventOwner({
sessionKey: targetSessionKey,
contextKey: params.deliveryIdempotencyKey
}, params.agentId));
} catch (err) {
await logCronDeliveryWarn(`[cron:${params.jobId}] failed to queue isolated cron awareness: ${formatErrorMessage(err)}`);
}
}
function isCustomCronSessionTarget(sessionTarget) {
return typeof sessionTarget === "string" && sessionTarget.startsWith("session:");
}
function buildDirectCronTranscriptMirrorPayloads(payloads) {
return payloads.map((payload) => {
const spokenText = normalizeOptionalString(payload.spokenText);
if (!spokenText) return payload;
const mediaUrls = [payload.mediaUrl, ...payload.mediaUrls ?? []].filter((url) => Boolean(url) && !isAudioFileName(url));
const { mediaUrl: _mediaUrl, mediaUrls: _mediaUrls, audioAsVoice: _audioAsVoice, spokenText: _spokenText, ...rest } = payload;
return {
...rest,
text: spokenText,
...mediaUrls.length ? { mediaUrls } : {}
};
});
}
function resolveDirectCronTranscriptMirrorText(params) {
const text = normalizeOptionalString(params.text);
const mediaText = resolveMirroredTranscriptText({ mediaUrls: params.mediaUrls }) ?? void 0;
if (text && mediaText) return `${text}\n${mediaText}`;
if (text || mediaText) return text ?? mediaText;
}
function pickDirectCronMirrorPayloadText(payload) {
return normalizeOptionalString(payload.hookContent) ?? normalizeOptionalString(payload.text);
}
function isTtsAudioMirrorOnly(params) {
return (params.payload.audioAsVoice === true || Boolean(params.payload.hookContent)) && isAudioFileName(params.mediaUrl);
}
function projectDeliveredDirectCronPayloadsForMirror(payloads) {
const textParts = [];
const mediaUrls = [];
for (const payload of payloads) {
const text = pickDirectCronMirrorPayloadText(payload);
if (text) textParts.push(text);
for (const mediaUrl of payload.mediaUrls) {
if (isTtsAudioMirrorOnly({
payload,
mediaUrl
})) continue;
mediaUrls.push(mediaUrl);
}
}
return {
text: textParts.join("\n"),
mediaUrls
};
}
function canonicalizeDirectCronRouteSessionKey(params) {
const sessionKey = params.sessionKey.trim();
const canonical = canonicalizeMainSessionAlias({
cfg: params.cfg,
agentId: params.agentId,
sessionKey
});
if (canonical !== sessionKey) return canonical;
const thread = parseThreadSessionSuffix(sessionKey);
if (!thread.baseSessionKey || !thread.threadId) return sessionKey;
const canonicalBase = canonicalizeMainSessionAlias({
cfg: params.cfg,
agentId: params.agentId,
sessionKey: thread.baseSessionKey
});
if (canonicalBase === thread.baseSessionKey || canonicalBase === "global") return sessionKey;
return `${canonicalBase}:thread:${thread.threadId}`;
}
async function resolveCronDeliveryRouteSessionKey(params) {
try {
const { resolveOutboundSessionRoute } = await outboundSessionRuntimeLoader.load();
const route = await resolveOutboundSessionRoute({
cfg: params.cfg,
channel: params.delivery.channel,
agentId: params.agentId,
accountId: params.delivery.accountId,
target: params.delivery.to,
currentSessionKey: selectCronRouteCurrentSessionKey(params.job, params.agentSessionKey, params.delivery.channel, params.delivery.to),
threadId: params.delivery.threadId
});
const routeSessionKey = route?.sessionKey?.trim();
if (!route || !routeSessionKey) return {
sessionKey: params.agentSessionKey,
route: null
};
const canonicalRouteSessionKey = canonicalizeDirectCronRouteSessionKey({
cfg: params.cfg,
agentId: params.agentId,
sessionKey: routeSessionKey
});
const canonicalRouteBaseSessionKey = canonicalizeDirectCronRouteSessionKey({
cfg: params.cfg,
agentId: params.agentId,
sessionKey: route.baseSessionKey
});
return {
sessionKey: canonicalRouteSessionKey,
route: canonicalRouteSessionKey === route.sessionKey && canonicalRouteBaseSessionKey === route.baseSessionKey ? route : {
...route,
sessionKey: canonicalRouteSessionKey,
baseSessionKey: canonicalRouteBaseSessionKey
}
};
} catch (err) {
await logCronDeliveryWarn(`[cron:${params.job.id}] failed to resolve destination session for ${params.warningContext}: ${formatErrorMessage(err)}`);
return {
sessionKey: params.agentSessionKey,
route: null
};
}
}
async function commitDirectCronOutboundRoute(params) {
if (!params.route) return;
try {
const { ensureOutboundSessionEntry } = await outboundSessionRuntimeLoader.load();
await ensureOutboundSessionEntry({
cfg: params.cfg,
channel: params.delivery.channel,
accountId: params.delivery.accountId,
route: params.route,
sourceSessionKey: params.runSessionKey
});
} catch (err) {
await logCronDeliveryWarn(`[cron] failed to persist outbound route after delivery: ${formatErrorMessage(err)}`);
}
}
/** Resolves the transcript mirror session key and route for direct cron delivery.
* The route must be persisted by the caller after successful platform delivery
* via `commitDirectCronOutboundRoute`. */
async function resolveDirectCronDeliverySessionKey(params) {
if (isCustomCronSessionTarget(params.job.sessionTarget)) return {
sessionKey: params.agentSessionKey,
route: null
};
return await resolveCronDeliveryRouteSessionKey({
cfg: params.cfg,
job: params.job,
agentId: params.agentId,
agentSessionKey: params.agentSessionKey,
delivery: params.delivery,
warningContext: "direct delivery mirror"
});
}
function resolveCronMessageToolAwarenessTarget(params) {
const { target } = params.delivery;
const text = normalizeOptionalString(target.text) ?? resolveMirroredTranscriptText({ mediaUrls: target.mediaUrls }) ?? void 0;
if (!text) return;
const targetChannel = normalizeOptionalString(target.provider);
const channel = targetChannel && targetChannel !== "message" ? targetChannel : params.delivery.verifiedTarget && params.resolvedDelivery.ok ? params.resolvedDelivery.channel : void 0;
const to = normalizeOptionalString(target.to) ?? (params.delivery.verifiedTarget && params.resolvedDelivery.ok ? params.resolvedDelivery.to : void 0);
if (!channel || !to) return;
const accountId = target.accountId ?? (params.delivery.verifiedTarget && params.resolvedDelivery.ok ? params.resolvedDelivery.accountId : void 0);
const threadId = target.threadId ?? (params.delivery.verifiedTarget && target.threadImplicit === true && params.resolvedDelivery.ok ? params.resolvedDelivery.threadId : void 0);
return {
ok: true,
channel,
to,
...accountId ? { accountId } : {},
...threadId ? { threadId } : {},
mode: "explicit",
text
};
}
/** Queues target-session context awareness for cron deliveries made via message tool. */
async function queueCronMessageToolDeliveryAwareness(params) {
const seen = /* @__PURE__ */ new Set();
const deferredAwareness = [];
for (const delivery of params.sourceDeliveryOutcome.visibleDeliveries) {
const target = resolveCronMessageToolAwarenessTarget({
delivery,
resolvedDelivery: params.resolvedDelivery
});
if (!target) continue;
const dedupeKey = [
target.channel,
normalizeDeliveryTarget(target.channel, target.to),
target.accountId ?? "",
target.threadId ?? "",
target.text
].join("\0");
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
const { sessionKey: targetSessionKey, route: targetRoute } = await resolveCronDeliveryRouteSessionKey({
cfg: params.cfg,
job: params.job,
agentId: params.agentId,
agentSessionKey: params.agentSessionKey,
delivery: target,
warningContext: "message-tool delivery awareness"
});
await commitDirectCronOutboundRoute({
cfg: params.cfg,
runSessionKey: params.runSessionKey,
delivery: target,
route: targetRoute
});
const deliveryIdempotencyKey = buildDirectCronDeliveryIdempotencyKey({
jobId: params.job.id,
runStartedAt: params.runStartedAt,
delivery: target
});
const awarenessParams = {
cfg: params.cfg,
jobId: params.job.id,
agentId: params.agentId,
deliveryIdempotencyKey,
queueMainSession: false,
targetSessionKey,
text: target.text
};
if (isSameSessionKey(targetSessionKey, params.deferredTargetSessionKey)) {
deferredAwareness.push(() => queueCronAwarenessSystemEvent(awarenessParams));
continue;
}
await queueCronAwarenessSystemEvent(awarenessParams);
}
if (deferredAwareness.length === 0) return;
return async () => {
for (const queue of deferredAwareness) await queue();
};
}
async function appendDirectCronDeliveryTranscriptMirror(params) {
if (!params.mirror.text && !params.mirror.mediaUrls?.length) return;
try {
const { appendAssistantMessageToSessionTranscript } = await transcriptRuntimeLoader.load();
const result = await appendAssistantMessageToSessionTranscript(params.mirror);
if (!result.ok) await logCronDeliveryWarn(`[cron:${params.job.id}] failed to mirror direct delivery into session transcript: ${result.reason}`);
} catch (err) {
await logCronDeliveryWarn(`[cron:${params.job.id}] failed to mirror direct delivery into session transcript: ${formatErrorMessage(err)}`);
}
}
async function appendAdmittedDirectCronDeliveryTranscriptMirror(params) {
const storePath = params.mirror.storePath;
const initial = storePath ? loadCronSessionEntryLatest(storePath, params.mirror.sessionKey) : void 0;
const expectedSessionId = params.mirror.expectedSessionId ?? initial?.sessionId;
const expectedLifecycleRevision = params.mirror.expectedLifecycleRevision ?? initial?.lifecycleRevision;
if (!storePath || !expectedSessionId) {
await logCronDeliveryWarn(`[cron:${params.job.id}] skipped transcript mirror without an exact session identity`);
return;
}
const admittedMirror = {
...params.mirror,
expectedSessionId,
...expectedLifecycleRevision ? { expectedLifecycleRevision } : {}
};
try {
const admission = await beginSessionWorkAdmission({
scope: storePath,
identities: [
params.mirror.sessionKey,
expectedSessionId,
expectedLifecycleRevision ? resolveCronLifecycleRevisionIdentity(expectedLifecycleRevision) : void 0
],
signal: params.abortSignal,
assertAllowed: () => {
const latest = loadCronSessionEntryLatest(storePath, params.mirror.sessionKey);
if (latest?.sessionId !== expectedSessionId || expectedLifecycleRevision !== void 0 && latest.lifecycleRevision !== expectedLifecycleRevision) throw new Error(`Session "${params.mirror.sessionKey}" changed before transcript mirror.`);
const archivedError = resolveSessionWorkStartError(params.mirror.sessionKey, latest);
if (archivedError) throw new Error(archivedError);
}
});
try {
await admission.run(() => appendDirectCronDeliveryTranscriptMirror({
job: params.job,
mirror: admittedMirror
}));
} finally {
admission.release();
}
} catch (err) {
await logCronDeliveryWarn(`[cron:${params.job.id}] skipped transcript mirror: ${formatErrorMessage(err)}`);
}
}
//#endregion
//#region src/cron/isolated-agent/current-session-completion.ts
async function commitCurrentSessionCronCompletion(params, text) {
const sourceSessionKey = params.sourceSessionKey?.trim();
if (!sourceSessionKey) return {
ok: false,
reason: "current cron delivery is missing its source session binding"
};
if (!params.sourceSessionGeneration) return {
ok: false,
reason: "current cron delivery is missing its source session generation"
};
const completionText = resolveDirectCronTranscriptMirrorText(projectOutboundPayloadPlanForMirror(createOutboundPayloadPlan(buildDirectCronTranscriptMirrorPayloads(params.deliveryPayloads)))) ?? normalizeOptionalString(text);
if (!completionText) return {
ok: false,
reason: "current cron completion has no durable transcript projection"
};
const runId = createCronExecutionId(params.job.id, params.runStartedAt);
const committed = await commitBackgroundResultToSession({
agentId: params.agentId,
sessionKey: sourceSessionKey,
expectedGeneration: params.sourceSessionGeneration,
text: completionText,
idempotencyKey: `cron-current-completion:${runId}`,
provenance: {
kind: "cron",
jobId: params.job.id,
runId
},
config: params.cfgWithAgentDefaults,
signal: params.abortSignal
});
if (!committed.ok) return committed;
if (params.sourceDeliveryOutcome.satisfiesSourceDelivery) return {
ok: true,
requiresExternalDelivery: false
};
if (params.resolvedDelivery.ok) return {
ok: true,
requiresExternalDelivery: true
};
if (resolvedDeliveryTargetsExternalChannel(params.resolvedDelivery)) return {
ok: true,
requiresExternalDelivery: false,
deliveryError: params.resolvedDelivery.error.message
};
return {
ok: true,
requiresExternalDelivery: false
};
}
//#endregion
//#region src/cron/isolated-agent/delivery-payload-normalization.ts
function normalizeDirectPayload(payload) {
const normalized = payload.text ? normalizeSilentReplyText(payload.text) : void 0;
return normalized ? copyReplyPayloadMetadata(payload, {
...payload,
text: normalized.strippedTrailingSilentToken ? void 0 : normalized.text
}) : payload;
}
function normalizeDirectCronDeliveryPayloads(params) {
const fallback = normalizeSilentReplyText(resolveDirectCronSummaryFallbackText(params));
const fallbackText = fallback.strippedTrailingSilentToken ? void 0 : fallback.text;
const candidates = params.deliveryPayloads.map(normalizeDirectPayload).filter((payload) => hasReplyPayloadContent(payload, { trimText: true }));
if (candidates.length === 0 && fallbackText) candidates.push({ text: fallbackText });
let fallbackSourceIndex = resolveDirectCronFallbackSourceIndex(candidates, fallbackText);
if (fallbackText && fallbackSourceIndex === void 0 && candidates.some(shouldAttachDirectCronFallbackText)) {
candidates.unshift({ text: fallbackText });
fallbackSourceIndex = 0;
}
const prepared = candidates.map((payload) => shouldAttachDirectCronFallbackText(payload) && fallbackText && fallbackSourceIndex !== void 0 ? copyReplyPayloadMetadata(payload, Object.assign({}, payload, { fallbackText: {
text: fallbackText,
replacesPayloadIndex: fallbackSourceIndex
} })) : payload);
const accepted = [];
let fallbackSourceSuppressed = false;
let channelSuppressed = false;
for (const [index, candidate] of prepared.entries()) {
const transformed = params.channelTransform ? params.channelTransform.apply(candidate) : candidate;
if (transformed === null) {
channelSuppressed = true;
fallbackSourceSuppressed ||= index === fallbackSourceIndex;
} else accepted.push({
payload: transformed,
sourceIndex: index
});
}
if (accepted.length === 0) return {
kind: "suppress",
reason: channelSuppressed ? "channel_transform" : "empty"
};
const acceptedFallbackIndex = accepted.findIndex(({ sourceIndex }) => sourceIndex === fallbackSourceIndex);
return {
kind: "deliver",
payload: accepted.map(({ payload }) => {
const fallbackMeta = payload.fallbackText;
if (!fallbackMeta || fallbackMeta.replacesPayloadIndex !== fallbackSourceIndex) return payload;
return copyReplyPayloadMetadata(payload, Object.assign({}, payload, { fallbackText: fallbackSourceSuppressed || acceptedFallbackIndex < 0 ? void 0 : {
...fallbackMeta,
replacesPayloadIndex: acceptedFallbackIndex
} }));
})
};
}
/**
* Appends the Control UI run-inspection link to the last visible payload.
* Callers invoke this only after silent-reply suppression so a link cannot turn
* a silent or empty run into a visible announcement; the payload is replaced
* rather than mutated because payload objects can be aliased by the caller.
*/
function appendCronRunInspectionLink(payloads, inspectionUrl) {
const index = payloads.findLastIndex((payload) => payload.text?.trim());
if (!inspectionUrl || index < 0) return payloads;
const payload = payloads[index];
const linked = copyReplyPayloadMetadata(payload, {
...payload,
text: `${payload.text}\nInspect: ${inspectionUrl}`
});
return payloads.map((entry, at) => at === index ? linked : entry);
}
//#endregion
//#region src/cron/isolated-agent/delivery-dispatch.ts
const deliveryOutboundRuntimeLoader = createLazyImportLoader(() => import("./delivery-outbound.runtime.js"));
/** Dispatches cron run output through verified message-tool or direct delivery paths. */
async function dispatchCronDelivery(params) {
const sourceDeliverySatisfied = params.sourceDeliveryOutcome.satisfiesSourceDelivery;
const requiresCurrentSessionCompletion = params.job.sessionTarget === "current";
const verifiedMessageToolDelivery = params.sourceDeliveryOutcome.verifiedMessageToolDelivery;
let summary = params.summary;
let outputText = params.outputText;
let synthesizedText = params.synthesizedText;
let deliveryPayloads = params.deliveryPayloads;
const deliveryState = {
status: params.deliveryRequested ? "not-delivered" : "not-requested",
delivered: false,
failureNotification: { status: "not-requested" }
};
const recordDelivery = (status, error, deliverySuppressionReason) => {
deliveryState.status = status;
deliveryState.delivered = status === "delivered" ? true : status === "not-delivered" ? false : void 0;
deliveryState.error = error;
deliveryState.deliverySuppressionReason = deliverySuppressionReason;
};
if (verifiedMessageToolDelivery) recordDelivery("delivered");
let deliveryAttempted = verifiedMessageToolDelivery;
let deferredDeletingSessionMirror;
const buildDeliveryState = async (result) => {
const completion = resolveAdmittedCronCompletionStatus(params.job, result?.status === "error" ? "error" : params.undeliveredRunStatus, deliveryState.status, deliveryState.deliverySuppressionReason);
if (deliveryState.status === "delivered" || deliveryState.status === "not-requested" || completion === "succeeded") await cleanupDirectCronSessionIfNeeded();
await params.queueSourceSessionMessageToolAwareness?.();
return {
...result ? { result } : {},
deliveryState,
delivered: deliveryState.delivered,
deliveryAttempted,
deliveryError: deliveryState.error,
deliverySuppressionReason: deliveryState.deliverySuppressionReason,
summary,
outputText,
synthesizedText,
deliveryPayloads
};
};
const formatDeliveryTargetError = (error) => params.sourceDeliveryOutcome.unverifiedMessageToolDelivery ? `${error}; the agent used the message tool, but OpenClaw could not verify that message matched the cron delivery target` : error;
const failDeliveryTarget = (error) => params.withRunSession({
status: "error",
error: formatDeliveryTargetError(error),
errorKind: "delivery-target",
summary,
outputText,
delivered: deliveryState.delivered,
deliveryAttempted,
deliveryError: deliveryState.error,
...params.telemetry
});
const cleanupDirectCronSessionIfNeeded = async () => {
const cleanupOutcome = await cleanupCronRunSessionAfterRun({
job: params.job,
agentSessionKey: params.agentSessionKey,
sessionId: params.sessionId,
lifecycleRevision: params.lifecycleRevision,
sessionUpdatedAt: params.sessionUpdatedAt,
beforeDelete: params.beforeSessionDelete,
reason: "cron-delete-after-run-fallback"
});
const survivingMirror = deferredDeletingSessionMirror;
deferredDeletingSessionMirror = void 0;
if (cleanupOutcome !== "not-requested" && cleanupOutcome !== "deleted" && survivingMirror) await appendAdmittedDirectCronDeliveryTranscriptMirror({
job: params.job,
mirror: survivingMirror,
abortSignal: params.abortSignal
});
};
const finishSilentReplyDelivery = (reason) => {
deliveryAttempted = true;
recordDelivery("not-delivered", void 0, reason);
return params.withRunSession({
status: "ok",
summary,
outputText,
delivered: false,
deliveryAttempted: true,
deliverySuppressionReason: reason,
...params.telemetry
});
};
const deliverViaDirect = async (delivery) => {
const { buildOutboundSessionContext, createOutboundSendDeps, durableMessageBatchMayHaveReachedRecipient, resolveAgentOutboundIdentity, resolveCronChannelReplyTransform, sendDurableMessageBatchCore } = await deliveryOutboundRuntimeLoader.load();
const payloadNormalization = normalizeDirectCronDeliveryPayloads({
deliveryPayloads,
outputText,
summary,
synthesizedText,
channelTransform: resolveCronChannelReplyTransform({
channel: delivery.channel,
cfg: params.cfgWithAgentDefaults,
accountId: delivery.accountId
})
});
if (payloadNormalization.kind === "suppress") return finishSilentReplyDelivery(payloadNormalization.reason);
const normalizedPayloads = payloadNormalization.payload;
const deliveryIdempotencyKey = buildDirectCronDeliveryIdempotencyKey({
jobId: params.job.id,
runStartedAt: params.runStartedAt,
delivery
});
let completedDelivery = false;
try {
completedDelivery = isCompletedDirectCronDelivery(deliveryIdempotencyKey);
} catch (err) {
if (!params.deliveryBestEffort) throw err;
await logCronDeliveryWarn(`[cron:${params.job.id}] durable delivery receipt unavailable; continuing best-effort delivery: ${formatErrorMessage(err)}`);
}
if (completedDelivery) {
recordDelivery("delivered");
deliveryAttempted = true;
return null;
}
const identity = resolveAgentOutboundIdentity(params.cfgWithAgentDefaults, params.agentId);
try {
if (params.isAborted()) return params.withRunSession({
status: "error",
error: params.abortReason(),
deliveryAttempted,
...params.telemetry
});
if (params.deliveryRequested && isStaleCronDelivery({
job: params.job,
runStartedAt: params.runStartedAt
})) {
deliveryAttempted = true;
const nowMs = Date.now();
const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
job: params.job,
runStartedAt: params.runStartedAt
});
const startDelayMs = resolveCronDeliveryStartDelayMs({
job: params.job,
runStartedAt: params.runStartedAt
});
const deliveryError = `skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, started ${Math.round(startDelayMs / 6e4)}m late, current age ${Math.round((nowMs - scheduledAtMs) / 6e4)}m`;
recordDelivery("not-delivered", deliveryError);
await logCronDeliveryWarn(`[cron:${params.job.id}] ${deliveryError}`);
return params.withRunSession({
status: "ok",
summary,
outputText,
deliveryAttempted,
delivered: false,
deliveryError,
...params.telemetry
});
}
const payloadsForDelivery = (await maybeApplyTtsToCronPayloads({
cfg: params.cfgWithAgentDefaults,
payloads: normalizedPayloads,
delivery,
agentId: params.agentId,
ttsAuto: params.ttsAuto
})).filter((p) => hasReplyPayloadContent(p, { trimText: true }));
if (payloadsForDelivery.length === 0) {
recordDelivery("not-delivered", "cron delivery payload was empty after TTS");
return null;
}
const linkedPayloadsForDelivery = appendCronRunInspectionLink(payloadsForDelivery, resolveControlUiSessionUrl(params.cfgWithAgentDefaults, {
sessionKey: params.runSessionKey,
fallbackAgentId: params.agentId,
exactKey: true
}));
deliveryAttempted = true;
const { sessionKey: deliverySessionKey, route: directCronOutboundRoute } = await resolveDirectCronDeliverySessionKey({
cfg: params.cfgWithAgentDefaults,
job: params.job,
agentId: params.agentId,
agentSessionKey: params.agentSessionKey,
delivery
});
const deliverySession = buildOutboundSessionContext({
cfg: params.cfgWithAgentDefaults,
agentId: params.agentId,
sessionKey: deliverySessionKey
});
const awarenessMainSessionKey = resolveCronAwarenessMainSessionKey({
cfg: params.cfgWithAgentDefaults,
agentId: params.agentId
});
const mirrorTargetsAwarenessMainSession = isSameSessionKey(deliverySessionKey, awarenessMainSessionKey);
const mirrorTargetsDeletingRunSession = params.job.deleteAfterRun === true && isCronSessionKey(params.agentSessionKey) && isSameSessionKey(deliverySessionKey, params.agentSessionKey);
let hadPartialFailure = false;
let completedByConcurrentDelivery = false;
let payloadMayHaveReachedRecipientBeforeFailure = false;
let directCronRouteCommitted = false;
const commitDirectCronRouteEarly = async () => {
if (directCronRouteCommitted || !directCronOutboundRoute) return;
directCronRouteCommitted = true;
await commitDirectCronOutboundRoute({
cfg: params.cfgWithAgentDefaults,
runSessionKey: params.runSessionKey,
delivery,
route: directCronOutboundRoute
});
};
const attemptedPayloadsForMirror = [];
const onError = params.deliveryBestEffort ? (err, _payload) => {
logCronDeliveryErrorDeferred(`[cron:${params.job.id}] delivery payload failed (bestEffort): ${formatErrorMessage(err)}`);
} : void 0;
const runDelivery = async () => {
attemptedPayloadsForMirror.length = 0;
const send = await sendDurableMessageBatchCore({
cfg: params.cfgWithAgentDefaults,
channel: delivery.channel,
to: delivery.to,
accountId: delivery.accountId,
threadId: delivery.threadId,
payloads: linkedPayloadsForDelivery,
session: deliverySession,
identity,
bestEffort: params.deliveryBestEffort,
durability: params.deliveryBestEffort ? "best_effort" : "required",
deliveryIntentId: deliveryIdempotencyKey,
reusePendingDeliveryIntent: true,
completionRetention: DIRECT_CRON_DELIVERY_COMPLETION_RETENTION,
deps: createOutboundSendDeps(params.deps),
signal: params.abortSignal,
onError,
onPayload: (payload) => {
attemptedPayloadsForMirror.push(payload);
},
onDeliveryResult: () => {
return commitDirectCronRouteEarly();
}
});
payloadMayHaveReachedRecipientBeforeFailure ||= durableMessageBatchMayHaveReachedRecipient(send);
if (send.status === "failed" && await waitForCompletedDirectCronDelivery({
id: deliveryIdempotencyKey,
signal: params.abortSignal
})) {
completedByConcurrentDelivery = true;
return [];
}
if (send.status === "failed") throw send.error;
if (send.status === "partial_failed") {
payloadMayHaveReachedRecipientBeforeFailure = true;
if (!params.deliveryBestEffort) throw send.error;
hadPartialFailure = true;
deliveryState.error ??= formatErrorMessage(send.error);
}
if (send.status === "suppressed") {
const uncertain = durableMessageBatchMayHaveReachedRecipient(send);
const reason = uncertain ? "adapter_returned_no_identity" : send.reason;
recordDelivery(uncertain ? "unknown" : "not-delivered", `cron delivery ${uncertain ? "outcome is unknown" : "was suppressed"}: ${reason}`);
}
return send.status === "sent" || send.status === "partial_failed" ? send.results : [];
};
let deliveryResults;
try {
deliveryResults = await retryTransientDirectCronDelivery({
jobId: params.job.id,
signal: params.abortSignal,
run: runDelivery,
shouldRetryError: () => !payloadMayHaveReachedRecipientBeforeFailure
});
} catch (err) {
const failureAwarenessText = formatTargetCronDeliveryFailureAwarenessText({
job: params.job,
channel: delivery.channel,
to: delivery.to,
threadId: stringifyRouteThreadId(delivery.threadId),
partialDelivered: payloadMayHaveReachedRecipientBeforeFailure
});
await queueCronAwarenessSystemEvent({
cfg: params.cfgWithAgentDefaults,
jobId: params.job.id,
agentId: params.agentId,
deliveryIdempotencyKey: `${deliveryIdempotencyKey}:failure`,
queueMainSession: false,
targetSessionKey: deliverySessionKey,
text: failureAwarenessText,
targetText: failureAwarenessText
});
if (payloadMayHaveReachedRecipientBeforeFailure) await commitDirectCronRouteEarly();
throw err;
}
if (completedByConcurrentDelivery) {
recordDelivery("delivered");
await commitDirectCronRouteEarly();
return null;
}
if (deliveryResults.length > 0) recordDelivery(hadPartialFailure ? "not-delivered" : "delivered", deliveryState.error);
if (deliveryState.delivered || payloadMayHaveReachedRecipientBeforeFailure) await commitDirectCronRouteEarly();
const deliveryAwarenessText = resolveCronAwarenessText({
outputText,
synthesizedText,
deliveryPayloads: linkedPayloadsForDelivery,
outboundPayloads: attemptedPayloadsForMirror
});
const shouldQueueAwarenessForDelivery = shouldQueueCronAwareness({
job: params.job,
delivery,
deliveryBestEffort: params.deliveryBestEffort
});
const targetSessionAwarenessText = !params.deliveryBestEffort && (shouldQueueAwarenessForDelivery || !isSameSessionKey(deliverySessionKey, awarenessMainSessionKey)) ? deliveryAwarenessText : void 0;
const deliveryWillReachAwarenessMainSession = mirrorTargetsAwarenessMainSession && Boolean(targetSessionAwarenessText);
const mirrorWouldBypassIsolatedAwarenessPolicy = mirrorTargetsAwarenessMainSession && params.job.sessionTarget === "isolated" && delivery.mode !== "explicit";
if (deliveryState.delivered && !requiresCurrentSessionCompletion && !deliveryWillReachAwarenessMainSession && !mirrorWouldBypassIsolatedAwarenessPolicy) {
const mirrorText = resolveDirectCronTranscriptMirrorText(attemptedPayloadsForMirror.length > 0 ? projectDeliveredDirectCronPayloadsForMirror(attemptedPayloadsForMirror) : projectOutboundPayloadPlanForMirror(createOutboundPayloadPlan(buildDirectCronTranscriptMirrorPayloads(linkedPayloadsForDelivery), {
cfg: params.cfgWithAgentDefaults,
sessionKey: deliverySessionKey,
surface: delivery.channel
})));
const transcriptMirror = {
sessionKey: deliverySessionKey,
agentId: params.agentId,
...mirrorTargetsDeletingRunSession ? {
expectedSessionId: params.sessionId,
expectedLifecycleRevision: params.lifecycleRevision
} : {},
text: mirrorText,
mediaUrls: void 0,
storePath: resolveSessionStorePathCore(params.cfgWithAgentDefaults.session?.store, { agentId: params.agentId }),
idempotencyKey: deliveryIdempotencyKey,
...targetSessionAwarenessText === void 0 ? { deliveryMirror: { kind: CRON_DIRECT_DELIVERY_CONTEXT_KIND } } : {},
config: params.cfgWithAgentDefaults
};
if (mirrorTargetsDeletingRunSession) deferredDeletingSessionMirror = transcriptMirror;
else await appendAdmittedDirectCronDeliveryTranscriptMirror({
job: params.job,
mirror: transcriptMirror,
abortSignal: params.abortSignal
});
}
if (deliveryState.delivered && targetSessionAwarenessText) await queueCronAwarenessSystemEvent({
cfg: params.cfgWithAgentDefaults,
jobId: params.job.id,
agentId: params.agentId,
deliveryIdempotencyKey,
queueMainSession: shouldQueueAwarenessForDelivery,
text: targetSessionAwarenessText,
targetSessionKey: deliverySessionKey
});
return null;
} catch (err) {
await logCronDeliveryError(`[cron:${params.job.id}] delivery failed (${params.deliveryBestEffort ? "bestEffort" : "required"}): ${formatErrorMessage(err)}`);
deliveryState.error = normalizeCronRunErrorText(err);
return null;
}
};
const finalizeTextDelivery = async (delivery) => {
if (!synthesizedText && !params.spawnOnlyHandoff && !(requiresCurrentSessionCompletion && params.deliveryPayloadHasStructuredContent)) return null;
const initialSynthesizedText = synthesizedText?.trim() ?? "";
const spawnOnlyHandoff = params.spawnOnlyHandoff;
const { finalReply, activeSubagentRuns, hadDescendants } = await resolveDescendantSubagentFollowup({
sessionKey: params.runSessionKey,
runStartedAt: params.runStartedAt,
timeoutMs: params.timeoutMs,
deliveryBestEffort: params.deliveryBestEffort,
spawnOnlyHandoff,
initialSynthesizedText
});
if (finalReply) {
outputText = finalReply;
summary = pickSummaryFromOutput(finalReply) ?? summary;
synthesizedText = finalReply;
deliveryPayloads = [{ text: finalReply }];
}
if (spawnOnlyHandoff && !synthesizedText?.trim()) {
const error = params.isAborted() ? params.abortReason() : activeSubagentRuns > 0 ? "cron child-session handoff timed out before producing a final assistant payload" : "cron child-session handoff completed without a final assistant payload";
deliveryAttempted = true;
return params.withRunSession({
status: "error",
error,
delivered: false,
deliveryAttempted,
...params.telemetry
});
}
if (!params.deliveryBestEffort && activeSubagentRuns > 0) {
deliveryAttempted = true;
recordDelivery("not-delivered", "cron descendants are still active without a final reply");
return params.withRunSession({
status: "ok",
summary,
outputText,
deliveryAttempted,
...params.telemetry
});
}
if (hadDescendants && synthesizedText?.trim() === initialSynthesizedText && isLikelyInterimCronMessage(initialSynthesizedText) && !isSilentReplyText(initialSynthesizedText, "NO_REPLY")) {
deliveryAttempted = true;
recordDelivery("not-delivered", "cron descendants completed without a final reply");
return params.withRunSession({
status: "ok",
summary,
outputText,
deliveryAttempted,
...params.telemetry
});
}
const normalizedSynthesizedText = normalizeSilentReplyText(synthesizedText);
const hasStructuredCurrentSessionCompletion = requiresCurrentSessionCompletion && params.deliveryPayloadHasStructuredContent;
if ((normalizedSynthesizedText.text === void 0 || normalizedSynthesizedText.strippedTrailingSilentToken) && !hasStructuredCurrentSessionCompletion) return finishSilentReplyDelivery("silent");
synthesizedText = normalizedSynthesizedText.text;
if (synthesizedText) outputText = synthesizedText;
if (params.isAborted()) return params.withRunSession({
status: "error",
error: params.abortReason(),
deliveryAttempted,
...params.telemetry
});
if (requiresCurrentSessionCompletion) {
deliveryAttempted = true;
const completion = await commitCurrentSessionCronCompletion(params, synthesizedText);
if (!completion.ok) {
recordDelivery("not-delivered", completion.reason);
return failDeliveryTarget(completion.reason);
}
params.queueSourceSessionMessageToolAwareness = void 0;
if (!completion.requiresExternalDelivery) {
recordDelivery(completion.deliveryError ? "not-delivered" : "delivered", completion.deliveryError);
return null;
}
recordDelivery("not-delivered");
}
if (!delivery) return null;
return await deliverViaDirect(delivery);
};
if (params.deliveryRequested && params.skipDelivery && !sourceDeliverySatisfied) return buildDeliveryState(finishSilentReplyDelivery(params.skipDelivery));
if (params.deliveryRequested && !params.skipDelivery && (!sourceDeliverySatisfied || requiresCurrentSessionCompletion)) {
if (!params.resolvedDelivery.ok) {
if (requiresCurrentSessionCompletion) return buildDeliveryState(await finalizeTextDelivery() ?? void 0);
if (!params.deliveryBestEffort) {
const error = params.resolvedDelivery.error.message;
recordDelivery("not-delivered", formatDeliveryTargetError(error));
return buildDeliveryState(failDeliveryTarget(error));
}
recordDelivery("not-delivered", params.resolvedDelivery.error.message);
await logCronDeliveryWarn(`[cron:${params.job.id}] ${params.resolvedDelivery.error.message}`);
return buildDeliveryState(params.withRunSession({
status: "ok",
summary,
outputText,
delivered: deliveryState.delivered,
deliveryError: deliveryState.error,
deliveryAttempted,
...params.telemetry
}));
}
if (!requiresCurrentSessionCompletion && (params.deliveryPayloadHasStructuredContent || params.resolvedDelivery.threadId != null && !params.spawnOnlyHandoff)) {
const directResult = await deliverViaDirect(params.resolvedDelivery);
if (directResult) return buildDeliveryState(directResult);
} else {
const finalizedTextResult = await finalizeTextDelivery(params.resolvedDelivery);
if (finalizedTextResult) return buildDeliveryState(finalizedTextResult);
}
}
return buildDeliveryState();
}
//#endregion
export { dispatchCronDelivery, queueCronMessageToolDeliveryAwareness, resolveCronDeliveryBestEffort, resolveDeliveryTarget };