openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
303 lines (302 loc) • 14.4 kB
JavaScript
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { a as writeRuntimeJson, t as ExitError } from "./runtime-CF2WjnNZ.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { t as isDiagnosticFlagEnabled } from "./diagnostic-flags-qVPAqdVk.js";
import { n as isRich } from "./theme-vjDs9tao.js";
import { n as info } from "./globals-CTaGxEqj.js";
import { n as isGatewaySecretRefUnavailableError } from "./credentials-CXbS4PS0.js";
import { a as buildGatewayProbeConnectionDetails, d as formatGatewayTransportErrorJson, i as buildGatewayConnectionDetails, l as formatGatewayAuthErrorJson, o as callGateway, p as isGatewayCredentialsRequiredError, u as formatGatewayClientRequestErrorJson } from "./call-BWR5pPgw.js";
import { t as formatDurationCompact } from "./format-duration-CeDWULoS.js";
import { a as resolvePreferredAccountId, t as buildChannelAccountBindings } from "./bindings-RJ9_sjHk.js";
import { i as resolveChannelDefaultAccountId } from "./helpers-VcK_WJ24.js";
import { t as listReadOnlyChannelPluginsForConfig } from "./read-only-0zbnWxaR.js";
import { r as withProgress } from "./progress-DSeqxbrQ.js";
import { t as styleHealthChannelLine } from "./health-style-BB4dwQyH.js";
import { t as formatExactDuration } from "./format-duration-exact-hT1gVF5x.js";
import { t as probeGatewayStatus } from "./probe-FCgI-faz.js";
import { r as formatHealthChannelLines, t as formatDeliveryQueueHealthLine } from "./health-format-BptmWUeD.js";
import { i as resolveHealthAccountContext, r as resolveHealthAgentOrder, t as buildHealthAgentSummaries } from "./collector-D5rcSn9Z.js";
import { a as GATEWAY_HEALTH_REACHABLE_LINE, c as gatewayConnectErrorWasRateLimited, l as gatewayProbeResultSawGateway, o as buildCredentialsRequiredHealthDiagnostic, s as buildRateLimitedHealthDiagnostic, u as gatewayProbeResultWasRateLimited } from "./gateway-health-auth-diagnostic-6ZBqs-mo.js";
import { t as logGatewayConnectionDetails } from "./status.gateway-connection-o1eqDxlb.js";
//#region src/commands/health.ts
/** Collects and renders gateway health for channels, agents, plugins, and sessions. */
const DEFAULT_TIMEOUT_MS = 1e4;
const healthLog = createSubsystemLogger("health");
const debugHealth = (cfg, message, meta) => {
if (isDiagnosticFlagEnabled("health", cfg)) healthLog.info(message, meta);
};
function isGatewayHealthAuthUnavailableError(error) {
return isGatewayCredentialsRequiredError(error) || isGatewaySecretRefUnavailableError(error);
}
async function emitReachableGatewayAuthDiagnostic(params) {
const directRateLimit = gatewayConnectErrorWasRateLimited(params.error);
if (!directRateLimit && !isGatewayHealthAuthUnavailableError(params.error)) return false;
if (directRateLimit) {
const diagnostic = buildRateLimitedHealthDiagnostic(params.error);
if (params.json) writeRuntimeJson(params.runtime, diagnostic);
else {
params.runtime.log(GATEWAY_HEALTH_REACHABLE_LINE);
params.runtime.log(diagnostic.error.message);
}
params.runtime.exit(1);
return true;
}
const details = await buildGatewayProbeConnectionDetails({
config: params.config,
token: params.token,
password: params.password,
ignoreEnvUrlOverride: params.ignoreEnvUrlOverride,
localPortOverride: params.localPortOverride
});
const probe = await probeGatewayStatus({
url: details.url,
token: params.token,
password: params.password,
tlsFingerprint: details.tlsFingerprint,
preauthHandshakeTimeoutMs: details.preauthHandshakeTimeoutMs,
timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
config: params.config,
json: params.json
});
if (!gatewayProbeResultSawGateway(probe)) return false;
const diagnostic = gatewayProbeResultWasRateLimited(probe) ? buildRateLimitedHealthDiagnostic() : buildCredentialsRequiredHealthDiagnostic();
if (params.json) {
writeRuntimeJson(params.runtime, diagnostic);
params.runtime.exit(1);
return true;
}
params.runtime.log(GATEWAY_HEALTH_REACHABLE_LINE);
params.runtime.log(diagnostic.error.message);
params.runtime.exit(1);
return true;
}
const loadConfigRuntime = async () => await import("./config/config.js");
function formatEventLoopHealthLine(summary) {
const eventLoop = summary.eventLoop;
if (!eventLoop) return null;
return `Gateway event loop: ${eventLoop.degraded ? "degraded" : "ok"}${eventLoop.degraded && eventLoop.degradedSinceMs != null ? ` for ${formatDurationCompact(eventLoop.degradedSinceMs) ?? "0s"}` : ""}${eventLoop.reasons.length > 0 ? ` reasons=${eventLoop.reasons.join(",")}` : ""} max=${Math.round(eventLoop.delayMaxMs)}ms p99=${Math.round(eventLoop.delayP99Ms)}ms util=${eventLoop.utilization} cpu=${eventLoop.cpuCoreRatio}`;
}
/** Formats context engine quarantine state for text health output. */
function formatContextEngineHealthLine(summary) {
const quarantined = summary.contextEngines?.quarantined ?? [];
if (quarantined.length === 0) return null;
const engines = quarantined.map((entry) => entry.engineId).join(", ");
return `Context engine: warning (${quarantined.length} quarantined; downgraded to legacy: ${engines})`;
}
/** Formats config hot-reload watcher degradation for text health output. */
function formatConfigReloadHealthLine(summary) {
if (summary.configReload?.hotReloadStatus !== "disabled") return null;
return "Config hot reload: disabled (watcher retries exhausted; restart the gateway to restore it)";
}
/** Runs the `openclaw health` command against the gateway and renders JSON or text. */
async function healthCommand(opts, runtime) {
const cfg = opts.config ?? await readNonObservingHealthConfig();
let summary;
try {
summary = await withProgress({
label: "Checking gateway health…",
indeterminate: true,
enabled: opts.json !== true
}, async () => await callGateway({
method: "health",
params: opts.verbose ? { probe: true } : void 0,
timeoutMs: opts.timeoutMs,
config: cfg,
token: opts.token,
password: opts.password,
sharedStateMode: "read-only",
ignoreEnvUrlOverride: opts.ignoreEnvUrlOverride,
localPortOverride: opts.localPortOverride
}));
} catch (error) {
if (await emitReachableGatewayAuthDiagnostic({
error,
config: cfg,
runtime,
timeoutMs: opts.timeoutMs,
token: opts.token,
password: opts.password,
ignoreEnvUrlOverride: opts.ignoreEnvUrlOverride,
localPortOverride: opts.localPortOverride,
json: opts.json
})) return;
if (opts.json) {
const payload = formatGatewayAuthErrorJson(error) ?? formatGatewayClientRequestErrorJson(error) ?? formatGatewayTransportErrorJson(error);
if (payload) {
writeRuntimeJson(runtime, payload);
runtime.exit(1);
return;
}
}
throw error;
}
if (opts.json) writeRuntimeJson(runtime, summary);
else {
const debugEnabled = isDiagnosticFlagEnabled("health", cfg);
const rich = isRich();
if (opts.verbose) {
const details = buildGatewayConnectionDetails({
config: cfg,
ignoreEnvUrlOverride: opts.ignoreEnvUrlOverride,
localPortOverride: opts.localPortOverride
});
logGatewayConnectionDetails({
runtime,
info,
message: details.message
});
}
const localAgents = resolveHealthAgentOrder(cfg);
const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId;
const agents = Array.isArray(summary.agents) ? summary.agents : [];
const resolvedAgents = agents.length > 0 ? agents : await buildHealthAgentSummaries(cfg, localAgents);
const displayAgents = opts.verbose || !defaultAgentId ? resolvedAgents : resolvedAgents.filter((agent) => agent.agentId === defaultAgentId);
const channelBindings = buildChannelAccountBindings(cfg);
const displayPlugins = listReadOnlyChannelPluginsForConfig(cfg, { includeSetupFallbackPlugins: false });
if (debugEnabled) {
runtime.log(info("[debug] local channel accounts"));
for (const plugin of displayPlugins) {
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds
});
runtime.log(` ${plugin.id}: accounts=${accountIds.join(", ") || "(none)"} default=${defaultAccountId}`);
for (const accountId of accountIds) {
const { inspectedAccount, probeAccount, configured, diagnostics } = await resolveHealthAccountContext({
plugin,
cfg,
accountId
});
const record = asNullableRecord(inspectedAccount ?? probeAccount);
const tokenSource = record && typeof record.tokenSource === "string" ? record.tokenSource : void 0;
runtime.log(` - ${accountId}: configured=${configured ?? "unknown"}${tokenSource ? ` tokenSource=${tokenSource}` : ""}`);
for (const diagnostic of diagnostics) runtime.log(` ! ${diagnostic}`);
}
}
runtime.log(info("[debug] bindings map"));
for (const [channelId, byAgent] of channelBindings.entries()) {
const entries = Array.from(byAgent.entries()).map(([agentId, ids]) => `${agentId}=[${ids.join(", ")}]`);
runtime.log(` ${channelId}: ${entries.join(" ")}`);
}
runtime.log(info("[debug] gateway channel probes"));
for (const [channelId, channelSummary] of Object.entries(summary.channels ?? {})) {
const accounts = channelSummary.accounts ?? {};
const probes = Object.entries(accounts).map(([accountId, accountSummary]) => {
const probe = asNullableRecord(accountSummary.probe);
const bot = probe ? asNullableRecord(probe.bot) : null;
return `${accountId}=${(bot && typeof bot.username === "string" ? bot.username : null) ?? "(no bot)"}`;
});
runtime.log(` ${channelId}: ${probes.join(", ") || "(none)"}`);
}
}
const accountIdsByChannel = (() => {
const entries = displayAgents.length > 0 ? displayAgents : resolvedAgents;
const byChannel = {};
for (const [channelId, byAgent] of channelBindings.entries()) {
const accountIds = [];
for (const agent of entries) {
const ids = byAgent.get(agent.agentId) ?? [];
for (const id of ids) if (!accountIds.includes(id)) accountIds.push(id);
}
if (accountIds.length > 0) byChannel[channelId] = accountIds;
}
return byChannel;
})();
const channelLines = Object.keys(accountIdsByChannel).length > 0 ? formatHealthChannelLines(summary, {
accountMode: opts.verbose ? "all" : "default",
accountIdsByChannel
}) : formatHealthChannelLines(summary, { accountMode: opts.verbose ? "all" : "default" });
for (const line of channelLines) runtime.log(styleHealthChannelLine(line, rich));
const eventLoopLine = formatEventLoopHealthLine(summary);
if (eventLoopLine) runtime.log(styleHealthChannelLine(eventLoopLine, rich));
const contextEngineLine = formatContextEngineHealthLine(summary);
if (contextEngineLine) runtime.log(styleHealthChannelLine(contextEngineLine, rich));
const deliveryQueueLine = formatDeliveryQueueHealthLine(summary);
if (deliveryQueueLine) runtime.log(styleHealthChannelLine(deliveryQueueLine, rich));
const configReloadLine = formatConfigReloadHealthLine(summary);
if (configReloadLine) runtime.log(styleHealthChannelLine(configReloadLine, rich));
for (const plugin of displayPlugins) {
const channelSummary = summary.channels?.[plugin.id];
if (!channelSummary || channelSummary.linked !== true) continue;
if (!plugin.status?.logSelfId) continue;
const boundAccounts = defaultAgentId ? channelBindings.get(plugin.id)?.get(defaultAgentId) ?? [] : [];
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds
});
const accountId = resolvePreferredAccountId({
accountIds,
defaultAccountId,
boundAccounts
});
const accountContext = await resolveHealthAccountContext({
plugin,
cfg,
accountId
});
if (accountContext.probeAccount === void 0 || !accountContext.enabled || accountContext.configured !== true) continue;
if (accountContext.diagnostics.length > 0) continue;
try {
plugin.status.logSelfId({
account: accountContext.probeAccount,
cfg,
runtime,
includeChannelPrefix: true
});
} catch (error) {
debugHealth(cfg, "logSelfId.failed", {
channel: plugin.id,
accountId,
error: formatErrorMessage(error)
});
}
}
if (Number.isFinite(summary.durationMs)) runtime.log(info(`Gateway probe duration: ${summary.durationMs}ms`));
if (resolvedAgents.length > 0) {
const agentLabels = resolvedAgents.map((agent) => agent.isDefault ? `${agent.agentId} (default)` : agent.agentId);
runtime.log(info(`Agents: ${agentLabels.join(", ")}`));
}
const heartbeatParts = displayAgents.map((agent) => {
const everyMs = agent.heartbeat?.everyMs;
return `${everyMs ? formatExactDuration(everyMs, "unknown", true) : "disabled"} (${agent.agentId})`;
}).filter(Boolean);
if (heartbeatParts.length > 0) runtime.log(info(`Heartbeat interval: ${heartbeatParts.join(", ")}`));
const sessionGroups = displayAgents.length > 0 ? displayAgents : [{
agentId: void 0,
sessions: summary.sessions
}];
for (const { agentId, sessions } of sessionGroups) {
const label = agentId ? `Session store (${agentId})` : "Session store";
runtime.log(info(`${label}: ${sessions.path} (${sessions.count} entries)`));
for (const { key, age } of sessions.recent) runtime.log(`- ${key} (${age === null ? "no activity" : `${Math.round(age / 6e4)}m ago`})`);
}
}
}
/**
* Runs `healthCommand` inside a host flow (wizard/onboard/doctor). The command's
* CLI-style `runtime.exit(1)` diagnostic paths surface as a thrown `ExitError`,
* so the host reports the failure and keeps running instead of dying mid-flow.
*/
async function healthCommandNonExiting(opts, runtime) {
await healthCommand(opts, {
...runtime,
exit: (code) => {
throw new ExitError(code);
}
});
}
async function readNonObservingHealthConfig() {
const { readConfigFileSnapshot } = await loadConfigRuntime();
const snapshot = await readConfigFileSnapshot({
observe: false,
pluginValidation: "core-only"
});
return snapshot.runtimeConfig ?? snapshot.config;
}
//#endregion
export { healthCommandNonExiting as a, healthCommand as i, formatConfigReloadHealthLine as n, readNonObservingHealthConfig as o, formatContextEngineHealthLine as r, emitReachableGatewayAuthDiagnostic as t };