UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

176 lines (175 loc) • 9.77 kB
import { t as formatCliCommand } from "./command-format-Cte1SWM4.mjs"; import { i as getNodeSqliteKysely, n as executeSqliteQuerySync, r as executeSqliteQueryTakeFirstSync } from "./kysely-sync-BMSZn9yz.mjs"; import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs"; import { t as GATEWAY_STARTUP_MAINTENANCE_REQUIRED_REASON } from "./startup-maintenance-required-CNRZ_k46.mjs"; import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-BvBEgHyD.mjs"; import { f as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BLZyK3So.mjs"; import { l as runOpenClawStateWriteTransaction, r as openOpenClawStateDatabase } from "./openclaw-state-db-VZFEEleX.mjs"; import { t as pathMayExistSync } from "./path-existence-O1bMV6vh.mjs"; import { randomUUID } from "node:crypto"; //#region src/infra/gateway-boot-lifecycle.ts const maintenanceStartupReasons = [GATEWAY_STARTUP_MAINTENANCE_REQUIRED_REASON, "gateway.agent_media_migration_required"]; const GATEWAY_BOOT_LOOP_UNCLEAN_THRESHOLD = 3; const GATEWAY_BOOT_LOOP_WINDOW_MS = 3e5; const GATEWAY_BOOT_LIFECYCLE_RETENTION_MS = 864e5; const GATEWAY_CRASH_LOOP_BREAKER_REASON = "gateway.crash_loop_breaker"; const GATEWAY_CRASH_LOOP_RECOVERED_REASON = "gateway.crash_loop_recovered"; const GATEWAY_SIGNAL_REPEAT_WINDOW_MS = 3e5; function formatGatewayRepeatedSignalHint(signal, count, observation = "received") { return `${observation} ${signal} ${count} times in 5 min: another supervisor may be managing this Gateway — see \`openclaw gateway status --deep\``; } /** * The breaker only self-clears after the full window drains. Operator surfaces name the manual * override command, not the internal RPC. Account hints carry accountId to avoid starting a * different default account than the warning named. */ function formatGatewayCrashLoopManualChannelStartHint(target) { const params = JSON.stringify({ channel: target?.channelId ?? "<id>", ...target?.accountId ? { accountId: target.accountId } : {} }); return `Start a channel manually with: ${formatCliCommand("openclaw gateway call channels.start")} --params '${params}'`; } const gatewayLifecycleLog = createSubsystemLogger("gateway/lifecycle"); function readGatewayLastShutdown(env = process.env) { try { return withExistingOpenClawStateDatabaseReadOnly(({ db }) => { const row = executeSqliteQueryTakeFirstSync(db, getNodeSqliteKysely(db).selectFrom("gateway_boot_lifecycle").select(["reason", "completed_at_ms as completedAtMs"]).where("outcome", "in", [ "clean_stop", "planned_restart", "forced_stop" ]).where("completed_at_ms", "is not", null).orderBy("completed_at_ms", "desc").limit(1)); return row?.completedAtMs == null ? void 0 : { ...row, completedAtMs: row.completedAtMs }; }, { env }); } catch { return; } } function readGatewayLastInstallationReplacement(env = process.env) { const lastShutdown = readGatewayLastShutdown(env); if (!lastShutdown?.reason?.startsWith("gateway.installation_replaced:")) return; return { reason: lastShutdown.reason, completedAtMs: lastShutdown.completedAtMs }; } function buildGatewayCrashLoopBreakerDecision(params) { const windowMs = params.windowMs ?? GATEWAY_BOOT_LOOP_WINDOW_MS; const tripped = params.uncleanBoots >= GATEWAY_BOOT_LOOP_UNCLEAN_THRESHOLD; const hasUnrecoveredBreakerMarker = typeof params.latestBreakerStartedAtMs === "number" && (typeof params.latestRecoveryStartedAtMs !== "number" || params.latestRecoveryStartedAtMs < params.latestBreakerStartedAtMs); return { tripped, uncleanBoots: params.uncleanBoots, windowMs, shouldWriteStabilityBundle: tripped && !hasUnrecoveredBreakerMarker, recovered: !tripped && hasUnrecoveredBreakerMarker }; } function inspectGatewayCrashLoopBreaker(env = process.env, nowMs = Date.now()) { try { const { db } = openOpenClawStateDatabase({ env }); const kysely = getNodeSqliteKysely(db); const windowStartMs = nowMs - GATEWAY_BOOT_LOOP_WINDOW_MS; const uncleanRow = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("gateway_boot_lifecycle").select((eb) => eb.fn.countAll().as("count")).where((eb) => eb.or([eb("startup_reason", "is", null), eb("startup_reason", "not in", maintenanceStartupReasons)])).where((eb) => eb.or([eb.and([eb("completed_at_ms", "is", null), eb("started_at_ms", ">=", windowStartMs)]), eb.and([eb("outcome", "=", "startup_failed"), eb("completed_at_ms", ">=", windowStartMs)])]))); const latestBreaker = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("gateway_boot_lifecycle").select("started_at_ms as startedAtMs").where("startup_reason", "=", GATEWAY_CRASH_LOOP_BREAKER_REASON).orderBy("started_at_ms", "desc").limit(1)); const latestRecovery = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("gateway_boot_lifecycle").select("started_at_ms as startedAtMs").where("startup_reason", "=", GATEWAY_CRASH_LOOP_RECOVERED_REASON).orderBy("started_at_ms", "desc").limit(1)); return buildGatewayCrashLoopBreakerDecision({ uncleanBoots: uncleanRow?.count ?? 0, latestBreakerStartedAtMs: latestBreaker?.startedAtMs, latestRecoveryStartedAtMs: latestRecovery?.startedAtMs }); } catch (err) { gatewayLifecycleLog.warn(`crash-loop breaker state unavailable; fail-open: ${String(err)}`); return buildGatewayCrashLoopBreakerDecision({ uncleanBoots: 0 }); } } function recordGatewayBootStart(env = process.env, nowMs = Date.now(), reason) { const bootId = randomUUID(); try { runOpenClawStateWriteTransaction(({ db }) => { const kysely = getNodeSqliteKysely(db); executeSqliteQuerySync(db, kysely.deleteFrom("gateway_boot_lifecycle").where("started_at_ms", "<", nowMs - GATEWAY_BOOT_LIFECYCLE_RETENTION_MS)); executeSqliteQuerySync(db, kysely.insertInto("gateway_boot_lifecycle").values({ boot_id: bootId, pid: process.pid, started_at_ms: nowMs, completed_at_ms: null, outcome: null, startup_reason: reason ?? null, reason: null })); }, { env }); return bootId; } catch (err) { gatewayLifecycleLog.warn(`failed to persist gateway boot start; fail-open: ${String(err)}`); return; } } /** * Split a stable safe-mode lifetime before channel autostart resumes. A fresh * open row makes a process death during recovered channel startup count toward * the next breaker decision instead of aging out with the original boot. */ function recordGatewayCrashLoopRecovery(bootId, env = process.env, nowMs = Date.now()) { const recoveredBootId = randomUUID(); try { runOpenClawStateWriteTransaction(({ db }) => { const kysely = getNodeSqliteKysely(db); if (bootId) executeSqliteQuerySync(db, kysely.updateTable("gateway_boot_lifecycle").set({ completed_at_ms: nowMs, outcome: "safe_mode_stable", reason: null }).where("boot_id", "=", bootId)); executeSqliteQuerySync(db, kysely.insertInto("gateway_boot_lifecycle").values({ boot_id: recoveredBootId, pid: process.pid, started_at_ms: nowMs, completed_at_ms: null, outcome: null, startup_reason: GATEWAY_CRASH_LOOP_RECOVERED_REASON, reason: null })); }, { env }); return recoveredBootId; } catch (err) { gatewayLifecycleLog.warn(`failed to persist gateway crash-loop recovery; fail-safe: ${String(err)}`); return; } } function completeGatewayBootLifecycle(bootId, completion, env = process.env, nowMs = Date.now()) { if (!bootId) return; const signal = completion.outcome !== "clean_stop" ? void 0 : completion.reason === "stop (SIGTERM)" ? "SIGTERM" : completion.reason === "stop (SIGINT)" ? "SIGINT" : void 0; try { const recentStops = runOpenClawStateWriteTransaction(({ db }) => { const kysely = getNodeSqliteKysely(db); executeSqliteQuerySync(db, kysely.updateTable("gateway_boot_lifecycle").set({ completed_at_ms: nowMs, outcome: completion.outcome, ...completion.startupReason ? { startup_reason: completion.startupReason } : {}, reason: completion.reason ?? null }).where("boot_id", "=", bootId)); return signal ? executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("gateway_boot_lifecycle").select((eb) => eb.fn.countAll().as("count")).where("outcome", "=", "clean_stop").where("reason", "=", completion.reason ?? null).where("completed_at_ms", ">=", nowMs - GATEWAY_SIGNAL_REPEAT_WINDOW_MS))?.count : void 0; }, { env }); if (signal && recentStops !== void 0 && recentStops >= 3) gatewayLifecycleLog.warn(formatGatewayRepeatedSignalHint(signal, recentStops, "stopped after")); } catch (err) { gatewayLifecycleLog.warn(`failed to persist gateway boot outcome; fail-open: ${String(err)}`); } } function repairGatewayMaintenanceStartupFailures(env = process.env) { if (!pathMayExistSync(resolveOpenClawStateSqlitePath(env))) return 0; try { return runOpenClawStateWriteTransaction(({ db }) => { const kysely = getNodeSqliteKysely(db); const result = executeSqliteQuerySync(db, kysely.updateTable("gateway_boot_lifecycle").set({ outcome: "startup_failure_repaired" }).where("outcome", "=", "startup_failed").where("startup_reason", "in", maintenanceStartupReasons)); return Number(result.numAffectedRows ?? 0); }, { env }); } catch (err) { gatewayLifecycleLog.warn(`failed to repair maintenance startup history; fail-open: ${String(err)}`); return 0; } } //#endregion export { formatGatewayCrashLoopManualChannelStartHint as a, readGatewayLastInstallationReplacement as c, recordGatewayCrashLoopRecovery as d, repairGatewayMaintenanceStartupFailures as f, completeGatewayBootLifecycle as i, readGatewayLastShutdown as l, GATEWAY_CRASH_LOOP_RECOVERED_REASON as n, formatGatewayRepeatedSignalHint as o, GATEWAY_SIGNAL_REPEAT_WINDOW_MS as r, inspectGatewayCrashLoopBreaker as s, GATEWAY_CRASH_LOOP_BREAKER_REASON as t, recordGatewayBootStart as u };