UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

313 lines (312 loc) • 12.5 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs"; import { r as defaultRuntime } from "./runtime-DlO3PTAd.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import "./agent-scope-config-CDYpzA0r.mjs"; import { l as resolveAgentIdFromSessionKey } from "./session-key-C09ow4h0.mjs"; import { i as listAgentIds } from "./agent-roster-BXD1uMJD.mjs"; import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs"; import "./config-DLycWFvB.mjs"; import { a as setHeartbeatWakeHandler, d as isSessionEventWakePollDeferred, l as getSessionEventWakeAbortSignal, s as areSessionEventWakesEnabled, u as isRetryableSessionEventWakeReason } from "./heartbeat-wake-ba6LAy2M.mjs"; import { a as resolveHeartbeatForWake, c as tryResolveAmbientHeartbeatAgentId, o as resolveHeartbeatIntervalMs, r as resolveHeartbeatAgents, t as isHeartbeatOwnerUnresolved } from "./heartbeat-config--CDRLwW6.mjs"; import { a as heartbeatLog, n as isConfiguredHeartbeatAgent, r as isTargetedUnscheduledWake } from "./heartbeat-wake-policy-C3iBUwep.mjs"; //#region src/infra/heartbeat-cooldown.ts const DEFAULT_MIN_WAKE_SPACING_MS = 3e4; const DEFAULT_FLOOD_WINDOW_MS = 6e4; const DEFAULT_FLOOD_THRESHOLD = 5; function shouldDeferWake(input) { if (input.intent === "manual") return { defer: false }; const floodDefer = checkFloodGuard(input); if (floodDefer) return floodDefer; if (input.intent === "immediate") return { defer: false }; if (input.intent === "scheduled") return input.now < input.nextDueMs ? { defer: true, reason: "not-due", retryAtMs: input.nextDueMs } : { defer: false }; if (input.lastRunStartedAtMs === void 0) return { defer: false }; if (input.intent !== "task" && !input.retainedWork && input.now < input.nextDueMs) { const spacingRetryAtMs = resolveMinSpacingRetryAtMs(input); return { defer: true, reason: "not-due", retryAtMs: Math.min(input.nextDueMs, spacingRetryAtMs ?? input.nextDueMs) }; } const spacingRetryAtMs = resolveMinSpacingRetryAtMs(input); if (spacingRetryAtMs !== void 0) return { defer: true, reason: "min-spacing", retryAtMs: spacingRetryAtMs }; return { defer: false }; } function resolveMinSpacingRetryAtMs(input) { if (input.lastRunStartedAtMs === void 0) return; const retryAtMs = input.lastRunStartedAtMs + DEFAULT_MIN_WAKE_SPACING_MS; return input.now < retryAtMs ? retryAtMs : void 0; } function checkFloodGuard(input) { if (!input.recentRunStarts || input.recentRunStarts.length < DEFAULT_FLOOD_THRESHOLD) return null; const windowStart = input.now - DEFAULT_FLOOD_WINDOW_MS; let inWindow = 0; for (let i = input.recentRunStarts.length - 1; i >= 0; i--) { const ts = input.recentRunStarts[i]; if (ts === void 0 || ts < windowStart) break; inWindow += 1; if (inWindow === DEFAULT_FLOOD_THRESHOLD) return { defer: true, reason: "flood", retryAtMs: ts + DEFAULT_FLOOD_WINDOW_MS + 1 }; } return null; } function recordRunStart(buffer, ts) { buffer.push(ts); while (buffer.length > 6) buffer.shift(); } //#endregion //#region src/infra/heartbeat-runner-scheduler.ts const loadHeartbeatExecution = createLazyRuntimeModule(() => import("./heartbeat-runner-run-BFuGCL0v.mjs")); function startHeartbeatRunner(opts) { const runtime = opts.runtime ?? defaultRuntime; const runOnce = opts.runOnce; const state = { cfg: opts.cfg ?? getRuntimeConfig(), runtime, agents: /* @__PURE__ */ new Map(), stopped: false }; const readCurrentConfig = opts.readCurrentConfig ?? (() => state.cfg); let initialized = false; const createAgentState = (agentId, now, heartbeat, intervalMs) => { const agent = state.agents.get(agentId) ?? { agentId, cooldownUntilMs: now, recentRunStarts: [], floodLoggedSinceLastRun: false }; agent.heartbeat = heartbeat; agent.intervalMs = intervalMs; agent.cooldownUntilMs = agent.lastRunStartedAtMs === void 0 ? now : agent.lastRunStartedAtMs + (intervalMs ?? 0); return agent; }; const evaluateWakeDeferral = (agent, now, reason, intent = "event", options = {}) => { const decision = shouldDeferWake({ intent, now, nextDueMs: options.authoritativeScheduledTick ? now : agent.cooldownUntilMs, lastRunStartedAtMs: agent.lastRunStartedAtMs, recentRunStarts: agent.recentRunStarts, retainedWork: options.retainedWork }); if (decision.defer && decision.reason === "flood") { if (!agent.floodLoggedSinceLastRun) { heartbeatLog.warn("heartbeat: flood guard tripped, deferring wake", { agentId: agent.agentId, reason: reason ?? "(none)", recentRunCount: agent.recentRunStarts.length }); agent.floodLoggedSinceLastRun = true; } } return decision; }; const recordRunBookkeeping = (agent, now) => { agent.lastRunStartedAtMs = now; agent.cooldownUntilMs = now + (agent.intervalMs ?? 0); recordRunStart(agent.recentRunStarts, now); agent.floodLoggedSinceLastRun = false; }; const updateConfig = (cfg) => { if (state.stopped) return; const now = Date.now(); const prevEnabled = Array.from(state.agents.values()).some((agent) => agent.intervalMs !== void 0); const nextAgents = /* @__PURE__ */ new Map(); const intervals = []; const enrolled = new Map(resolveHeartbeatAgents(cfg).map((agent) => [agent.agentId, agent])); for (const agentId of /* @__PURE__ */ new Set([...listAgentIds(cfg), ...enrolled.keys()])) { const agent = enrolled.get(agentId); const intervalMs = agent ? resolveHeartbeatIntervalMs(cfg, void 0, agent.heartbeat) : void 0; if (intervalMs) intervals.push(intervalMs); nextAgents.set(agentId, createAgentState(agentId, now, agent?.heartbeat, intervalMs ?? void 0)); } state.cfg = cfg; state.agents = nextAgents; const nextEnabled = intervals.length > 0; if (!initialized || prevEnabled !== nextEnabled) { if (nextEnabled) heartbeatLog.info("heartbeat: started", { intervalMs: Math.min(...intervals) }); else { heartbeatLog.info("heartbeat: disabled", { enabled: false }); if (isHeartbeatOwnerUnresolved(cfg)) heartbeatLog.warn("heartbeat: multi-agent config has no ambient heartbeat owner; set agents.defaults.heartbeat.agentId or agents.defaults.systemAgent.agentId"); } } initialized = true; }; const run = async (params) => { if (state.stopped || !areSessionEventWakesEnabled()) return { status: "skipped", reason: "disabled" }; const reason = params.reason; const wakeSignal = getSessionEventWakeAbortSignal(); const intent = params.intent; const execEventWake = params.source === "exec-event"; const requestedAgentId = params.agentId ? normalizeAgentId(params.agentId) : void 0; const requestedSessionKey = normalizeOptionalString(params.sessionKey); const requestedHeartbeat = params.heartbeat; const scheduledEveryMs = typeof params.scheduledEveryMs === "number" && Number.isSafeInteger(params.scheduledEveryMs) && params.scheduledEveryMs > 0 ? params.scheduledEveryMs : void 0; const authoritativeScheduledTick = scheduledEveryMs !== void 0; const requestedTasks = params.tasks ?? []; const retainedWork = params.retainedWork === true; const wakeConfig = readCurrentConfig(); const requestedTargetAgentId = requestedAgentId ?? (requestedSessionKey ? resolveAgentIdFromSessionKey(requestedSessionKey) : void 0); const isInterval = reason === "interval"; const startedAt = Date.now(); const now = startedAt; const runOneAgent = async (agent, targeted = false) => { const { agentId } = agent; if (agent.intervalMs !== void 0 && scheduledEveryMs !== void 0) { agent.intervalMs = scheduledEveryMs; agent.heartbeat = { ...agent.heartbeat, every: `${scheduledEveryMs}ms` }; } const deferral = evaluateWakeDeferral(agent, now, reason, intent, { authoritativeScheduledTick, retainedWork }); if (deferral.defer) { if (deferral.reason !== "not-due" && agent.cooldownUntilMs <= now && (!execEventWake || authoritativeScheduledTick)) agent.cooldownUntilMs = now + (agent.intervalMs ?? 0); return { result: { status: "skipped", reason: deferral.reason, retryAtMs: deferral.retryAtMs } }; } const useEnrolledHeartbeat = !targeted || (isInterval || authoritativeScheduledTick) && !requestedSessionKey && !requestedHeartbeat; let res; try { const runOptions = { cfg: wakeConfig, agentId, heartbeat: useEnrolledHeartbeat ? agent.heartbeat : resolveHeartbeatForWake({ cfg: wakeConfig, agentId, configuredHeartbeat: agent.heartbeat, requestedHeartbeat, source: params.source }), source: params.source, intent, reason, ...scheduledEveryMs !== void 0 ? { scheduledEveryMs } : {}, ...targeted ? { sessionKey: requestedSessionKey } : {}, tasks: requestedTasks, deps: { runtime: state.runtime } }; const execute = runOnce ?? (await loadHeartbeatExecution()).runHeartbeatOnce; if (state.stopped || opts.abortSignal?.aborted || wakeSignal?.aborted || !areSessionEventWakesEnabled()) return { result: { status: "skipped", reason: "disabled" } }; res = await execute(runOptions); } catch (err) { const errMsg = formatErrorMessage(err); heartbeatLog.error(`heartbeat runner: runOnce threw unexpectedly: ${errMsg}`, { error: errMsg, agentId }); recordRunBookkeeping(agent, now); return { result: { status: "failed", reason: errMsg } }; } if (res.status === "skipped" && isSessionEventWakePollDeferred()) { recordRunBookkeeping(agent, now); return { result: res }; } if (res.status === "skipped" && isRetryableSessionEventWakeReason(res.reason)) return { result: res, retryable: true }; if (params.source === "exec-event" && res.status === "skipped" && res.reason === "no-pending-event") return { result: res }; recordRunBookkeeping(agent, now); return { result: res }; }; if (requestedSessionKey || requestedAgentId) { const targetAgentId = requestedTargetAgentId ?? tryResolveAmbientHeartbeatAgentId(wakeConfig); if (!targetAgentId) return { status: "skipped", reason: "disabled" }; let targetAgent = state.agents.get(targetAgentId); if (targetAgent?.intervalMs === void 0) { if (!(requestedTargetAgentId !== void 0 && isConfiguredHeartbeatAgent(wakeConfig, requestedTargetAgentId) && isTargetedUnscheduledWake({ source: params.source, intent, reason, agentId: requestedAgentId, sessionKey: requestedSessionKey }))) return { status: "skipped", reason: "disabled" }; } if (!targetAgent) { targetAgent = createAgentState(targetAgentId, now); state.agents.set(targetAgentId, targetAgent); } const { result } = await runOneAgent(targetAgent, true); return result.status === "ran" ? { status: "ran", durationMs: Date.now() - startedAt } : result; } const enrolledAgents = Array.from(state.agents.values()).filter((agent) => agent.intervalMs !== void 0); if (enrolledAgents.length === 0) return { status: "skipped", reason: "disabled" }; const agentOutcomes = await Promise.all(enrolledAgents.map((agent) => runOneAgent(agent))); let ran = false; let firstResult; let firstFailure; let firstGuardSkip; for (const { result, retryable } of agentOutcomes) { if (retryable) return result; ran ||= result.status === "ran"; firstResult ??= result; if (result.status === "failed") firstFailure ??= result; if (!ran && result.status === "skipped" && result.retryAtMs !== void 0 && (!firstGuardSkip || result.retryAtMs < (firstGuardSkip.retryAtMs ?? Infinity))) firstGuardSkip = result; } if (ran) return firstFailure ?? { status: "ran", durationMs: Date.now() - startedAt }; return firstGuardSkip ?? firstFailure ?? firstResult ?? { status: "skipped", reason: isInterval ? "not-due" : "disabled" }; }; const disposeWakeHandler = setHeartbeatWakeHandler(run); updateConfig(state.cfg); const cleanup = () => { if (state.stopped) return; state.stopped = true; opts.abortSignal?.removeEventListener("abort", cleanup); disposeWakeHandler(); }; if (opts.abortSignal?.aborted) cleanup(); else opts.abortSignal?.addEventListener("abort", cleanup, { once: true }); return { stop: cleanup, updateConfig }; } //#endregion export { startHeartbeatRunner as t };