openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
339 lines (338 loc) • 15.9 kB
JavaScript
import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js";
import { a as logWarn } from "./logger-lqqYRtFw.js";
import { g as normalizeVerboseLevel } from "./thinking-OG7pb4lf.js";
import { u as registerAgentRunContext } from "./agent-events-C1B8VhOg.js";
import { o as resolveSessionTranscriptPath } from "./paths-NEwU8m3X.js";
import { h as isCliProvider } from "./model-selection-CA_65iXi.js";
import { a as resolveCliRuntimeExecutionProvider } from "./model-runtime-aliases-6XX4uD6a.js";
import { f as LiveSessionModelSwitchError, o as runWithModelFallback } from "./model-fallback-BvQwEryh.js";
import { r as resolveCronAgentLane } from "./lanes-CI0_P-yC.js";
import { l as resolveBootstrapWarningSignaturesSeen } from "./bootstrap-budget-bmmzkOyn.js";
import { t as ensureSelectedAgentHarnessPlugin } from "./runtime-plugin-CzEDIZOU.js";
import { a as syncCronSessionLiveSelection, c as getCliSessionId, d as resolveCurrentChannelTarget, l as runCliAgent, o as resolveCronFallbacksOverride, u as resolveCronChannelOutputPolicy } from "./run-session-state-D2OnW1-O.js";
import { i as resolveCronPayloadOutcome } from "./helpers-CYQZyDV5.js";
import { n as isLikelyInterimCronMessage } from "./subagent-followup-hints-WJCMG0pw.js";
import { createHash } from "node:crypto";
//#region src/cron/isolated-agent/run-executor.ts
/** Executes isolated cron prompts with model fallbacks and interim-ack retries. */
const cronEmbeddedRuntimeLoader = createLazyImportLoader(() => import("./run-embedded.runtime.js"));
const cronSubagentRegistryRuntimeLoader = createLazyImportLoader(() => import("./run-subagent-registry.runtime.js"));
async function loadCronEmbeddedRuntime() {
return await cronEmbeddedRuntimeLoader.load();
}
async function loadCronSubagentRegistryRuntime() {
return await cronSubagentRegistryRuntimeLoader.load();
}
const COMMAND_STYLE_CRON_PREFIX = /^(?:(?:[A-Z_][A-Z0-9_]*=\S+\s+)+)?(?:cd\s+\S+|(?:\.{1,2}|~)?\/\S+|[A-Za-z]:[\\/]\S+|(?:bash|bun|cargo|deno|docker|gh|git|go|make|node|npm|npx|pnpm|python|python3|ruby|sh|tsx|uv|zsh)\b)/u;
function resolveIsolatedCronPromptCacheKey(params) {
if (params.job.sessionTarget !== "isolated") return;
const material = JSON.stringify({
version: 1,
kind: "isolated-cron",
jobId: params.job.id,
agentId: params.agentId,
agentSessionKey: params.agentSessionKey,
provider: params.provider,
model: params.model
});
return `openclaw-cron-${createHash("sha256").update(material).digest("hex").slice(0, 32)}`;
}
/** Detects single-line cron prompts that look like shell commands or command invocations. */
function isCommandStyleCronMessage(message) {
const trimmed = message.trim();
if (!trimmed || trimmed.includes("\n")) return false;
return COMMAND_STYLE_CRON_PREFIX.test(trimmed);
}
function resolveCronBootstrapContextMode(payload) {
if (payload?.lightContext === true) return "lightweight";
if (payload?.lightContext === false) return;
return isCommandStyleCronMessage(payload?.message ?? "") ? "lightweight" : void 0;
}
/** Creates the model-fallback executor for one isolated cron prompt run. */
function createCronPromptExecutor(params) {
const sessionFile = params.cronSession.sessionEntry.sessionFile?.trim() || resolveSessionTranscriptPath(params.cronSession.sessionEntry.sessionId, params.agentId);
if (!params.cronSession.sessionEntry.sessionFile?.trim()) params.cronSession.sessionEntry.sessionFile = sessionFile;
const cronFallbacksOverride = params.modelFallbacksOverride ?? resolveCronFallbacksOverride({
cfg: params.cfg,
job: params.job,
agentId: params.agentId,
useSubagentFallbacks: params.useSubagentFallbacks,
inheritDefaultFallbacksForAgentStringModel: params.inheritDefaultFallbacksForAgentStringModel
});
let runResult;
let fallbackProvider = params.liveSelection.provider;
let fallbackModel = params.liveSelection.model;
let runEndedAt = Date.now();
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(params.cronSession.sessionEntry.systemPromptReport);
const bootstrapContextMode = resolveCronBootstrapContextMode(params.agentPayload);
const sourceReplyDeliveryMode = params.sourceDelivery.sourceReplyDeliveryMode;
const messageChannel = params.sourceDelivery.target.channel ?? params.resolvedDelivery.channel;
const runPrompt = async (promptText) => {
const fallbackResult = await runWithModelFallback({
cfg: params.cfgWithAgentDefaults,
provider: params.liveSelection.provider,
model: params.liveSelection.model,
runId: params.cronSession.sessionEntry.sessionId,
sessionId: params.cronSession.sessionEntry.sessionId,
lane: resolveCronAgentLane(params.lane),
agentDir: params.agentDir,
agentId: params.agentId,
sessionKey: params.runSessionKey,
prepareAgentHarnessRuntime: async ({ provider, model, agentHarnessRuntimeOverride }) => {
await ensureSelectedAgentHarnessPlugin({
config: params.cfgWithAgentDefaults,
provider,
modelId: model,
agentId: params.agentId,
sessionKey: params.runSessionKey,
agentHarnessRuntimeOverride,
workspaceDir: params.workspaceDir
});
},
fallbacksOverride: cronFallbacksOverride,
run: async (providerOverride, modelOverride, runOptions) => {
if (params.abortSignal?.aborted) throw new Error(params.abortReason());
const executionProvider = resolveCliRuntimeExecutionProvider({
provider: providerOverride,
cfg: params.cfgWithAgentDefaults,
agentId: params.agentId,
modelId: modelOverride
}) ?? providerOverride;
const bootstrapPromptWarningSignature = bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
if (isCliProvider(executionProvider, params.cfgWithAgentDefaults)) {
const cliSessionId = params.cronSession.isNewSession ? void 0 : await getCliSessionId(params.cronSession.sessionEntry, executionProvider);
const result = await runCliAgent({
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.runSessionKey,
sessionEntry: params.cronSession.sessionEntry,
agentId: params.agentId,
trigger: "cron",
jobId: params.job.id,
sessionFile,
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,
prompt: promptText,
provider: executionProvider,
model: modelOverride,
thinkLevel: params.thinkLevel,
timeoutMs: params.timeoutMs,
runId: params.cronSession.sessionEntry.sessionId,
lane: resolveCronAgentLane(params.lane),
cliSessionId,
skillsSnapshot: params.skillsSnapshot,
messageChannel,
sourceReplyDeliveryMode,
abortSignal: params.abortSignal,
onExecutionStarted: params.onExecutionStarted,
onExecutionPhase: params.onExecutionPhase,
bootstrapContextMode,
bootstrapContextRunKind: "cron",
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(result.meta?.systemPromptReport);
return result;
}
const { resolveFastModeState, runEmbeddedAgent } = await loadCronEmbeddedRuntime();
const promptCacheKey = resolveIsolatedCronPromptCacheKey({
job: params.job,
agentId: params.agentId,
agentSessionKey: params.agentSessionKey,
provider: providerOverride,
model: modelOverride
});
const currentChannelId = await resolveCurrentChannelTarget({
channel: messageChannel,
to: params.resolvedDelivery.to,
threadId: params.resolvedDelivery.threadId
});
const result = await runEmbeddedAgent({
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.runSessionKey,
promptCacheKey,
agentId: params.agentId,
trigger: "cron",
jobId: params.job.id,
cleanupBundleMcpOnRunEnd: params.job.sessionTarget === "isolated",
allowGatewaySubagentBinding: true,
messageChannel,
agentAccountId: params.resolvedDelivery.accountId,
messageTo: params.resolvedDelivery.to,
messageThreadId: params.resolvedDelivery.threadId,
currentChannelId,
sessionFile,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,
skillsSnapshot: params.skillsSnapshot,
prompt: promptText,
lane: resolveCronAgentLane(params.lane),
provider: providerOverride,
model: modelOverride,
modelFallbacksOverride: cronFallbacksOverride,
authProfileId: params.liveSelection.authProfileId,
authProfileIdSource: params.liveSelection.authProfileId ? params.liveSelection.authProfileIdSource : void 0,
thinkLevel: params.thinkLevel,
fastMode: resolveFastModeState({
cfg: params.cfgWithAgentDefaults,
provider: providerOverride,
model: modelOverride,
agentId: params.agentId,
sessionEntry: params.cronSession.sessionEntry
}).enabled,
verboseLevel: params.resolvedVerboseLevel,
timeoutMs: params.timeoutMs,
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
bootstrapContextMode,
bootstrapContextRunKind: "cron",
toolsAllow: params.agentPayload?.toolsAllow,
execOverrides: params.suppressExecNotifyOnExit ? {
notifyOnExit: false,
notifyOnExitEmptySuccess: false
} : void 0,
sourceReplyDeliveryMode,
runId: params.cronSession.sessionEntry.sessionId,
requireExplicitMessageTarget: params.sourceDelivery.messageTool.requireExplicitTarget,
disableMessageTool: !params.sourceDelivery.messageTool.enabled,
forceMessageTool: params.sourceDelivery.messageTool.force,
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
abortSignal: params.abortSignal,
onExecutionStarted: params.onExecutionStarted,
onExecutionPhase: params.onExecutionPhase,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(result.meta?.systemPromptReport);
return result;
}
});
runResult = fallbackResult.result;
fallbackProvider = fallbackResult.provider;
fallbackModel = fallbackResult.model;
params.liveSelection.provider = fallbackResult.provider;
params.liveSelection.model = fallbackResult.model;
runEndedAt = Date.now();
};
return {
runPrompt,
getState: () => ({
runResult,
fallbackProvider,
fallbackModel,
runEndedAt,
liveSelection: params.liveSelection
})
};
}
/** Executes an isolated cron prompt, including live model-switch and interim-ack retries. */
async function executeCronRun(params) {
const resolvedVerboseLevel = normalizeVerboseLevel(params.cronSession.sessionEntry.verboseLevel) ?? normalizeVerboseLevel(params.agentVerboseDefault) ?? "off";
registerAgentRunContext(params.cronSession.sessionEntry.sessionId, {
sessionKey: params.runSessionKey,
sessionId: params.cronSession.sessionEntry.sessionId,
verboseLevel: resolvedVerboseLevel
});
const executor = createCronPromptExecutor({
cfg: params.cfg,
cfgWithAgentDefaults: params.cfgWithAgentDefaults,
job: params.job,
agentId: params.agentId,
agentDir: params.agentDir,
agentSessionKey: params.agentSessionKey,
runSessionKey: params.runSessionKey,
workspaceDir: params.workspaceDir,
lane: params.lane,
resolvedVerboseLevel,
thinkLevel: params.thinkLevel,
timeoutMs: params.timeoutMs,
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
suppressExecNotifyOnExit: params.suppressExecNotifyOnExit,
resolvedDelivery: params.resolvedDelivery,
deliveryRequested: params.deliveryRequested,
sourceDelivery: params.sourceDelivery,
skillsSnapshot: params.skillsSnapshot,
agentPayload: params.agentPayload,
useSubagentFallbacks: params.useSubagentFallbacks,
inheritDefaultFallbacksForAgentStringModel: params.inheritDefaultFallbacksForAgentStringModel,
modelFallbacksOverride: params.modelFallbacksOverride,
liveSelection: params.liveSelection,
cronSession: params.cronSession,
abortSignal: params.abortSignal,
abortReason: params.abortReason,
onExecutionStarted: params.onExecutionStarted,
onExecutionPhase: params.onExecutionPhase
});
const runStartedAt = params.runStartedAt ?? Date.now();
const MAX_MODEL_SWITCH_RETRIES = 2;
let modelSwitchRetries = 0;
while (true) try {
await executor.runPrompt(params.commandBody);
break;
} catch (err) {
if (!(err instanceof LiveSessionModelSwitchError)) throw err;
modelSwitchRetries += 1;
if (modelSwitchRetries > MAX_MODEL_SWITCH_RETRIES) {
logWarn(`[cron:${params.job.id}] LiveSessionModelSwitchError retry limit reached (${MAX_MODEL_SWITCH_RETRIES}); aborting`);
throw err;
}
params.liveSelection.provider = err.provider;
params.liveSelection.model = err.model;
params.liveSelection.authProfileId = err.authProfileId;
params.liveSelection.authProfileIdSource = err.authProfileId ? err.authProfileIdSource : void 0;
syncCronSessionLiveSelection({
entry: params.cronSession.sessionEntry,
liveSelection: params.liveSelection
});
try {
await params.persistSessionEntry();
} catch (persistErr) {
logWarn(`[cron:${params.job.id}] Failed to persist model switch session entry: ${String(persistErr)}`);
}
continue;
}
let { runResult, fallbackProvider, fallbackModel, runEndedAt } = executor.getState();
if (!runResult) throw new Error("cron isolated run returned no result");
if (!params.isAborted()) {
const interimPayloads = runResult.payloads ?? [];
const { deliveryPayloadHasStructuredContent: interimPayloadHasStructuredContent, hasFatalErrorPayload: interimHasFatalErrorPayload, outputText: interimOutputText } = resolveCronPayloadOutcome({
payloads: interimPayloads,
runLevelError: runResult.meta?.error,
failureSignal: runResult.meta?.failureSignal,
finalAssistantVisibleText: runResult.meta?.finalAssistantVisibleText,
preferFinalAssistantVisibleText: (await resolveCronChannelOutputPolicy(params.resolvedDelivery.channel, { deliveryRequested: params.deliveryRequested })).preferFinalAssistantVisibleText
});
const interimText = interimOutputText?.trim() ?? "";
const shouldRetryInterimAck = !runResult.meta?.error && !interimHasFatalErrorPayload && !runResult.didSendViaMessagingTool && !interimPayloadHasStructuredContent && !interimPayloads.some((payload) => payload?.isError === true) && isLikelyInterimCronMessage(interimText);
let hasFreshDescendants = false;
let hasActiveDescendants = false;
if (shouldRetryInterimAck) {
const { countActiveDescendantRuns, listDescendantRunsForRequester } = await loadCronSubagentRegistryRuntime();
hasFreshDescendants = listDescendantRunsForRequester(params.runSessionKey).some((entry) => {
const descendantStartedAt = typeof entry.startedAt === "number" ? entry.startedAt : entry.createdAt;
return typeof descendantStartedAt === "number" && descendantStartedAt >= runStartedAt;
});
hasActiveDescendants = countActiveDescendantRuns(params.runSessionKey) > 0;
}
if (shouldRetryInterimAck && !hasFreshDescendants && !hasActiveDescendants) {
const continuationPrompt = [
"Your previous response was only an acknowledgement and did not complete this cron task.",
"Complete the original task now.",
"Do not send a status update like 'on it'.",
"Use tools when needed, including sessions_spawn for parallel subtasks, wait for spawned subagents to finish, then return only the final summary."
].join(" ");
await executor.runPrompt(continuationPrompt);
({runResult, fallbackProvider, fallbackModel, runEndedAt} = executor.getState());
}
}
if (!runResult) throw new Error("cron isolated run returned no result");
return {
runResult,
fallbackProvider,
fallbackModel,
runStartedAt,
runEndedAt,
liveSelection: params.liveSelection
};
}
//#endregion
export { executeCronRun };