diff --git a/plugin/index.ts b/plugin/index.ts index 33b02b8..dea52aa 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -34,12 +34,18 @@ import path from "node:path"; import fs from "node:fs"; import { validateYonexusServerConfig } from "./core/config.js"; import { createYonexusServerStore } from "./core/store.js"; -import { createServerTransport } from "./core/transport.js"; +import { createServerTransport, type ServerTransport } from "./core/transport.js"; import { createYonexusServerRuntime } from "./core/runtime.js"; -import { createServerRuleRegistry } from "./core/rules.js"; +import { createServerRuleRegistry, YonexusServerRuleRegistry } from "./core/rules.js"; import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js"; import type { ServerPersistenceData } from "./core/persistence.js"; +const _G = globalThis as Record; +const _STARTED_KEY = "_yonexusServerStarted"; +const _TRANSPORT_KEY = "_yonexusServerTransport"; +const _REGISTRY_KEY = "_yonexusServerRegistry"; +const _CALLBACKS_KEY = "_yonexusServerOnAuthCallbacks"; + export interface YonexusServerPluginManifest { readonly name: "Yonexus.Server"; readonly version: string; @@ -52,8 +58,6 @@ const manifest: YonexusServerPluginManifest = { description: "Yonexus central hub plugin for cross-instance OpenClaw communication" }; -let _serverStarted = false; - export function createYonexusServerPlugin(api: { rootDir: string; pluginConfig: unknown; @@ -138,15 +142,34 @@ export function createYonexusServerPlugin(api: { }); }, { commands: ["yonexus-server"] }); - if (_serverStarted) return; - _serverStarted = true; + // 1. Ensure shared state survives hot-reload — only initialise when absent + if (!(_G[_REGISTRY_KEY] instanceof YonexusServerRuleRegistry)) { + _G[_REGISTRY_KEY] = createServerRuleRegistry(); + } + if (!Array.isArray(_G[_CALLBACKS_KEY])) { + _G[_CALLBACKS_KEY] = []; + } + + const ruleRegistry = _G[_REGISTRY_KEY] as YonexusServerRuleRegistry; + const onClientAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<(identifier: string) => void>; + + // 2. Refresh the cross-plugin API object every call so that sendRule closure + // always reads the live transport from globalThis. + _G["__yonexusServer"] = { + ruleRegistry, + sendRule: (identifier: string, ruleId: string, content: string): boolean => + (_G[_TRANSPORT_KEY] as ServerTransport | undefined)?.send(identifier, encodeRuleMessage(ruleId, content)) ?? false, + onClientAuthenticated: onClientAuthenticatedCallbacks + }; + + // 3. Start the runtime only once — the globalThis flag survives hot-reload + if (_G[_STARTED_KEY]) return; + _G[_STARTED_KEY] = true; const config = validateYonexusServerConfig(api.pluginConfig); const store = createYonexusServerStore(stateFilePath); - const ruleRegistry = createServerRuleRegistry(); - const onClientAuthenticatedCallbacks: Array<(identifier: string) => void> = []; - + // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid let runtimeRef: ReturnType | null = null; const transport = createServerTransport({ config, @@ -161,14 +184,7 @@ export function createYonexusServerPlugin(api: { } } }); - - // Expose registry and helpers for other plugins loaded in the same process - (globalThis as Record)["__yonexusServer"] = { - ruleRegistry, - sendRule: (identifier: string, ruleId: string, content: string): boolean => - transport.send(identifier, encodeRuleMessage(ruleId, content)), - onClientAuthenticated: onClientAuthenticatedCallbacks - }; + _G[_TRANSPORT_KEY] = transport; const runtime = createYonexusServerRuntime({ config,