UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

275 lines (274 loc) 12 kB
import { d as asPositiveSafeInteger } from "./number-coercion-CLj0HTDM.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js"; import { s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js"; import { randomUUID } from "node:crypto"; //#region src/infra/restart-handoff.ts const GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND = "gateway-supervisor-restart-handoff"; const GATEWAY_SUPERVISOR_RESTART_HANDOFF_KEY = "current"; const GATEWAY_RESTART_HANDOFF_SCHEMA_VERSION = 1; const GATEWAY_RESTART_HANDOFF_TTL_MS = 6e4; const GATEWAY_RESTART_TRACE_HANDOFF_MAX_DURATION_MS = 6e5; const MAX_INTENT_ID_LENGTH = 120; const MAX_PROCESS_INSTANCE_ID_LENGTH = 120; const MAX_REASON_LENGTH = 200; const handoffLog = createSubsystemLogger("restart-handoff"); function formatShortDuration(ms) { const clamped = Math.max(0, Math.floor(ms)); if (clamped < 1e3) return `${clamped}ms`; const seconds = Math.floor(clamped / 1e3); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`; } function formatDiagnosticValue(value) { let normalized = ""; let previousWasSpace = true; for (const char of value) { const code = char.charCodeAt(0); if (code <= 31 || code === 127 || /\s/u.test(char)) { if (!previousWasSpace) { normalized += " "; previousWasSpace = true; } continue; } normalized += char; previousWasSpace = false; } return normalized.trimEnd(); } /** Format a compact diagnostic for a recently consumed restart handoff. */ function formatGatewayRestartHandoffDiagnostic(handoff, now = Date.now()) { const reason = handoff.reason ? formatDiagnosticValue(handoff.reason) : void 0; return `Recent restart handoff: ${[ `${handoff.restartKind} via ${handoff.supervisorMode}`, `source=${handoff.source}`, reason ? `reason=${reason}` : void 0, `pid=${handoff.pid}`, `age=${formatShortDuration(now - handoff.createdAt)}`, `expiresIn=${formatShortDuration(handoff.expiresAt - now)}` ].filter((value) => Boolean(value)).join("; ")}`; } function normalizePid(pid) { return asPositiveSafeInteger(pid) ?? null; } function normalizeText(value, maxLength) { const text = typeof value === "string" ? value.trim() : ""; return text ? truncateUtf16Safe(text, maxLength) : void 0; } function normalizeCreatedAt(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : Date.now(); } function normalizeTtlMs(value) { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return GATEWAY_RESTART_HANDOFF_TTL_MS; return Math.min(Math.floor(value), GATEWAY_RESTART_HANDOFF_TTL_MS); } function normalizeRestartTraceHandoff(value) { if (typeof value !== "object" || value === null || Array.isArray(value)) return; const record = value; if (typeof record.startedAt !== "number" || !Number.isFinite(record.startedAt) || typeof record.lastAt !== "number" || !Number.isFinite(record.lastAt) || record.startedAt <= 0 || record.lastAt < record.startedAt || record.lastAt - record.startedAt > GATEWAY_RESTART_TRACE_HANDOFF_MAX_DURATION_MS) return; return { startedAt: Math.floor(record.startedAt), lastAt: Math.floor(record.lastAt) }; } function normalizeSource(source, reason) { if (source) return source; if (!reason) return "unknown"; const normalized = reason.toLowerCase(); if (normalized === "update.run") return "gateway-update"; if (normalized === "sigusr1") return "signal"; if (normalized === "gateway.restart") return "operator-restart"; if (normalized.includes("plugin")) return "plugin-change"; if (normalized.includes("config") || normalized.includes("include")) return "config-write"; return "unknown"; } function isSource(value) { return value === "config-write" || value === "gateway-update" || value === "operator-restart" || value === "plugin-change" || value === "signal" || value === "unknown"; } function isRestartKind(value) { return value === "full-process" || value === "update-process"; } function isSupervisorMode(value) { return value === "launchd" || value === "systemd" || value === "schtasks" || value === "external"; } function normalizeGatewayRestartHandoffRow(row) { const intentId = normalizeText(row.intent_id, MAX_INTENT_ID_LENGTH); if (row.kind !== GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND || row.version !== GATEWAY_RESTART_HANDOFF_SCHEMA_VERSION || !intentId || typeof row.pid !== "number" || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.created_at !== "number" || !Number.isFinite(row.created_at) || typeof row.expires_at !== "number" || !Number.isFinite(row.expires_at) || row.expires_at <= row.created_at || row.expires_at - row.created_at > GATEWAY_RESTART_HANDOFF_TTL_MS || !isSource(row.source) || !isRestartKind(row.restart_kind) || !isSupervisorMode(row.supervisor_mode)) return null; const restartTrace = normalizeRestartTraceHandoff(row.restart_trace_started_at !== null && row.restart_trace_last_at !== null ? { startedAt: row.restart_trace_started_at, lastAt: row.restart_trace_last_at } : null); const processInstanceId = normalizeText(row.process_instance_id, MAX_PROCESS_INSTANCE_ID_LENGTH); const reason = normalizeText(row.reason, MAX_REASON_LENGTH); return { kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND, version: GATEWAY_RESTART_HANDOFF_SCHEMA_VERSION, intentId, pid: row.pid, ...processInstanceId ? { processInstanceId } : {}, createdAt: Math.floor(row.created_at), expiresAt: Math.floor(row.expires_at), ...reason ? { reason } : {}, source: row.source, restartKind: row.restart_kind, supervisorMode: row.supervisor_mode, ...restartTrace ? { restartTrace } : {} }; } function selectGatewayRestartHandoffRowSync(db) { const stateDb = getNodeSqliteKysely(db); return executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("gateway_restart_handoff").select([ "kind", "version", "intent_id", "pid", "process_instance_id", "created_at", "expires_at", "reason", "restart_trace_started_at", "restart_trace_last_at", "source", "restart_kind", "supervisor_mode" ]).where("handoff_key", "=", GATEWAY_SUPERVISOR_RESTART_HANDOFF_KEY)); } function readGatewayRestartHandoffRowSync(env) { try { return withExistingOpenClawStateDatabaseReadOnly(({ db }) => selectGatewayRestartHandoffRowSync(db), { env }) ?? null; } catch { return null; } } /** Write the bounded supervisor restart handoff atomically. */ function writeGatewayRestartHandoffSync(opts) { const pid = normalizePid(opts.pid ?? process.pid); if (pid === null || !isRestartKind(opts.restartKind)) return null; if (opts.source !== void 0 && !isSource(opts.source)) return null; const supervisorMode = opts.supervisorMode ?? "external"; if (!isSupervisorMode(supervisorMode)) return null; const env = opts.env ?? process.env; const createdAt = normalizeCreatedAt(opts.createdAt); const ttlMs = normalizeTtlMs(opts.ttlMs); const reason = normalizeText(opts.reason, MAX_REASON_LENGTH); const processInstanceId = normalizeText(opts.processInstanceId, MAX_PROCESS_INSTANCE_ID_LENGTH); const restartTrace = normalizeRestartTraceHandoff(opts.restartTrace); const payload = { kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND, version: GATEWAY_RESTART_HANDOFF_SCHEMA_VERSION, intentId: randomUUID(), pid, ...processInstanceId ? { processInstanceId } : {}, createdAt, expiresAt: createdAt + ttlMs, ...reason ? { reason } : {}, source: normalizeSource(opts.source, reason), restartKind: opts.restartKind, supervisorMode, ...restartTrace ? { restartTrace } : {} }; try { runOpenClawStateWriteTransaction(({ db }) => { const stateDb = getNodeSqliteKysely(db); executeSqliteQuerySync(db, stateDb.insertInto("gateway_restart_handoff").values({ handoff_key: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KEY, kind: payload.kind, version: payload.version, intent_id: payload.intentId, pid: payload.pid, process_instance_id: payload.processInstanceId ?? null, created_at: payload.createdAt, expires_at: payload.expiresAt, reason: payload.reason ?? null, restart_trace_started_at: payload.restartTrace?.startedAt ?? null, restart_trace_last_at: payload.restartTrace?.lastAt ?? null, source: payload.source, restart_kind: payload.restartKind, supervisor_mode: payload.supervisorMode, updated_at_ms: Date.now() }).onConflict((conflict) => conflict.column("handoff_key").doUpdateSet({ kind: (eb) => eb.ref("excluded.kind"), version: (eb) => eb.ref("excluded.version"), intent_id: (eb) => eb.ref("excluded.intent_id"), pid: (eb) => eb.ref("excluded.pid"), process_instance_id: (eb) => eb.ref("excluded.process_instance_id"), created_at: (eb) => eb.ref("excluded.created_at"), expires_at: (eb) => eb.ref("excluded.expires_at"), reason: (eb) => eb.ref("excluded.reason"), restart_trace_started_at: (eb) => eb.ref("excluded.restart_trace_started_at"), restart_trace_last_at: (eb) => eb.ref("excluded.restart_trace_last_at"), source: (eb) => eb.ref("excluded.source"), restart_kind: (eb) => eb.ref("excluded.restart_kind"), supervisor_mode: (eb) => eb.ref("excluded.supervisor_mode"), updated_at_ms: (eb) => eb.ref("excluded.updated_at_ms") }))); }, { env }); return payload; } catch (err) { handoffLog.warn(`failed to write gateway restart handoff: ${String(err)}`); return null; } } /** Read the current unexpired restart handoff without consuming it. */ function readGatewayRestartHandoffSync(env = process.env, now = Date.now()) { const row = readGatewayRestartHandoffRowSync(env); const payload = row ? normalizeGatewayRestartHandoffRow(row) : null; if (!payload || now < payload.createdAt || now >= payload.expiresAt) return null; return payload; } /** * Atomically validate and consume the current restart handoff for one exited process. * * PID mismatches are retained for the matching supervisor. Accepted, expired, and * malformed rows are removed while the immediate transaction still owns the write lock. */ function consumeGatewayRestartHandoffSync(opts) { const expectedPid = normalizePid(opts.expectedPid); if (expectedPid === null) throw new Error("expectedPid must be a positive safe integer"); const fixedNow = typeof opts.now === "number" && Number.isFinite(opts.now) && opts.now >= 0 ? Math.floor(opts.now) : void 0; return runOpenClawStateWriteTransaction(({ db }) => { const now = fixedNow ?? Date.now(); const stateDb = getNodeSqliteKysely(db); const row = selectGatewayRestartHandoffRowSync(db); if (!row) return { status: "none", reason: "missing" }; const removeCurrent = () => { executeSqliteQuerySync(db, stateDb.deleteFrom("gateway_restart_handoff").where("handoff_key", "=", GATEWAY_SUPERVISOR_RESTART_HANDOFF_KEY)); }; const handoff = normalizeGatewayRestartHandoffRow(row); if (!handoff || now < handoff.createdAt) { removeCurrent(); return { status: "rejected", reason: "invalid" }; } if (now >= handoff.expiresAt) { removeCurrent(); return { status: "rejected", reason: "expired", handoffPid: handoff.pid }; } if (handoff.pid !== expectedPid) return { status: "rejected", reason: "pid-mismatch", handoffPid: handoff.pid }; removeCurrent(); return { status: "accepted", handoff }; }, { env: opts.env ?? process.env }); } //#endregion export { writeGatewayRestartHandoffSync as i, formatGatewayRestartHandoffDiagnostic as n, readGatewayRestartHandoffSync as r, consumeGatewayRestartHandoffSync as t };