openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
221 lines (220 loc) • 7.66 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { j as resolveIntegerOption } from "./number-coercion-CkhMiRw8.mjs";
import { t as HEARTBEAT_DECLARATION_PREFIX } from "./system-owned-declaration-BdxGzmHz.mjs";
import { o as readStoredDeviceIdentityReadOnly } from "./device-identity-store-BO1tgcFb.mjs";
import { r as loadOrCreateDeviceIdentity } from "./device-identity-CKtmS8Us.mjs";
import "./heartbeat-nZ1C5oCa.mjs";
import { n as loadOrCreateDeviceIdentityAsync } from "./device-identity-async-D7c_3lSx.mjs";
import { o as resolveHeartbeatIntervalMs, r as resolveHeartbeatAgents } from "./heartbeat-config-BsKBfvT-.mjs";
import { isDeepStrictEqual } from "node:util";
import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
//#region src/infra/heartbeat-schedule.ts
/** Doctor previews and migrations retain their synchronous one-shot admission. */
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 fallbackSchedulerSeed(env);
}
async function resolveHeartbeatSchedulerSeedAsync(explicitSeed, options = {}) {
const normalized = normalizeOptionalString(explicitSeed);
if (normalized) return normalized;
const env = options.env ?? process.env;
const fallback = fallbackSchedulerSeed(env);
try {
return (await loadOrCreateDeviceIdentityAsync({ env })).deviceId;
} catch {}
return fallback;
}
function fallbackSchedulerSeed(env) {
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/cron/system-monitor-jobs.ts
function partitionSystemMonitors(jobs, resolveAgentId) {
const retained = /* @__PURE__ */ new Map();
const duplicates = [];
for (const job of jobs) {
const agentId = resolveAgentId(job);
if (!agentId) continue;
const current = retained.get(agentId);
if (!current) retained.set(agentId, job);
else if (job.updatedAtMs > current.updatedAtMs) {
retained.set(agentId, job);
duplicates.push({
agentId,
job: current
});
} else duplicates.push({
agentId,
job
});
}
return {
retained,
duplicates
};
}
//#endregion
//#region src/cron/heartbeat-monitor.ts
/** Canonical projection from heartbeat config to system-owned cron monitor jobs. */
function heartbeatMonitorDeclarationKey(agentId) {
return `${HEARTBEAT_DECLARATION_PREFIX}${agentId}`;
}
function heartbeatMonitorAgentId(job) {
const key = job.declarationKey;
if (!key?.startsWith("heartbeat:") || job.payload.kind !== "heartbeat") return;
return key.slice(10) || void 0;
}
/** Keeps declarative upserts scoped to the exact system-owned monitor. */
function heartbeatMonitorAddOptions(agentId) {
return {
enabledExplicit: true,
systemOwned: true,
matchesExisting: (job) => heartbeatMonitorAgentId(job) === agentId
};
}
function heartbeatMonitorDeclarativeFields(job) {
return {
declarationKey: job.declarationKey,
name: job.name,
agentId: job.agentId,
schedule: job.schedule,
pacing: job.pacing,
trigger: job.trigger,
payload: job.payload,
delivery: job.delivery,
displayName: job.displayName,
enabled: job.enabled,
sessionTarget: job.sessionTarget,
wakeMode: job.wakeMode
};
}
/** Projects configured monitor state and its create/update/remove changes together. */
function resolveHeartbeatMonitorPlan(cfg, existingJobs, options) {
const { retained: existingByAgentId, duplicates } = partitionSystemMonitors(existingJobs, heartbeatMonitorAgentId);
const { schedulerSeed } = options;
const specs = resolveHeartbeatAgents(cfg).flatMap((agent) => {
const configuredIntervalMs = resolveHeartbeatIntervalMs(cfg, void 0, agent.heartbeat);
const existing = existingByAgentId.get(agent.agentId);
const intervalMs = configuredIntervalMs ?? (existing?.schedule.kind === "every" ? existing.schedule.everyMs : void 0) ?? resolveHeartbeatIntervalMs(cfg, "30m", agent.heartbeat);
if (!intervalMs) return [];
return [{
agentId: agent.agentId,
input: {
declarationKey: heartbeatMonitorDeclarationKey(agent.agentId),
displayName: `Heartbeat (${agent.agentId})`,
name: `heartbeat-${agent.agentId}`,
agentId: agent.agentId,
enabled: configuredIntervalMs !== null,
schedule: {
kind: "every",
everyMs: intervalMs,
anchorMs: resolveHeartbeatPhaseMs({
schedulerSeed,
agentId: agent.agentId,
intervalMs
})
},
payload: { kind: "heartbeat" },
sessionTarget: "main",
wakeMode: "next-heartbeat"
}
}];
});
const changes = duplicates.map(({ agentId, job }) => ({
kind: "remove",
agentId,
job
}));
for (const spec of specs) {
const existing = existingByAgentId.get(spec.agentId);
if (!existing) {
changes.push({
kind: "create",
...spec
});
continue;
}
existingByAgentId.delete(spec.agentId);
if (!isDeepStrictEqual(heartbeatMonitorDeclarativeFields(existing), heartbeatMonitorDeclarativeFields(spec.input))) changes.push({
kind: "update",
...spec
});
}
for (const [agentId, job] of existingByAgentId) changes.push({
kind: "remove",
agentId,
job
});
return {
specs,
changes
};
}
/** Applies the canonical heartbeat monitor plan while isolating per-row failures. */
async function applyHeartbeatMonitorJobs(params) {
let jobs;
try {
jobs = await params.cron.list({ includeDisabled: true });
} catch (error) {
params.logger?.warn({ err: String(error) }, "cron-heartbeat: monitor inventory failed");
return {
ok: false,
applied: [],
failures: [{ error }]
};
}
params.commitGuard?.();
const schedulerSeed = await resolveHeartbeatSchedulerSeedAsync(params.schedulerSeed);
params.commitGuard?.();
const { changes } = resolveHeartbeatMonitorPlan(params.cfg, jobs, { schedulerSeed });
const applied = [];
const failures = [];
for (const change of changes) {
await setImmediate();
params.commitGuard?.();
try {
if (change.kind === "remove") await params.cron.remove(change.job.id, {
systemOwned: true,
...params.commitGuard ? { commitGuard: params.commitGuard } : {}
});
else await params.cron.add(change.input, {
...heartbeatMonitorAddOptions(change.agentId),
...params.commitGuard ? { commitGuard: params.commitGuard } : {}
});
applied.push(change);
} catch (error) {
params.commitGuard?.();
failures.push({
change,
error
});
params.logger?.warn({
agentId: change.agentId,
err: String(error)
}, change.kind === "remove" ? "cron-heartbeat: stale monitor cleanup failed" : "cron-heartbeat: monitor convergence failed");
}
}
return {
ok: failures.length === 0,
applied,
failures
};
}
/** Gateway-facing reconciliation keeps the established compact result contract. */
async function reconcileHeartbeatMonitorJobs(params) {
const { ok } = await applyHeartbeatMonitorJobs(params);
return { ok };
}
//#endregion
export { resolveHeartbeatSchedulerSeed as a, resolveHeartbeatMonitorPlan as i, heartbeatMonitorAddOptions as n, resolveHeartbeatSchedulerSeedAsync as o, reconcileHeartbeatMonitorJobs as r, applyHeartbeatMonitorJobs as t };