UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

208 lines (207 loc) 6.83 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { j as resolveTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js"; import "./number-coercion-Z7n6tXLk.js"; //#region src/infra/heartbeat-reason.ts /** Normalize a heartbeat wake reason for logs and UI. */ function normalizeHeartbeatWakeReason(reason) { return normalizeOptionalString(reason) ?? "requested"; } //#endregion //#region src/infra/heartbeat-wake.ts const HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT = "requests-in-flight"; const HEARTBEAT_SKIP_CRON_IN_PROGRESS = "cron-in-progress"; const HEARTBEAT_SKIP_LANES_BUSY = "lanes-busy"; const RETRYABLE_BUSY_SKIP_REASONS = new Set([ HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, HEARTBEAT_SKIP_CRON_IN_PROGRESS, HEARTBEAT_SKIP_LANES_BUSY ]); function isRetryableHeartbeatBusySkipReason(reason) { return RETRYABLE_BUSY_SKIP_REASONS.has(reason); } let heartbeatsEnabled = true; function setHeartbeatsEnabled(enabled) { heartbeatsEnabled = enabled; } function areHeartbeatsEnabled() { return heartbeatsEnabled; } let handler = null; let handlerGeneration = 0; const pendingWakes = /* @__PURE__ */ new Map(); let scheduled = false; let running = false; let timer = null; let timerDueAt = null; let timerKind = null; const DEFAULT_COALESCE_MS = 250; const DEFAULT_RETRY_MS = 1e3; const REASON_PRIORITY = { RETRY: 0, INTERVAL: 1, DEFAULT: 2, ACTION: 3 }; function resolveWakePriority(params) { if (params.intent === "manual" || params.intent === "immediate") return REASON_PRIORITY.ACTION; if (params.source === "retry" || params.reason === "retry") return REASON_PRIORITY.RETRY; if (params.intent === "scheduled" || params.source === "interval" || params.reason === "interval") return REASON_PRIORITY.INTERVAL; return REASON_PRIORITY.DEFAULT; } function normalizeWakeReason(reason) { return normalizeHeartbeatWakeReason(reason); } function normalizeWakeTarget(value) { return (normalizeOptionalString(value) ?? "") || void 0; } function getWakeTargetKey(params) { const agentId = normalizeWakeTarget(params.agentId); const sessionKey = normalizeWakeTarget(params.sessionKey); return `${agentId ?? ""}::${sessionKey ?? ""}`; } function queuePendingWakeReason(params) { const requestedAt = params.requestedAt ?? Date.now(); const normalizedReason = normalizeWakeReason(params.reason); const normalizedAgentId = normalizeWakeTarget(params.agentId); const normalizedSessionKey = normalizeWakeTarget(params.sessionKey); const wakeTargetKey = getWakeTargetKey({ agentId: normalizedAgentId, sessionKey: normalizedSessionKey }); const next = { source: params.source, intent: params.intent, reason: normalizedReason, priority: resolveWakePriority({ source: params.source, intent: params.intent, reason: normalizedReason }), requestedAt, agentId: normalizedAgentId, sessionKey: normalizedSessionKey, heartbeat: params.heartbeat }; const previous = pendingWakes.get(wakeTargetKey); if (!previous) { pendingWakes.set(wakeTargetKey, next); return; } const merged = next.heartbeat ?? previous.heartbeat ? { ...next, heartbeat: next.heartbeat ?? previous.heartbeat } : next; if (next.priority > previous.priority) { pendingWakes.set(wakeTargetKey, merged); return; } if (next.priority === previous.priority && next.requestedAt >= previous.requestedAt) pendingWakes.set(wakeTargetKey, merged); } function schedule(coalesceMs, kind = "normal") { const delay = resolveTimerTimeoutMs(coalesceMs, DEFAULT_COALESCE_MS, 0); const dueAt = Date.now() + delay; if (timer) { if (timerKind === "retry") return; if (typeof timerDueAt === "number" && timerDueAt <= dueAt) return; clearTimeout(timer); timer = null; timerDueAt = null; timerKind = null; } timerDueAt = dueAt; timerKind = kind; timer = setTimeout(() => { (async () => { timer = null; timerDueAt = null; timerKind = null; scheduled = false; const active = handler; if (!active) return; if (running) { scheduled = true; schedule(delay, kind); return; } const pendingBatch = Array.from(pendingWakes.values()); pendingWakes.clear(); running = true; try { for (const pendingWake of pendingBatch) { const res = await active({ source: pendingWake.source, intent: pendingWake.intent, reason: pendingWake.reason ?? void 0, ...pendingWake.agentId ? { agentId: pendingWake.agentId } : {}, ...pendingWake.sessionKey ? { sessionKey: pendingWake.sessionKey } : {}, ...pendingWake.heartbeat ? { heartbeat: pendingWake.heartbeat } : {} }); if (res.status === "skipped" && isRetryableHeartbeatBusySkipReason(res.reason)) { queuePendingWakeReason({ source: pendingWake.source, intent: pendingWake.intent, reason: pendingWake.reason ?? "retry", agentId: pendingWake.agentId, sessionKey: pendingWake.sessionKey, heartbeat: pendingWake.heartbeat }); schedule(DEFAULT_RETRY_MS, "retry"); } } } catch { for (const pendingWake of pendingBatch) queuePendingWakeReason({ source: pendingWake.source, intent: pendingWake.intent, reason: pendingWake.reason ?? "retry", agentId: pendingWake.agentId, sessionKey: pendingWake.sessionKey, heartbeat: pendingWake.heartbeat }); schedule(DEFAULT_RETRY_MS, "retry"); } finally { running = false; if (pendingWakes.size > 0 || scheduled) schedule(delay, "normal"); } })(); }, delay); timer.unref?.(); } /** * Register (or clear) the heartbeat wake handler. * Returns a disposer function that clears this specific registration. * Stale disposers (from previous registrations) are no-ops, preventing * a race where an old runner's cleanup clears a newer runner's handler. */ function setHeartbeatWakeHandler(next) { handlerGeneration += 1; const generation = handlerGeneration; handler = next; if (next) { if (timer) clearTimeout(timer); timer = null; timerDueAt = null; timerKind = null; running = false; scheduled = false; } if (handler && pendingWakes.size > 0) schedule(DEFAULT_COALESCE_MS, "normal"); return () => { if (handlerGeneration !== generation) return; if (handler !== next) return; handlerGeneration += 1; handler = null; }; } function requestHeartbeat(opts) { queuePendingWakeReason({ source: opts.source, intent: opts.intent, reason: opts.reason, agentId: opts.agentId, sessionKey: opts.sessionKey, heartbeat: opts.heartbeat }); schedule(opts.coalesceMs ?? DEFAULT_COALESCE_MS, "normal"); } //#endregion export { isRetryableHeartbeatBusySkipReason as a, setHeartbeatsEnabled as c, areHeartbeatsEnabled as i, HEARTBEAT_SKIP_LANES_BUSY as n, requestHeartbeat as o, HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT as r, setHeartbeatWakeHandler as s, HEARTBEAT_SKIP_CRON_IN_PROGRESS as t };