UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

370 lines (369 loc) 16.2 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { _ as sleep } from "./utils-CCC-BEJH.js"; import { t as killProcessTree } from "./kill-tree-kSm0C74g.js"; import { r as createConfigIO } from "./io-Gi7-pyU-.js"; import { c as classifyPortListener, l as formatPortDiagnostics, o as inspectPortUsage } from "./ports-CoxQQbiY.js"; import { a as probeGateway } from "./probe-2_flQlhR.js"; import { r as resolveGatewayProbeAuthSafeWithSecretInputs } from "./probe-auth-DoCLHThw.js"; //#region src/cli/daemon-cli/restart-health.ts const DEFAULT_RESTART_HEALTH_TIMEOUT_MS = 6e4; const DEFAULT_RESTART_HEALTH_DELAY_MS = 500; const DEFAULT_RESTART_HEALTH_ATTEMPTS = Math.ceil(DEFAULT_RESTART_HEALTH_TIMEOUT_MS / 500); const STOPPED_FREE_EARLY_EXIT_GRACE_MS = 1e4; const WINDOWS_STOPPED_FREE_EARLY_EXIT_GRACE_MS = 9e4; function hasListenerAttributionGap(portUsage) { if (portUsage.status !== "busy" || portUsage.listeners.length > 0) return false; if (portUsage.errors?.length) return true; return portUsage.hints.some((hint) => hint.includes("process details are unavailable")); } function listenerOwnedByRuntimePid(params) { return params.listener.pid === params.runtimePid || params.listener.ppid === params.runtimePid; } function looksLikeAuthClose(code, reason) { if (code !== 1008) return false; const normalized = normalizeLowercaseStringOrEmpty(reason); if (!normalized) return false; return normalized === "auth required" || normalized === "owner auth required" || normalized === "connect failed" || normalized === "device required" || normalized === "pairing required" || normalized.startsWith("pairing required:") || normalized.startsWith("unauthorized: gateway token missing") || normalized.startsWith("unauthorized: gateway token mismatch") || normalized.startsWith("unauthorized: gateway token not configured") || normalized.startsWith("unauthorized: gateway password missing") || normalized.startsWith("unauthorized: gateway password mismatch") || normalized.startsWith("unauthorized: gateway password not configured") || normalized.startsWith("unauthorized: bootstrap token invalid or expired") || normalized.startsWith("unauthorized: tailscale identity missing") || normalized.startsWith("unauthorized: tailscale proxy headers missing") || normalized.startsWith("unauthorized: tailscale identity check failed") || normalized.startsWith("unauthorized: tailscale identity mismatch") || normalized.startsWith("unauthorized: too many failed authentication attempts") || normalized.startsWith("unauthorized: device token mismatch") || normalized.startsWith("unauthorized: device token rejected"); } function applyExpectedVersion(snapshot, expectedVersion) { if (!expectedVersion) return snapshot; if (snapshot.gatewayVersion === expectedVersion) return { ...snapshot, expectedVersion }; if (snapshot.gatewayVersion == null) return { ...snapshot, healthy: false, expectedVersion }; return { ...snapshot, healthy: false, expectedVersion, versionMismatch: { expected: expectedVersion, actual: snapshot.gatewayVersion ?? null } }; } function readActivatedPluginErrors(health) { if (!health || typeof health !== "object") return []; const plugins = health.plugins; if (!plugins || typeof plugins !== "object") return []; const errors = plugins.errors; if (!Array.isArray(errors)) return []; return errors.filter((entry) => { if (!entry || typeof entry !== "object") return false; const candidate = entry; return candidate.activated === true && typeof candidate.id === "string" && typeof candidate.error === "string"; }).map((entry) => { const error = { id: entry.id, origin: typeof entry.origin === "string" ? entry.origin : "unknown", activated: true, error: entry.error }; if (typeof entry.activationSource === "string") error.activationSource = entry.activationSource; if (typeof entry.activationReason === "string") error.activationReason = entry.activationReason; if (typeof entry.failurePhase === "string") error.failurePhase = entry.failurePhase; return error; }); } function readChannelProbeErrors(health) { if (!health || typeof health !== "object") return []; const channels = health.channels; if (!channels || typeof channels !== "object" || Array.isArray(channels)) return []; const errors = []; for (const [id, summary] of Object.entries(channels)) { if (!summary || typeof summary !== "object") continue; const probe = summary.probe; if (!probe || typeof probe !== "object") continue; if (probe.ok !== false) continue; const error = probe.error; errors.push({ id, error: typeof error === "string" && error.trim() ? error : "probe failed" }); } return errors; } function applyActivatedPluginErrors(snapshot) { if (!snapshot.activatedPluginErrors?.length) return snapshot; return { ...snapshot, healthy: false }; } function applyChannelProbeErrors(snapshot) { if (!snapshot.channelProbeErrors?.length) return snapshot; return { ...snapshot, healthy: false }; } async function confirmGatewayReachable(params) { const token = normalizeOptionalString(params.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN); const password = normalizeOptionalString(params.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD); const probe = await probeGateway({ url: `ws://127.0.0.1:${params.port}`, auth: token || password ? { token, password } : void 0, timeoutMs: 3e3, includeDetails: params.includeHealthDetails === true, env: params.env }); return { reachable: probe.ok || looksLikeAuthClose(probe.close?.code, probe.close?.reason) || probe.connectLatencyMs != null && probe.server?.version != null && probe.auth.capability === "connected_no_operator_scope", gatewayVersion: probe.server?.version ?? null, activatedPluginErrors: readActivatedPluginErrors(probe.health), channelProbeErrors: readChannelProbeErrors(probe.health) }; } async function resolveGatewayRestartProbeAuth(env) { const mergedEnv = { ...process.env, ...env ?? void 0 }; return (await resolveGatewayProbeAuthSafeWithSecretInputs({ cfg: await createConfigIO({ env: mergedEnv, pluginValidation: "skip", suppressFutureVersionWarning: true }).readBestEffortConfig().catch(() => ({})), mode: "local", env: mergedEnv })).auth; } async function inspectGatewayPortHealth(params) { let portUsage; try { portUsage = await inspectPortUsage(params.port); } catch (err) { portUsage = { port: params.port, status: "unknown", listeners: [], hints: [], errors: [String(err)] }; } let healthy = false; if (portUsage.status === "busy") try { healthy = (await confirmGatewayReachable({ port: params.port, auth: params.auth, env: process.env })).reachable; } catch {} return { portUsage, healthy }; } async function inspectGatewayRestart(params) { const env = params.env ?? process.env; const expectedVersion = normalizeOptionalString(params.expectedVersion); let reachability = null; let activatedPluginErrors = []; let channelProbeErrors = []; const loadReachability = async () => { if (!reachability) { reachability = await confirmGatewayReachable({ port: params.port, includeHealthDetails: Boolean(expectedVersion), auth: params.probeAuth, env }); activatedPluginErrors = reachability.activatedPluginErrors; channelProbeErrors = reachability.channelProbeErrors; } return reachability; }; let runtime = { status: "unknown" }; try { runtime = await params.service.readRuntime(env); } catch (err) { runtime = { status: "unknown", detail: String(err) }; } let portUsage; try { portUsage = await inspectPortUsage(params.port); } catch (err) { portUsage = { port: params.port, status: "unknown", listeners: [], hints: [], errors: [String(err)] }; } if (portUsage.status === "busy" && runtime.status !== "running") try { const reachable = await loadReachability(); if (reachable.reachable) return applyChannelProbeErrors(applyActivatedPluginErrors(applyExpectedVersion({ runtime, portUsage, healthy: true, staleGatewayPids: [], gatewayVersion: reachable.gatewayVersion, ...reachable.activatedPluginErrors.length > 0 ? { activatedPluginErrors: reachable.activatedPluginErrors } : {}, ...reachable.channelProbeErrors.length > 0 ? { channelProbeErrors: reachable.channelProbeErrors } : {} }, expectedVersion))); } catch {} const gatewayListeners = portUsage.status === "busy" ? portUsage.listeners.filter((listener) => classifyPortListener(listener, params.port) === "gateway") : []; const fallbackListenerPids = params.includeUnknownListenersAsStale && process.platform === "win32" && runtime.status !== "running" && portUsage.status === "busy" ? portUsage.listeners.filter((listener) => classifyPortListener(listener, params.port) === "unknown").map((listener) => listener.pid).filter((pid) => Number.isFinite(pid)) : []; const running = runtime.status === "running"; const runtimePid = runtime.pid; const listenerAttributionGap = hasListenerAttributionGap(portUsage); const ownsPort = runtimePid != null ? portUsage.listeners.some((listener) => listenerOwnedByRuntimePid({ listener, runtimePid })) || listenerAttributionGap : gatewayListeners.length > 0 || listenerAttributionGap; let healthy = running && ownsPort; let gatewayVersion; if (expectedVersion && healthy && portUsage.status === "busy") try { const reachable = await loadReachability(); healthy = reachable.reachable; gatewayVersion = reachable.gatewayVersion; if (reachable.activatedPluginErrors.length > 0) healthy = false; if (reachable.channelProbeErrors.length > 0) healthy = false; } catch { healthy = false; } if (!healthy && running && portUsage.status === "busy" && !expectedVersion) try { const reachable = await loadReachability(); healthy = reachable.reachable; gatewayVersion = reachable.gatewayVersion; } catch {} const staleGatewayPids = Array.from(new Set([...gatewayListeners.filter((listener) => Number.isFinite(listener.pid)).filter((listener) => { if (!running) return true; if (runtimePid == null) return false; return !listenerOwnedByRuntimePid({ listener, runtimePid }); }).map((listener) => listener.pid), ...fallbackListenerPids.filter((pid) => runtime.pid == null || pid !== runtime.pid || !running)])); return applyChannelProbeErrors(applyActivatedPluginErrors(applyExpectedVersion({ runtime, portUsage, healthy, staleGatewayPids, ...gatewayVersion !== void 0 ? { gatewayVersion } : {}, ...activatedPluginErrors.length ? { activatedPluginErrors } : {}, ...channelProbeErrors.length ? { channelProbeErrors } : {} }, expectedVersion))); } function shouldEarlyExitStoppedFree(snapshot, attempt, minAttempt) { return attempt >= minAttempt && snapshot.runtime.status === "stopped" && snapshot.portUsage.status === "free"; } function stoppedFreeEarlyExitGraceMs() { return process.platform === "win32" ? WINDOWS_STOPPED_FREE_EARLY_EXIT_GRACE_MS : STOPPED_FREE_EARLY_EXIT_GRACE_MS; } function withWaitContext(snapshot, waitOutcome, elapsedMs) { return { ...snapshot, waitOutcome, elapsedMs }; } async function waitForGatewayHealthyRestart(params) { const attempts = params.attempts ?? DEFAULT_RESTART_HEALTH_ATTEMPTS; const delayMs = params.delayMs ?? 500; const probeAuth = await resolveGatewayRestartProbeAuth(params.env).catch(() => void 0); let snapshot = await inspectGatewayRestart({ service: params.service, port: params.port, env: params.env, expectedVersion: params.expectedVersion, includeUnknownListenersAsStale: params.includeUnknownListenersAsStale, probeAuth }); let consecutiveStoppedFreeCount = 0; const STOPPED_FREE_THRESHOLD = 6; const minAttemptForEarlyExit = Math.min(Math.ceil(stoppedFreeEarlyExitGraceMs() / delayMs), Math.floor(attempts / 2)); for (let attempt = 0; attempt < attempts; attempt += 1) { if (snapshot.healthy && (!params.requireRunningService || snapshot.runtime.status === "running")) return withWaitContext(snapshot, "healthy", attempt * delayMs); if (snapshot.activatedPluginErrors?.length) return withWaitContext(snapshot, "plugin-errors", attempt * delayMs); if (snapshot.channelProbeErrors?.length) return withWaitContext(snapshot, "channel-errors", attempt * delayMs); if (snapshot.versionMismatch) return withWaitContext(snapshot, "version-mismatch", attempt * delayMs); if (snapshot.staleGatewayPids.length > 0 && snapshot.runtime.status !== "running") return withWaitContext(snapshot, "stale-pids", attempt * delayMs); if (shouldEarlyExitStoppedFree(snapshot, attempt, minAttemptForEarlyExit)) { consecutiveStoppedFreeCount += 1; if (consecutiveStoppedFreeCount >= STOPPED_FREE_THRESHOLD) return withWaitContext(snapshot, "stopped-free", attempt * delayMs); } else if (snapshot.runtime.status !== "stopped" || snapshot.portUsage.status !== "free") consecutiveStoppedFreeCount = 0; await sleep(delayMs); snapshot = await inspectGatewayRestart({ service: params.service, port: params.port, env: params.env, expectedVersion: params.expectedVersion, includeUnknownListenersAsStale: params.includeUnknownListenersAsStale, probeAuth }); } return withWaitContext(snapshot, "timeout", attempts * delayMs); } async function waitForGatewayHealthyListener(params) { const attempts = params.attempts ?? DEFAULT_RESTART_HEALTH_ATTEMPTS; const delayMs = params.delayMs ?? 500; const probeAuth = await resolveGatewayRestartProbeAuth(void 0).catch(() => void 0); let snapshot = await inspectGatewayPortHealth({ port: params.port, auth: probeAuth }); for (let attempt = 0; attempt < attempts; attempt += 1) { if (snapshot.healthy) return snapshot; await sleep(delayMs); snapshot = await inspectGatewayPortHealth({ port: params.port, auth: probeAuth }); } return snapshot; } function renderPortUsageDiagnostics(snapshot) { const lines = []; if (snapshot.portUsage.status === "busy") lines.push(...formatPortDiagnostics(snapshot.portUsage)); else lines.push(`Gateway port ${snapshot.portUsage.port} status: ${snapshot.portUsage.status}.`); if (snapshot.portUsage.errors?.length) lines.push(`Port diagnostics errors: ${snapshot.portUsage.errors.join("; ")}`); return lines; } function renderRestartDiagnostics(snapshot) { const lines = []; if (snapshot.versionMismatch) { const actual = snapshot.versionMismatch.actual ?? "unavailable"; lines.push(`Gateway version mismatch: expected ${snapshot.versionMismatch.expected}, running gateway reported ${actual}.`); } if (snapshot.activatedPluginErrors?.length) { lines.push("Activated plugin load errors:"); for (const plugin of snapshot.activatedPluginErrors) lines.push(`- ${plugin.id}: ${plugin.error}`); } if (snapshot.channelProbeErrors?.length) { lines.push("Channel health probe errors:"); for (const channel of snapshot.channelProbeErrors) lines.push(`- ${channel.id}: ${channel.error}`); } const runtimeSummary = [ snapshot.runtime.status ? `status=${snapshot.runtime.status}` : null, snapshot.runtime.state ? `state=${snapshot.runtime.state}` : null, snapshot.runtime.pid != null ? `pid=${snapshot.runtime.pid}` : null, snapshot.runtime.lastExitStatus != null ? `lastExit=${snapshot.runtime.lastExitStatus}` : null ].filter(Boolean).join(", "); if (runtimeSummary) lines.push(`Service runtime: ${runtimeSummary}`); lines.push(...renderPortUsageDiagnostics(snapshot)); return lines; } function renderGatewayPortHealthDiagnostics(snapshot) { return renderPortUsageDiagnostics(snapshot); } async function terminateStaleGatewayPids(pids) { const targets = Array.from(new Set(pids.filter((pid) => Number.isFinite(pid) && pid > 0))); for (const pid of targets) killProcessTree(pid, { graceMs: 300 }); if (targets.length > 0) await sleep(500); return targets; } //#endregion export { renderGatewayPortHealthDiagnostics as a, waitForGatewayHealthyListener as c, inspectGatewayRestart as i, waitForGatewayHealthyRestart as l, DEFAULT_RESTART_HEALTH_DELAY_MS as n, renderRestartDiagnostics as o, DEFAULT_RESTART_HEALTH_TIMEOUT_MS as r, terminateStaleGatewayPids as s, DEFAULT_RESTART_HEALTH_ATTEMPTS as t };