import { loadConfig } from "../../config/config.js"; import { extractDeliveryInfo } from "../../config/sessions.js"; import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js"; import { formatDoctorNonInteractiveHint, type RestartSentinelPayload, writeRestartSentinel, } from "../../infra/restart-sentinel.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { normalizeUpdateChannel } from "../../infra/update-channels.js"; import { runGatewayUpdate } from "../../infra/update-runner.js"; import { formatControlPlaneActor, resolveControlPlaneActor } from "../control-plane-audit.js"; import { validateUpdateRunParams } from "../protocol/index.js"; import { parseRestartRequestParams } from "./restart-request.js"; import type { GatewayRequestHandlers } from "./types.js"; import { assertValidParams } from "./validation.js"; export const updateHandlers: GatewayRequestHandlers = { "update.run": async ({ params, respond, client, context }) => { if (!assertValidParams(params, validateUpdateRunParams, "update.run", respond)) { return; } const actor = resolveControlPlaneActor(client); const { sessionKey, note, restartDelayMs } = parseRestartRequestParams(params); const { deliveryContext, threadId } = extractDeliveryInfo(sessionKey); const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs; const timeoutMs = typeof timeoutMsRaw === "number" && Number.isFinite(timeoutMsRaw) ? Math.max(1000, Math.floor(timeoutMsRaw)) : undefined; let result: Awaited>; try { const config = loadConfig(); const configChannel = normalizeUpdateChannel(config.update?.channel); const root = (await resolveOpenClawPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd(), })) ?? process.cwd(); result = await runGatewayUpdate({ timeoutMs, cwd: root, argv1: process.argv[1], channel: configChannel ?? undefined, }); } catch (err) { result = { status: "error", mode: "unknown", reason: String(err), steps: [], durationMs: 0, }; } const payload: RestartSentinelPayload = { kind: "update", status: result.status, ts: Date.now(), sessionKey, deliveryContext, threadId, message: note ?? null, doctorHint: formatDoctorNonInteractiveHint(), stats: { mode: result.mode, root: result.root ?? undefined, before: result.before ?? null, after: result.after ?? null, steps: result.steps.map((step) => ({ name: step.name, command: step.command, cwd: step.cwd, durationMs: step.durationMs, log: { stdoutTail: step.stdoutTail ?? null, stderrTail: step.stderrTail ?? null, exitCode: step.exitCode ?? null, }, })), reason: result.reason ?? null, durationMs: result.durationMs, }, }; let sentinelPath: string | null = null; try { sentinelPath = await writeRestartSentinel(payload); } catch { sentinelPath = null; } // Only restart the gateway when the update actually succeeded. // Restarting after a failed update leaves the process in a broken state // (corrupted node_modules, partial builds) and causes a crash loop. const restart = result.status === "ok" ? scheduleGatewaySigusr1Restart({ delayMs: restartDelayMs, reason: "update.run", audit: { actor: actor.actor, deviceId: actor.deviceId, clientIp: actor.clientIp, changedPaths: [], }, }) : null; context?.logGateway?.info( `update.run completed ${formatControlPlaneActor(actor)} changedPaths= restartReason=update.run status=${result.status}`, ); if (restart?.coalesced) { context?.logGateway?.warn( `update.run restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`, ); } respond( true, { ok: result.status !== "error", result, restart, sentinel: { path: sentinelPath, payload, }, }, undefined, ); }, };