From 4adb1873311a15ccf74c1789cc2b8e51273e8e14 Mon Sep 17 00:00:00 2001 From: hzhang Date: Fri, 10 Apr 2026 20:41:27 +0100 Subject: [PATCH] fix: migrate startup guard and shared state to globalThis Module-level _clientStarted / ruleRegistry / onAuthenticatedCallbacks reset on hot-reload (new VM context), causing a second runtime to start and the exposed __yonexusClient API to point at orphaned objects. - Replace let _clientStarted with _G["_yonexusClientStarted"] - Store ruleRegistry and onAuthenticatedCallbacks under globalThis keys, initialising only when absent (survives hot-reload) - Store runtime under _G["_yonexusClientRuntime"]; sendRule / submitPairingCode closures read it from globalThis instead of capturing a module-local ref - Re-write __yonexusClient every register() call so closures stay current, but skip runtime.start() when the globalThis flag is already set Co-Authored-By: Claude Sonnet 4.6 --- plugin/index.ts | 59 ++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/plugin/index.ts b/plugin/index.ts index b006bd1..7054776 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -43,8 +43,14 @@ import path from "node:path"; import { validateYonexusClientConfig } from "./core/config.js"; import { createYonexusClientStateStore } from "./core/state.js"; import { createClientTransport } from "./core/transport.js"; -import { createYonexusClientRuntime } from "./core/runtime.js"; -import { createClientRuleRegistry } from "./core/rules.js"; +import { createYonexusClientRuntime, type YonexusClientRuntime } from "./core/runtime.js"; +import { createClientRuleRegistry, YonexusClientRuleRegistry } from "./core/rules.js"; + +const _G = globalThis as Record; +const _STARTED_KEY = "_yonexusClientStarted"; +const _RUNTIME_KEY = "_yonexusClientRuntime"; +const _REGISTRY_KEY = "_yonexusClientRegistry"; +const _CALLBACKS_KEY = "_yonexusClientOnAuthCallbacks"; export interface YonexusClientPluginManifest { readonly name: "Yonexus.Client"; @@ -58,41 +64,48 @@ const manifest: YonexusClientPluginManifest = { description: "Yonexus client plugin for cross-instance OpenClaw communication" }; -let _clientStarted = false; - export function createYonexusClientPlugin(api: { rootDir: string; pluginConfig: unknown }): void { - if (_clientStarted) return; - _clientStarted = true; + // 1. Ensure shared state survives hot-reload — only initialise when absent + if (!(_G[_REGISTRY_KEY] instanceof YonexusClientRuleRegistry)) { + _G[_REGISTRY_KEY] = createClientRuleRegistry(); + } + if (!Array.isArray(_G[_CALLBACKS_KEY])) { + _G[_CALLBACKS_KEY] = []; + } + + const ruleRegistry = _G[_REGISTRY_KEY] as YonexusClientRuleRegistry; + const onAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<() => void>; + + // 2. Refresh the cross-plugin API object every call so that sendRule / submitPairingCode + // closures always read the live runtime from globalThis. + _G["__yonexusClient"] = { + ruleRegistry, + sendRule: (ruleId: string, content: string): boolean => + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.sendRuleMessage(ruleId, content) ?? false, + submitPairingCode: (code: string): boolean => + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.submitPairingCode(code) ?? false, + onAuthenticated: onAuthenticatedCallbacks + }; + + // 3. Start the runtime only once — the globalThis flag survives hot-reload + if (_G[_STARTED_KEY]) return; + _G[_STARTED_KEY] = true; const config = validateYonexusClientConfig(api.pluginConfig); const stateStore = createYonexusClientStateStore(path.join(api.rootDir, "state.json")); - const ruleRegistry = createClientRuleRegistry(); - const onAuthenticatedCallbacks: Array<() => void> = []; - - let runtimeRef: ReturnType | null = null; const transport = createClientTransport({ config, onMessage: (msg) => { - runtimeRef?.handleMessage(msg).catch((err: unknown) => { + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleMessage(msg).catch((err: unknown) => { console.error("[yonexus-client] message handler error:", err); }); }, onStateChange: (state) => { - runtimeRef?.handleTransportStateChange(state); + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleTransportStateChange(state); } }); - // Expose registry and helpers for other plugins loaded in the same process - (globalThis as Record)["__yonexusClient"] = { - ruleRegistry, - sendRule: (ruleId: string, content: string): boolean => - runtimeRef?.sendRuleMessage(ruleId, content) ?? false, - submitPairingCode: (code: string): boolean => - runtimeRef?.submitPairingCode(code) ?? false, - onAuthenticated: onAuthenticatedCallbacks - }; - const runtime = createYonexusClientRuntime({ config, transport, @@ -102,7 +115,7 @@ export function createYonexusClientPlugin(api: { rootDir: string; pluginConfig: for (const cb of onAuthenticatedCallbacks) cb(); } }); - runtimeRef = runtime; + _G[_RUNTIME_KEY] = runtime; const shutdown = (): void => { runtime.stop().catch((err: unknown) => {