import { __toCommonJS } from "../../_virtual/_rolldown/runtime.js";
import { PACKAGE_VERSION, init_package_version } from "../package-version.js";
import { getLogDir } from "../utils/logger/config.js";
import { getLogStats } from "../utils/logger/stats.js";
import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_paths, resolveAgentDir, resolveConfigPath, resolveCronJobsPath, resolveExtensionsDir } from "../config/paths.js";
import { loadConfig, saveConfig } from "../config/loader.js";
import { getDefaultAgentId, init_resolve_route } from "../routing/resolve-route.js";
import { prewarmModelRegistry } from "../providers/model-registry.js";
import { init_providers, providers_exports } from "../providers/index.js";
import { resolveEffectiveGatewayPort } from "./host.js";
import { disposeAllSessionMcpRuntimes } from "../agent/mcp/bundle-mcp-runtime.js";
import "../agent/mcp/bundle-mcp-tools.js";
import { SessionIndex } from "../session/manager.js";
import "../session/index.js";
import { areExtensionsGloballyDisabled } from "../extensions/discover-extensions.js";
import { registerClarifyBridge } from "./clarify-runtime.js";
import { setPairingBroadcastSink } from "../channels/pairing/pairing-events.js";
import { CHAT_CHANNEL_ORDER, getChatChannelMeta } from "../channels/registry.js";
import { getWorkspacePath } from "../config/workspace-path-helpers.js";
import { ExtensionLoader } from "../extensions/loader.js";
import { buildExtensionMetadataSnapshot } from "../extensions/extension-metadata-snapshot.js";
import "../extensions/index.js";
import { MessageBus, MessageBusShutdownError } from "../infra/bus/queue.js";
import "../infra/bus/index.js";
import { AgentService } from "../agent/service.js";
import { ChannelManager } from "../channels/manager.js";
import "../config/index.js";
import { CronService } from "../cron/service.js";
import "../cron/index.js";
import { assertGatewayAuthConfigured, extractToken, resolveGatewayAuth, validateToken } from "./auth.js";
import { HeartbeatService, heartbeatRunnerConfigFromConfig } from "./heartbeat/service.js";
import "./heartbeat/index.js";
import { wireTunnelEventsToGateway } from "../tunnel/gateway-lifecycle.js";
import { stopTailscaleExposure } from "./tailscale-lifecycle.js";
import { getExposureManager } from "../remote-access/exposure-manager.js";
import { sanitizeTunnelConfig } from "../tunnel/tunnel-config.js";
import { assertGatewayAuthNotKnownWeak } from "./security/known-weak-secrets.js";
import { buckets } from "./rate-limit/buckets.js";
import { isGatewayStrictSecurityEnabled } from "./rate-limit/env-flags.js";
import "./rate-limit/index.js";
import { assertGatewayRuntimeConfig } from "./runtime-config.js";
import { auditGatewayConfig } from "./security/audit.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
import { resolveChannelConnectDeferSet } from "./resolve-channel-connect-defer.js";
import { restartGatewayProcessWithFreshPid } from "./respawn.js";
import { GatewaySessionsApi } from "./service/sessions-api.js";
import { GatewayMarketplaceService } from "./service/marketplace-service.js";
import { GatewayConfigCoordinator } from "./service/config-coordinator.js";
import { GatewayAgentRunner } from "./service/agent-runner.js";
import { GatewaySseHub } from "./service/sse-hub.js";
import { GatewayReadiness } from "./startup-readiness.js";
import { createGatewayStartupTrace } from "./startup-trace.js";
import crypto from "crypto";
//#region src/gateway/service.ts
init_providers();
init_logger();
init_paths();
init_package_version();
init_resolve_route();
const log = createLogger("GatewayService");
var GatewayService = class {
	bus;
	config;
	configPath;
	_agentService = null;
	channelManager;
	cronService;
	extensionLoader = null;
	extensionMetadataSnapshot = null;
	browserExtensionProvider = null;
	browserExtensionRelease = null;
	/** `${host}:${port}` when the gateway holds the extension bridge listener. */
	browserExtensionBindKey = null;
	heartbeatService = null;
	sessionIndex;
	running = false;
	startTime = Date.now();
	workspacePath;
	configCoordinator;
	auth;
	sse = new GatewaySseHub();
	stopGatewayUpdateCheck = null;
	/** When set (e.g. by `GatewayServer`), `triggerGatewayProcessRestart` can stop HTTP then exit. */
	gatewayShutdownForRestart = null;
	/** Snapshot for phase-2 metrics / logs (ids deferred at phase-1 `start()`). */
	lastDeferredChannelConnectIds = [];
	lastChannelConnectDeferMode = "auto";
	lastChannelConnectDeferSource = "off";
	readiness = new GatewayReadiness();
	startupTrace = null;
	/**
	* Webchat agent invocation surface (`runAgent`, `abortAgentRun`, `steer*`,
	* `submitClarifyResponse`, clarify-bridge dispatch). Owns the
	* `activeWebchatRunBySession` + `runAbortControllers` maps.
	*/
	agentRunner;
	/** Read-only alias re-exported from `agentRunner.runRelay` for legacy callers. */
	get runRelay() {
		return this.agentRunner.runRelay;
	}
	/**
	* Session CRUD / search / compaction / tag-archive-pin / stats — the gateway
	* REST surface for sessions. Routes should depend on this narrow service
	* rather than the full GatewayService composition root.
	*/
	sessions;
	/**
	* Skills + extensions marketplace surface (browse / install / uninstall) plus
	* local-only managed-skill ops. Routes depend on this narrow service.
	*/
	marketplace;
	constructor(serviceConfig = {}) {
		this.serviceConfig = serviceConfig;
		this.bus = new MessageBus();
		this.configPath = serviceConfig.configPath || resolveConfigPath();
		this.config = loadConfig(this.configPath);
		if (sanitizeTunnelConfig(this.config)) saveConfig(this.config, this.configPath).catch((err) => {
			const em = err instanceof Error ? err.message : String(err);
			log.warn({
				err,
				phase: "tunnel_sanitize",
				errorMessage: em
			}, `Tunnel config sanitize persist failed: ${em}`);
		});
		this.auth = resolveGatewayAuth({ authConfig: this.config.gateway?.auth });
		assertGatewayAuthConfigured(this.auth);
		assertGatewayAuthNotKnownWeak(this.auth);
		const gatewayPort = this.getEffectiveListenPort();
		const runtimeConfig = assertGatewayRuntimeConfig({
			cfg: this.config,
			auth: this.auth,
			bindOverride: serviceConfig.listenBind,
			port: gatewayPort
		});
		auditGatewayConfig({
			auth: this.auth,
			bindHost: runtimeConfig.bindHost,
			corsOrigins: runtimeConfig.corsOrigins,
			rateLimitEnabled: runtimeConfig.rateLimitEnabled,
			tlsEnabled: runtimeConfig.tlsEnabled,
			trustedProxies: this.config.gateway?.trustedProxies,
			allowRealIpFallback: this.config.gateway?.allowRealIpFallback === true,
			dangerouslyAllowHostHeaderOriginFallback: runtimeConfig.dangerouslyAllowHostHeaderOriginFallback,
			strictSecurityEnabled: isGatewayStrictSecurityEnabled(this.config),
			rateLimitConfigured: this.config.gateway?.auth?.rateLimit !== void 0
		});
		if (this.auth.mode === "token") {
			const tokenPreview = this.auth.token ? `${this.auth.token.slice(0, 4)}***` : "none";
			log.info({
				mode: this.auth.mode,
				token: tokenPreview
			}, "Authentication configured");
		} else if (this.auth.mode === "trusted-proxy") log.info({
			mode: this.auth.mode,
			userHeader: this.auth.trustedProxy?.userHeader,
			trustedProxyCount: this.config.gateway?.trustedProxies?.length ?? 0
		}, "Trusted-proxy authentication configured");
		else log.info({ mode: this.auth.mode }, "Authentication configured");
		this.channelManager = new ChannelManager(this.config, this.bus);
		this.workspacePath = getWorkspacePath(this.config) || "./workspace";
		this.initializeExtensionLoader();
		this.sessionIndex = new SessionIndex({ config: this.config });
		this.cronService = new CronService({ filePath: resolveCronJobsPath() });
		this.agentRunner = new GatewayAgentRunner({
			bus: this.bus,
			sessionIndex: this.sessionIndex,
			getAgentService: () => this.ensureAgentService(),
			getChannelManager: () => this.channelManager,
			getConfig: () => this.config,
			emit: (type, payload) => this.sse.emit(type, payload)
		});
		this.sessions = new GatewaySessionsApi({
			sessionIndex: this.sessionIndex,
			getAgentService: () => this.ensureAgentService(),
			getActiveWebchatRunId: (sk) => this.agentRunner.getActiveRunId(sk)
		});
		this.marketplace = new GatewayMarketplaceService({
			getConfig: () => this.config,
			getAgentService: () => this.ensureAgentService(),
			getExtensionLoader: () => this.extensionLoader,
			getChannelManager: () => this.channelManager,
			saveConfig: (cfg) => this.saveConfig(cfg),
			emit: (type, payload) => this.emit(type, payload)
		});
		this.configCoordinator = new GatewayConfigCoordinator({
			configPath: this.configPath,
			bus: this.bus,
			enableHotReload: this.serviceConfig.enableHotReload !== false,
			getConfig: () => this.config,
			setConfig: (next) => {
				this.config = next;
			},
			getAgentService: () => this.ensureAgentService(),
			getChannelManager: () => this.channelManager,
			getCronService: () => this.cronService,
			getHeartbeatService: () => this.heartbeatService,
			getExtensionLoader: () => this.extensionLoader,
			reconcileBrowserExtensionServer: () => this.reconcileBrowserExtensionServer(),
			getChannelsStatus: () => this.getChannelsStatus(),
			emit: (type, payload) => this.emit(type, payload)
		});
	}
	/** Lazy AgentService — constructed on first use or during `start()`. */
	get agentService() {
		return this.ensureAgentService();
	}
	ensureAgentService() {
		if (this._agentService) return this._agentService;
		const cronRef = { service: this.cronService };
		this._agentService = new AgentService(this.bus, {
			workspace: this.workspacePath,
			model: this.config.agents?.defaults?.model?.primary,
			config: this.config,
			sessionStore: this.sessionIndex.getStore(),
			onSessionMetadataUpdated: (sessionKey) => {
				this.sessionIndex.emit("sessionUpdated", { key: sessionKey });
			},
			onSessionTranscriptUpdated: (sessionKey) => {
				this.emit("session.transcript_updated", { key: sessionKey });
			},
			extensionRegistry: this.extensionLoader?.getRegistry(),
			getCronService: () => cronRef.service,
			gatewayClarify: { requestClarification: (sessionKey, request) => this.agentRunner.requestClarification({
				sessionKey,
				request,
				publishSseFor: (_runId) => (e) => {
					this._agentService.turnDispatcher.enqueueWebchatSseEvent(sessionKey, e);
				}
			}) }
		});
		this._agentService.setChannelManager(this.channelManager);
		this.channelManager.setSessionModelHooks({
			getModelForSession: (sk) => this._agentService.getModelForSession(sk),
			switchModelForSession: (sk, id) => this._agentService.switchModelForSession(sk, id)
		});
		this.cronService.setDeps({
			agentService: this._agentService,
			messageBus: this.bus,
			heartbeatService: this.ensureHeartbeatService(),
			sessionStore: this.sessionIndex.getStore(),
			getDefaultCronAgentId: () => getDefaultAgentId(this.config)
		});
		cronRef.service = this.cronService;
		this._agentService.persistentGoals.setWebchatContinuationScheduler((sessionKey, message) => {
			const scheduleWhenIdle = () => {
				if (this._agentService.getInboundTurnDepth(sessionKey) > 0) {
					setTimeout(scheduleWhenIdle, 50);
					return;
				}
				if (this.agentRunner.hasActiveRun(sessionKey)) {
					setTimeout(scheduleWhenIdle, 50);
					return;
				}
				this.agentRunner.drainScheduledWebchatContinuation(sessionKey, message);
			};
			queueMicrotask(scheduleWhenIdle);
		});
		return this._agentService;
	}
	ensureHeartbeatService() {
		if (this.heartbeatService) return this.heartbeatService;
		this.heartbeatService = new HeartbeatService({
			agentService: this.ensureAgentService(),
			messageBus: this.bus,
			cronService: this.cronService,
			sessionStore: this.sessionIndex.getStore(),
			getConfig: () => this.config
		});
		return this.heartbeatService;
	}
	enqueueWebchatPersistentGoalKickoff(sessionKey, goalText) {
		this.agentRunner.enqueueWebchatPersistentGoalKickoff(sessionKey, goalText);
	}
	runAgent(...args) {
		return this.agentRunner.runAgent(...args);
	}
	abortAgentRun(runId) {
		return this.agentRunner.abortAgentRun(runId);
	}
	steerWebchatAgent(chatId, message) {
		return this.agentRunner.steerWebchatAgent(chatId, message);
	}
	submitClarifyResponse(requestId, answer) {
		return this.agentRunner.submitClarifyResponse(requestId, answer);
	}
	initializeExtensionLoader() {
		try {
			if (areExtensionsGloballyDisabled(this.config)) {
				log.info("Extensions globally disabled — skipping loader initialization");
				return;
			}
			const loaderOptions = {
				workspaceDir: this.workspacePath,
				extensionsDir: resolveExtensionsDir()
			};
			this.extensionMetadataSnapshot = buildExtensionMetadataSnapshot(loaderOptions, this.config);
			this.extensionLoader = new ExtensionLoader(loaderOptions);
			this.extensionLoader.setManifestSnapshot(this.extensionMetadataSnapshot);
			this.extensionLoader.setConfig(this.config);
		} catch (error) {
			log.warn({ error }, "Failed to initialize extension loader");
		}
	}
	registerExtensionChannelPlugins() {
		if (!this.extensionLoader) return;
		const reg = this.extensionLoader.getRegistry();
		for (const plugin of reg.channelPlugins) this.channelManager.registerPlugin(plugin);
	}
	/**
	* Load extensions and register SDK / full ChannelPlugin instances with ChannelManager.
	*/
	async loadExtensionsAndRegisterChannels() {
		if (!this.extensionLoader) return;
		try {
			await this.extensionLoader.loadByActivationPlan({ phase: "startup" });
			this.registerExtensionChannelPlugins();
			const reg = this.extensionLoader.getRegistry();
			log.debug({
				extensionRecords: reg.extensions.size,
				channelPlugins: reg.channelPlugins.length
			}, "Startup-phase extensions loaded and channel plugins registered");
		} catch (err) {
			log.warn({ err }, "Failed to load startup-phase extensions");
		}
	}
	async loadDeferredExtensions() {
		if (!this.extensionLoader) return;
		try {
			await this.extensionLoader.loadByActivationPlan({ phase: "deferred" });
			this.registerExtensionChannelPlugins();
			log.debug("Deferred-phase extensions loaded");
		} catch (err) {
			log.warn({ err }, "Failed to load deferred extensions");
		}
	}
	schedulePostReadySidecars() {
		queueMicrotask(() => {
			this.runPostReadySidecars();
		});
	}
	async runPostReadySidecars() {
		const trace = this.startupTrace;
		try {
			if (trace) await trace.measure("sidecars.model-prewarm", () => prewarmModelRegistry());
			else await prewarmModelRegistry();
		} catch (err) {
			const em = err instanceof Error ? err.message : String(err);
			log.warn({
				err,
				errorMessage: em,
				phase: "sidecars.model_prewarm"
			}, `Model registry prewarm failed: ${em}`);
		}
		if (!this.extensionLoader || areExtensionsGloballyDisabled(this.config)) return;
		try {
			if (trace) await trace.measure("extensions.deferred-load", () => this.loadDeferredExtensions());
			else await this.loadDeferredExtensions();
		} catch (err) {
			const em = err instanceof Error ? err.message : String(err);
			log.warn({
				err,
				errorMessage: em,
				phase: "extensions.deferred_load"
			}, `Deferred extension load failed: ${em}`);
		}
	}
	async start() {
		if (this.running) return;
		setPairingBroadcastSink((type, payload) => {
			this.emit(type, payload);
		});
		log.debug("Starting gateway service...");
		this.startTime = Date.now();
		this.running = true;
		this.startupTrace = createGatewayStartupTrace();
		this.readiness.markStarting(this.startTime);
		const trace = this.startupTrace;
		registerClarifyBridge(this.agentRunner.getClarifyBridge());
		this.ensureAgentService();
		this.channelManager.setOutboundHooks({
			runMessageSending: (to, content, channel) => this.agentService.outboundCoordinator.invokeOutboundMessageSending(to, content, channel),
			runMessageSent: (to, content, success, error, channel) => this.agentService.outboundCoordinator.invokeOutboundMessageSent(to, content, success, error, channel)
		});
		this.channelManager.enableOutboundPersistence(resolveAgentDir(this.config, getDefaultAgentId(this.config)));
		if (this.extensionLoader) this.extensionLoader.setRuntimeContext({
			bus: this.bus,
			sessionManager: this.sessionIndex,
			scheduleWebchatContinuation: (sessionKey, continuationMessage) => {
				queueMicrotask(() => {
					this.agentRunner.drainScheduledWebchatContinuation(sessionKey, continuationMessage);
				});
			}
		});
		await trace.measure("extensions.load", () => this.loadExtensionsAndRegisterChannels());
		const skipChannels = process.env.XOPC_SKIP_CHANNELS === "1" || process.env.XOPC_SKIP_CHANNELS === "true" || process.env.XOPC_SKIP_PROVIDERS === "1" || process.env.XOPC_SKIP_PROVIDERS === "true";
		const phase1StartedAt = performance.now();
		let channelInitMs = 0;
		let deferPlanMs = 0;
		let channelPhase1StartMs = 0;
		let replayOutboundMs = null;
		let deferConnect = /* @__PURE__ */ new Set();
		if (skipChannels) log.info("Skipping channel startup (XOPC_SKIP_CHANNELS or XOPC_SKIP_PROVIDERS)");
		else {
			const t0 = performance.now();
			await trace.measure("channels.initialize", () => this.channelManager.initialize());
			channelInitMs = performance.now() - t0;
			const t1 = performance.now();
			const deferResolution = resolveChannelConnectDeferSet({
				config: this.config,
				channelManager: this.channelManager,
				deferChannelConnectUntilAfterHttp: this.serviceConfig.deferChannelConnectUntilAfterHttp === true
			});
			deferConnect = deferResolution.deferPluginIds;
			deferPlanMs = performance.now() - t1;
			this.lastDeferredChannelConnectIds = [...deferConnect];
			this.lastChannelConnectDeferMode = deferResolution.mode;
			this.lastChannelConnectDeferSource = deferResolution.source;
			if (deferConnect.size > 0) log.info({ channels: [...deferConnect] }, "Deferring channel outbound connect until HTTP listen");
			const t2 = performance.now();
			await trace.measure("channels.start", () => this.channelManager.start(deferConnect.size > 0 ? { deferConnectPluginIds: deferConnect } : void 0));
			channelPhase1StartMs = performance.now() - t2;
			if (this.serviceConfig.deferChannelConnectUntilAfterHttp !== true) {
				const tr = performance.now();
				await trace.measure("channels.replay-outbound", () => this.channelManager.replayPendingOutboundMessages());
				replayOutboundMs = performance.now() - tr;
			}
		}
		const channelStartupPhase1TotalMs = performance.now() - phase1StartedAt;
		const gwDeferMode = this.config.gateway?.channelConnectDeferMode ?? "auto";
		const phase1Metrics = {
			deferChannelConnectUntilAfterHttp: this.serviceConfig.deferChannelConnectUntilAfterHttp === true,
			channelConnectDeferMode: this.serviceConfig.deferChannelConnectUntilAfterHttp ? this.lastChannelConnectDeferMode : gwDeferMode,
			channelConnectDeferSource: this.lastChannelConnectDeferSource,
			deferredChannelIds: this.lastDeferredChannelConnectIds,
			deferredChannelCount: this.lastDeferredChannelConnectIds.length,
			channelInitMs: Math.round(channelInitMs),
			deferPlanMs: Math.round(deferPlanMs),
			channelPhase1StartMs: Math.round(channelPhase1StartMs),
			replayOutboundMs: replayOutboundMs === null ? null : Math.round(replayOutboundMs),
			channelStartupPhase1TotalMs: Math.round(channelStartupPhase1TotalMs)
		};
		log.info({
			phase: "gateway.channel_startup",
			stage: "phase1",
			...phase1Metrics
		}, "Gateway channel startup phase-1 complete");
		await trace.measure("sessions.initialize", () => this.sessionIndex.initialize());
		log.debug("Session manager initialized");
		this.cronService.setDeps({
			agentService: this.agentService,
			messageBus: this.bus,
			heartbeatService: this.ensureHeartbeatService(),
			sessionStore: this.sessionIndex.getStore(),
			getDefaultCronAgentId: () => getDefaultAgentId(this.config)
		});
		this.sessionIndex.on("sessionUpdated", (data) => {
			this.emit("session.updated", {
				key: data.key,
				name: data.name,
				tags: data.tags
			});
		});
		if (this.config.cron?.enabled !== false) await trace.measure("cron.initialize", () => this.cronService.initialize());
		this.ensureHeartbeatService().start(heartbeatRunnerConfigFromConfig(this.config));
		import("../browser/providers/browser-ext-install.js").then(({ ensureBrowserExtensionOnStartup }) => ensureBrowserExtensionOnStartup(this.config)).catch((err) => log.warn({ err }, "Browser extension artifact ensure failed"));
		await trace.measure("browser-extension.start", () => this.startBrowserExtensionServerIfNeeded());
		this.agentService.start().catch((err) => {
			log.error({ err }, "Agent service error");
		});
		if (this.serviceConfig.deferChannelConnectUntilAfterHttp !== true) this.startOutboundProcessor().catch((err) => {
			log.error({ err }, "Outbound processor error");
		});
		if (this.serviceConfig.enableHotReload !== false) this.configCoordinator.startHotReloader();
		this.stopGatewayUpdateCheck = scheduleGatewayUpdateCheck({
			config: this.config,
			onUpdateAvailableChange: (update) => {
				this.emit("update.available", update);
			}
		});
		wireTunnelEventsToGateway(this);
		import("../share/share-auto.js").then(({ runStagingSweep }) => runStagingSweep()).catch((err) => log.warn({ err }, "Share staging sweep failed"));
		if (this.serviceConfig.deferChannelConnectUntilAfterHttp !== true) this.markGatewayReady();
		else trace.mark("service.started-awaiting-http");
		log.debug("Gateway service started");
	}
	/** Called when the HTTP listener is bound (before deferred channel work). */
	markHttpListening() {
		this.readiness.markHttpListening();
		this.startupTrace?.mark("http.listening");
	}
	isGatewayReady() {
		return this.readiness.isReady();
	}
	getGatewayReadiness() {
		return this.readiness.getSnapshot();
	}
	async applyStartupReadyDelayForTesting() {
		const raw = process.env.XOPC_GATEWAY_STARTUP_SLOW_MS?.trim();
		if (!raw) return;
		const delayMs = Number.parseInt(raw, 10);
		if (!Number.isFinite(delayMs) || delayMs <= 0) return;
		await new Promise((resolve) => setTimeout(resolve, delayMs));
	}
	markGatewayReady() {
		this.readiness.markReady();
		this.startupTrace?.mark("ready");
		this.schedulePostReadySidecars();
	}
	/** After HTTP is listening: exposure auto-start (Tailscale, then FRP tunnel). */
	async runExposureAutoStartIfConfigured() {
		const port = this.getEffectiveListenPort();
		await getExposureManager().autoStart(this.config, port, this.getAuthToken());
	}
	/**
	* Called by `GatewayServer` when the HTTP listener is bound. Starts channels that
	* opted into `meta.deferConnectUntilAfterListen`, then replays outbound queue.
	*/
	async onHttpListening() {
		await this.runExposureAutoStartIfConfigured();
		if (this.serviceConfig.deferChannelConnectUntilAfterHttp !== true) return;
		const listenStartedAt = performance.now();
		const trace = this.startupTrace;
		try {
			await this.applyStartupReadyDelayForTesting();
			const tDef = performance.now();
			if (trace) await trace.measure("channels.deferred-connect", () => this.channelManager.startDeferredConnects());
			else await this.channelManager.startDeferredConnects();
			const channelPhase2DeferredMs = performance.now() - tDef;
			const tr = performance.now();
			if (trace) await trace.measure("channels.replay-outbound", () => this.channelManager.replayPendingOutboundMessages());
			else await this.channelManager.replayPendingOutboundMessages();
			const replayOutboundMs = performance.now() - tr;
			this.startOutboundProcessor().catch((err) => {
				log.error({ err }, "Outbound processor error");
			});
			this.emit("channels.status", { channels: this.getChannelsStatus() });
			const onHttpListeningTotalMs = performance.now() - listenStartedAt;
			const phase2Metrics = {
				channelConnectDeferMode: this.lastChannelConnectDeferMode,
				channelConnectDeferSource: this.lastChannelConnectDeferSource,
				deferredChannelIds: this.lastDeferredChannelConnectIds,
				channelPhase2DeferredMs: Math.round(channelPhase2DeferredMs),
				replayOutboundMs: Math.round(replayOutboundMs),
				onHttpListeningTotalMs: Math.round(onHttpListeningTotalMs)
			};
			log.info({
				phase: "gateway.channel_startup",
				stage: "phase2",
				...phase2Metrics
			}, "Gateway channel startup phase-2 complete (HTTP listening)");
		} catch (err) {
			const em = err instanceof Error ? err.message : String(err);
			log.error({
				err,
				errorMessage: em,
				phase: "gateway.channel_startup",
				stage: "phase2",
				deferredChannelIds: this.lastDeferredChannelConnectIds,
				elapsedMs: Math.round(performance.now() - listenStartedAt)
			}, `Deferred channel startup after HTTP listen failed: ${em}`);
		} finally {
			this.markGatewayReady();
		}
	}
	async stop() {
		if (!this.running) return;
		setPairingBroadcastSink(null);
		log.debug("Stopping gateway service...");
		this.readiness.markStarting();
		await stopTailscaleExposure().catch((err) => {
			log.warn({ err }, "Tailscale exposure shutdown failed");
		});
		if (this.stopGatewayUpdateCheck) {
			this.stopGatewayUpdateCheck();
			this.stopGatewayUpdateCheck = null;
		}
		await this.configCoordinator.stopHotReloader();
		this.heartbeatService?.stop();
		if (this.browserExtensionRelease) {
			await this.browserExtensionRelease();
			this.browserExtensionRelease = null;
		}
		this.browserExtensionProvider = null;
		this.browserExtensionBindKey = null;
		registerClarifyBridge(null);
		this.agentRunner.disposeClarifyBridge();
		await disposeAllSessionMcpRuntimes().catch((err) => {
			log.warn({ err }, "MCP runtime shutdown failed");
		});
		this._agentService?.stop();
		this.running = false;
		this.bus.shutdown();
		this.lastDeferredChannelConnectIds = [];
		this.lastChannelConnectDeferMode = "auto";
		this.lastChannelConnectDeferSource = "off";
		await this.channelManager.stop();
		await this.cronService.stop();
		buckets.destroyAll();
		log.debug("Gateway service stopped");
	}
	/** Start the browser extension WS server when backend is 'extension'. */
	async startBrowserExtensionServerIfNeeded() {
		await this.reconcileBrowserExtensionServer();
	}
	/** Release the gateway's hold on the shared extension bridge (does not restart). */
	async releaseBrowserExtensionBridge() {
		if (!this.browserExtensionRelease) return;
		await this.browserExtensionRelease();
		this.browserExtensionRelease = null;
		this.browserExtensionProvider = null;
		this.browserExtensionBindKey = null;
		log.debug("Browser extension WS server released");
	}
	/**
	* Start/stop/rebind the Chrome extension bridge when `agents.defaults.browser` changes.
	* PATCH saves update config in memory without re-running gateway startup, so this must run on save too.
	*/
	async reconcileBrowserExtensionServer() {
		const { shouldRunExtensionBridgeServer } = await import("../browser/backend-from-config.js");
		if (!shouldRunExtensionBridgeServer(this.config)) {
			if (this.browserExtensionRelease) {
				await this.browserExtensionRelease();
				this.browserExtensionRelease = null;
				this.browserExtensionProvider = null;
				this.browserExtensionBindKey = null;
				log.debug("Browser extension WS server stopped (backend is not extension)");
			}
			return;
		}
		const browser = (this.config.agents?.defaults)?.browser;
		const ext = browser?.extension;
		const port = typeof ext?.port === "number" ? ext.port : 19820;
		const host = typeof ext?.host === "string" && ext.host ? ext.host : "127.0.0.1";
		const connectionTimeout = typeof ext?.connectionTimeout === "number" && ext.connectionTimeout >= 1e3 ? Math.floor(ext.connectionTimeout) : void 0;
		const cmdSec = browser?.commandTimeout;
		const commandTimeout = typeof cmdSec === "number" && Number.isFinite(cmdSec) && cmdSec > 0 ? Math.floor(cmdSec * 1e3) : void 0;
		const bindKey = `${host}:${port}`;
		if (this.browserExtensionRelease && this.browserExtensionBindKey === bindKey) return;
		if (this.browserExtensionRelease) {
			await this.browserExtensionRelease();
			this.browserExtensionRelease = null;
			this.browserExtensionProvider = null;
			this.browserExtensionBindKey = null;
		}
		try {
			const { acquireExtensionBrowserServer } = await import("../browser/providers/extension-ws-acquire.js");
			const { provider, release } = await acquireExtensionBrowserServer({
				port,
				host,
				connectionTimeout,
				commandTimeout
			});
			this.browserExtensionProvider = provider;
			this.browserExtensionRelease = release;
			this.browserExtensionBindKey = bindKey;
			log.info({
				port,
				host
			}, "Browser extension WS server started");
		} catch (err) {
			const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
			log.error({
				err,
				phase: "browser_extension_ws",
				...code === "EADDRINUSE" ? {
					bindPort: port,
					bindHost: host,
					hint: "Another process holds this port (default 19820). Stop it or set agents.defaults.browser.extension.port."
				} : {}
			}, `Failed to start browser extension WS server: ${err instanceof Error ? err.message : String(err)}`);
		}
	}
	/**
	* Start processing outbound messages and send through channels
	*/
	async startOutboundProcessor() {
		log.debug("Starting outbound message processor");
		while (this.running) try {
			const msg = await this.bus.consumeOutbound();
			await this.channelManager.send(msg);
		} catch (error) {
			if (error instanceof MessageBusShutdownError) break;
			const em = error instanceof Error ? error.message : String(error);
			log.error({
				err: error,
				errorMessage: em,
				phase: "outbound_consume"
			}, `Outbound pipeline failed (will retry in 1s): ${em}`);
			await new Promise((resolve) => setTimeout(resolve, 1e3));
		}
	}
	reloadHeartbeatFromCurrentConfig() {
		this.configCoordinator.reloadHeartbeatFromCurrentConfig();
	}
	reloadConfig() {
		return this.configCoordinator.reloadConfig();
	}
	afterWeixinCredentialsPersisted() {
		return this.configCoordinator.afterWeixinCredentialsPersisted();
	}
	afterFeishuCredentialsPersisted() {
		return this.configCoordinator.afterFeishuCredentialsPersisted();
	}
	saveConfig(config) {
		return this.configCoordinator.saveConfig(config);
	}
	setBundledExtensionActivationTarget(extensionId, wanted) {
		return this.configCoordinator.setBundledExtensionActivationTarget(extensionId, wanted);
	}
	updateConfig(updates) {
		return this.configCoordinator.updateConfig(updates);
	}
	/**
	* Send message through a channel
	*/
	async sendMessage(channel, chatId, content) {
		try {
			await this.channelManager.send({
				channel,
				chat_id: chatId,
				content
			});
			const messageId = `msg_${Date.now()}`;
			this.emit("message.sent", {
				channel,
				chatId,
				messageId
			});
			return {
				sent: true,
				messageId
			};
		} catch (error) {
			log.error({
				channel,
				chatId,
				error
			}, "Failed to send message");
			throw error;
		}
	}
	/**
	* Get channel statuses
	*/
	getChannelsStatus() {
		const runningChannels = new Set(this.channelManager.getRunningChannels());
		const channels = this.config.channels;
		const builtinOrder = CHAT_CHANNEL_ORDER;
		const rows = CHAT_CHANNEL_ORDER.map((name) => ({
			name,
			enabled: !!channels?.[name]?.enabled,
			connected: runningChannels.has(name)
		}));
		const extraIds = (this.extensionLoader?.getRegistry())?.channelPlugins.map((p) => p.id).filter((id) => !builtinOrder.includes(id)) ?? [];
		if (extraIds.length === 0) return rows;
		const seen = new Set(builtinOrder);
		for (const name of extraIds) {
			if (seen.has(name)) continue;
			seen.add(name);
			rows.push({
				name,
				enabled: channels?.[name]?.enabled !== false,
				connected: runningChannels.has(name)
			});
		}
		return rows;
	}
	/**
	* Hub metadata for gateway console (built-in registry + registered channel plugins).
	*/
	getChannelsHubMeta() {
		const manageableIds = new Set([
			"telegram",
			"weixin",
			"feishu"
		]);
		const byId = /* @__PURE__ */ new Map();
		for (const plugin of this.channelManager.getAllPlugins()) byId.set(plugin.id, {
			id: plugin.id,
			label: plugin.meta.label,
			description: plugin.meta.blurb,
			manageable: manageableIds.has(plugin.id),
			order: plugin.meta.order ?? 999
		});
		CHAT_CHANNEL_ORDER.forEach((id, index) => {
			if (byId.has(id)) return;
			const meta = getChatChannelMeta(id);
			byId.set(id, {
				id,
				label: meta.label,
				description: meta.description,
				manageable: true,
				order: index
			});
		});
		return Array.from(byId.values()).toSorted((a, b) => {
			if (a.order !== b.order) return a.order - b.order;
			return a.id.localeCompare(b.id);
		});
	}
	/**
	* Request an immediate heartbeat run (coalesced like interval/cron wakes).
	*/
	requestHeartbeatNow(opts) {
		this.heartbeatService?.requestNow({ reason: opts?.reason ?? "manual" });
	}
	/**
	* Register graceful shutdown used after spawning a replacement gateway process (foreground CLI server).
	*/
	registerGatewayShutdownForRestart(handler) {
		this.gatewayShutdownForRestart = handler;
	}
	/**
	* Respawn the gateway process when supported (spawn + exit, supervisor exit, or disabled when XOPC_NO_RESPAWN).
	*/
	triggerGatewayProcessRestart() {
		const result = restartGatewayProcessWithFreshPid();
		if (result.mode === "failed") return {
			ok: false,
			mode: result.mode,
			message: result.detail ?? "spawn failed"
		};
		if (result.mode === "disabled") return {
			ok: false,
			mode: "disabled",
			message: "Process respawn is disabled (XOPC_NO_RESPAWN). Restart the gateway manually (e.g. xopc gateway restart)."
		};
		const shutdown = this.gatewayShutdownForRestart;
		if (!shutdown) return {
			ok: false,
			mode: result.mode,
			message: "Gateway restart is not available in this process."
		};
		setImmediate(() => {
			shutdown().finally(() => {
				process.exit(0);
			});
		});
		return {
			ok: true,
			mode: result.mode
		};
	}
	/**
	* Get health status
	*/
	getHealth() {
		const runningChannels = this.channelManager.getRunningChannels();
		const allChannels = this.channelManager.getAllChannels();
		const logStats = getLogStats();
		const readiness = this.readiness.getSnapshot();
		return {
			status: "ok",
			service: "xopc-gateway",
			version: PACKAGE_VERSION,
			uptime: Math.floor((Date.now() - this.startTime) / 1e3),
			ready: readiness.ready,
			httpListening: readiness.httpListening,
			startupDurationMs: readiness.startupDurationMs,
			channels: {
				running: runningChannels.length,
				total: allChannels.length
			},
			configPath: this.configPath,
			logs: {
				dir: getLogDir(),
				errors24h: logStats.errorsLast24h,
				stats: logStats.byLevel
			}
		};
	}
	get isRunning() {
		return this.running;
	}
	/**
	* Get extension registry for external access (HTTP routes, gateway methods)
	*/
	getExtensionRegistry() {
		return this.extensionLoader?.getRegistry();
	}
	/** Extension loader for discovery and frontend asset APIs (may be null if extensions failed to init). */
	getExtensionLoader() {
		return this.extensionLoader;
	}
	/**
	* Get model registry for external access (HTTP routes)
	*/
	getModelRegistry() {
		const { getModelRegistry } = (init_providers(), __toCommonJS(providers_exports));
		return getModelRegistry();
	}
	/**
	* Invoke a gateway method registered by extensions
	*/
	async invokeGatewayMethod(method, params) {
		const registry = this.getExtensionRegistry();
		if (!registry) throw new Error("Extension registry not available");
		const handler = registry.getGatewayMethod(method);
		if (!handler) throw new Error(`Gateway method not found: ${method}`);
		return await handler(params);
	}
	get currentConfig() {
		return this.config;
	}
	get currentWorkspacePath() {
		return this.workspacePath;
	}
	get messageBusInstance() {
		return this.bus;
	}
	/** Effective HTTP listen port (CLI `--port` override or config default). */
	getEffectiveListenPort() {
		return resolveEffectiveGatewayPort(this.config, this.serviceConfig.listenPort);
	}
	get cronServiceInstance() {
		return this.cronService;
	}
	get sessionIndexInstance() {
		return this.sessionIndex;
	}
	/** Process a message directly through the agent (for CLI mode). */
	async processDirect(content, sessionKey = "agent:main:main") {
		return this.agentService.turnDispatcher.processDirect(content, sessionKey);
	}
	subscribe(sessionId, listener) {
		return this.sse.subscribe(sessionId, listener);
	}
	emit(type, payload) {
		this.sse.emit(type, payload);
	}
	/** Replay events since `lastEventId` for SSE reconnection. */
	getEventsSince(sessionId, lastEventId) {
		return this.sse.getEventsSince(sessionId, lastEventId);
	}
	/**
	* Validate authentication token from request headers.
	* Returns true if auth is disabled (mode: 'none') or token is valid.
	*/
	validateAuth(headers) {
		const token = extractToken(headers);
		return validateToken(this.auth, token);
	}
	/**
	* Get current auth mode.
	*/
	getAuthMode() {
		return this.auth.mode;
	}
	/** Resolved gateway auth (mode, credentials, trusted-proxy config). */
	getResolvedAuth() {
		return this.auth;
	}
	/**
	* Get current auth token (for CLI server integration).
	* Returns undefined if mode is not token.
	*/
	getAuthToken() {
		return this.auth.mode === "token" ? this.auth.token : void 0;
	}
	/**
	* Refresh (regenerate) the gateway auth token.
	* Returns the new token.
	*/
	async refreshAuthToken() {
		if (this.auth.mode !== "token") throw new Error("Cannot refresh token: auth mode is not token");
		const newToken = crypto.randomBytes(24).toString("hex");
		this.auth.token = newToken;
		this.config = {
			...this.config,
			gateway: {
				...this.config.gateway,
				auth: {
					...this.config.gateway?.auth,
					mode: "token",
					token: newToken
				}
			}
		};
		await this.saveConfig(this.config);
		log.info({ tokenPreview: `${newToken.slice(0, 8)}...` }, "Gateway token refreshed");
		return newToken;
	}
};
//#endregion
export { GatewayService };

//# sourceMappingURL=service.js.map