import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js" ;
import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js" ;
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js" ;
import type { SkillSnapshot } from "../../agents/skills.js" ;
import type { ThinkLevel } from "../../auto-reply/thinking.js" ;
import type { CliDeps } from "../../cli/outbound-send-deps.js" ;
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js" ;
import type { OpenClawConfig } from "../../config/types.openclaw.js" ;
import { normalizeOptionalString } from "../../shared/string-coerce.js" ;
import { resolveCronDeliveryPlan, type CronDeliveryPlan } from "../delivery-plan.js" ;
import type {
CronDeliveryTrace,
CronDeliveryTraceMessageTarget,
CronDeliveryTraceTarget,
CronJob,
CronRunTelemetry,
} from "../types.js" ;
import { resolveCronChannelOutputPolicy } from "./channel-output-policy.js" ;
import {
isHeartbeatOnlyResponse,
resolveCronPayloadOutcome,
resolveHeartbeatAckMaxChars,
} from "./helpers.js" ;
import { resolveCronModelSelection } from "./model-selection.js" ;
import { buildCronAgentDefaultsConfig } from "./run-config.js" ;
import {
createPersistCronSessionEntry,
markCronSessionPreRun,
persistCronSkillsSnapshotIfChanged,
type CronLiveSelection,
type MutableCronSession,
type PersistCronSessionEntry,
} from "./run-session-state.js" ;
import {
DEFAULT_CONTEXT_TOKENS,
deriveSessionTotalTokens,
ensureAgentWorkspace,
hasNonzeroUsage,
isCliProvider,
isExternalHookSession,
logWarn,
mapHookExternalContentSource,
normalizeAgentId,
normalizeThinkLevel,
resolveAgentConfig,
resolveAgentDir,
resolveAgentTimeoutMs,
resolveAgentWorkspaceDir,
resolveCronStyleNow,
resolveDefaultAgentId,
resolveHookExternalContentSource,
isThinkingLevelSupported,
resolveSupportedThinkingLevel,
resolveSessionTranscriptPath,
resolveThinkingDefault,
setSessionRuntimeModel,
} from "./run.runtime.js" ;
import type { RunCronAgentTurnResult } from "./run.types.js" ;
import { resolveCronAgentSessionKey } from "./session-key.js" ;
import { resolveCronSession } from "./session.js" ;
import { resolveCronSkillsSnapshot } from "./skills-snapshot.js" ;
let sessionStoreRuntimePromise:
| Promise<typeof import ("../../config/sessions/store.runtime.js" )>
| undefined;
let cronExecutorRuntimePromise: Promise<typeof import ("./run-executor.runtime.js" )> | undefined;
let cronExternalContentRuntimePromise:
| Promise<typeof import ("./run-external-content.runtime.js" )>
| undefined;
let cronAuthProfileRuntimePromise:
| Promise<typeof import ("./run-auth-profile.runtime.js" )>
| undefined;
let cronContextRuntimePromise: Promise<typeof import ("./run-context.runtime.js" )> | undefined;
let cronModelCatalogRuntimePromise:
| Promise<typeof import ("./run-model-catalog.runtime.js" )>
| undefined;
let cronDeliveryRuntimePromise: Promise<typeof import ("./run-delivery.runtime.js" )> | undefined;
async function loadSessionStoreRuntime() {
sessionStoreRuntimePromise ??= import ("../../config/sessions/store.runtime.js" );
return await sessionStoreRuntimePromise;
}
async function loadCronExecutorRuntime() {
cronExecutorRuntimePromise ??= import ("./run-executor.runtime.js" );
return await cronExecutorRuntimePromise;
}
async function loadCronExternalContentRuntime() {
cronExternalContentRuntimePromise ??= import ("./run-external-content.runtime.js" );
return await cronExternalContentRuntimePromise;
}
async function loadCronAuthProfileRuntime() {
cronAuthProfileRuntimePromise ??= import ("./run-auth-profile.runtime.js" );
return await cronAuthProfileRuntimePromise;
}
async function loadCronContextRuntime() {
cronContextRuntimePromise ??= import ("./run-context.runtime.js" );
return await cronContextRuntimePromise;
}
async function loadCronModelCatalogRuntime() {
cronModelCatalogRuntimePromise ??= import ("./run-model-catalog.runtime.js" );
return await cronModelCatalogRuntimePromise;
}
async function loadCronDeliveryRuntime() {
cronDeliveryRuntimePromise ??= import ("./run-delivery.runtime.js" );
return await cronDeliveryRuntimePromise;
}
function hasConfiguredAuthProfiles(cfg: OpenClawConfig): boolean {
return (
Boolean (cfg.auth?.profiles && Object.keys(cfg.auth.profiles).length > 0 ) ||
Boolean (cfg.auth?.order && Object.keys(cfg.auth.order).length > 0 )
);
}
function resolveNonNegativeNumber(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
async function retireRolledCronSessionMcpRuntime(params: {
job: CronJob;
cronSession: MutableCronSession;
}) {
if (params.job.sessionTarget === "isolated" ) {
return ;
}
const previousSessionId = normalizeOptionalString(params.cronSession.previousSessionId);
const currentSessionId = normalizeOptionalString(params.cronSession.sessionEntry.sessionId);
if (!previousSessionId || previousSessionId === currentSessionId) {
return ;
}
await retireSessionMcpRuntime({
sessionId: previousSessionId,
reason: "cron-session-rollover" ,
onError: (error, sessionId) => {
logWarn(
`[cron:${params.job.id}] Failed to dispose retired bundle MCP runtime for session ${sessionId}: ${String(error)}`,
);
},
});
}
export type { RunCronAgentTurnResult } from "./run.types.js" ;
type CronExecutionRuntime = typeof import ("./run-executor.runtime.js" );
type CronExecutionResult = Awaited<ReturnType<CronExecutionRuntime["executeCronRun" ]>>;
type CronModelCatalogRuntime = typeof import ("./run-model-catalog.runtime.js" );
type CronDeliveryRuntime = typeof import ("./run-delivery.runtime.js" );
type ResolvedCronDeliveryTarget = Awaited<ReturnType<CronDeliveryRuntime["resolveDeliveryTarget" ]>>;
function normalizeCronTraceTarget(
target: CronDeliveryTraceTarget | undefined,
): CronDeliveryTraceTarget | undefined {
if (!target) {
return undefined;
}
return {
...(target.channel ? { channel: target.channel } : {}),
...(target.to !== undefined ? { to: target.to } : {}),
...(target.accountId ? { accountId: target.accountId } : {}),
...(target.threadId !== undefined ? { threadId: target.threadId } : {}),
...(target.source ? { source: target.source } : {}),
};
}
type MessagingToolTargetMatcher = (
target: { provider?: string; to?: string; accountId?: string },
delivery: { channel?: string; to?: string; accountId?: string },
) => boolean ;
function normalizeMessagingToolTarget(
target: MessagingToolSend,
resolvedDelivery: ResolvedCronDeliveryTarget,
matchesMessagingToolDeliveryTarget: MessagingToolTargetMatcher,
): CronDeliveryTraceMessageTarget | undefined {
const channel = target.provider?.trim();
if (!channel) {
return undefined;
}
const traceChannel =
channel === "message" &&
resolvedDelivery.ok &&
matchesMessagingToolDeliveryTarget(target, {
channel: resolvedDelivery.channel,
to: resolvedDelivery.to,
accountId: resolvedDelivery.accountId,
})
? resolvedDelivery.channel
: channel;
return {
channel: traceChannel,
...(target.to ? { to: target.to } : {}),
...(target.accountId ? { accountId: target.accountId } : {}),
...(target.threadId ? { threadId: target.threadId } : {}),
};
}
function buildCronDeliveryTrace(params: {
deliveryPlan: CronDeliveryPlan;
resolvedDelivery: ResolvedCronDeliveryTarget;
messagingToolSentTargets: MessagingToolSend[];
matchesMessagingToolDeliveryTarget: MessagingToolTargetMatcher;
fallbackUsed: boolean ;
delivered: boolean ;
}): CronDeliveryTrace {
const intended = normalizeCronTraceTarget({
channel: params.deliveryPlan.channel ?? "last" ,
to: params.deliveryPlan.to ?? null ,
accountId: params.deliveryPlan.accountId,
threadId: params.deliveryPlan.threadId,
source:
params.deliveryPlan.channel === "last" || !params.deliveryPlan.channel ? "last" : "explicit" ,
});
const resolved = params.resolvedDelivery.ok
? {
ok: true ,
...normalizeCronTraceTarget({
channel: params.resolvedDelivery.channel,
to: params.resolvedDelivery.to,
accountId: params.resolvedDelivery.accountId,
threadId: params.resolvedDelivery.threadId,
source: params.resolvedDelivery.mode === "implicit" ? "last" : "explicit" ,
}),
}
: {
ok: false ,
...normalizeCronTraceTarget({
channel: params.resolvedDelivery.channel,
to: params.resolvedDelivery.to ?? null ,
accountId: params.resolvedDelivery.accountId,
threadId: params.resolvedDelivery.threadId,
source: params.resolvedDelivery.mode === "implicit" ? "last" : "explicit" ,
}),
error: params.resolvedDelivery.error.message,
};
const messageToolSentTo = params.messagingToolSentTargets
.map((target) =>
normalizeMessagingToolTarget(
target,
params.resolvedDelivery,
params.matchesMessagingToolDeliveryTarget,
),
)
.filter((target): target is CronDeliveryTraceMessageTarget => Boolean (target));
return {
...(intended ? { intended } : {}),
resolved,
...(messageToolSentTo.length > 0 ? { messageToolSentTo } : {}),
fallbackUsed: params.fallbackUsed,
delivered: params.delivered,
};
}
function resolveMessagingToolSentTargets(params: {
resolvedDelivery: ResolvedCronDeliveryTarget;
runResult: CronExecutionResult["runResult" ];
}): MessagingToolSend[] {
const explicitTargets = params.runResult.messagingToolSentTargets ?? [];
if (explicitTargets.length > 0 || params.runResult.didSendViaMessagingTool !== true ) {
return explicitTargets;
}
if (!params.resolvedDelivery.ok) {
return [];
}
return [
{
tool: "message" ,
provider: params.resolvedDelivery.channel,
...(params.resolvedDelivery.accountId
? { accountId: params.resolvedDelivery.accountId }
: {}),
...(params.resolvedDelivery.to ? { to: params.resolvedDelivery.to } : {}),
...(params.resolvedDelivery.threadId
? { threadId: String(params.resolvedDelivery.threadId) }
: {}),
},
];
}
function resolveCronToolPolicy(params: { deliveryMode: "announce" | "webhook" | "none" }) {
const enableMessageTool = params.deliveryMode !== "webhook" ;
return {
requireExplicitMessageTarget: false ,
disableMessageTool: !enableMessageTool,
forceMessageTool: enableMessageTool,
};
}
function canPromptForMessageTool(params: {
disableMessageTool: boolean ;
toolsAllow?: string[];
}): boolean {
if (params.disableMessageTool) {
return false ;
}
return !params.toolsAllow?.length || params.toolsAllow.includes("message" );
}
async function resolveCronDeliveryContext(params: {
cfg: OpenClawConfig;
job: CronJob;
agentId: string;
}) {
const deliveryPlan = resolveCronDeliveryPlan(params.job);
if (deliveryPlan.mode === "webhook" ) {
const resolvedDelivery = {
ok: false as const ,
channel: undefined,
to: undefined,
accountId: undefined,
threadId: undefined,
mode: "implicit" as const ,
error: new Error("webhook delivery has no chat target" ),
};
return {
deliveryPlan,
deliveryRequested: deliveryPlan.requested,
resolvedDelivery,
toolPolicy: resolveCronToolPolicy({
deliveryMode: deliveryPlan.mode,
}),
};
}
const { resolveDeliveryTarget } = await loadCronDeliveryRuntime();
const resolvedDelivery = await resolveDeliveryTarget(params.cfg, params.agentId, {
channel: deliveryPlan.channel ?? "last" ,
to: deliveryPlan.to,
threadId: deliveryPlan.threadId,
accountId: deliveryPlan.accountId,
sessionKey: params.job.sessionKey,
});
return {
deliveryPlan,
deliveryRequested: deliveryPlan.requested,
resolvedDelivery,
toolPolicy: resolveCronToolPolicy({
deliveryMode: deliveryPlan.mode,
}),
};
}
function appendCronDeliveryInstruction(params: {
commandBody: string;
deliveryRequested: boolean ;
messageToolEnabled: boolean ;
resolvedDeliveryOk: boolean ;
}) {
if (!params.deliveryRequested) {
return params.commandBody;
}
if (params.messageToolEnabled) {
const targetHint = params.resolvedDeliveryOk
? "for the current chat"
: "with an explicit target" ;
return `${params.commandBody}\n\nUse the message tool if you need to notify the user directly ${targetHint}. If you do not send directly, your final plain-text reply will be delivered automatically.`.trim();
}
return `${params.commandBody}\n\nReturn your response 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();
}
function resolvePositiveContextTokens(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}
async function loadCliRunnerRuntime() {
return await import ("../../agents/cli-runner.runtime.js" );
}
async function loadUsageFormatRuntime() {
return await import ("../../utils/usage-format.js" );
}
type RunCronAgentTurnParams = {
cfg: OpenClawConfig;
deps: CliDeps;
job: CronJob;
message: string;
abortSignal?: AbortSignal;
signal?: AbortSignal;
sessionKey: string;
agentId?: string;
lane?: string;
};
type WithRunSession = (
result: Omit<RunCronAgentTurnResult, "sessionId" | "sessionKey" >,
) => RunCronAgentTurnResult;
type PreparedCronRunContext = {
input: RunCronAgentTurnParams;
cfgWithAgentDefaults: OpenClawConfig;
agentId: string;
agentCfg: AgentDefaultsConfig;
agentDir: string;
agentSessionKey: string;
runSessionId: string;
runSessionKey: string;
workspaceDir: string;
commandBody: string;
cronSession: MutableCronSession;
persistSessionEntry: PersistCronSessionEntry;
withRunSession: WithRunSession;
agentPayload: Extract<CronJob["payload" ], { kind: "agentTurn" }> | null ;
deliveryPlan: CronDeliveryPlan;
resolvedDelivery: ResolvedCronDeliveryTarget;
deliveryRequested: boolean ;
suppressExecNotifyOnExit: boolean ;
toolPolicy: ReturnType<typeof resolveCronToolPolicy>;
skillsSnapshot: SkillSnapshot;
liveSelection: CronLiveSelection;
thinkLevel: ThinkLevel | undefined;
timeoutMs: number;
};
type CronPreparationResult =
| { ok: true ; context: PreparedCronRunContext }
| { ok: false ; result: RunCronAgentTurnResult };
async function prepareCronRunContext(params: {
input: RunCronAgentTurnParams;
isFastTestEnv: boolean ;
}): Promise<CronPreparationResult> {
const { input } = params;
const defaultAgentId = resolveDefaultAgentId(input.cfg);
const requestedAgentId =
typeof input.agentId === "string" && input.agentId.trim()
? input.agentId
: typeof input.job.agentId === "string" && input.job.agentId.trim()
? input.job.agentId
: undefined;
const normalizedRequested = requestedAgentId ? normalizeAgentId(requestedAgentId) : undefined;
const agentConfigOverride = normalizedRequested
? resolveAgentConfig(input.cfg, normalizedRequested)
: undefined;
const agentId = normalizedRequested ?? defaultAgentId;
const agentCfg: AgentDefaultsConfig = buildCronAgentDefaultsConfig({
defaults: input.cfg.agents?.defaults,
agentConfigOverride,
});
const cfgWithAgentDefaults: OpenClawConfig = {
...input.cfg,
agents: Object.assign({}, input.cfg.agents, { defaults: agentCfg }),
};
let catalog: Awaited<ReturnType<CronModelCatalogRuntime["loadModelCatalog" ]>> | undefined;
const loadCatalog = async () => {
if (!catalog) {
catalog = await (
await loadCronModelCatalogRuntime()
).loadModelCatalog({
config: cfgWithAgentDefaults,
});
}
return catalog;
};
const baseSessionKey = (input.sessionKey?.trim() || `cron:${input.job.id}`).trim();
const agentSessionKey = resolveCronAgentSessionKey({
sessionKey: baseSessionKey,
agentId,
mainKey: input.cfg.session?.mainKey,
cfg: input.cfg,
});
const payloadHookExternalContentSource =
input.job.payload.kind === "agentTurn" ? input.job.payload.externalContentSource : undefined;
const hookExternalContentSource =
payloadHookExternalContentSource ?? resolveHookExternalContentSource(baseSessionKey);
const workspaceDirRaw = resolveAgentWorkspaceDir(input.cfg, agentId);
const agentDir = resolveAgentDir(input.cfg, agentId);
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !params.isFastTestEnv,
});
const workspaceDir = workspace.dir;
const isGmailHook = hookExternalContentSource === "gmail" ;
const now = Date.now();
const cronSession = resolveCronSession({
cfg: input.cfg,
sessionKey: agentSessionKey,
agentId,
nowMs: now,
forceNew: input.job.sessionTarget === "isolated" ,
});
const runSessionId = cronSession.sessionEntry.sessionId;
if (!cronSession.sessionEntry.sessionFile?.trim()) {
cronSession.sessionEntry.sessionFile = resolveSessionTranscriptPath(runSessionId, agentId);
}
const runSessionKey = baseSessionKey.startsWith("cron:" )
? `${agentSessionKey}:run:${runSessionId}`
: agentSessionKey;
const persistSessionEntry = createPersistCronSessionEntry({
isFastTestEnv: params.isFastTestEnv,
cronSession,
agentSessionKey,
runSessionKey,
updateSessionStore: async (storePath, update) => {
const { updateSessionStore } = await loadSessionStoreRuntime();
await updateSessionStore(storePath, update);
},
});
const withRunSession: WithRunSession = (result) => ({
...result,
sessionId: runSessionId,
sessionKey: runSessionKey,
});
if (!cronSession.sessionEntry.label?.trim() && baseSessionKey.startsWith("cron:" )) {
const labelSuffix =
typeof input.job.name === "string" && input.job.name.trim()
? input.job.name.trim()
: input.job.id;
cronSession.sessionEntry.label = `Cron: ${labelSuffix}`;
}
const resolvedModelSelection = await resolveCronModelSelection({
cfg: input.cfg,
cfgWithAgentDefaults,
agentConfigOverride,
sessionEntry: cronSession.sessionEntry,
payload: input.job.payload,
isGmailHook,
});
if (!resolvedModelSelection.ok) {
return {
ok: false ,
result: withRunSession({ status: "error" , error: resolvedModelSelection.error }),
};
}
let provider = resolvedModelSelection.provider;
let model = resolvedModelSelection.model;
if (resolvedModelSelection.warning) {
logWarn(resolvedModelSelection.warning);
}
const hooksGmailThinking = isGmailHook
? normalizeThinkLevel(input.cfg.hooks?.gmail?.thinking)
: undefined;
const jobThink = normalizeThinkLevel(
(input.job.payload.kind === "agentTurn" ? input.job.payload.thinking : undefined) ?? undefined,
);
let thinkLevel: ThinkLevel | undefined = jobThink ?? hooksGmailThinking;
if (!thinkLevel) {
thinkLevel = resolveThinkingDefault({
cfg: cfgWithAgentDefaults,
provider,
model,
catalog: await loadCatalog(),
});
}
if (!isThinkingLevelSupported({ provider, model, level: thinkLevel })) {
const fallbackThinkLevel = resolveSupportedThinkingLevel({
provider,
model,
level: thinkLevel,
});
if (fallbackThinkLevel !== thinkLevel) {
logWarn(
`[cron:${input.job.id}] Thinking level "${thinkLevel}" is not supported for ${provider}/${model}; downgrading to "${fallbackThinkLevel}" .`,
);
thinkLevel = fallbackThinkLevel;
}
}
const timeoutMs = resolveAgentTimeoutMs({
cfg: cfgWithAgentDefaults,
overrideSeconds:
input.job.payload.kind === "agentTurn" ? input.job.payload.timeoutSeconds : undefined,
});
const agentPayload = input.job.payload.kind === "agentTurn" ? input.job.payload : null ;
const { deliveryPlan, deliveryRequested, resolvedDelivery, toolPolicy } =
await resolveCronDeliveryContext({
cfg: cfgWithAgentDefaults,
job: input.job,
agentId,
});
const { formattedTime, timeLine } = resolveCronStyleNow(input.cfg, now);
const base = `[cron:${input.job.id} ${input.job.name}] ${input.message}`.trim();
const isExternalHook =
hookExternalContentSource !== undefined || isExternalHookSession(baseSessionKey);
const allowUnsafeExternalContent =
agentPayload?.allowUnsafeExternalContent === true ||
(isGmailHook && input.cfg.hooks?.gmail?.allowUnsafeExternalContent === true );
const shouldWrapExternal = isExternalHook && !allowUnsafeExternalContent;
let commandBody: string;
if (isExternalHook) {
const { detectSuspiciousPatterns } = await loadCronExternalContentRuntime();
const suspiciousPatterns = detectSuspiciousPatterns(input.message);
if (suspiciousPatterns.length > 0 ) {
logWarn(
`[security] Suspicious patterns detected in external hook content ` +
`(session=${baseSessionKey}, patterns=${suspiciousPatterns.length}): ${suspiciousPatterns.slice(0 , 3 ).join(", " )}`,
);
}
}
if (shouldWrapExternal) {
const { buildSafeExternalPrompt } = await loadCronExternalContentRuntime();
const hookType = mapHookExternalContentSource(hookExternalContentSource ?? "webhook" );
const safeContent = buildSafeExternalPrompt({
content: input.message,
source: hookType,
jobName: input.job.name,
jobId: input.job.id,
timestamp: formattedTime,
});
commandBody = `${safeContent}\n\n${timeLine}`.trim();
} else {
commandBody = `${base}\n${timeLine}`.trim();
}
commandBody = appendCronDeliveryInstruction({
commandBody,
deliveryRequested,
messageToolEnabled: canPromptForMessageTool({
disableMessageTool: toolPolicy.disableMessageTool,
toolsAllow: agentPayload?.toolsAllow,
}),
resolvedDeliveryOk: resolvedDelivery.ok,
});
const skillsSnapshot = await resolveCronSkillsSnapshot({
workspaceDir,
config: cfgWithAgentDefaults,
agentId,
existingSnapshot: cronSession.sessionEntry.skillsSnapshot,
isFastTestEnv: params.isFastTestEnv,
});
await persistCronSkillsSnapshotIfChanged({
isFastTestEnv: params.isFastTestEnv,
cronSession,
skillsSnapshot,
nowMs: Date.now(),
persistSessionEntry,
});
markCronSessionPreRun({ entry: cronSession.sessionEntry, provider, model });
try {
await persistSessionEntry();
} catch (err) {
logWarn(`[cron:${input.job.id}] Failed to persist pre-run session entry: ${String(err)}`);
}
await retireRolledCronSessionMcpRuntime({
job: input.job,
cronSession,
});
const hasSessionAuthProfileOverride = Boolean (
cronSession.sessionEntry.authProfileOverride?.trim(),
);
const authProfileId =
!hasSessionAuthProfileOverride &&
!hasConfiguredAuthProfiles(cfgWithAgentDefaults) &&
!hasAnyAuthProfileStoreSource(agentDir)
? undefined
: await (
await loadCronAuthProfileRuntime()
).resolveSessionAuthProfileOverride({
cfg: cfgWithAgentDefaults,
provider,
agentDir,
sessionEntry: cronSession.sessionEntry,
sessionStore: cronSession.store,
sessionKey: agentSessionKey,
storePath: cronSession.storePath,
isNewSession: cronSession.isNewSession && input.job.sessionTarget !== "isolated" ,
});
const liveSelection: CronLiveSelection = {
provider,
model,
authProfileId,
authProfileIdSource: authProfileId
? cronSession.sessionEntry.authProfileOverrideSource
: undefined,
};
return {
ok: true ,
context: {
input,
cfgWithAgentDefaults,
agentId,
agentCfg,
agentDir,
agentSessionKey,
runSessionId,
runSessionKey,
workspaceDir,
commandBody,
cronSession,
persistSessionEntry,
withRunSession,
agentPayload,
deliveryPlan,
resolvedDelivery,
deliveryRequested,
suppressExecNotifyOnExit: deliveryPlan.mode === "none" ,
toolPolicy,
skillsSnapshot,
liveSelection,
thinkLevel,
timeoutMs,
},
};
}
async function finalizeCronRun(params: {
prepared: PreparedCronRunContext;
execution: CronExecutionResult;
abortReason: () => string;
isAborted: () => boolean ;
}): Promise<RunCronAgentTurnResult> {
const { prepared, execution } = params;
const finalRunResult = execution.runResult;
const payloads = finalRunResult.payloads ?? [];
let telemetry: CronRunTelemetry | undefined;
if (finalRunResult.meta?.systemPromptReport) {
prepared.cronSession.sessionEntry.systemPromptReport = finalRunResult.meta.systemPromptReport;
}
const usage = finalRunResult.meta?.agentMeta?.usage;
const promptTokens = finalRunResult.meta?.agentMeta?.promptTokens;
const modelUsed =
finalRunResult.meta?.agentMeta?.model ??
execution.fallbackModel ??
execution.liveSelection.model;
const providerUsed =
finalRunResult.meta?.agentMeta?.provider ??
execution.fallbackProvider ??
execution.liveSelection.provider;
const contextTokens =
resolvePositiveContextTokens(prepared.agentCfg?.contextTokens) ??
(await loadCronContextRuntime()).lookupContextTokens(modelUsed, {
allowAsyncLoad: false ,
}) ??
resolvePositiveContextTokens(prepared.cronSession.sessionEntry.contextTokens) ??
DEFAULT_CONTEXT_TOKENS;
setSessionRuntimeModel(prepared.cronSession.sessionEntry, {
provider: providerUsed,
model: modelUsed,
});
prepared.cronSession.sessionEntry.contextTokens = contextTokens;
if (isCliProvider(providerUsed, prepared.cfgWithAgentDefaults)) {
const cliSessionId = finalRunResult.meta?.agentMeta?.sessionId?.trim();
if (cliSessionId) {
const { setCliSessionId } = await loadCliRunnerRuntime();
setCliSessionId(prepared.cronSession.sessionEntry, providerUsed, cliSessionId);
}
}
if (hasNonzeroUsage(usage)) {
const { estimateUsageCost, resolveModelCostConfig } = await loadUsageFormatRuntime();
const input = usage.input ?? 0 ;
const output = usage.output ?? 0 ;
const totalTokens = deriveSessionTotalTokens({
usage,
contextTokens,
promptTokens,
});
const runEstimatedCostUsd = resolveNonNegativeNumber(
estimateUsageCost({
usage,
cost: resolveModelCostConfig({
provider: providerUsed,
model: modelUsed,
config: prepared.cfgWithAgentDefaults,
}),
}),
);
prepared.cronSession.sessionEntry.inputTokens = input;
prepared.cronSession.sessionEntry.outputTokens = output;
const telemetryUsage: NonNullable<CronRunTelemetry["usage" ]> = {
input_tokens: input,
output_tokens: output,
};
if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0 ) {
prepared.cronSession.sessionEntry.totalTokens = totalTokens;
prepared.cronSession.sessionEntry.totalTokensFresh = true ;
telemetryUsage.total_tokens = totalTokens;
} else {
prepared.cronSession.sessionEntry.totalTokens = undefined;
prepared.cronSession.sessionEntry.totalTokensFresh = false ;
}
prepared.cronSession.sessionEntry.cacheRead = usage.cacheRead ?? 0 ;
prepared.cronSession.sessionEntry.cacheWrite = usage.cacheWrite ?? 0 ;
// Snapshot cost like tokens (runEstimatedCostUsd is already computed from
// cumulative run usage, so assign directly instead of accumulating).
// Fixes #69347: cost was inflated 1x-72x by accumulating on every persist.
if (runEstimatedCostUsd !== undefined) {
prepared.cronSession.sessionEntry.estimatedCostUsd = runEstimatedCostUsd;
}
telemetry = {
model: modelUsed,
provider: providerUsed,
usage: telemetryUsage,
};
} else {
telemetry = { model: modelUsed, provider: providerUsed };
}
await prepared.persistSessionEntry();
if (params.isAborted()) {
return prepared.withRunSession({ status: "error" , error: params.abortReason(), ...telemetry });
}
let {
summary,
outputText,
synthesizedText,
deliveryPayloads,
deliveryPayloadHasStructuredContent,
hasFatalErrorPayload,
embeddedRunError,
} = resolveCronPayloadOutcome({
payloads,
runLevelError: finalRunResult.meta?.error,
finalAssistantVisibleText: finalRunResult.meta?.finalAssistantVisibleText,
preferFinalAssistantVisibleText: (
await resolveCronChannelOutputPolicy(prepared.resolvedDelivery.channel)
).preferFinalAssistantVisibleText,
});
const resolveRunOutcome = (result?: {
delivered?: boolean ;
deliveryAttempted?: boolean ;
delivery?: CronDeliveryTrace;
}) =>
prepared.withRunSession({
status: hasFatalErrorPayload ? "error" : "ok" ,
...(hasFatalErrorPayload
? { error: embeddedRunError ?? "cron isolated run returned an error payload" }
: {}),
summary,
outputText,
delivered: result?.delivered,
deliveryAttempted: result?.deliveryAttempted,
delivery: result?.delivery,
...telemetry,
});
const skipHeartbeatDelivery =
prepared.deliveryRequested &&
isHeartbeatOnlyResponse(payloads, resolveHeartbeatAckMaxChars(prepared.agentCfg));
const {
dispatchCronDelivery,
matchesMessagingToolDeliveryTarget,
resolveCronDeliveryBestEffort,
} = await loadCronDeliveryRuntime();
const messagingToolSentTargets = resolveMessagingToolSentTargets({
resolvedDelivery: prepared.resolvedDelivery,
runResult: finalRunResult,
});
const didSendViaMessagingTool =
finalRunResult.didSendViaMessagingTool === true && messagingToolSentTargets.length > 0 ;
const skipMessagingToolDelivery =
didSendViaMessagingTool &&
prepared.resolvedDelivery.ok &&
messagingToolSentTargets.some((target) =>
matchesMessagingToolDeliveryTarget(target, {
channel: prepared.resolvedDelivery.channel,
to: prepared.resolvedDelivery.to,
accountId: prepared.resolvedDelivery.accountId,
}),
);
const deliveryResult = await dispatchCronDelivery({
cfg: prepared.input.cfg,
cfgWithAgentDefaults: prepared.cfgWithAgentDefaults,
deps: prepared.input.deps,
job: prepared.input.job,
agentId: prepared.agentId,
agentSessionKey: prepared.agentSessionKey,
sessionId: prepared.runSessionId,
runStartedAt: execution.runStartedAt,
runEndedAt: execution.runEndedAt,
timeoutMs: prepared.timeoutMs,
resolvedDelivery: prepared.resolvedDelivery,
deliveryRequested: prepared.deliveryRequested,
skipHeartbeatDelivery,
skipMessagingToolDelivery,
unverifiedMessagingToolDelivery: didSendViaMessagingTool && !prepared.resolvedDelivery.ok,
deliveryBestEffort: resolveCronDeliveryBestEffort(prepared.input.job),
deliveryPayloadHasStructuredContent,
deliveryPayloads,
synthesizedText,
summary,
outputText,
telemetry,
abortSignal: prepared.input.abortSignal ?? prepared.input.signal,
isAborted: params.isAborted,
abortReason: params.abortReason,
withRunSession: prepared.withRunSession,
});
const deliveryTrace = buildCronDeliveryTrace({
deliveryPlan: prepared.deliveryPlan,
resolvedDelivery: prepared.resolvedDelivery,
messagingToolSentTargets,
matchesMessagingToolDeliveryTarget,
fallbackUsed: deliveryResult.deliveryAttempted && !skipMessagingToolDelivery,
delivered: deliveryResult.delivered,
});
if (deliveryResult.result) {
const resultWithDeliveryMeta: RunCronAgentTurnResult = {
...deliveryResult.result,
deliveryAttempted:
deliveryResult.result.deliveryAttempted ?? deliveryResult.deliveryAttempted,
delivery: deliveryTrace,
};
if (!hasFatalErrorPayload || deliveryResult.result.status !== "ok" ) {
return resultWithDeliveryMeta;
}
return resolveRunOutcome({
delivered: deliveryResult.result.delivered,
deliveryAttempted: resultWithDeliveryMeta.deliveryAttempted,
delivery: deliveryTrace,
});
}
summary = deliveryResult.summary;
outputText = deliveryResult.outputText;
return resolveRunOutcome({
delivered: deliveryResult.delivered,
deliveryAttempted: deliveryResult.deliveryAttempted,
delivery: deliveryTrace,
});
}
export async function runCronIsolatedAgentTurn(params: {
cfg: OpenClawConfig;
deps: CliDeps;
job: CronJob;
message: string;
abortSignal?: AbortSignal;
signal?: AbortSignal;
sessionKey: string;
agentId?: string;
lane?: string;
}): Promise<RunCronAgentTurnResult> {
const abortSignal = params.abortSignal ?? params.signal;
const isAborted = () => abortSignal?.aborted === true ;
const abortReason = () => {
const reason = abortSignal?.reason;
return typeof reason === "string" && reason.trim()
? reason.trim()
: "cron: job execution timed out" ;
};
const isFastTestEnv = process.env.OPENCLAW_TEST_FAST === "1" ;
const prepared = await prepareCronRunContext({ input: params, isFastTestEnv });
if (!prepared.ok) {
return prepared.result;
}
try {
const { executeCronRun } = await loadCronExecutorRuntime();
const execution = await executeCronRun({
cfg: params.cfg,
cfgWithAgentDefaults: prepared.context.cfgWithAgentDefaults,
job: params.job,
agentId: prepared.context.agentId,
agentDir: prepared.context.agentDir,
agentSessionKey: prepared.context.agentSessionKey,
workspaceDir: prepared.context.workspaceDir,
lane: params.lane,
resolvedDelivery: {
channel: prepared.context.resolvedDelivery.channel,
to: prepared.context.resolvedDelivery.to,
accountId: prepared.context.resolvedDelivery.accountId,
threadId: prepared.context.resolvedDelivery.threadId,
},
toolPolicy: prepared.context.toolPolicy,
skillsSnapshot: prepared.context.skillsSnapshot,
agentPayload: prepared.context.agentPayload,
agentVerboseDefault: prepared.context.agentCfg?.verboseDefault,
liveSelection: prepared.context.liveSelection,
cronSession: prepared.context.cronSession,
commandBody: prepared.context.commandBody,
persistSessionEntry: prepared.context.persistSessionEntry,
abortSignal,
abortReason,
isAborted,
thinkLevel: prepared.context.thinkLevel,
timeoutMs: prepared.context.timeoutMs,
suppressExecNotifyOnExit: prepared.context.suppressExecNotifyOnExit,
});
if (isAborted()) {
return prepared.context.withRunSession({ status: "error" , error: abortReason() });
}
return await finalizeCronRun({
prepared: prepared.context,
execution,
abortReason,
isAborted,
});
} catch (err) {
return prepared.context.withRunSession({ status: "error" , error: String(err) });
}
}
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.13Bemerkung:
(vorverarbeitet am 2026-06-05)
¤
*Bot Zugriff