// Shared bootstrap for status scans. // Starts update, Tailscale, agent, and gateway probes with cold-start shortcuts for first-run users. import type { OpenClawConfig } from "../config/types.js"; import type { UpdateCheckResult } from "../infra/update-check.js"; import { runExec } from "../process/exec.js"; import { createEmptyTaskAuditSummary } from "../tasks/task-registry.audit.shared.js"; import { createEmptyTaskRegistrySummary } from "../tasks/task-registry.summary.js"; import { buildTailscaleHttpsUrl, resolveGatewayProbeSnapshot } from "./status.scan.shared.js"; function buildColdStartUpdateResult(): UpdateCheckResult { return { root: null, installKind: "unknown", packageManager: "unknown", }; } function buildColdStartAgentLocalStatuses() { return { defaultId: "main", agents: [], totalSessions: 0, bootstrapPendingCount: 0, }; } /** Builds an empty summary for cold-start status paths that skip network and session work. */ export function buildColdStartStatusSummary() { return { runtimeVersion: null, heartbeat: { defaultAgentId: "main", agents: [], }, channelSummary: [], queuedSystemEvents: [], degradedSecretOwners: [], tasks: createEmptyTaskRegistrySummary(), taskAudit: createEmptyTaskAuditSummary(), sessions: { paths: [], count: 0, defaults: { model: null, contextTokens: null }, recent: [], byAgent: [], }, }; } function shouldSkipStatusScanNetworkChecks(params: { coldStart: boolean; hasConfiguredChannels: boolean; all?: boolean; }): boolean { // First-run users without channels should get instant status instead of waiting on network probes. return params.coldStart && !params.hasConfiguredChannels && params.all !== true; } type StatusScanExecRunner = ( command: string, args: string[], opts?: number | { timeoutMs?: number; maxBuffer?: number; cwd?: string }, ) => Promise<{ stdout: string; stderr: string }>; type StatusScanCoreBootstrapParams = { coldStart: boolean; cfg: OpenClawConfig; hasConfiguredChannels: boolean; opts: { timeoutMs?: number; all?: boolean }; skipUpdateCheck?: boolean; fetchGitUpdate?: boolean; includeRegistryUpdate?: boolean; includeLocalStatusRpcFallback?: boolean; gatewayProbeTimeoutMs?: number; getTailnetHostname: (runner: StatusScanExecRunner) => Promise; getUpdateCheckResult: (params: { timeoutMs: number; fetchGit: boolean; includeRegistry: boolean; updateConfigChannel?: string | null; }) => Promise; getAgentLocalStatuses: (cfg: OpenClawConfig) => Promise; }; /** Starts the common async probes used by status scans and exposes their promises to callers. */ export async function createStatusScanCoreBootstrap( params: StatusScanCoreBootstrapParams, ) { const tailscaleMode = params.cfg.gateway?.tailscale?.mode ?? "off"; const skipColdStartNetworkChecks = shouldSkipStatusScanNetworkChecks({ coldStart: params.coldStart, hasConfiguredChannels: params.hasConfiguredChannels, all: params.opts.all, }); const statusTimeoutMs = params.opts.timeoutMs ?? 10_000; const updateTimeoutMs = Math.min(params.opts.all ? 6500 : 2500, statusTimeoutMs); const tailscaleTimeoutMs = Math.min(1200, statusTimeoutMs); const tailscaleDnsPromise = tailscaleMode === "off" ? Promise.resolve(null) : params .getTailnetHostname((cmd, args) => runExec(cmd, args, { timeoutMs: tailscaleTimeoutMs, maxBuffer: 200_000 }), ) .catch(() => null); const skipNetworkUpdate = skipColdStartNetworkChecks || params.skipUpdateCheck === true; // Update checks can hit git/registry, so cold-start status uses a synthetic unknown result. const updatePromise = skipNetworkUpdate ? Promise.resolve(buildColdStartUpdateResult()) : params.getUpdateCheckResult({ timeoutMs: updateTimeoutMs, fetchGit: params.fetchGitUpdate ?? true, includeRegistry: params.includeRegistryUpdate ?? true, updateConfigChannel: params.cfg.update?.channel ?? null, }); const agentStatusPromise = skipColdStartNetworkChecks ? Promise.resolve(buildColdStartAgentLocalStatuses() as TAgentStatus) : params.getAgentLocalStatuses(params.cfg); const gatewayProbePromise = resolveGatewayProbeSnapshot({ cfg: params.cfg, opts: { ...params.opts, ...(params.gatewayProbeTimeoutMs !== undefined ? { timeoutMs: params.gatewayProbeTimeoutMs } : {}), ...(skipColdStartNetworkChecks ? { skipProbe: true } : {}), localStatusRpcFallback: params.includeLocalStatusRpcFallback !== false, }, }); return { tailscaleMode, tailscaleDnsPromise, updatePromise, agentStatusPromise, gatewayProbePromise, skipColdStartNetworkChecks, resolveTailscaleHttpsUrl: async () => buildTailscaleHttpsUrl({ tailscaleMode, tailscaleDns: await tailscaleDnsPromise, serviceName: params.cfg.gateway?.tailscale?.serviceName, controlUiBasePath: params.cfg.gateway?.controlUi?.basePath, }), }; }