openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,763 lines • 78.7 kB
JavaScript
import { j as resolveIntegerOption } from "./number-coercion-CLj0HTDM.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import "./utils-P__uGsPB.js";
import { t as escapeRegExp } from "./regexp-BZyMFTlj.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { c as resolveAgentConfig, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js";
import { O as parseAgentSessionKey, f as resolveAgentIdFromSessionKey } from "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { i as isSilentReplyPayloadText, n as SILENT_REPLY_TOKEN, t as HEARTBEAT_TOKEN } from "./tokens-DbeAFqg7.js";
import { y as resolveModelRefFromString } from "./model-selection-shared-BlLyx1r2.js";
import "./agent-scope-DbtJyKUL.js";
import "./config-Cs0XXL3x.js";
import { d as readStoredDeviceIdentityReadOnly, r as loadOrCreateDeviceIdentity } from "./device-identity-J83pn_rP.js";
import { c as getAgentEventLifecycleGeneration } from "./agent-events-CoxiItUi.js";
import { a as getActivePluginChannelRegistry } from "./runtime-BL4wZfTq.js";
import { t as normalizeChatType } from "./chat-type-CG0X_HJM.js";
import { a as getReplyPayloadMetadata, i as copyReplyPayloadMetadata } from "./reply-payload-DahKk-7n.js";
import { a as HEARTBEAT_SKIP_PREEMPTED, c as isRetryableHeartbeatSkipReason, f as setHeartbeatWakeHandler, i as HEARTBEAT_SKIP_NO_PENDING_EVENT, m as getHeartbeatWakeAbortSignal, n as HEARTBEAT_SKIP_CHANNEL_NOT_READY, o as HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, r as HEARTBEAT_SKIP_CRON_IN_PROGRESS, s as areHeartbeatsEnabled, t as HEARTBEAT_IDLE_RETRY_GRACE_MS } from "./heartbeat-wake-dlWl8dXS.js";
import { a as enqueueSystemEvent, h as withSystemEventOwner, m as selectAgentSystemEvents, p as resolveSystemEventDeliveryContext, t as consumeSelectedSystemEventEntries, u as peekSystemEventEntries } from "./system-events-C03jsM0j.js";
import { Wt as applySessionEntryLifecycleMutation } from "./session-accessor-YsytfDtG.js";
import { t as STREAM_ERROR_FALLBACK_TEXT } from "./stream-message-shared-Cyrn1UHN.js";
import { d as patchSessionEntryCore, w as loadExactSessionEntryReadOnly, x as loadExactSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { t as getChannelPlugin } from "./registry-Cz6cv4VC.js";
import "./plugins-Cozj1Enf.js";
import { r as mergeSessionEntry } from "./types-BHV0IaPK.js";
import { o as resolveEffectiveAgentRuntime } from "./thinking-runtime-BiFbTVdI.js";
import "./model-selection-di2kjKCB.js";
import { T as replaceGenericExternalRunFailureText } from "./user-copy-CxiPNpae.js";
import { o as resolveSourceReplyDeliveryMode } from "./source-reply-delivery-mode-Ct9PBal_.js";
import { h as replyRunRegistry, p as listActiveReplyRunSessionKeys } from "./reply-run-registry.registry-BMo0d96L.js";
import "./reply-run-registry-BlmBKV23.js";
import { f as resolveCronJobsStorePathFromConfig } from "./store-K9I-tbar.js";
import { r as listActiveEmbeddedRunSessionKeys } from "./active-run-projections-C4mdt8WG.js";
import { c as isCronActiveJobMarkerCurrent, d as listCronHeartbeatWaitOwners, o as hasActiveCronJobs, s as hasActiveCronJobsExceptMarkers } from "./active-jobs-C_biqiZG.js";
import { c as isCommandLaneTaskMarkerCurrent, o as getQueueSize } from "./command-queue-C3Fv2rcU.js";
import { t as resolveMirroredTranscriptText } from "./transcript-mirror-BTvMHa-8.js";
import { c as resolveHeartbeatPromptCore, l as resolveHeartbeatPromptForResponseTool, o as isHeartbeatAcknowledgementText, r as HEARTBEAT_RESPONSE_TOOL_INSTRUCTIONS, s as isHeartbeatContentEffectivelyEmpty, u as stripHeartbeatToken } from "./heartbeat-ofet59jF.js";
import { a as resolveResponsePrefixTemplate } from "./normalize-reply-C1iRjnEc.js";
import { a as hasOutboundReplyContent } from "./reply-payload-Bfsi665c.js";
import { a as resolveSendableOutboundReplyParts } from "./reply-payload-parts-BsFA4sDv.js";
import { c as resolveOutboundPayloadMirrorText } from "./payloads-DC1mf-yF.js";
import { n as suppressPendingFinalDelivery, t as clearPendingFinalDeliveryAfterSuccess } from "./dispatch-from-config.pending-final-DOlooYOM.js";
import { t as buildOutboundSessionContext } from "./session-context-CpdGMrv6.js";
import { r as resolveReplyOperationAgentTurn, t as REPLY_OPERATION_RUN_STATE } from "./reply-operation-run-state-B5ZoFcCR.js";
import { s as transitionMainSessionRecovery } from "./main-session-recovery-state-DD1DvBOq.js";
import { a as getHeartbeatToolNotificationText, c as resolveHeartbeatToolResponseFromReplyResult, s as resolveHeartbeatScratchProposalFromReplyResult } from "./heartbeat-tool-response-B4VYc0rg.js";
import { a as tryResolveAmbientHeartbeatAgentId, i as resolveHeartbeatIntervalMs, n as resolveHeartbeatAgents, r as resolveHeartbeatConfig, t as isHeartbeatOwnerUnresolved } from "./heartbeat-config-DYNUPVum.js";
import { t as isHeartbeatEnabledForAgent } from "./heartbeat-summary-DTnNVs3l.js";
import { r as persistHeartbeatOutcome } from "./heartbeat-outcome-store-BTmMEGqY.js";
import { o as resolveUserTimezone } from "./date-time-C7YbYLih.js";
import { t as appendCronStyleCurrentTimeLine } from "./current-time-Cwxy-JQb.js";
import { t as resolveEmbeddedSessionLane } from "./lanes-CVttd5qX.js";
import { n as sendDurableMessageBatchCore } from "./send-Dk4bo3m5.js";
import "./runtime-zD0iWK04.js";
import { i as resolveHeartbeatSenderContext, r as resolveHeartbeatDeliveryTargetWithSessionRoute } from "./targets-CP7C4mlG.js";
import { n as withReplySystemEventContext } from "./system-event-session-key-d4mk05_u.js";
import { t as createReplyPrefixContext } from "./reply-prefix-CgWk2S7r.js";
import { t as createTypingCallbacks } from "./typing-CbATUwrs.js";
import { n as resolveHeartbeatTerminalToolFailure, t as resolveHeartbeatReplyPayload } from "./heartbeat-reply-payload-DWyFPqnB.js";
import { t as resolveDefaultModel } from "./directive-handling.defaults-Bi_VGQU-.js";
import { t as prepareReplyConversation } from "./prompt-session-context-DLWyZ1VG.js";
import { a as isExecCompletionEvent, i as isCronSystemEvent, n as buildCronEventPrompt, o as isHeartbeatDeliveryAwarenessEvent, r as buildExecEventPrompt, s as isRelayableExecCompletionEvent, t as HEARTBEAT_DELIVERY_CONTEXT_KEY_PREFIX } from "./heartbeat-events-filter-CEGCW5Gh.js";
import { n as resolveAgentOutboundIdentity } from "./identity-DgagocEO.js";
import { a as resolveIndicatorType, t as emitHeartbeatEvent } from "./heartbeat-events-bg9alNGv.js";
import { t as resolveHeartbeatVisibility } from "./heartbeat-visibility-UVwDVBL7.js";
import { i as readHeartbeatMonitorScratch, o as writeCronJobScratch } from "./scratch-store-BeMQfZeF.js";
import { a as restoreHeartbeatUpdatedAt, i as resolveStaleHeartbeatIsolatedSessionKey, r as resolveHeartbeatSessionSelection, t as resolveHeartbeatSession } from "./heartbeat-runner-session-Daebrrmj.js";
import { n as resolveCronSession } from "./session-ByAcYXZI.js";
import { createHash } from "node:crypto";
//#region src/infra/heartbeat-schedule.ts
function resolveHeartbeatSchedulerSeed(explicitSeed, options = {}) {
const normalized = normalizeOptionalString(explicitSeed);
if (normalized) return normalized;
const env = options.env ?? process.env;
try {
const identity = options.readOnly ? readStoredDeviceIdentityReadOnly({ env }) : loadOrCreateDeviceIdentity({ env });
if (identity) return identity.deviceId;
} catch {}
return createHash("sha256").update(env.HOME ?? "").update("\0").update(process.cwd()).digest("hex");
}
function resolveHeartbeatPhaseMs(params) {
const intervalMs = resolveIntegerOption(params.intervalMs, 1, { min: 1 });
return createHash("sha256").update(`${params.schedulerSeed}:${params.agentId}`).digest().readUInt32BE(0) % intervalMs;
}
//#endregion
//#region src/infra/heartbeat-runner-config.ts
const heartbeatLog = createSubsystemLogger("gateway/heartbeat");
const DEFAULT_HEARTBEAT_TIMEOUT_SECONDS = 600;
function resolveHeartbeatChannelPlugin(channel) {
return getActivePluginChannelRegistry()?.channels.find((entry) => entry.plugin.id === channel)?.plugin ?? getChannelPlugin(channel);
}
function resolveHeartbeatTimeoutOverrideSeconds(cfg, heartbeat) {
if (typeof heartbeat?.timeoutSeconds === "number") return heartbeat.timeoutSeconds;
const agentDefaultTimeoutSeconds = cfg.agents?.defaults?.timeoutSeconds;
if (typeof agentDefaultTimeoutSeconds === "number" && Number.isFinite(agentDefaultTimeoutSeconds)) return agentDefaultTimeoutSeconds === 0 ? 0 : Math.max(1, Math.floor(agentDefaultTimeoutSeconds));
const intervalMs = resolveHeartbeatIntervalMs(cfg, void 0, heartbeat);
if (!intervalMs) return DEFAULT_HEARTBEAT_TIMEOUT_SECONDS;
return Math.max(1, Math.min(DEFAULT_HEARTBEAT_TIMEOUT_SECONDS, Math.ceil(intervalMs / 1e3)));
}
function omitExplicitHeartbeatDestination(heartbeat) {
if (!heartbeat) return;
const next = { ...heartbeat };
delete next.to;
delete next.accountId;
return next;
}
function resolveHeartbeatForWake(params) {
const configuredHeartbeat = params.configuredHeartbeat ?? resolveHeartbeatConfig(params.cfg, params.agentId);
const heartbeat = params.requestedHeartbeat ? {
...configuredHeartbeat,
...params.requestedHeartbeat
} : configuredHeartbeat;
return params.source === "cron" && params.requestedHeartbeat?.target === "last" ? omitExplicitHeartbeatDestination(heartbeat) : heartbeat;
}
function resolveHeartbeatPromptRaw(cfg, heartbeat) {
return heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt;
}
function resolveConfiguredHeartbeatPrompt(cfg, heartbeat) {
return resolveHeartbeatPromptCore(resolveHeartbeatPromptRaw(cfg, heartbeat));
}
function resolveHeartbeatResponseToolPrompt(cfg, heartbeat) {
return resolveHeartbeatPromptForResponseTool(resolveHeartbeatPromptRaw(cfg, heartbeat));
}
function resolveHeartbeatModelRef(params) {
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
cfg: params.cfg,
agentId: params.agentId
});
const heartbeatRaw = normalizeOptionalString(params.heartbeat?.model) ?? normalizeOptionalString(params.cfg.agents?.defaults?.heartbeat?.model) ?? "";
const heartbeatRef = heartbeatRaw ? resolveModelRefFromString({
raw: heartbeatRaw,
defaultProvider,
aliasIndex
})?.ref : void 0;
if (heartbeatRef) return heartbeatRef;
return {
provider: normalizeOptionalString(params.entry?.providerOverride) ?? normalizeOptionalString(params.entry?.modelProvider) ?? defaultProvider,
model: normalizeOptionalString(params.entry?.modelOverride) ?? normalizeOptionalString(params.entry?.model) ?? defaultModel
};
}
function usesCodexHarness(params) {
const modelRef = resolveHeartbeatModelRef(params);
return resolveEffectiveAgentRuntime({
cfg: params.cfg,
provider: modelRef.provider,
modelId: modelRef.model,
agentId: params.agentId,
sessionKey: params.sessionKey,
sessionEntry: params.entry
}) === "codex";
}
function shouldUseHeartbeatResponseToolPrompt(params) {
const chatType = normalizeChatType(params.chatType);
const visibleReplies = chatType === "group" || chatType === "channel" ? params.cfg.messages?.groupChat?.visibleReplies ?? params.cfg.messages?.visibleReplies : params.cfg.messages?.visibleReplies;
if (visibleReplies === "message_tool") return true;
if (visibleReplies === "automatic") return false;
return usesCodexHarness(params);
}
function isHeartbeatTypingEnabled(params) {
if (!params.hasChatDelivery) return false;
return (resolveAgentConfig(params.cfg, params.agentId)?.typingMode ?? params.cfg.agents?.defaults?.typingMode) !== "never";
}
function resolveHeartbeatTypingIntervalSeconds(cfg) {
const configured = cfg.agents?.defaults?.typingIntervalSeconds;
return typeof configured === "number" && configured > 0 ? configured : void 0;
}
//#endregion
//#region src/infra/heartbeat-delivery-normalization.ts
function stripLeadingHeartbeatResponsePrefix(text, responsePrefix) {
const normalizedPrefix = responsePrefix?.trim();
if (!normalizedPrefix) return text;
const prefixPattern = new RegExp(`^${escapeRegExp(normalizedPrefix)}(?=$|\\s|[\\p{P}\\p{S}])\\s*`, "iu");
return text.replace(prefixPattern, "");
}
function isStreamErrorFallbackPlaceholderOnly(text) {
let remaining = text.trim();
if (!remaining) return false;
while (remaining.startsWith(STREAM_ERROR_FALLBACK_TEXT)) remaining = remaining.slice(STREAM_ERROR_FALLBACK_TEXT.length).trimStart();
return remaining.length === 0;
}
const TRAILING_HEARTBEAT_NOTIFY_FALSE_RE = /(?:^|[\r\n])[ \t]*notify=false[ \t]*(?:\r?\n[ \t]*)*$/i;
function stripTrailingHeartbeatNotifyFalse(text) {
const match = TRAILING_HEARTBEAT_NOTIFY_FALSE_RE.exec(text);
return match ? {
text: text.slice(0, match.index).trimEnd(),
silent: true
} : {
text,
silent: false
};
}
function normalizeHeartbeatReply(payload, responsePrefix, ackMaxChars, mode = "heartbeat") {
const textForStrip = stripLeadingHeartbeatResponsePrefix(typeof payload.text === "string" ? payload.text : "", responsePrefix);
const isSilentReply = isSilentReplyPayloadText(textForStrip);
const stripped = stripHeartbeatToken(isSilentReply ? "" : textForStrip, {
mode,
maxAckChars: ackMaxChars
});
const hasMedia = resolveSendableOutboundReplyParts(payload).hasMedia;
const notifyFalse = stripTrailingHeartbeatNotifyFalse(stripped.text);
notifyFalse.silent ||= isSilentReply;
const isInternalPlaceholderOnly = isStreamErrorFallbackPlaceholderOnly(notifyFalse.text);
if ((stripped.shouldSkip || isInternalPlaceholderOnly) && !hasMedia) return {
shouldSkip: true,
text: "",
hasMedia,
isInternalPlaceholderOnly,
...notifyFalse.silent ? { silent: true } : {}
};
let finalText = isInternalPlaceholderOnly ? "" : notifyFalse.text;
if (responsePrefix && finalText && !finalText.startsWith(responsePrefix)) finalText = `${responsePrefix} ${finalText}`;
return {
shouldSkip: !hasMedia && finalText.trim().length === 0,
text: finalText,
hasMedia,
isInternalPlaceholderOnly,
...notifyFalse.silent ? { silent: true } : {}
};
}
function normalizeHeartbeatToolNotification(response, responsePrefix) {
let finalText = getHeartbeatToolNotificationText(response);
if (responsePrefix && finalText && !finalText.startsWith(responsePrefix)) finalText = `${responsePrefix} ${finalText}`;
return {
shouldSkip: finalText.trim().length === 0,
text: finalText,
hasMedia: false,
isInternalPlaceholderOnly: false,
...response.notify ? {} : { silent: true }
};
}
//#endregion
//#region src/infra/heartbeat-failure-notice.ts
/** Deliver a heartbeat failure notice without acknowledging the underlying work. */
async function handleHeartbeatFailureNotice(params) {
await params.restoreUpdatedAt();
const finish = (channel, silent) => {
emitHeartbeatEvent({
status: "failed",
reason: params.reason,
preview: params.preview(params.normalized.text || params.previewText),
durationMs: Date.now() - params.startedAt,
channel,
accountId: params.delivery.accountId,
...silent === true ? { silent: true } : {},
indicatorType: params.useIndicator ? resolveIndicatorType("failed") : void 0
});
return {
status: "failed",
reason: params.reason
};
};
if (params.shouldSkipMain || params.delivery.channel === "none" || !params.delivery.to) return finish(params.delivery.channel !== "none" ? params.delivery.channel : void 0, true);
if (!params.showAlerts) return finish(params.delivery.channel, true);
let readiness;
try {
readiness = await params.checkReady?.();
} catch (error) {
params.onDeliveryError?.(error);
return finish(params.delivery.channel, true);
}
if (readiness && !readiness.ok) {
params.onChannelNotReady(readiness.reason);
return finish(params.delivery.channel, true);
}
let deliveryStatus;
try {
deliveryStatus = await params.deliver?.();
} catch (error) {
params.onDeliveryError?.(error);
}
if (deliveryStatus === "sent") await params.clearSatisfiedPendingFinalDelivery?.();
return finish(params.delivery.channel, deliveryStatus !== "sent" || params.normalized.silent === true);
}
//#endregion
//#region src/infra/heartbeat-wake-policy.ts
function inferHeartbeatWakeSourceFromReason(reason) {
const trimmed = (reason ?? "").trim();
if (trimmed === "exec-event") return "exec-event";
if (trimmed.startsWith("cron:")) return "cron";
if (trimmed === "wake" || trimmed.startsWith("hook:")) return "hook";
if (trimmed.startsWith("acp:spawn:")) return "acp-spawn";
if (trimmed.startsWith("session-state:")) return "session-state";
}
function resolveHeartbeatWakePayloadFlags(params) {
const source = params.source ?? inferHeartbeatWakeSourceFromReason(params.reason);
const reason = (params.reason ?? "").trim();
return {
isExecEventWake: source === "exec-event",
isCronWake: source === "cron",
isWakePayload: source === "hook" || source === "acp-spawn" || source === "session-state" || source === "background-task" || source === "background-task-blocked" || reason === "wake"
};
}
function isTargetedUnscheduledWake(params) {
const hasSessionTarget = normalizeOptionalString(params.sessionKey) !== void 0;
if (!hasSessionTarget && normalizeOptionalString(params.agentId) === void 0) return false;
const reason = params.reason?.trim();
switch (params.source) {
case "cron": return params.intent === "immediate" && (reason?.startsWith("cron:") ?? false);
case "manual":
case "notifications-event":
case "restart-sentinel": return params.intent === "immediate" && hasSessionTarget && reason === "wake";
case "hook": return params.intent === "immediate" && (reason?.startsWith("hook:") ?? false);
case "exec-event": return params.intent === "event" && reason === "exec-event";
case "background-task":
case "background-task-blocked": return params.intent === "immediate";
default: return false;
}
}
function isConfiguredHeartbeatAgent(cfg, agentId) {
const normalized = normalizeAgentId(agentId);
return listAgentIds(cfg).some((candidate) => normalizeAgentId(candidate) === normalized);
}
//#endregion
//#region src/infra/heartbeat-runner-prompt.ts
const log$4 = heartbeatLog;
function truncateHeartbeatPreview(value) {
return value ? truncateUtf16Safe(value, 200) : void 0;
}
function shouldPreflightExecEventWake(source, scheduledEveryMs, scheduledTaskCount) {
return source === "exec-event" && !(typeof scheduledEveryMs === "number" && Number.isSafeInteger(scheduledEveryMs) && scheduledEveryMs > 0) && scheduledTaskCount === 0;
}
async function resolveHeartbeatPreflight(params) {
const wakeFlags = resolveHeartbeatWakePayloadFlags({
source: params.source,
reason: params.reason
});
const session = resolveHeartbeatSessionSelection(params.cfg, params.agentId, params.heartbeat, params.sessionKey);
const pendingEventEntries = selectAgentSystemEvents(peekSystemEventEntries(session.sessionKey), params.agentId).filter((event) => !isHeartbeatDeliveryAwarenessEvent(event));
const turnSourceDeliveryContext = resolveSystemEventDeliveryContext(pendingEventEntries);
const hasTaggedCronEvents = pendingEventEntries.some((event) => event.contextKey?.startsWith("cron:"));
const shouldInspectWakePendingEvents = wakeFlags.isWakePayload && session.inspectsRunQueue;
const shouldInspectPendingEvents = wakeFlags.isExecEventWake || wakeFlags.isCronWake || shouldInspectWakePendingEvents || hasTaggedCronEvents;
const shouldBypassScratchGates = wakeFlags.isExecEventWake || wakeFlags.isCronWake || wakeFlags.isWakePayload || hasTaggedCronEvents;
let monitorScratch;
try {
monitorScratch = readHeartbeatMonitorScratch(resolveCronJobsStorePathFromConfig(params.cfg), params.agentId);
} catch (error) {
log$4.warn(`heartbeat: scratch read failed: ${formatErrorMessage(error)}`);
}
const heartbeatScratchContent = monitorScratch?.state.scratch?.content;
const basePreflight = {
...wakeFlags,
session,
pendingEventEntries,
turnSourceDeliveryContext,
hasTaggedCronEvents,
shouldInspectPendingEvents,
authoritativeScheduledTick: typeof params.scheduledEveryMs === "number" && Number.isSafeInteger(params.scheduledEveryMs) && params.scheduledEveryMs > 0,
...monitorScratch?.jobId ? {
scratchJobId: monitorScratch.jobId,
scratchRevision: monitorScratch.state.currentRevision
} : {},
...!shouldBypassScratchGates && heartbeatScratchContent !== void 0 ? { heartbeatScratchContent } : {}
};
if (wakeFlags.isExecEventWake && !basePreflight.authoritativeScheduledTick && !params.scheduledTasks?.length && !hasTaggedCronEvents && !pendingEventEntries.some((event) => isExecCompletionEvent(event.text))) return {
...basePreflight,
skipReason: HEARTBEAT_SKIP_NO_PENDING_EVENT
};
if (shouldBypassScratchGates) return basePreflight;
if (params.scheduledTasks?.length) return basePreflight;
if (heartbeatScratchContent === void 0) return basePreflight;
if (isHeartbeatContentEffectivelyEmpty(heartbeatScratchContent)) return {
...basePreflight,
skipReason: "empty-heartbeat-file"
};
return basePreflight;
}
/** Appends monitor scratch prose to the generated heartbeat prompt. */
function appendHeartbeatScratch(prompt, heartbeatScratchContent) {
if (!heartbeatScratchContent) return prompt;
const directives = heartbeatScratchContent.trim();
if (!directives || prompt.includes(directives)) return prompt;
return `${prompt}\n\nHeartbeat monitor scratch:\n${directives}`;
}
function resolveHeartbeatRunPrompt(params) {
const pendingEventEntries = params.preflight.pendingEventEntries;
const genericEvents = [];
const cronEvents = [];
const execEvents = [];
const cronNoise = [];
for (const event of pendingEventEntries) if (isExecCompletionEvent(event.text)) {
if (params.preflight.shouldInspectPendingEvents) execEvents.push(event);
} else if (params.preflight.isCronWake || event.contextKey?.startsWith("cron:")) (isCronSystemEvent(event.text) ? cronEvents : cronNoise).push(event);
else genericEvents.push(event);
const hasExecCompletion = execEvents.length > 0;
const hasRelayableExecCompletion = params.canRelayToUser && execEvents.some((event) => isRelayableExecCompletionEvent(event.text));
const hasCronEvents = cronEvents.length > 0;
if (params.scheduledTasks.length > 0) return {
prompt: appendHeartbeatScratch(`Run the following periodic tasks (only those due based on their intervals):
${params.scheduledTasks.map((task) => `- ${task.name}: ${task.prompt}`).join("\n")}
${params.useHeartbeatResponseTool ? `After completing all due tasks:\n${HEARTBEAT_RESPONSE_TOOL_INSTRUCTIONS}` : `After completing all due tasks, reply ${SILENT_REPLY_TOKEN}.`}`, params.heartbeatScratchContent),
hasExecCompletion: false,
hasRelayableExecCompletion: false,
hasCronEvents: false,
usesHeartbeatResponseTool: params.useHeartbeatResponseTool,
genericEvents,
inspectedSystemEventsToConsume: cronNoise
};
const baseUsesHeartbeatResponseTool = params.useHeartbeatResponseTool;
return {
prompt: appendHeartbeatScratch(hasExecCompletion ? buildExecEventPrompt(execEvents.map((event) => event.text), {
deliverToUser: params.canRelayToUser,
useHeartbeatResponseTool: baseUsesHeartbeatResponseTool
}) : hasCronEvents ? buildCronEventPrompt(cronEvents.map((event) => event.text), {
deliverToUser: params.canRelayToUser,
useHeartbeatResponseTool: baseUsesHeartbeatResponseTool
}) : baseUsesHeartbeatResponseTool ? resolveHeartbeatResponseToolPrompt(params.cfg, params.heartbeat) : resolveConfiguredHeartbeatPrompt(params.cfg, params.heartbeat), params.heartbeatScratchContent),
hasExecCompletion,
hasRelayableExecCompletion,
hasCronEvents,
usesHeartbeatResponseTool: baseUsesHeartbeatResponseTool,
genericEvents,
inspectedSystemEventsToConsume: [...cronNoise, ...hasExecCompletion ? execEvents : cronEvents]
};
}
//#endregion
//#region src/infra/heartbeat-runner-delivery.ts
const log$3 = heartbeatLog;
const FIRST_HEARTBEAT_ALERT_PREAMBLE = "First heartbeat alert: your bot runs periodic background checks and messages you only when something needs attention. Set agents.defaults.heartbeat.target: \"none\" to keep these internal.";
const MAX_HEARTBEAT_TARGET_AWARENESS_CHARS = 1e3;
function resolveHeartbeatTargetProjection(params) {
const sessionKey = params.targetSessionKey?.trim();
if (!sessionKey || sessionKey === params.runSessionKey) return;
try {
if (resolveAgentIdFromSessionKey(sessionKey, params.agentId) !== params.agentId) return;
const entry = loadExactSessionEntryReadOnly({
storePath: params.storePath,
sessionKey
})?.entry;
if (!entry?.sessionId) return;
return {
agentId: params.agentId,
sessionKey,
storePath: params.storePath,
expectedSessionId: entry.sessionId,
expectedLifecycleRevision: entry.lifecycleRevision,
idempotencyKey: `${HEARTBEAT_DELIVERY_CONTEXT_KEY_PREFIX}${params.startedAt}:${params.runSessionKey}`
};
} catch (error) {
log$3.warn("heartbeat: failed to resolve existing target session projection", { error: formatErrorMessage(error) });
return;
}
}
function queueHeartbeatTargetAwareness(params) {
try {
const latest = loadExactSessionEntryReadOnly({
storePath: params.projection.storePath,
sessionKey: params.projection.sessionKey
})?.entry;
if (latest?.sessionId !== params.projection.expectedSessionId || latest.lifecycleRevision !== params.projection.expectedLifecycleRevision) return;
const deliveredText = resolveMirroredTranscriptText({
text: params.payload.hookContent ?? resolveOutboundPayloadMirrorText(params.payload),
mediaUrls: params.payload.mediaUrls
});
if (!deliveredText) return;
const text = truncateUtf16Safe(deliveredText, MAX_HEARTBEAT_TARGET_AWARENESS_CHARS);
const suffix = text.length < deliveredText.length ? "\n[truncated]" : "";
enqueueSystemEvent(`A heartbeat delivered this message to this channel:\n${text}${suffix}`, withSystemEventOwner({
sessionKey: params.projection.sessionKey,
contextKey: params.projection.idempotencyKey
}, params.projection.agentId));
} catch (error) {
log$3.warn("heartbeat: failed to queue target session awareness", { error: formatErrorMessage(error) });
}
}
function classifyHeartbeatAgentOutcome(params) {
const { agentRunFailed, heartbeatToolResponse, heartbeatTerminalToolFailure, replyPayload } = params.agentRun;
const replyMetadata = replyPayload ? getReplyPayloadMetadata(replyPayload) : void 0;
const hasExplicitFailure = Boolean(heartbeatTerminalToolFailure || agentRunFailed);
const shouldSuppressSourceReply = params.suppressUnmarkedSourceReplies && !params.hasRelayableExecCompletion && replyPayload && replyPayload.isError !== true && replyMetadata?.deliverDespiteSourceReplySuppression !== true && (!hasExplicitFailure && !heartbeatToolResponse || agentRunFailed && !heartbeatTerminalToolFailure);
if (heartbeatToolResponse && !heartbeatToolResponse.notify && !hasExplicitFailure) return {
kind: "ack",
eventStatus: "ok-token",
preview: truncateHeartbeatPreview(heartbeatToolResponse.summary),
response: heartbeatToolResponse
};
if (shouldSuppressSourceReply && !hasExplicitFailure) return {
kind: "ack",
eventStatus: "ok-token",
silent: true
};
if (!heartbeatToolResponse && !hasExplicitFailure && (!replyPayload || !hasOutboundReplyContent(replyPayload))) return {
kind: "ack",
eventStatus: "ok-empty"
};
const mode = params.hasRelayableExecCompletion ? "message" : "heartbeat";
const normalized = shouldSuppressSourceReply ? {
shouldSkip: true,
text: "",
hasMedia: false,
isInternalPlaceholderOnly: false
} : hasExplicitFailure && replyPayload ? normalizeHeartbeatReply(replyPayload, params.responsePrefix, params.ackMaxChars, mode) : heartbeatToolResponse ? normalizeHeartbeatToolNotification(heartbeatToolResponse, params.responsePrefix) : replyPayload ? normalizeHeartbeatReply(replyPayload, params.responsePrefix, params.ackMaxChars, mode) : {
shouldSkip: true,
text: "",
hasMedia: false,
isInternalPlaceholderOnly: false
};
if (agentRunFailed) {
const replacement = replaceGenericExternalRunFailureText(normalized.text);
if (replacement.replaced) {
normalized.text = replacement.text;
normalized.shouldSkip = false;
}
}
const hasStructuredReplyContent = !shouldSuppressSourceReply && (!heartbeatToolResponse || agentRunFailed) && replyPayload !== void 0 && hasOutboundReplyContent({
...replyPayload,
text: void 0,
mediaUrl: void 0,
mediaUrls: void 0
});
const shouldSkipMain = normalized.shouldSkip && !normalized.hasMedia && (!hasStructuredReplyContent || normalized.isInternalPlaceholderOnly);
if (hasExplicitFailure) return {
kind: "failure",
reason: heartbeatTerminalToolFailure ? "agent-tool-failure" : "agent-runner-failure",
...heartbeatTerminalToolFailure ? { previewText: heartbeatToolResponse?.summary || heartbeatTerminalToolFailure.toolName } : {},
replyPayload: shouldSuppressSourceReply ? void 0 : replyPayload,
normalized,
shouldSkipMain
};
if (shouldSkipMain) return {
kind: "ack",
eventStatus: "ok-token",
silent: normalized.silent && !(mode === "heartbeat" && isSilentReplyPayloadText(replyPayload?.text))
};
return {
kind: "delivery",
response: heartbeatToolResponse,
normalized,
hasStructuredReplyContent,
replyPayload: heartbeatToolResponse ? void 0 : replyPayload,
mediaUrls: heartbeatToolResponse || !replyPayload ? [] : resolveSendableOutboundReplyParts(replyPayload).mediaUrls
};
}
async function finalizeHeartbeatOutcome(params) {
const { cfg, agentId, scheduledTasks, startedAt, wakeSource } = params.wake;
const { delivery, previousUpdatedAt } = params.prepared;
const { runSessionKey, sessionKey, storePath, visibility } = params.prepared;
const stateKey = params.prepared.outboundPolicySessionKey ?? sessionKey;
const stateEntry = loadExactSessionEntryReadOnly({
storePath,
sessionKey: stateKey
})?.entry;
const outcome = params.outcome;
const replyPayloadSource = params.replyPayloadSource;
const pendingFinalIdentity = replyPayloadSource ? getReplyPayloadMetadata(replyPayloadSource)?.pendingFinalDeliveryCompletion : void 0;
const finish = (event, consumeEvents = true) => {
emitHeartbeatEvent({
...event,
durationMs: Date.now() - startedAt,
accountId: delivery.accountId
});
if (consumeEvents && params.wake.preflight.shouldInspectPendingEvents && params.prepared.inspectedSystemEventsToConsume.length) consumeSelectedSystemEventEntries(sessionKey, params.prepared.inspectedSystemEventsToConsume);
return {
status: "ran",
durationMs: Date.now() - startedAt
};
};
const recordOutcome = (response) => persistHeartbeatOutcome({
agentId,
sessionKey,
storePath,
runSessionKey,
response,
taskNames: scheduledTasks.map((task) => task.name),
wakeSource,
wakeReason: params.opts.reason,
occurredAt: startedAt
});
const recordUnconfirmedAlert = (reason) => {
if (outcome.kind !== "delivery" || !outcome.response) return;
const response = outcome.response;
recordOutcome({
...response,
outcome: "blocked",
notify: false,
summary: `Alert delivery was not confirmed for this attempt.\n${response.notificationText ?? response.summary}${response.notificationText ? `\nModel summary: ${response.summary}` : ""}`,
reason: `notify:true; delivery=${reason}; model outcome=${response.outcome}; ${response.reason ?? response.summary}`
});
};
if (outcome.kind === "failure") {
const failureReplyPayload = outcome.replyPayload;
const failureChannel = delivery.channel;
const failureTarget = delivery.to;
const checkReady = (failureChannel !== "none" ? resolveHeartbeatChannelPlugin(failureChannel) : void 0)?.heartbeat?.checkReady;
return await handleHeartbeatFailureNotice({
reason: outcome.reason,
...outcome.previewText ? { previewText: outcome.previewText } : {},
normalized: outcome.normalized,
shouldSkipMain: outcome.shouldSkipMain,
delivery,
showAlerts: visibility.showAlerts,
useIndicator: visibility.useIndicator,
startedAt,
preview: truncateHeartbeatPreview,
restoreUpdatedAt: async () => {
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt
});
},
checkReady: checkReady ? async () => await checkReady({
cfg,
accountId: delivery.accountId,
deps: params.opts.deps
}) : void 0,
deliver: failureChannel !== "none" && failureTarget ? async () => {
const send = await sendDurableMessageBatchCore({
cfg,
channel: failureChannel,
to: failureTarget,
accountId: delivery.accountId,
session: params.outboundSession,
identity: params.outboundIdentity,
threadId: delivery.threadId,
payloads: [copyReplyPayloadMetadata(replyPayloadSource ?? {}, {
...failureReplyPayload,
text: outcome.normalized.text || void 0
})],
deps: params.opts.deps,
silent: outcome.normalized.silent
});
if (send.status === "failed" || send.status === "partial_failed") throw send.error;
return send.status === "sent" ? "sent" : "suppressed";
} : void 0,
clearSatisfiedPendingFinalDelivery: () => clearPendingFinalDeliveryAfterSuccess(pendingFinalIdentity, { preserveActivity: true }),
onChannelNotReady: (reason) => {
log$3.info("heartbeat: channel not ready for failure notice", {
channel: failureChannel,
reason
});
},
onDeliveryError: (error) => {
log$3.warn("heartbeat: failure notice delivery failed", {
channel: failureChannel,
error: formatErrorMessage(error)
});
}
});
}
if (outcome.kind === "ack") {
if ("response" in outcome && outcome.response) recordOutcome(outcome.response);
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt
});
await suppressPendingFinalDelivery(replyPayloadSource, { preserveActivity: true });
const okSent = "silent" in outcome && outcome.silent ? false : await params.maybeSendHeartbeatOk();
return finish({
status: outcome.eventStatus,
reason: params.opts.reason,
..."preview" in outcome ? { preview: outcome.preview } : {},
channel: delivery.channel !== "none" ? delivery.channel : void 0,
silent: !okSent,
indicatorType: visibility.useIndicator ? resolveIndicatorType(outcome.eventStatus) : void 0
});
}
const { hasStructuredReplyContent, mediaUrls, normalized, replyPayload } = outcome;
const prevHeartbeatText = stateEntry?.lastHeartbeatText ?? "";
const prevHeartbeatAt = stateEntry?.lastHeartbeatSentAt;
if (!mediaUrls.length && !hasStructuredReplyContent && Boolean(prevHeartbeatText.trim()) && normalized.text.trim() === prevHeartbeatText.trim() && typeof prevHeartbeatAt === "number" && prevHeartbeatAt <= startedAt && startedAt - prevHeartbeatAt < 864e5) {
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt
});
await suppressPendingFinalDelivery(replyPayloadSource, { preserveActivity: true });
return finish({
status: "skipped",
reason: "duplicate",
preview: truncateHeartbeatPreview(normalized.text),
hasMedia: false,
channel: delivery.channel !== "none" ? delivery.channel : void 0
});
}
const deliveryText = delivery.implicitDefaultRoute && prevHeartbeatAt === void 0 ? `${FIRST_HEARTBEAT_ALERT_PREAMBLE}\n${normalized.text}` : normalized.text;
if (delivery.channel === "none" || !delivery.to) {
recordUnconfirmedAlert(delivery.reason ?? "no-target");
await suppressPendingFinalDelivery(replyPayloadSource, { preserveActivity: true });
return finish({
status: "skipped",
reason: delivery.reason ?? "no-target",
preview: truncateHeartbeatPreview(deliveryText),
hasMedia: mediaUrls.length > 0
});
}
if (!visibility.showAlerts) {
recordUnconfirmedAlert("alerts-disabled");
await suppressPendingFinalDelivery(replyPayloadSource, { preserveActivity: true });
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt
});
return finish({
status: "skipped",
reason: "alerts-disabled",
preview: truncateHeartbeatPreview(deliveryText),
channel: delivery.channel,
hasMedia: mediaUrls.length > 0,
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : void 0
});
}
const deliveryAccountId = delivery.accountId;
const heartbeatPlugin = resolveHeartbeatChannelPlugin(delivery.channel);
if (heartbeatPlugin?.heartbeat?.checkReady) {
const readiness = await heartbeatPlugin.heartbeat.checkReady({
cfg,
accountId: deliveryAccountId,
deps: params.opts.deps
}).catch((error) => ({
ok: false,
reason: formatErrorMessage(error)
}));
if (!readiness.ok) {
recordUnconfirmedAlert(readiness.reason);
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt
});
emitHeartbeatEvent({
status: "skipped",
reason: readiness.reason,
preview: truncateHeartbeatPreview(deliveryText),
durationMs: Date.now() - startedAt,
hasMedia: mediaUrls.length > 0,
channel: delivery.channel,
accountId: delivery.accountId
});
log$3.info("heartbeat: channel not ready", {
channel: delivery.channel,
reason: readiness.reason
});
return {
status: "skipped",
reason: HEARTBEAT_SKIP_CHANNEL_NOT_READY,
retryAtMs: Date.now() + HEARTBEAT_IDLE_RETRY_GRACE_MS
};
}
}
const targetProjection = resolveHeartbeatTargetProjection({
agentId,
storePath,
runSessionKey,
targetSessionKey: delivery.targetSessionKey,
startedAt
});
const send = await sendDurableMessageBatchCore({
cfg,
channel: delivery.channel,
to: delivery.to,
accountId: deliveryAccountId,
session: params.outboundSession,
identity: params.outboundIdentity,
threadId: delivery.threadId,
payloads: [copyReplyPayloadMetadata(replyPayloadSource ?? {}, {
...replyPayload,
text: deliveryText,
mediaUrls
})],
deps: params.opts.deps,
silent: normalized.silent,
onDeliveredPayload: targetProjection ? (payload) => queueHeartbeatTargetAwareness({
projection: targetProjection,
payload
}) : void 0
}).catch((error) => {
recordUnconfirmedAlert(formatErrorMessage(error));
throw error;
});
if (send.status !== "sent") recordUnconfirmedAlert("reason" in send ? send.reason : formatErrorMessage(send.error));
if (send.status === "failed" || send.status === "partial_failed") throw send.error;
const visibleSendSucceeded = send.status === "sent";
await clearPendingFinalDeliveryAfterSuccess(pendingFinalIdentity, { preserveActivity: true });
if (visibleSendSucceeded && deliveryText.trim()) await patchSessionEntryCore({
storePath,
sessionKey: stateKey
}, () => ({
lastHeartbeatText: normalized.text,
lastHeartbeatSentAt: startedAt
}), {
fallbackEntry: mergeSessionEntry(void 0, { updatedAt: startedAt }),
preserveActivity: true
});
const eventStatus = visibleSendSucceeded ? "sent" : "skipped";
return finish({
status: eventStatus,
to: delivery.to,
...!visibleSendSucceeded ? { reason: send.reason } : {},
preview: truncateHeartbeatPreview(deliveryText),
hasMedia: mediaUrls.length > 0,
channel: delivery.channel,
...normalized.silent === true ? { silent: true } : {},
indicatorType: visibility.useIndicator ? resolveIndicatorType(eventStatus) : void 0
}, visibleSendSucceeded);
}
//#endregion
//#region src/infra/heartbeat-active-hours.ts
const ACTIVE_HOURS_TIME_PATTERN = /^(?:([01]\d|2[0-3]):([0-5]\d)|24:00)$/;
/** Resolve the timezone used to evaluate heartbeat active hours. */
function resolveActiveHoursTimezone(cfg, raw) {
const trimmed = raw?.trim();
if (!trimmed || trimmed === "user") return resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
if (trimmed === "local") return Intl.DateTimeFormat().resolvedOptions().timeZone?.trim() || "UTC";
try {
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(/* @__PURE__ */ new Date());
return trimmed;
} catch {
return resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
}
}
function parseActiveHoursTime(opts, raw) {
if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw)) return null;
const [hourStr, minuteStr] = raw.split(":");
const hour = Number(hourStr);
const minute = Number(minuteStr);
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
if (hour === 24) {
if (!opts.allow24 || minute !== 0) return null;
return 1440;
}
return hour * 60 + minute;
}
function resolveMinutesInTimeZone(nowMs, formatter) {
try {
const parts = formatter.formatToParts(new Date(nowMs));
const map = {};
for (const part of parts) if (part.type !== "literal") map[part.type] = part.value;
const hour = Number(map.hour);
const minute = Number(map.minute);
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
return hour * 60 + minute;
} catch {
return null;
}
}
/** Return true when the current time is inside the configured heartbeat window. */
function isWithinActiveHours(cfg, heartbeat, nowMs) {
const active = heartbeat?.activeHours;
if (!active) return true;
const startMin = parseActiveHoursTime({ allow24: false }, active.start);
const endMin = parseActiveHoursTime({ allow24: true }, active.end);
if (startMin === null || endMin === null) return true;
if (startMin === endMin) return false;
const timeZone = resolveActiveHoursTimezone(cfg, active.timezone);
let formatter;
try {
formatter = new Intl.DateTimeFormat("en-US", {
timeZone,
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23"
});
} catch {
return true;
}
const currentMin = resolveMinutesInTimeZone(nowMs ?? Date.now(), formatter);
if (currentMin === null) return true;
return endMin > startMin ? currentMin >= startMin && currentMin < endMin : currentMin >= startMin || currentMin < endMin;
}
//#endregion
//#region src/infra/heartbeat-runner-execution.ts
const log$2 = heartbeatLog;
const CRON_COMMAND_LANE = "cron";
const loadHeartbeatRunnerRuntime = createLazyRuntimeModule(() => import("./heartbeat-runner.runtime.js"));
function hasActiveRunForAgent(agentId, listSessionKeys) {
const normalizedAgentId = normalizeAgentId(agentId);
return listSessionKeys().some((sessionKey) => {
const parsed = parseAgentSessionKey(sessionKey);
return parsed ? normalizeAgentId(parsed.agentId) === normalizedAgentId : false;
});
}
function hasActiveRunForSession(sessionKey, listSessionKeys) {
const normalizedSessionKey = sessionKey.trim();
return Boolean(normalizedSessionKey) && listSessionKeys().includes(normalizedSessionKey);
}
function skippedHeartbeatStage(reason, startedAt) {
emitHeartbeatEvent({
status: "skipped",
reason,
durationMs: Date.now() - startedAt
});
return {
kind: "skipped",
reason
};
}
async function resolveHeartbeatWakeStage(opts) {
const cfg = opts.cfg ?? getRuntimeConfig();
const explicitAgentId = typeof opts.agentId === "string" ? opts.agentId.trim() : "";
const forcedSessionAgentId = explicitAgentId.length > 0 ? void 0 : parseAgentSessionKey(opts.sessionKey)?.agentId;
const resolvedAgentId = explicitAgentId || forcedSessionAgentId || tryResolveAmbientHeartbeatAgentId(cfg);
if (!resolvedAgentId) return {
kind: "skipped",
reason: "disabled"
};
const agentId = normalizeAgentId(resolvedAgentId);
const wakeSource = opts.source ?? inferHeartbeatWakeSourceFromReason(opts.reason);
const heartbeat = resolveHeartbeatForWake({
cfg,
agentId,
requestedHeartbeat: opts.heartbeat,
source: wakeSource
});
const scheduledTasks = [...opts.tasks ?? []].toSorted((left, right) => left.jobId.localeCompare(right.jobId));
const allowsUnscheduledTarget = isTargetedUnscheduledWake(opts) && isConfiguredHeartbeatAgent(cfg, agentId);
if (!areHeartbeatsEnabled()) return {
kind: "skipped",
reason: "disabled"
};
if (!allowsUnscheduledTarget && !isHeartbeatEnabledForAgent(cfg, agentId)) return {
kind: "skipped",
reason: "disabled"
};
if (!allowsUnscheduledTarget && !resolveHeartbeatIntervalMs(cfg, void 0, heartbeat)) return {
kind: "skipped",
reason: "disabled"
};
const startedAt = opts.deps?.nowMs?.() ?? Date.now();
if (!allowsUnscheduledTarget && wakeSource !== "cron" && !isWithinActiveHours(cfg, heartbeat, startedAt)) return skippedHeartbeatStage("quiet-hours", startedAt);
const shouldInspectExecWakeBeforeBusy = shouldPreflightExecEventWake(wakeSource, opts.scheduledEveryMs, scheduledTasks.length);
const resolvePreflight = () => resolveHeartbeatPreflight({
...opts,
cfg,
agentId,
heartbeat,
source: wakeSource,
scheduledTasks
});
let preflight = shouldInspectExecWakeBeforeBusy ? await resolvePreflight() : void 0;
if (preflight?.skipReason) return skippedHeartbeatStage(preflight.skipReason, startedAt);
const getSize = opts.deps?.getQueueSize ?? getQueueSize;
if (getSize("main") > 0) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
const heartbeatWaitOwners = listCronHeartbeatWaitOwners();
const directOwner = opts.owningCronJobMarker && isCronActiveJobMarkerCurrent(opts.owningCronJobMarker) ? opts.owningCronJobMarker : void 0;
const owningCronJobMarkers = [...heartbeatWaitOwners.activeJobMarkers, ...directOwner ? [directOwner] : []];
const cronBusy = owningCronJobMarkers.length > 0 ? hasActiveCronJobsExceptMarkers(owningCronJobMarkers) : hasActiveCronJobs();
const owningCronLaneTaskIds = new Set([...heartbeatWaitOwners.owningCronLaneTaskMarkers, ...directOwner && opts.owningCronLaneTaskMarker ? [opts.owningCronLaneTaskMarker] : []].filter((marker) => marker?.lane === CRON_COMMAND_LANE && isCommandLaneTaskMarkerCurrent(marker)).map((marker) => marker.taskId));
const cronLaneBusy = getSize("cron") > owningCronLaneTaskIds.size || getSize("cron-nested") > 0 || getSize("hook-dispatch") > 0;
if (cronBusy || cronLaneBusy) return skippedHeartbeatStage(HEARTBEAT_SKIP_CRON_IN_PROGRESS, startedAt);
const shouldHonorActiveReplyRuns = opts.intent !== "immediate" && opts.intent !== "manual";
const listActiveReplyRuns = opts.deps?.listActiveReplyRunSessionKeys ?? listActiveReplyRunSessionKeys;
const listActiveEmbeddedRuns = opts.deps?.listActiveEmbeddedRunSessionKeys ?? listActiveEmbeddedRunSessionKeys;
if (shouldHonorActiveReplyRuns && (hasActiveRunForAgent(agentId, listActiveReplyRuns) || hasActiveRunForAgent(agentId, listActiveEmbeddedRuns))) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
const { sessionKey: recentSessionKey, entry: recentSessionEntry } = resolveHeartbeatSession(cfg, agentId, heartbeat, opts.sessionKey);
const lifecycleGeneration = getAgentEventLifecycleGeneration();
const mainSessionRecovery = opts.intent !== "manual" && recentSessionEntry ? transitionMainSessionRecovery(recentSessionEntry, {
kind: "inspect",
lifecycleGeneration,
sessionKey: recentSessionKey
}) : void 0;
const activeRestartRecoveryRunId = normalizeOptionalString(recentSessionEntry?.restartRecoveryDeliveryRunId);
const hasCurrentRestartRecoveryDelivery = opts.intent !== "manual" && activeRestartRecoveryRunId !== void 0 && recentSessionEntry?.restartRecoveryRuns?.some((run) => run.runId === activeRestartRecoveryRunId && run.lifecycleGeneration === lifecycleGeneration) === true;
if (mainSessionRecovery?.kind === "observed" && (mainSessionRecovery.view.status === "blocked" || mainSessionRecovery.view.status === "recoverable") || hasCurrentRestartRecoveryDelivery) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
const HEARTBEAT_DEFER_WINDOW_MS = 3e4;
const pendingFinalDeliveryText = recentSessionEntry?.pendingFinalDelivery?.kind === "replayable" ? recentSessionEntry.pendingFinalDelivery.text : void 0;
const pendingFinalDeliveryIsHeartbeatAck = typeof pendingFinalDeliveryText === "string" && isHeartbeatAcknowledgementText(pendingFinalDeliveryText);
if (recentSessionEntry?.pendingFinalDelivery !== void 0 && !pendingFinalDeliveryIsHeartbeatAck && recentSessionEntry?.updatedAt && startedAt - recentSessionEntry.updatedAt < HEARTBEAT_DEFER_WINDOW_MS) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
if (!preflight) preflight = await resolvePreflight();
if (preflight.skipReason) return skippedHeartbeatStage(preflight.skipReason, startedAt);
const { sessionKey } = preflight.session;
const isReplyRunActive = opts.deps?.isReplyRunActive ?? ((key) => replyRunRegistry.isActive(key));
if (isReplyRunActive(sessionKey) || hasActiveRunForSession(sessionKey, listActiveEmbeddedRuns)) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
if (getSize(resolveEmbeddedSessionLane(sessionKey)) > 0) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
return {
kind: "ready",
cfg,
agentId,
wakeSource,
heartbeat,
scheduledTasks,
startedAt,
listActiveEmbeddedRuns,
isReplyRunActive,
preflight
};
}
async function prepareHeartbeatRunStage(wake) {
const { cfg, agentId, heartbeat, preflight } = wake;
const { scheduledTasks, startedAt } = wake;
const { listActiveEmbeddedRuns, isReplyRunActive } = wake;
const { entry, sessionKey, run, conversationEntry } = preflight.session;
const previousUpdatedAt = entry?.updatedAt;
const delivery = await resolveHeartbeatDeliveryTargetWithSessionRoute({
cfg,
agentId,
entry: conversationEntry,
heartbeat,
currentSessionKey: sessionKey,
turnSource: preflight.session.inspectsRunQueue ? preflight.turnSourceDeliveryContext : void 0
});
if (delivery.channel === "none" && delivery.reason === "no-route" && (wake.wakeSource === void 0 || wake.wakeSource === "interval") && preflight.pendingEventEntries.length === 0 && scheduledTasks.length === 0) return skippedHeartbeatStage("no-route", startedAt);
const heartbeatAccountId = heartbeat?.accountId?.trim();
if (delivery.reason === "unknown-account") log$2.warn("heartbeat: unknown accountId", {
accountId: delivery.accountId ?? heartbeatAccountId ?? null,
target: heartbeat?.target ?? "owner"
});
else if (heartbeatAccountId) log$2.info("heartbeat: using explicit accountId", {
accountId: delivery.accountId ?? heartbeatAccountId,
target: heartbeat?.target ?? "owner",
channel: delivery.channel
});
const visibility = delivery.channel !== "none" ? resolveHeartbeatVisibility({
cfg,
channel: delivery.channel,
accountId: delivery.accountId
}) : {
showOk: false,
showAlerts: true,
useIndicator: true
};
const { sender } = resolveHeartbeatSenderContext({
cfg,
entry,
delivery
});
const replyPrefix = createReplyPrefixContext({
cfg,
agentId,
channel: delivery.channel !== "none" ? delivery.channel : void 0,
accountId: delivery.accountId
});
const canRelayToUser = Boolean(delivery.channel !== "none" && delivery.to && visibility.showAlerts);
let useHeartbeatResponseToolPrompt = shouldUseHeartbeatResponseToolPrompt({
cfg,
agentId,
heartbeat,
entry,
sessionKey,
chatType: delivery.chatType
});
let heartbeatRunPrompt = resolveHeartbeatRunPrompt({
cfg,
heartbeat,
preflight,
canRelayToUser,
startedAt,
scheduledTasks,
heartbeatScratchContent: preflight.heartbeatScratchContent,
useHeartbeatResponseTool: useHeartbeatResponseToolPrompt
});
const runSessionKey = run.sessionKey;
let runSessionEntry = entry;
let outboundPolicySessionKey;
if (run.kind === "isolated") {
const { sessionKey: isolatedSessionKey, baseSessionKey: isolatedBaseSessionKey } = run;
const isolatedStorePath = preflight.session.storePath;
const staleIsolatedSessionKey = resolveStaleHeartbeatIsolatedSessionKey({
sessionKey,
isolatedSessionKey,
isolatedBaseSessionKey
});
if (isReplyRunActive(isolatedSessionKey) || hasActiveRunForSession(isolatedSessionKey, listActiveEmbeddedRuns)) return skippedHeartbeatStage(HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT, startedAt);
const staleIsolatedEntry = staleIsolatedSessionKey ? loadExactSessionEntry({
storePath: isolatedStorePath,
sessionKey: staleIsolatedSessionKey
})?.entry : void 0;
const removals = staleIsolatedSessionKey ? [{
sessionKey: staleIsolatedSessionKey,
...staleIsolatedEntry ? { expectedEntry: staleIsolatedEntry } : {},
...staleIsolatedEntry?.sessionId ? { expectedSessionId: staleIsolatedEntry.sessionId } : {},
archiveRemovedTranscript: true
}] : [];
const lifecycleResult = await applySessionEntryLifecycleMutation({
activeSessionKey: isolatedSessionKey,
storePath: isolatedStorePath,
removals,
upserts: [{
sessionKey: isolatedSessionKey,
buildEntry: ({ currentEntry }) => {
const nextEntry = {
...resolveCronSession({
cfg,
sessionKey: isolatedSessionKey,
agentId,
nowMs: startedAt,
forceNew: true,
store: currentEntry ? { [isolatedSessionKey]: currentEntry } : {}
}).sessionEntry,
heartbeatIsolatedBaseSessionKey: isolatedBaseSessionKey
};
runSessionEntry = nextEntry;
return nextEntry;
}
}],
captureArtifactCleanupError: true
});
if (lifecycleResult.artifactCleanupError) log$2.warn("heartbeat: failed to archive stale isolated session transcript", {
err: formatErrorMessage(lifecycleResult.artifactCleanupError),
sessionKey: staleIsolatedSessionKey
});
outboundPolicySessionKey = isolatedBaseSessionKey;
const actualUseHeartbeatResponseToolPrompt = shouldUseHeartbeatResponseToolPrompt({
cfg,
agentId,
heartbeat,
entry: runSessionEntry,
sessionKey: runSessionKey,
chatType: delivery.chatType
});
if (actualUseHeartbeatResponseToolPrompt !== useHeartbeatResponseToolPrompt) {
useHeartbeatResponseToolPrompt = actualUseHeartbeatResponseToolPrompt;
heartbeatRunPrompt = resolveHeartbeatRunPrompt({
cfg,
heartbeat,
preflight,
canRelayToUser,
startedAt,
scheduledTasks,
heartbeatScratchContent: preflight.heartbeatScratchContent,
useHeartbeatResponseTool: useHeartbeatResponseToolPrompt
});
}
}
return {
kind: "ready",
...preflight.session,
previousUpdatedAt,
delivery,
visibility,
sender,
replyPrefix,
runSessionKey,
outboundPolicySessionKey,
...heartbeatRunPrompt
};
}
async function invokeHeartbeatAgentRun(opts, wake, prepared) {
const { cfg, agentId, heartbeat, startedAt, preflight } = wake;
const { delivery, hasExecCompletion, hasCronEvents, prompt } = prepared;
const { replyPrefix, runSessionKey, sender, suppressOriginatingContext } = prepared;
const { usesHeartbeatResponseTool } = prepared;
const replyOperationRunState = {};
const heartbeatModelOverride = normalizeOptionalString(heartbeat?.model);
const getReplyFromConfig = opts.deps?.getReplyFromConfig ?? (await loadHeartbeatRunnerRuntime()).getHeartbeatReplyFromConfig;
const heartbeatWakeAbortSignal = getHeartbeatWakeAbortSignal();
const heartbeatContext = {
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
From: sender,
To: sender,
OriginatingChannel: !suppressOriginatingContext && delivery.channel !== "none" ? delivery.channel : void 0,
OriginatingTo: !suppressOriginatingContext ? delivery.to : void 0,
AccountId: delivery.accountId,
ChatType: delivery.chatType,
MessageThreadId: delivery.threadId,
InternalTurnSource: hasExecCompletion ? "exec" : hasCronEvents ? "cron" : "heartbeat",
SessionKey: runSessionKey,
AgentId: agentId
};
const replyResult = await getReplyFromConfig(heartbeatContext, withReplySystemEventContext({
isHeartbeat: true,
replyConversation: prepareReplyConversation({
ctx: heartbeatContext,
sessionEntry: suppressOriginatingContext ? void 0 : prepared.conversationEntry,
isHeartbeat: true
}),
[REPLY_OPERATION_RUN_STATE]: replyOperationRunState,
...heartbeatModelOverride ? { heartbeatModelOverride } : {},
...usesHeartbeatResponseTool ? {
enableHeartbeatTool: true,
forceHeartbeatTool: true
} : {},
...usesHeartbeatResponseTool ? { sourceReplyDeliveryMode: "message_tool_only" } : {},
...heartbeatWakeAbortSignal ? { abortSignal: heartbeatWakeAbortSignal } : {},
timeoutOverrideSeconds: resolveHeartbeatTimeoutOverrideSeconds(cfg, heartbeat),
bootstrapContextMode: heartbeat?.lightContext === true ? "lightweight" : void 0,
onModelSelected: replyPrefix.onModelSelected
}, {
sessionKey: prepared.inspectsRunQueue ? prepared.sessionKey : runSessionKey,
events: prepared.inspectsRunQueue ? prepared.genericEvents : []
}), cfg);
const agentTurnStatus = resolveReplyOperationAgentTurn(replyOperationRunState);
if (agentTurnStatus === "superseded" || agentTurnStatus === "cancelled") return { kind: agentTurnStatus === "superseded" ? "preempted" : "cancelled" };
const heartbeatToolResponse = resolveHeartbeatToolResponseFromReplyResult(replyResult);
const heartbeatScratchProposal = resolveHeartbeatScratchProposalFromReplyResult(replyResult);
const heartbeatTerminalToolFailure = resolveHeartbeatTerminalToolFailure(replyResult);
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
const agentRunFailed = agentTurnStatus === "failed";
if (heartbeatScratchProposal !== void 0 && heartbeatToolResponse && !heartbeatTerminalToolFailure) {
if (!preflight.scratchJobId) log$2.warn("heartbeat: scratch update ignored because no monitor job exists");
else try {
if (!writeCronJobScratch({
storePath: resolveCronJobsStorePathFromConfig(cfg),
jobId: preflight.scratchJobId,
content: heartbeatScratchProposal,
expectedRevision: preflight.scratchRevision ?? 0
}).ok) log$2.warn("heartbeat: scratch update lost a concurrent revision race");
} catch (error) {
log$2.warn(`heartbeat: scratch update failed: ${formatErrorMessage(error)}`);
}
}
if (!heartbeatToolResponse && (!replyPayload || !hasOutboundReplyContent(replyPayload)) && replyOperationRunState.admission?.status === "skipped" && replyOperationRunState.admission.reason === "active-run") return { kind: "busy" };
return {
kind: "completed",
heartbeatToolResponse,
heartbeatTerminalToolFailure,
agentRunFailed,
replyPayload
};
}
//#endregion
//#region src/infra/heartbeat-typing.ts
const DEFAULT_HEARTBEAT_TYPING_INTERVAL_SECONDS = 6;
/** Create typing start/stop/keepalive callbacks for a heartbeat delivery target. */
function createHeartbeatTypingCallbacks(params) {
const sendTyping = params.plugin?.heartbeat?.sendTyping;
const to = params.target.to?.trim();
if (!sendTyping || !to) return;
const clearTyping = params.plugin?.heartbeat?.clearTyping;
const keepaliveIntervalMs = typeof params.typingIntervalSeconds === "number" && params.typingIntervalSeconds > 0 ? params.typingIntervalSeconds * 1e3 : DEFAULT_HEARTBEAT_TYPING_INTERVAL_SECONDS * 1e3;
const target = {
cfg: params.cfg,
to,
...params.target.accountId !== void 0 ? { accountId: params.target.accountId } : {},
...params.target.threadId !== void 0 ? { threadId: params.target.threadId } : {},
...params.deps ? { deps: params.deps } : {}
};
return createTypingCallbacks({
start: async () => {
await sendTyping(target);
},
...clearTyping ? { stop: async () => {
await clearTyping(target);
} } : {},
...keepaliveIntervalMs ? { keepaliveIntervalMs } : {},
onStartError: (err) => {
params.log?.debug?.(`heartbeat typing failed for ${params.target.channel}`, {
error: String(err),
channel: params.target.channel,
accountId: params.target.accountId
});
}
});
}
//#endregion
//#region src/infra/heartbeat-runner-run.ts
const log$1 = heartbeatLog;
async function runHeartbeatOnce(opts) {
const wake = await resolveHeartbeatWakeStage(opts);
if (wake.kind === "skipped") return {
status: "skipped",
reason: wake.reason
};
const prepared = await prepareHeartbeatRunStage(wake);
if (prepared.kind === "skipped") return {
status: "skipped",
reason: prepared.reason
};
const { cfg, agentId, startedAt } = wake;
const { delivery, visibility, replyPrefix, runSessionKey } = prepared;
const { outboundPolicySessionKey, hasRelayableExecCompletion } = prepared;
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
emitHeartbeatEvent({
status: "skipped",
reason: "alerts-disabled",
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : void 0,
accountId: delivery.accountId
});
return {
status: "skipped",
reason: "alerts-disabled"
};
}
const resolveHeartbeatResponsePrefix = () => resolveResponsePrefixTemplate(replyPrefix.responsePrefix, replyPrefix.responsePrefixContextProvider());
const resolveHeartbeatOkText = () => {
const responsePrefix = resolveHeartbeatResponsePrefix();
return responsePrefix ? `${responsePrefix} ${HEARTBEAT_TOKEN}` : HEARTBEAT_TOKEN;
};
const outboundSession = buildOutboundSessionContext({
cfg,
agentId,
sessionKey: runSessionKey,
policySessionKey: outboundPolicySessionKey
});
const outboundIdentity = resolveAgentOutboundIdentity(cfg, agentId);
const hasChatDelivery = Boolean(delivery.channel !== "none" && delivery.to && (visibility.showAlerts || visibility.showOk));
const heartbeatTypingIntervalSeconds = resolveHeartbeatTypingIntervalSeconds(cfg);
const heartbeatChannelPlugin = delivery.channel !== "none" ? resolveHeartbeatChannelPlugin(delivery.channel) : void 0;
const heartbeatTyping = delivery.channel !== "none" && isHeartbeatTypingEnabled({
cfg,
agentId,
hasChatDelivery
}) ? createHeartbeatTypingCallbacks({
cfg,
target: {
channel: delivery.channel,
...delivery.to !== void 0 ? { to: delivery.to } : {},
...delivery.accountId !== void 0 ? { accountId: delivery.accountId } : {},
...delivery.threadId !== void 0 ? { threadId: delivery.threadId } : {}
},
...heartbeatChannelPlugin ? { plugin: heartbeatChannelPlugin } : {},
...opts.deps ? { deps: opts.deps } : {},
...heartbeatTypingIntervalSeconds !== void 0 ? { typingIntervalSeconds: heartbeatTypingIntervalSeconds } : {},
log: log$1
}) : void 0;
const maybeSendHeartbeatOk = async () => {
if (!visibility.showOk || delivery.channel === "none" || !delivery.to) return false;
try {
const heartbeatPlugin = resolveHeartbeatChannelPlugin(delivery.channel);
if (heartbeatPlugin?.heartbeat?.checkReady) {
if (!(await heartbeatPlugin.heartbeat.checkReady({
cfg,
accountId: delivery.accountId,
deps: opts.deps
})).ok) return false;
}
const send = await sendDurableMessageBatchCore({
cfg,
channel: delivery.channel,
to: delivery.to,
accountId: delivery.accountId,
threadId: delivery.threadId,
payloads: [{ text: resolveHeartbeatOkText() }],
session: outboundSession,
identity: outboundIdentity,
deps: opts.deps
});
if (send.status === "failed" || send.status === "partial_failed") throw send.error;
return send.status === "sent";
} catch (err) {
log$1.warn(`heartbeat: HEARTBEAT_OK delivery failed: ${formatErrorMessage(err)}`);
return false;
}
};
try {
await heartbeatTyping?.onReplyStart();
const agentRun = await invokeHeartbeatAgentRun(opts, wake, prepared);
if (agentRun.kind !== "completed") {
const reason = agentRun.kind === "busy" ? HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT : agentRun.kind === "preempted" ? HEARTBEAT_SKIP_PREEMPTED : "agent-runner-cancelled";
emitHeartbeatEvent({
status: "skipped",
reason,
durationMs: Date.now() - startedAt
});
return {
status: "skipped",
reason
};
}
return await finalizeHeartbeatOutcome({
opts,
wake,
prepared,
outcome: classifyHeartbeatAgentOutcome({
agentRun,
hasRelayableExecCompletion,
suppressUnmarkedSourceReplies: resolveSourceReplyDeliveryMode({
cfg,
ctx: {
ChatType: delivery.chatType,
Provider: delivery.channel
}
}) === "message_tool_only",
responsePrefix: resolveHeartbeatResponsePrefix(),
ackMaxChars: 300
}),
replyPayloadSource: agentRun.replyPayload,
maybeSendHeartbeatOk,
outboundSession,
outboundIdentity
});
} catch (err) {
const reason = formatErrorMessage(err);
emitHeartbeatEvent({
status: "failed",
reason,
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : void 0,
accountId: delivery.accountId,
indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : void 0
});
log$1.error(`heartbeat failed: ${reason}`, { error: reason });
return {
status: "failed",
reason
};
} finally {
heartbeatTyping?.onCleanup?.();
}
}
//#endregion
//#region src/infra/heartbeat-cooldown.ts
const DEFAULT_MIN_WAKE_SPACING_MS = 3e4;
const DEFAULT_FLOOD_WINDOW_MS = 6e4;
const DEFAULT_FLOOD_THRESHOLD = 5;
/**
* Decide whether an incoming wake should be deferred.
*
* The decision matrix:
*
* | Wake intent | First wake (no prior run) | Subsequent wakes |
* |---------------|----------------------------|-----------------------------------------|
* | manual | Run | Run (never deferred) |
* | immediate | Run | Run (never deferred, except flood) |
* | scheduled | Defer if now < nextDueMs | Defer if now < nextDueMs |
* | task | Run | Defer only within floor or on flood |
* | event | Run (bootstrap responsive) | Defer if now < nextDueMs OR within floor |
*
* Immediate is for documented wake-now delivery paths such as `openclaw system
* event --mode now`, task completion follow-ups, cron `--wake now`, and
* `/hooks/wake mode=now`. Event is for external/system notifications such as
* background exec exits, node notification changes, hook/cron next-heartbeat
* handoffs, ACP spawn stream updates, and retry wakes.
*
* Additional gates layered on top of the reason matrix:
*
* 1. **Minimum spacing floor** (`min-spacing`): even if `nextDueMs` has been
* passed, defer if a run started within the last `minSpacingMs`. Catches
* the race where a second wake arrives between `runOnce` returning and
* `advanceAgentSchedule` updating `nextDueMs`.
* 2. **Flood guard** (`flood`): if `recentRunStarts` shows ≥ `floodThreshold`
* runs within `floodWindowMs`, defer regardless of reason (except
* `manual`-class immediate intent). Caller should also emit a single
* warning log when this fires.
*/
function shouldDeferWake(input) {
if (input.intent === "manual") return { defer: false };
if (input.intent === "immediate") return checkFloodGuard(input) ?? { defer: false };
if (input.intent === "task") {
const floodDefer = checkFloodGuard(input);
if (floodDefer) return floodDefer;
const spacingRetryAtMs = resolveMinSpacingRetryAtMs(input);
if (spacingRetryAtMs !== void 0) return {
defer: true,
reason: "min-spacing",
retryAtMs: spacingRetryAtMs
};
return { defer: false };
}
const floodDefer = checkFloodGuard(input);
if (floodDefer) return floodDefer;
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.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) {
const minSpacing = input.minSpacingMs ?? DEFAULT_MIN_WAKE_SPACING_MS;
if (minSpacing <= 0 || input.lastRunStartedAtMs === void 0) return;
const retryAtMs = input.lastRunStartedAtMs + minSpacing;
return input.now < retryAtMs ? retryAtMs : void 0;
}
function checkFloodGuard(input) {
const floodWindow = input.floodWindowMs ?? DEFAULT_FLOOD_WINDOW_MS;
const floodThreshold = input.floodThreshold ?? DEFAULT_FLOOD_THRESHOLD;
if (!input.recentRunStarts || input.recentRunStarts.length < floodThreshold || floodWindow <= 0) return null;
const windowStart = input.now - floodWindow;
let inWindow = 0;
let thresholdOldestTs;
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 === floodThreshold) thresholdOldestTs = ts;
}
return inWindow >= floodThreshold && thresholdOldestTs !== void 0 ? {
defer: true,
reason: "flood",
retryAtMs: thresholdOldestTs + floodWindow + 1
} : null;
}
/**
* Append a run-start timestamp to a bounded recent-runs buffer. Caller passes
* the previous buffer; this returns a new (mutated) buffer with the entry
* appended and trimmed to `floodThreshold + 1` entries (only the newest matter
* for flood detection).
*/
function recordRunStart(buffer, ts, floodThreshold = DEFAULT_FLOOD_THRESHOLD) {
buffer.push(ts);
const max = floodThreshold + 1;
while (buffer.length > max) buffer.shift();
return buffer;
}
//#endregion
//#region src/infra/heartbeat-runner-scheduler.ts
const log = heartbeatLog;
function startHeartbeatRunner(opts) {
const runtime = opts.runtime ?? defaultRuntime;
const runOnce = opts.runOnce ?? runHeartbeatOnce;
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,
reason,
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) {
log.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) log.info("heartbeat: started", { intervalMs: Math.min(...intervals) });
else {
log.info("heartbeat: disabled", { enabled: false });
if (isHeartbeatOwnerUnresolved(cfg)) log.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) return {
status: "skipped",
reason: "disabled"
};
if (!areHeartbeatsEnabled()) return {
status: "skipped",
reason: "disabled"
};
const reason = params.reason;
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 allowsUnscheduledTarget = requestedTargetAgentId !== void 0 && isConfiguredHeartbeatAgent(wakeConfig, requestedTargetAgentId) && isTargetedUnscheduledWake({
source: params.source,
intent,
reason,
agentId: requestedAgentId,
sessionKey: requestedSessionKey
});
const enrolledAgents = Array.from(state.agents.values()).filter((agent) => agent.intervalMs !== void 0);
if (enrolledAgents.length === 0 && !allowsUnscheduledTarget) return {
status: "skipped",
reason: "disabled"
};
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 {
ran: false,
result: {
status: "skipped",
reason: deferral.reason,
retryAtMs: deferral.retryAtMs
}
};
}
const useEnrolledHeartbeat = !targeted || (isInterval || authoritativeScheduledTick) && !requestedSessionKey && !requestedHeartbeat;
let res;
try {
res = await runOnce({
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 }
});
} catch (err) {
const errMsg = formatErrorMessage(err);
log.error(`heartbeat runner: runOnce threw unexpectedly: ${errMsg}`, {
error: errMsg,
agentId
});
recordRunBookkeeping(agent, now);
return {
ran: false,
result: {
status: "failed",
reason: errMsg
}
};
}
if (res.status === "skipped" && isRetryableHeartbeatSkipReason(res.reason)) return {
ran: false,
retryableSkip: res
};
if (params.source === "exec-event" && res.status === "skipped" && res.reason === "no-pending-event") return {
ran: false,
result: res
};
recordRunBookkeeping(agent, now);
return {
ran: res.status === "ran",
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 && !allowsUnscheduledTarget) return {
status: "skipped",
reason: "disabled"
};
if (!targetAgent) {
targetAgent = createAgentState(targetAgentId, now);
state.agents.set(targetAgentId, targetAgent);
}
const outcome = await runOneAgent(targetAgent, true);
if (outcome.retryableSkip) return outcome.retryableSkip;
return outcome.ran ? {
status: "ran",
durationMs: Date.now() - startedAt
} : outcome.result ?? {
status: "skipped",
reason: "not-due"
};
}
const agentOutcomes = await Promise.all(enrolledAgents.map((agent) => runOneAgent(agent)));
let ran = false;
let firstResult;
let firstGuardSkip;
for (const outcome of agentOutcomes) {
if (outcome.retryableSkip) return outcome.retryableSkip;
ran ||= outcome.ran;
firstResult ??= outcome.result;
const result = outcome.result;
if (!ran && result?.status === "skipped" && result.retryAtMs !== void 0 && (!firstGuardSkip || result.retryAtMs < (firstGuardSkip.retryAtMs ?? Infinity))) firstGuardSkip = result;
}
if (ran) return {
status: "ran",
durationMs: Date.now() - startedAt
};
return firstGuardSkip ?? firstResult ?? {
status: "skipped",
reason: isInterval ? "not-due" : "disabled"
};
};
const wakeHandler = async (params) => run({
reason: params.reason,
agentId: params.agentId,
sessionKey: params.sessionKey,
heartbeat: params.heartbeat,
scheduledEveryMs: params.scheduledEveryMs,
tasks: params.tasks,
retainedWork: params.retainedWork,
source: params.source,
intent: params.intent
});
const disposeWakeHandler = setHeartbeatWakeHandler(wakeHandler);
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 { resolveHeartbeatTimeoutOverrideSeconds as a, resolveHeartbeatForWake as i, runHeartbeatOnce as n, resolveHeartbeatPhaseMs as o, resolveConfiguredHeartbeatPrompt as r, resolveHeartbeatSchedulerSeed as s, startHeartbeatRunner as t };