/** * GatewayConfigCoordinator — owns config persistence, hot-reload, and the * per-section reload handlers. * * Was 350 lines of `GatewayService` covering nine concerns: * - manual `reloadConfig()` (CLI/UI trigger) * - `saveConfig()` / `updateConfig()` (PATCH /api/config) * - `setBundledExtensionActivationTarget` (extension store install) * - `afterWeixinCredentialsPersisted` / `afterFeishuCredentialsPersisted` * (QR-login follow-ups that bypass the watcher) * - `ConfigHotReloader` (fs.watch → debounced per-section dispatch) * - Section reload handlers (models / agents / channels / * heartbeat / tools / mcp / extensions) * - `scheduleChannelPluginsAfterPersist` (coalesces rapid saves so * Telegram/Weixin do not stop/start repeatedly) * * Pulled out so the gateway composition root stays focused on lifecycle, and * each handler is reachable from one place when adding a new config section. * * **Config state ownership.** `GatewayService.config` is still the single * source of truth — this coordinator reads it via `getConfig()` and writes it * back via `setConfig()` after every reload / persist. We pass through rather * than holding our own copy so other coordinators (sessions, marketplace, * agent runner) see the latest config the moment a reload commits. */ import type { Config } from '../../config/schema.js'; import type { AgentService } from '../../agent/service.js'; import type { ChannelManager } from '../../channels/manager.js'; import type { HeartbeatService } from '../heartbeat/index.js'; import type { ExtensionLoader } from '../../extensions/loader.js'; import type { MessageBus } from '../../infra/bus/index.js'; export interface GatewayConfigCoordinatorOptions { configPath: string; bus: MessageBus; /** Hot reload (fs.watch) — disabled in tests / certain CLI modes. */ enableHotReload: boolean; getConfig: () => Config; /** Writes the new config back into `GatewayService.config`. */ setConfig: (next: Config) => void; getAgentService: () => AgentService; getChannelManager: () => ChannelManager; getHeartbeatService: () => HeartbeatService | null; getExtensionLoader: () => ExtensionLoader | null; /** Re-evaluate browser-extension server attachment after agent defaults change. */ reconcileBrowserExtensionServer: () => Promise; /** Sync the built-in consolidation automation after userContext.dreaming changes. */ reconcileDreamingAutomations: () => Promise; /** Latest channel status snapshot for the `channels.status` event. */ getChannelsStatus: () => unknown; /** Realtime emit used for `config.reload` and `channels.status`. */ emit: (type: string, payload: unknown) => void; } export declare class GatewayConfigCoordinator { private readonly opts; private configReloader; private channelReloadFlushPromise; private channelReloadPending; constructor(opts: GatewayConfigCoordinatorOptions); /** Start the fs.watch-based reloader (idempotent — only starts once). */ startHotReloader(): void; stopHotReloader(): Promise; reloadConfig(): Promise<{ reloaded: boolean; error?: string; }>; saveConfig(config: Config): Promise<{ saved: boolean; error?: string; }>; /** Merge partial updates into `currentConfig` and persist. */ updateConfig(updates: Partial): Promise<{ updated: boolean; error?: string; }>; /** * App store (phase 1): persist `extensions.enabled` / `extensions.disabled` * for a bundled extension. Marketplace-only extensions hot-load on enable; * disable still needs a gateway restart to unload. */ setBundledExtensionActivationTarget(extensionId: string, wanted: boolean): Promise<{ ok: boolean; error?: string; requiresGatewayRestart: boolean; }>; afterWeixinCredentialsPersisted(): Promise; afterFeishuCredentialsPersisted(): Promise; /** * Apply `latest.channels` to every registered channel plugin (Telegram, * Weixin, extensions). Single runtime path for: file watcher hot reload, API * saves, and Weixin QR follow-up. */ handleChannelsReload(newConfig: Config): Promise; /** * Apply `gateway.heartbeat` from current config after PATCH /api/config (and * when hot reload is off). File watcher uses `handleHeartbeatReload` with * the same effect when paths match. */ reloadHeartbeatFromCurrentConfig(): void; private handleModelsReload; private handleAgentDefaultsReload; /** * Coalesces rapid saves so Telegram/Weixin do not stop/start repeatedly. * The persist path schedules the channel apply; the same coalescer absorbs * follow-up saves until the first flush settles. */ private scheduleChannelPluginsAfterPersist; private handleAutomationReload; private handleHeartbeatReload; private handleToolsReload; private handleMcpReload; /** Dispatch config hot reload to extensions that registered `registerReload`. */ private handleExtensionsReload; /** * Persist and replace `currentConfig` with the validated file contents so * runtime matches disk (PATCH merge objects can drift from Zod-normalized * output). */ private writeConfigAndReloadFromDisk; }