openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
423 lines (422 loc) • 18.7 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { F as resolveTimerTimeoutMs } from "./number-coercion-CuVhdCBs.mjs";
import { c as trackAsyncWork } from "./async-work-scope-BYeX3gh6.mjs";
import "./src-CdRlGRnW.mjs";
import { t as expectDefined } from "./expect-lbe3Hgrh.mjs";
import { n as asNullableObjectRecord } from "./record-coerce-TIsquoVR.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { x as redactToolPayloadTextWithConfig } from "./redact-DJ8j6ADa.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs";
import { n as awaitWithinDeadline, t as ABSOLUTE_DEADLINE_EXPIRED } from "./absolute-deadline-DoGHpXtD.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { T as tryResolveLegacyCompatibilityAgentId } from "./agent-scope-config-CDYpzA0r.mjs";
import "./session-key-C09ow4h0.mjs";
import { n as listAgentEntries } from "./agent-roster-BXD1uMJD.mjs";
import { l as resolveSessionStorePathCore } from "./paths-dpReqUI8.mjs";
import { _ as degradedPluginMatchesRoot, w as toPublicPluginVerificationDiagnostic, x as listActiveDegradedPlugins } from "./discovery-Dma3bVNk.mjs";
import { t as isDiagnosticFlagEnabled } from "./diagnostic-flags-NlcA0RRK.mjs";
import "./legacy.default-agent-owner-CiZjlN5C.mjs";
import { l as getActivePluginRegistry } from "./runtime-NsVZTERq.mjs";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.mjs";
import { t as listReadOnlyChannelPluginsForConfig } from "./read-only-BUz6WPkw.mjs";
import { i as resolvePreferredAccountId, t as buildChannelAccountBindings } from "./bindings-DZZ-X6IG.mjs";
import { i as resolveChannelDefaultAccountId } from "./helpers-Bm4RKejp.mjs";
import { s as redactChannelStatusSummaryBaseUrl } from "./account-snapshot-fields-e1ulxwox.mjs";
import { a as resolveUnavailableChannelAccountSnapshot } from "./account-state-CXuFP2J6.mjs";
import { n as buildChannelAccountSnapshotFromRuntime, t as buildChannelAccountSnapshotFromInspection } from "./account-summary-Dp3tB0X5.mjs";
import { i as resolveHeartbeatSummariesForAgents } from "./heartbeat-summary-projection-DjVlqspG.mjs";
import { t as createPermitPool } from "./permit-pool-D1LtDFRv.mjs";
import { t as buildChannelAccountSnapshotFromAccount } from "./status-yZDdEPX4.mjs";
import { i as resolveChannelHealthState, n as DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS, t as DEFAULT_CHANNEL_CONNECT_GRACE_MS } from "./channel-health-policy-CQLQKpCe.mjs";
import { n as resolveHealthAccountContext, t as buildNonSensitiveProbeFailure } from "./account-context-CiUk42Ju.mjs";
import { n as listPluginServiceHealthFailures } from "./service-health-DdlAkI7p.mjs";
import { n as captureDeliveryQueueHealthContext, r as buildContextEngineHealthSummary, t as buildDeliveryQueueHealthSummary } from "./delivery-queue-CPL7P06i.mjs";
//#region src/gateway/health/collector.ts
const HEALTH_COLLECTION_TIMEOUT_MS = 7e3;
const HEALTH_PROBE_CONCURRENCY = 5;
const HEALTH_RECENT_SESSION_LIMIT = 5;
const healthLog = createSubsystemLogger("health");
const debugHealth = (cfg, message, meta) => {
if (isDiagnosticFlagEnabled("health", cfg)) healthLog.info(message, meta);
};
function resolveHealthAgentOrder(cfg) {
const defaultAgentId = tryResolveLegacyCompatibilityAgentId(cfg);
const entries = listAgentEntries(cfg);
const seen = /* @__PURE__ */ new Set();
const ordered = [];
for (const entry of entries) {
if (typeof entry.id !== "string" || !entry.id.trim()) continue;
const id = normalizeAgentId(entry.id);
if (seen.has(id)) continue;
seen.add(id);
ordered.push({
id,
name: typeof entry.name === "string" ? entry.name : void 0
});
}
if (defaultAgentId && !seen.has(defaultAgentId)) ordered.unshift({ id: defaultAgentId });
return {
defaultAgentId,
ordered
};
}
async function createHealthSessionStoreReader(agentIds, projection) {
const { createStatusSessionStoreReader } = await import("./session-stores-CWpD7QN9.mjs");
const { isTransientSqliteError } = await import("./unhandled-rejections-B3DEjwQf.mjs");
return createStatusSessionStoreReader(agentIds, HEALTH_RECENT_SESSION_LIMIT, {
projection,
recoverReadError(error) {
if (!isTransientSqliteError(error)) throw error;
return {
count: 0,
recent: [],
byAgent: /* @__PURE__ */ new Map()
};
}
});
}
function projectHealthSessions(path, summary) {
const recent = summary.recent.map(({ sessionKey: key, entry }) => ({
key,
updatedAt: entry.updatedAt || null,
age: entry.updatedAt ? Date.now() - entry.updatedAt : null
}));
return {
path,
count: summary.count,
recent
};
}
async function buildHealthSessionSummary(storePath, agentId, projection) {
const store = await (await createHealthSessionStoreReader(agentId ? [agentId] : [], projection)).read(storePath, agentId);
return projectHealthSessions(store.path, store);
}
/** Shares one bounded session snapshot across every configured agent in this collection. */
async function buildHealthAgentSummaries(cfg, { defaultAgentId, ordered }, projection) {
const agentIds = ordered.map((entry) => entry.id);
const reader = await createHealthSessionStoreReader(agentIds, projection);
const heartbeats = resolveHeartbeatSummariesForAgents(cfg, agentIds);
const agents = [];
for (const [index, entry] of ordered.entries()) {
const store = await reader.read(resolveSessionStorePathCore(cfg.session?.store, { agentId: entry.id }), entry.id);
agents.push({
agentId: entry.id,
name: entry.name,
isDefault: entry.id === defaultAgentId,
heartbeat: expectDefined(heartbeats[index], "heartbeat summary"),
sessions: projectHealthSessions(store.path, store)
});
}
return agents;
}
function buildPluginHealthSummary(cfg) {
function projectError(plugin, error) {
return {
...error,
activationSource: plugin?.activationSource,
activationReason: plugin?.activationReason,
error: truncateUtf16Safe(redactToolPayloadTextWithConfig(error.error, cfg.logging), 1e3)
};
}
const registry = getActivePluginRegistry();
const degradedPlugins = listActiveDegradedPlugins();
const unavailable = degradedPlugins.map(({ pluginId, state, diagnostic }) => ({
id: pluginId,
state,
diagnostic: toPublicPluginVerificationDiagnostic(diagnostic)
})).toSorted((left, right) => left.id.localeCompare(right.id));
const loaded = (registry?.plugins ?? []).filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id).toSorted((left, right) => left.localeCompare(right));
const loadErrors = (registry?.plugins ?? []).filter((plugin) => plugin.status === "error" && !degradedPlugins.some((degraded) => plugin.id === degraded.pluginId && plugin.failurePhase === "validation" && plugin.activationReason === `configured-unavailable: ${degraded.diagnostic.reason}` && Boolean(plugin.rootDir) && degradedPluginMatchesRoot(degraded, plugin.rootDir ?? ""))).map((plugin) => projectError(plugin, {
id: plugin.id,
origin: plugin.origin,
activated: plugin.activated === true,
error: plugin.error ?? "unknown plugin load error",
...plugin.failurePhase ? { failurePhase: plugin.failurePhase } : {}
}));
const serviceErrors = registry ? listPluginServiceHealthFailures(registry).map((failure) => projectError(registry.plugins.find((entry) => entry.id === failure.pluginId), {
id: failure.pluginId,
origin: failure.origin,
activated: true,
failurePhase: "service",
error: `service ${failure.serviceId}: ${failure.error}`
})) : [];
const errors = [...loadErrors, ...serviceErrors].toSorted((left, right) => left.id.localeCompare(right.id) || left.error.localeCompare(right.error));
if (loaded.length === 0 && errors.length === 0 && unavailable.length === 0) return;
return {
loaded,
errors,
unavailable
};
}
const healthOperationPermits = createPermitPool(HEALTH_PROBE_CONCURRENCY);
function buildHealthTimeoutRecord(accountId, timeoutMs) {
const error = `health collection timed out after ${timeoutMs}ms`;
return {
accountId,
lastError: error,
probe: {
ok: false,
timedOut: true,
error
}
};
}
function resolveHealthProbeTimeoutMs(deadlineAtMs) {
return Math.max(1, deadlineAtMs - Date.now());
}
async function buildHealthAccountRecord(params) {
const timedOut = () => buildHealthTimeoutRecord(params.accountId, params.timeoutMs);
const runtimeAccount = params.runtimeSnapshot?.channelAccounts[params.plugin.id]?.[params.accountId];
const runtimeSnapshot = runtimeAccount ?? (params.accountId === params.defaultAccountId ? params.runtimeSnapshot?.channels[params.plugin.id] : void 0);
if (params.runtimeOnly && runtimeAccount) {
const snapshot = buildChannelAccountSnapshotFromRuntime(runtimeAccount);
const unavailable = resolveUnavailableChannelAccountSnapshot(params.cfg, {
channelId: params.plugin.id,
accountId: params.accountId,
runtime: snapshot
});
if (unavailable) return unavailable;
const healthState = resolveChannelHealthState(snapshot, {
channelId: params.plugin.id,
now: Date.now(),
staleEventThresholdMs: DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS,
channelConnectGraceMs: DEFAULT_CHANNEL_CONNECT_GRACE_MS
});
return {
...snapshot,
...healthState !== void 0 ? { healthState } : {}
};
}
const unavailable = resolveUnavailableChannelAccountSnapshot(params.cfg, {
channelId: params.plugin.id,
accountId: params.accountId,
runtime: runtimeSnapshot
});
if (unavailable) return unavailable;
const { probeAccount, inspectedAccount, enabled, configured, diagnostics } = await resolveHealthAccountContext({
plugin: params.plugin,
cfg: params.cfg,
accountId: params.accountId
});
if (Date.now() >= params.deadlineAtMs) return timedOut();
if (diagnostics.length > 0) debugHealth(params.cfg, "account.diagnostics", {
channel: params.plugin.id,
accountId: params.accountId,
diagnostics
});
let probe;
let lastProbeAt = null;
if (probeAccount !== void 0 && enabled && configured === true && params.probe && params.plugin.status?.probeAccount) {
try {
probe = await params.plugin.status.probeAccount({
account: probeAccount,
timeoutMs: resolveHealthProbeTimeoutMs(params.deadlineAtMs),
cfg: params.cfg
});
} catch (error) {
probe = {
ok: false,
error: formatErrorMessage(error)
};
}
lastProbeAt = Date.now();
}
if (Date.now() >= params.deadlineAtMs) return timedOut();
const bot = asNullableObjectRecord(asNullableObjectRecord(probe)?.bot);
if (bot?.username) debugHealth(params.cfg, "probe.bot", {
channel: params.plugin.id,
accountId: params.accountId,
username: bot.username
});
const nonSensitiveProbeFailure = buildNonSensitiveProbeFailure(params.plugin.id, probe);
const snapshotProbe = params.includeSensitive ? probe : nonSensitiveProbeFailure;
const snapshot = probeAccount === void 0 ? buildChannelAccountSnapshotFromInspection({
account: inspectedAccount,
accountId: params.accountId,
runtime: runtimeSnapshot,
probe: snapshotProbe
}) : await buildChannelAccountSnapshotFromAccount({
plugin: params.plugin,
cfg: params.cfg,
accountId: params.accountId,
account: probeAccount,
runtime: runtimeSnapshot,
probe: snapshotProbe,
enabledFallback: enabled,
configuredFallback: configured
});
if (Date.now() >= params.deadlineAtMs) return timedOut();
if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt;
const healthState = resolveChannelHealthState(snapshot, {
channelId: params.plugin.id,
now: Date.now(),
staleEventThresholdMs: DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS,
channelConnectGraceMs: DEFAULT_CHANNEL_CONNECT_GRACE_MS
});
if (healthState !== void 0) snapshot.healthState = healthState;
const summary = probeAccount !== void 0 && params.plugin.status?.buildChannelSummary ? await params.plugin.status.buildChannelSummary({
account: probeAccount,
cfg: params.cfg,
defaultAccountId: params.accountId,
snapshot
}) : void 0;
if (Date.now() >= params.deadlineAtMs) return timedOut();
const record = redactChannelStatusSummaryBaseUrl(summary && typeof summary === "object" ? {
...snapshot,
...summary
} : {
...snapshot,
accountId: params.accountId
});
if (record.configured === void 0 && probeAccount !== void 0) record.configured = configured;
if (params.includeSensitive && record.probe === void 0 && probe !== void 0) record.probe = probe;
if (!params.includeSensitive) {
const safeProbeFailure = buildNonSensitiveProbeFailure(params.plugin.id, record.probe) ?? nonSensitiveProbeFailure;
if (safeProbeFailure) record.probe = safeProbeFailure;
else delete record.probe;
}
if (record.lastProbeAt === void 0 && lastProbeAt) record.lastProbeAt = lastProbeAt;
record.accountId = params.accountId;
return record;
}
async function runHealthAccountWithinDeadline(params) {
const operation = trackAsyncWork(async () => {
const release = await healthOperationPermits.acquire({ deadlineAtMs: params.deadlineAtMs });
if (!release) return buildHealthTimeoutRecord(params.accountId, params.timeoutMs);
try {
return await buildHealthAccountRecord(params);
} finally {
release();
}
});
const result = await awaitWithinDeadline(() => operation, params.deadlineAtMs);
return result === ABSOLUTE_DEADLINE_EXPIRED ? buildHealthTimeoutRecord(params.accountId, params.timeoutMs) : result;
}
/** Collects the gateway-owned health snapshot for an explicit trust audience. */
async function collectGatewayHealthSnapshot(params) {
const stateContext = captureDeliveryQueueHealthContext();
const start = Date.now();
const timeoutMs = Math.min(resolveTimerTimeoutMs(params.timeoutMs, HEALTH_COLLECTION_TIMEOUT_MS, 50), HEALTH_COLLECTION_TIMEOUT_MS);
const deadlineAtMs = start + timeoutMs;
const cfg = await readRuntimeHealthConfig();
const { defaultAgentId, ordered } = resolveHealthAgentOrder(cfg);
const channelBindings = buildChannelAccountBindings(cfg);
const agents = await buildHealthAgentSummaries(cfg, {
defaultAgentId,
ordered
}, params.sessionRowProjection);
const summaryAgent = agents.find((agent) => agent.isDefault) ?? agents[0];
const configuredHeartbeatAgentId = normalizeOptionalString(cfg.agents?.defaults?.heartbeat?.agentId);
const heartbeatSummaryAgent = (configuredHeartbeatAgentId ? agents.find((agent) => agent.heartbeat.enabled && agent.agentId === normalizeAgentId(configuredHeartbeatAgentId)) : void 0) ?? agents.find((agent) => agent.heartbeat.enabled) ?? summaryAgent;
const heartbeatSeconds = heartbeatSummaryAgent?.heartbeat.everyMs ? Math.round(heartbeatSummaryAgent.heartbeat.everyMs / 1e3) : 0;
const sessions = summaryAgent?.sessions ?? await buildHealthSessionSummary(resolveSessionStorePathCore(cfg.session?.store, { agentId: summaryAgent?.agentId }), summaryAgent?.agentId, params.sessionRowProjection);
const includeSensitive = params.audience === "admin";
const channels = {};
const plugins = listReadOnlyChannelPluginsForConfig(cfg, {
includeSetupFallbackPlugins: false,
includePersistedAuthState: false
});
const channelOrder = plugins.map((plugin) => plugin.id);
const channelLabels = {};
const channelPlans = plugins.map((plugin) => {
channelLabels[plugin.id] = plugin.meta.label ?? plugin.id;
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds
});
const boundAccounts = defaultAgentId ? channelBindings.get(plugin.id)?.get(defaultAgentId) ?? [] : [];
const preferredAccountId = resolvePreferredAccountId({
defaultAccountId,
boundAccounts
});
const boundAccountIdsAll = Array.from(new Set(Array.from(channelBindings.get(plugin.id)?.values() ?? []).flat()));
const accountIdsToProbe = Array.from(new Set([
preferredAccountId,
defaultAccountId,
...accountIds,
...boundAccountIdsAll,
...Object.keys(params.runtimeSnapshot?.channelAccounts[plugin.id] ?? {})
].filter((value) => value && value.trim())));
debugHealth(cfg, "channel", {
id: plugin.id,
accountIds,
defaultAccountId,
boundAccounts,
preferredAccountId,
accountIdsToProbe
});
return {
plugin,
defaultAccountId,
preferredAccountId,
accountIds: accountIdsToProbe,
configuredAccountIds: new Set(accountIds),
accountSummaries: {}
};
});
const accountTasks = channelPlans.flatMap((plan) => plan.accountIds.map((accountId) => ({
plan,
accountId
})));
const { results: accountResults } = await runTasksWithConcurrency({
tasks: accountTasks.map(({ plan, accountId }) => async () => ({
plan,
accountId,
record: await runHealthAccountWithinDeadline({
plugin: plan.plugin,
cfg,
accountId,
defaultAccountId: plan.defaultAccountId,
includeSensitive,
probe: params.probe,
deadlineAtMs,
timeoutMs,
runtimeSnapshot: params.runtimeSnapshot,
runtimeOnly: !plan.configuredAccountIds.has(accountId)
})
})),
limit: params.probe ? HEALTH_PROBE_CONCURRENCY : 1,
throwOnError: true
});
for (const result of accountResults) if (result) result.plan.accountSummaries[result.accountId] = result.record;
for (const plan of channelPlans) {
const fallbackSummary = plan.accountSummaries[plan.preferredAccountId] ?? plan.accountSummaries[plan.defaultAccountId] ?? plan.accountSummaries[plan.accountIds[0] ?? plan.preferredAccountId] ?? plan.accountSummaries[expectDefined(Object.keys(plan.accountSummaries)[0], "object.keys(account summaries) entry at 0")];
if (fallbackSummary) channels[plan.plugin.id] = {
...fallbackSummary,
accounts: plan.accountSummaries
};
}
const pluginHealth = buildPluginHealthSummary(cfg);
const contextEngineHealth = await buildContextEngineHealthSummary();
const deliveryQueueHealth = await buildDeliveryQueueHealthSummary(void 0, stateContext);
return {
ok: true,
ts: Date.now(),
durationMs: Date.now() - start,
...params.eventLoop ? { eventLoop: params.eventLoop } : {},
...pluginHealth ? { plugins: pluginHealth } : {},
...contextEngineHealth ? { contextEngines: contextEngineHealth } : {},
...deliveryQueueHealth ? { deliveryQueues: deliveryQueueHealth } : {},
...params.configReloadHotReloadStatus ? { configReload: { hotReloadStatus: params.configReloadHotReloadStatus } } : {},
channels,
channelOrder,
channelLabels,
heartbeatSeconds,
...defaultAgentId ? { defaultAgentId } : {},
agents,
sessions: {
path: sessions.path,
count: sessions.count,
recent: sessions.recent
}
};
}
async function readRuntimeHealthConfig() {
const { getRuntimeConfig } = await import("./config/config.js");
return getRuntimeConfig();
}
//#endregion
export { buildHealthAgentSummaries, collectGatewayHealthSnapshot, resolveHealthAgentOrder };