import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { migrateWorkspaceIfNeeded } from "../agents/workspace.js";
import { initSubagentRegistry } from "../agents/subagent-registry.js";
import { registerSkillsChangeListener, ensureSkillsWatcher, } from "../agents/skills/refresh.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
import fs from "node:fs";
import path from "node:path";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
import { skillsHandlers } from "./server-methods/skills.js";
import { listChannelPlugins } from "../channels/plugins/index.js";
import { createDefaultDeps } from "../cli/deps.js";
import { formatCliCommand } from "../cli/command-format.js";
import { CONFIG_PATH, isNixMode, loadConfig, migrateLegacyConfig, readConfigFileSnapshot, writeConfigFile, } from "../config/config.js";
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
import { logAcceptedEnvOption } from "../infra/env.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import { ensureClawdbotCliOnPath } from "../infra/path-env.js";
import { primeRemoteSkillsCache, refreshRemoteBinsForConnectedNodes, setSkillsRemoteRegistry, } from "../infra/skills-remote.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import { runOnboardingWizard } from "../wizard/onboarding.js";
import { startGatewayConfigReloader } from "./config-reload.js";
import { getHealthCache, getHealthVersion, getPresenceVersion, incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "./server/health-state.js";
import { startGatewayDiscovery } from "./server-discovery-runtime.js";
import { ExecApprovalManager } from "./exec-approval-manager.js";
import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
import { createChannelManager } from "./server-channels.js";
import { createAgentEventHandler } from "./server-chat.js";
import { createGatewayCloseHandler } from "./server-close.js";
import { buildGatewayCronService } from "./server-cron.js";
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
import { NodeRegistry } from "./node-registry.js";
import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
import { safeParseJson } from "./server-methods/nodes.helpers.js";
import { loadGatewayPlugins } from "./server-plugins.js";
import { createGatewayReloadHandlers } from "./server-reload-handlers.js";
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
import { createGatewayRuntimeState } from "./server-runtime-state.js";
import { hasConnectedMobileNode } from "./server-mobile-nodes.js";
import { resolveSessionKeyForRun } from "./server-session-key.js";
import { startGatewaySidecars } from "./server-startup.js";
import { logGatewayStartup } from "./server-startup-log.js";
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
import { loadGatewayTlsRuntime } from "./server/tls.js";
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
export { __resetModelCatalogCacheForTest } from "./server-model-catalog.js";
ensureClawdbotCliOnPath();
const log = createSubsystemLogger("gateway");
const logCanvas = log.child("canvas");
const logDiscovery = log.child("discovery");
const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
const logBrowser = log.child("browser");
const logHealth = log.child("health");
const logCron = log.child("cron");
const logReload = log.child("reload");
const logHooks = log.child("hooks");
const logPlugins = log.child("plugins");
const logWsControl = log.child("ws");
const canvasRuntime = runtimeForLogger(logCanvas);
export async function startGatewayServer(port = 18789, opts = {}) {
    // Ensure all default port derivations (browser/canvas) see the actual runtime port.
    process.env.OPENCLAW_GATEWAY_PORT = String(port);
    // Migrate legacy workspace directory (~/clawd -> ~/openclaw) on gateway startup
    const migrationResult = await migrateWorkspaceIfNeeded();
    if (migrationResult.migrated) {
        log.info(`gateway: migrated workspace directory from ${migrationResult.legacyDir} to ${migrationResult.newDir}`);
    }
    logAcceptedEnvOption({
        key: "OPENCLAW_RAW_STREAM",
        description: "raw stream logging enabled",
    });
    logAcceptedEnvOption({
        key: "OPENCLAW_RAW_STREAM_PATH",
        description: "raw stream log path override",
    });
    let configSnapshot = await readConfigFileSnapshot();
    if (configSnapshot.legacyIssues.length > 0) {
        if (isNixMode) {
            throw new Error("Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart.");
        }
        const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
        if (!migrated) {
            throw new Error(`Legacy config entries detected but auto-migration failed. Run "${formatCliCommand("openclaw-cn doctor")}" to migrate.`);
        }
        await writeConfigFile(migrated);
        if (changes.length > 0) {
            log.info(`gateway: migrated legacy config entries:\n${changes
                .map((entry) => `- ${entry}`)
                .join("\n")}`);
        }
    }
    configSnapshot = await readConfigFileSnapshot();
    if (configSnapshot.exists && !configSnapshot.valid) {
        const issues = configSnapshot.issues.length > 0
            ? configSnapshot.issues
                .map((issue) => `${issue.path || "<root>"}: ${issue.message}`)
                .join("\n")
            : "Unknown validation issue.";
        throw new Error(`Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("openclaw-cn doctor")}" to repair, then retry.`);
    }
    const autoEnable = applyPluginAutoEnable({ config: configSnapshot.config, env: process.env });
    if (autoEnable.changes.length > 0) {
        try {
            await writeConfigFile(autoEnable.config);
            log.info(`gateway: auto-enabled plugins:\n${autoEnable.changes
                .map((entry) => `- ${entry}`)
                .join("\n")}`);
        }
        catch (err) {
            log.warn(`gateway: failed to persist plugin auto-enable changes: ${String(err)}`);
        }
    }
    const cfgAtStart = loadConfig();
    const diagnosticsEnabled = isDiagnosticsEnabled(cfgAtStart);
    if (diagnosticsEnabled) {
        startDiagnosticHeartbeat();
    }
    setGatewaySigusr1RestartPolicy({ allowExternal: cfgAtStart.commands?.restart === true });
    initSubagentRegistry();
    const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
    const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
    const baseMethods = listGatewayMethods();
    const { pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayPlugins({
        cfg: cfgAtStart,
        workspaceDir: defaultWorkspaceDir,
        log,
        coreGatewayHandlers,
        baseMethods,
    });
    const channelLogs = Object.fromEntries(listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]));
    const channelRuntimeEnvs = Object.fromEntries(Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]));
    const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
    const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
    let pluginServices = null;
    const runtimeConfig = await resolveGatewayRuntimeConfig({
        cfg: cfgAtStart,
        port,
        bind: opts.bind,
        host: opts.host,
        controlUiEnabled: opts.controlUiEnabled,
        openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
        openResponsesEnabled: opts.openResponsesEnabled,
        auth: opts.auth,
        tailscale: opts.tailscale,
    });
    const { bindHost, controlUiEnabled, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, controlUiBasePath, resolvedAuth, tailscaleConfig, tailscaleMode, } = runtimeConfig;
    let hooksConfig = runtimeConfig.hooksConfig;
    const canvasHostEnabled = runtimeConfig.canvasHostEnabled;
    const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
    const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
    const deps = createDefaultDeps();
    let canvasHostServer = null;
    const gatewayTls = await loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls"));
    if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
        throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
    }
    const { canvasHost, httpServer, httpServers, httpBindHosts, wss, clients, broadcast, agentRunSeq, dedupe, chatRunState, chatRunBuffers, chatDeltaSentAt, addChatRun, removeChatRun, chatAbortControllers, } = await createGatewayRuntimeState({
        cfg: cfgAtStart,
        bindHost,
        port,
        controlUiEnabled,
        controlUiBasePath,
        openAiChatCompletionsEnabled,
        openResponsesEnabled,
        openResponsesConfig,
        resolvedAuth,
        gatewayTls,
        hooksConfig: () => hooksConfig,
        pluginRegistry,
        deps,
        canvasRuntime,
        canvasHostEnabled,
        allowCanvasHostInTests: opts.allowCanvasHostInTests,
        logCanvas,
        log,
        logHooks,
        logPlugins,
    });
    let bonjourStop = null;
    const nodeRegistry = new NodeRegistry();
    const nodePresenceTimers = new Map();
    const nodeSubscriptions = createNodeSubscriptionManager();
    const nodeSendEvent = (opts) => {
        const payload = safeParseJson(opts.payloadJSON ?? null);
        nodeRegistry.sendEvent(opts.nodeId, opts.event, payload);
    };
    const nodeSendToSession = (sessionKey, event, payload) => nodeSubscriptions.sendToSession(sessionKey, event, payload, nodeSendEvent);
    const nodeSendToAllSubscribed = (event, payload) => nodeSubscriptions.sendToAllSubscribed(event, payload, nodeSendEvent);
    const nodeSubscribe = nodeSubscriptions.subscribe;
    const nodeUnsubscribe = nodeSubscriptions.unsubscribe;
    const nodeUnsubscribeAll = nodeSubscriptions.unsubscribeAll;
    const broadcastVoiceWakeChanged = (triggers) => {
        broadcast("voicewake.changed", { triggers }, { dropIfSlow: true });
    };
    const hasMobileNodeConnected = () => hasConnectedMobileNode(nodeRegistry);
    applyGatewayLaneConcurrency(cfgAtStart);
    let cronState = buildGatewayCronService({
        cfg: cfgAtStart,
        deps,
        broadcast,
    });
    let { cron, storePath: cronStorePath } = cronState;
    const channelManager = createChannelManager({
        loadConfig,
        channelLogs,
        channelRuntimeEnvs,
    });
    const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;
    const machineDisplayName = await getMachineDisplayName();
    const discovery = await startGatewayDiscovery({
        machineDisplayName,
        port,
        gatewayTls: gatewayTls.enabled
            ? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
            : undefined,
        wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
        tailscaleMode,
        mdnsMode: cfgAtStart.discovery?.mdns?.mode,
        logDiscovery,
    });
    bonjourStop = discovery.bonjourStop;
    setSkillsRemoteRegistry(nodeRegistry);
    void primeRemoteSkillsCache();
    // Ensure skills watcher is running for all agent workspaces
    {
        logHooks.info(`ensure skills watcher is running for all agent workspaces`);
        const cfg = loadConfig();
        const agentList = cfg.agents?.list ?? [];
        const agentIds = agentList
            .map((e) => (e && typeof e === "object" && typeof e.id === "string" ? e.id : null))
            .filter((v) => Boolean(v));
        const workspaces = new Set();
        for (const id of agentIds) {
            workspaces.add(resolveAgentWorkspaceDir(cfg, id));
        }
        workspaces.add(resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)));
        for (const dir of workspaces) {
            ensureSkillsWatcher({ workspaceDir: dir, config: cfg });
        }
    }
    async function handleSkillsChangeForSecurityHook(event) {
        const runner = getGlobalHookRunner();
        const hasHooks = runner?.hasHooks("before_skills_load") ?? false;
        if (!hasHooks) {
            return;
        }
        if (!event.changedPath || event.changedPath?.trim() === "") {
            return;
        }
        if (!fs.existsSync(event.changedPath)) {
            logHooks.info(`watched skills file removed: ${event.changedPath}`);
            return;
        }
        logHooks.info(`watched skills file changed: ${event?.changedPath}`);
        const cfg = loadConfig();
        const skillsRootPath = new Set();
        if (event.workspaceDir?.trim()) {
            skillsRootPath.add(path.join(event.workspaceDir, "skills"));
        }
        skillsRootPath.add(path.join(CONFIG_DIR, "skills"));
        const extraSkillsDirs = cfg?.skills?.load?.extraDirs ?? [];
        const extraSkillsDirsPath = extraSkillsDirs
            .map((d) => (typeof d === "string" ? d.trim() : ""))
            .filter(Boolean)
            .map((dir) => resolveUserPath(dir));
        for (const d of extraSkillsDirsPath) {
            if (d.trim()) {
                skillsRootPath.add(d);
            }
        }
        logHooks.info(`skillsRootPath path is ${JSON.stringify(Array.from(skillsRootPath))}`);
        const p = path.resolve(event.changedPath);
        let skillsFileDir = null;
        let skillsPackageDirName = null;
        let skillsName = null;
        let matchedRoot = null;
        for (const root of skillsRootPath) {
            const r = path.resolve(root);
            if (p === r || p.startsWith(r + path.sep)) {
                if (!matchedRoot || r.length > matchedRoot.length) {
                    matchedRoot = r;
                }
            }
        }
        if (matchedRoot) {
            const rel = path.relative(matchedRoot, p);
            const segs = rel.split(path.sep).filter(Boolean);
            if (segs.length >= 1) {
                skillsPackageDirName = segs[0];
                skillsFileDir = path.join(matchedRoot, skillsPackageDirName);
            }
        }
        if (skillsFileDir) {
            try {
                const workspaceSkillsRoot = path.join(event.workspaceDir ?? "", "skills");
                const configSkillsRoot = path.join(CONFIG_DIR, "skills");
                const root = matchedRoot ?? path.dirname(skillsFileDir);
                const sourceLabel = root === workspaceSkillsRoot
                    ? "openclaw-workspace"
                    : root === configSkillsRoot
                        ? "openclaw-managed"
                        : extraSkillsDirsPath.includes(root)
                            ? "openclaw-extra"
                            : "not-support";
                logHooks.info(`skills file dir is ${skillsFileDir} and source is ${sourceLabel}`);
                const loaded = loadSkillsFromDir({ dir: skillsFileDir, source: sourceLabel });
                let skills = [];
                if (Array.isArray(loaded)) {
                    skills = loaded;
                }
                else if (loaded &&
                    typeof loaded === "object" &&
                    "skills" in loaded &&
                    Array.isArray(loaded.skills)) {
                    skills = loaded.skills;
                }
                const target = path.resolve(skillsFileDir);
                const matchedSkills = skills.find((s) => {
                    const base = path.resolve(s.baseDir ?? "");
                    return base === target;
                });
                if (matchedSkills?.name) {
                    skillsName = matchedSkills.name;
                }
            }
            catch { }
            logHooks.info(`skills name is ${skillsName}, skills package dir name is ${skillsPackageDirName}`);
            if (!skillsName && skillsPackageDirName) {
                skillsName = skillsPackageDirName;
            }
        }
        if (skillsFileDir && skillsName) {
            const loadSkill = { skillsName: skillsName, skillsFileDir: skillsFileDir };
            runner
                ?.runBeforeSkillsLoad({ loadSkill }, { workspaceDir: event.workspaceDir ?? "" })
                .then((hookResult) => {
                if (!hookResult) {
                    // no hook result, skip
                    return;
                }
                logHooks.info(`skills hook security scan result blocked is ${hookResult?.blocked}`);
                const securityInfo = `security info:${hookResult?.securityInfo}(severity:${hookResult?.severity}, risk score:${hookResult?.riskScore})`;
                const req = {
                    type: "req",
                    id: `internal-${skillsName}-disable`,
                    method: "skills.update",
                };
                const ctx = {};
                let params = {};
                if (hookResult?.blocked === true) {
                    params = {
                        skillKey: skillsName,
                        enabled: false,
                        securityInfo: securityInfo,
                        securityBlocked: true,
                    };
                }
                else {
                    params = {
                        skillKey: skillsName,
                        securityInfo: "",
                        securityBlocked: false,
                    };
                }
                void skillsHandlers["skills.update"]({
                    req,
                    params: params,
                    client: null,
                    isWebchatConnect: () => false,
                    respond: (ok, error) => {
                        if (!ok) {
                            logHooks.error(`[skills.update] skillsName: "${skillsName}" , failed: ${JSON.stringify(error ?? {})}}`);
                        }
                    },
                    context: ctx,
                });
                broadcast("skills", {
                    kind: "skills_security_scan",
                    name: skillsName,
                    enabled: hookResult?.blocked === false,
                    blocked: hookResult?.blocked === true,
                    reason: securityInfo,
                }, { dropIfSlow: true });
            })
                .catch(() => { });
        }
    }
    // Debounce skills-triggered node probes to avoid feedback loops and rapid-fire invokes.
    // Skills changes can happen in bursts (e.g., file watcher events), and each probe
    // takes time to complete. A 30-second delay ensures we batch changes together.
    let skillsRefreshTimer = null;
    const skillsRefreshDelayMs = 30_000;
    const skillsChangeUnsub = registerSkillsChangeListener((event) => {
        if (event.reason === "remote-node")
            return;
        if (skillsRefreshTimer)
            clearTimeout(skillsRefreshTimer);
        skillsRefreshTimer = setTimeout(() => {
            skillsRefreshTimer = null;
            const latest = loadConfig();
            void refreshRemoteBinsForConnectedNodes(latest);
            void handleSkillsChangeForSecurityHook(event);
        }, skillsRefreshDelayMs);
    });
    const { tickInterval, healthInterval, dedupeCleanup } = startGatewayMaintenanceTimers({
        broadcast,
        nodeSendToAllSubscribed,
        getPresenceVersion,
        getHealthVersion,
        refreshGatewayHealthSnapshot,
        logHealth,
        dedupe,
        chatAbortControllers,
        chatRunState,
        chatRunBuffers,
        chatDeltaSentAt,
        removeChatRun,
        agentRunSeq,
        nodeSendToSession,
    });
    const agentUnsub = onAgentEvent(createAgentEventHandler({
        broadcast,
        nodeSendToSession,
        agentRunSeq,
        chatRunState,
        resolveSessionKeyForRun,
        clearAgentRunContext,
    }));
    const heartbeatUnsub = onHeartbeatEvent((evt) => {
        broadcast("heartbeat", evt, { dropIfSlow: true });
    });
    let heartbeatRunner = startHeartbeatRunner({ cfg: cfgAtStart });
    void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
    // Recover pending outbound deliveries from previous crash/restart.
    void (async () => {
        const { recoverPendingDeliveries } = await import("../infra/outbound/delivery-queue.js");
        const { deliverOutboundPayloads } = await import("../infra/outbound/deliver.js");
        const logRecovery = log.child("delivery-recovery");
        await recoverPendingDeliveries({
            deliver: deliverOutboundPayloads,
            log: logRecovery,
            cfg: cfgAtStart,
        });
    })().catch((err) => log.error(`Delivery recovery failed: ${String(err)}`));
    const execApprovalManager = new ExecApprovalManager();
    const execApprovalForwarder = createExecApprovalForwarder();
    const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
        forwarder: execApprovalForwarder,
    });
    const canvasHostServerPort = canvasHostServer?.port;
    attachGatewayWsHandlers({
        wss,
        clients,
        port,
        gatewayHost: bindHost ?? undefined,
        canvasHostEnabled: Boolean(canvasHost),
        canvasHostServerPort,
        resolvedAuth,
        gatewayMethods,
        events: GATEWAY_EVENTS,
        logGateway: log,
        logHealth,
        logWsControl,
        extraHandlers: {
            ...pluginRegistry.gatewayHandlers,
            ...execApprovalHandlers,
        },
        broadcast,
        context: {
            deps,
            cron,
            cronStorePath,
            loadGatewayModelCatalog,
            getHealthCache,
            refreshHealthSnapshot: refreshGatewayHealthSnapshot,
            logHealth,
            logGateway: log,
            incrementPresenceVersion,
            getHealthVersion,
            broadcast,
            nodeSendToSession,
            nodeSendToAllSubscribed,
            nodeSubscribe,
            nodeUnsubscribe,
            nodeUnsubscribeAll,
            hasConnectedMobileNode: hasMobileNodeConnected,
            nodeRegistry,
            agentRunSeq,
            chatAbortControllers,
            chatAbortedRuns: chatRunState.abortedRuns,
            chatRunBuffers: chatRunState.buffers,
            chatDeltaSentAt: chatRunState.deltaSentAt,
            addChatRun,
            removeChatRun,
            dedupe,
            wizardSessions,
            findRunningWizard,
            purgeWizardSession,
            getRuntimeSnapshot,
            startChannel,
            stopChannel,
            markChannelLoggedOut,
            wizardRunner,
            broadcastVoiceWakeChanged,
        },
    });
    logGatewayStartup({
        cfg: cfgAtStart,
        bindHost,
        bindHosts: httpBindHosts,
        port,
        tlsEnabled: gatewayTls.enabled,
        log,
        isNixMode,
    });
    scheduleGatewayUpdateCheck({ cfg: cfgAtStart, log, isNixMode });
    const tailscaleCleanup = await startGatewayTailscaleExposure({
        tailscaleMode,
        resetOnExit: tailscaleConfig.resetOnExit,
        port,
        controlUiBasePath,
        logTailscale,
    });
    let browserControl = null;
    ({ browserControl, pluginServices } = await startGatewaySidecars({
        cfg: cfgAtStart,
        pluginRegistry,
        defaultWorkspaceDir,
        deps,
        startChannels,
        log,
        logHooks,
        logChannels,
        logBrowser,
    }));
    const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
        deps,
        broadcast,
        getState: () => ({
            hooksConfig,
            heartbeatRunner,
            cronState,
            browserControl,
        }),
        setState: (nextState) => {
            hooksConfig = nextState.hooksConfig;
            heartbeatRunner = nextState.heartbeatRunner;
            cronState = nextState.cronState;
            cron = cronState.cron;
            cronStorePath = cronState.storePath;
            browserControl = nextState.browserControl;
        },
        startChannel,
        stopChannel,
        logHooks,
        logBrowser,
        logChannels,
        logCron,
        logReload,
    });
    const configReloader = startGatewayConfigReloader({
        initialConfig: cfgAtStart,
        readSnapshot: readConfigFileSnapshot,
        onHotReload: applyHotReload,
        onRestart: requestGatewayRestart,
        log: {
            info: (msg) => logReload.info(msg),
            warn: (msg) => logReload.warn(msg),
            error: (msg) => logReload.error(msg),
        },
        watchPath: CONFIG_PATH,
    });
    const close = createGatewayCloseHandler({
        bonjourStop,
        tailscaleCleanup,
        canvasHost,
        canvasHostServer,
        stopChannel,
        pluginServices,
        cron,
        heartbeatRunner,
        nodePresenceTimers,
        broadcast,
        tickInterval,
        healthInterval,
        dedupeCleanup,
        agentUnsub,
        heartbeatUnsub,
        chatRunState,
        clients,
        configReloader,
        browserControl,
        wss,
        httpServer,
        httpServers,
    });
    return {
        close: async (opts) => {
            if (diagnosticsEnabled) {
                stopDiagnosticHeartbeat();
            }
            if (skillsRefreshTimer) {
                clearTimeout(skillsRefreshTimer);
                skillsRefreshTimer = null;
            }
            skillsChangeUnsub();
            await close(opts);
        },
    };
}
