From b696c6a0afadd5670049025b278774a7194c5876 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 May 2026 08:08:52 +0000 Subject: [PATCH] fix: gate runtime startup behind gateway_start; migrate to current plugin SDK Lifecycle: - Move runtime.start() and shutdown handlers out of register() into api.on("gateway_start", ...) and api.on("gateway_stop", ...). register() runs in every CLI subprocess that loads plugins (e.g. `openclaw completion`, `openclaw doctor`); without this gate the runtime would open a network connection / bind a listener every time those one-shot commands ran. - Drop process.once("SIGTERM"/"SIGINT") in favour of the gateway_stop hook, which is the documented way for plugins to react to shutdown. - Stop relying on the non-standard `api.rootDir` field (not present on the current OpenClawPluginApi); compute the per-plugin data directory as ~/.openclaw/yonexus-server and ensure it exists before use. Plugin SDK convention update: - Wrap default export with definePluginEntry({ id, name, description, register }) per the current openclaw plugin authoring contract. - Re-type the register function to accept OpenClawPluginApi instead of the hand-crafted { rootDir, pluginConfig, ... } shape. - Use focused subpath imports openclaw/plugin-sdk/plugin-entry and openclaw/plugin-sdk/core. - Add openclaw as a devDependency (file:/usr/lib/node_modules/openclaw) so tsc resolves the SDK type subpaths at build time. - Modernize openclaw.plugin.json: drop version/entry/permissions, add activation.onStartup so gateway_start fires for this plugin at boot. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 3 +- plugin/index.ts | 115 ++++++++++++++++++++---------------- plugin/openclaw.plugin.json | 9 ++- 3 files changed, 71 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index ef8d99a..a525111 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@types/node": "^25.5.2", "typescript": "^5.6.3", - "vitest": "^4.1.3" + "vitest": "^4.1.3", + "openclaw": "file:/usr/lib/node_modules/openclaw" } } diff --git a/plugin/index.ts b/plugin/index.ts index dea52aa..f3fc5ee 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -32,6 +32,9 @@ export { import path from "node:path"; import fs from "node:fs"; +import os from "node:os"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import { validateYonexusServerConfig } from "./core/config.js"; import { createYonexusServerStore } from "./core/store.js"; import { createServerTransport, type ServerTransport } from "./core/transport.js"; @@ -40,6 +43,8 @@ import { createServerRuleRegistry, YonexusServerRuleRegistry } from "./core/rule import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js"; import type { ServerPersistenceData } from "./core/persistence.js"; +const PLUGIN_DATA_DIR = path.join(os.homedir(), ".openclaw", "yonexus-server"); + const _G = globalThis as Record; const _STARTED_KEY = "_yonexusServerStarted"; const _TRANSPORT_KEY = "_yonexusServerTransport"; @@ -58,14 +63,8 @@ const manifest: YonexusServerPluginManifest = { description: "Yonexus central hub plugin for cross-instance OpenClaw communication" }; -export function createYonexusServerPlugin(api: { - rootDir: string; - pluginConfig: unknown; - registrationMode?: string; // "full" (gateway) | "cli-metadata" | "setup-only" | "setup-runtime" - // eslint-disable-next-line @typescript-eslint/no-explicit-any - registerCli?: (registrar: (ctx: { program: any }) => void, opts?: { commands?: string[] }) => void; -}): void { - const stateFilePath = path.join(api.rootDir, "state.json"); +export function createYonexusServerPlugin(api: OpenClawPluginApi): void { + const stateFilePath = path.join(PLUGIN_DATA_DIR, "state.json"); // Register CLI regardless of whether the gateway is already running. // The CLI process is a separate invocation that reads from the persisted state file. @@ -162,60 +161,72 @@ export function createYonexusServerPlugin(api: { onClientAuthenticated: onClientAuthenticatedCallbacks }; - // 3. Start the runtime only once — the globalThis flag survives hot-reload - if (_G[_STARTED_KEY]) return; - _G[_STARTED_KEY] = true; + // 3. Runtime startup — only fire when the gateway boots, not eagerly during + // register() inside one-shot CLI subprocesses (e.g. `openclaw completion`). + // Without this gate, every CLI invocation that loads plugins would try to + // bind the WebSocket listener; EADDRINUSE recovery handled the gateway- + // already-running case but a genuine first-bind in a subprocess would + // keep that process alive forever. + api.on("gateway_start", () => { + if (_G[_STARTED_KEY]) return; + _G[_STARTED_KEY] = true; - const config = validateYonexusServerConfig(api.pluginConfig); - const store = createYonexusServerStore(stateFilePath); + fs.mkdirSync(PLUGIN_DATA_DIR, { recursive: true }); - // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid - let runtimeRef: ReturnType | null = null; - const transport = createServerTransport({ - config, - onMessage: (conn, msg) => { - runtimeRef?.handleMessage(conn, msg).catch((err: unknown) => { - console.error("[yonexus-server] message handler error:", err); - }); - }, - onDisconnect: (identifier) => { - if (identifier && runtimeRef) { - runtimeRef.handleDisconnect(identifier); + const config = validateYonexusServerConfig(api.pluginConfig); + const store = createYonexusServerStore(stateFilePath); + + // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid + let runtimeRef: ReturnType | null = null; + const transport = createServerTransport({ + config, + onMessage: (conn, msg) => { + runtimeRef?.handleMessage(conn, msg).catch((err: unknown) => { + console.error("[yonexus-server] message handler error:", err); + }); + }, + onDisconnect: (identifier) => { + if (identifier && runtimeRef) { + runtimeRef.handleDisconnect(identifier); + } } - } - }); - _G[_TRANSPORT_KEY] = transport; + }); + _G[_TRANSPORT_KEY] = transport; - const runtime = createYonexusServerRuntime({ - config, - store, - transport, - ruleRegistry, - onClientAuthenticated: (identifier) => { - for (const cb of onClientAuthenticatedCallbacks) cb(identifier); - } - }); - runtimeRef = runtime; + const runtime = createYonexusServerRuntime({ + config, + store, + transport, + ruleRegistry, + onClientAuthenticated: (identifier) => { + for (const cb of onClientAuthenticatedCallbacks) cb(identifier); + } + }); + runtimeRef = runtime; + _G["_yonexusServerRuntime"] = runtime; - const shutdown = (): void => { - runtime.stop().catch((err: unknown) => { + runtime.start().catch((err: unknown) => { + const code = (err as NodeJS.ErrnoException | undefined)?.code; + if (code !== "EADDRINUSE") { + console.error("[yonexus-server] failed to start:", err); + } + }); + }); + + api.on("gateway_stop", () => { + const runtime = _G["_yonexusServerRuntime"] as ReturnType | undefined; + runtime?.stop().catch((err: unknown) => { console.error("[yonexus-server] shutdown error:", err); }); - }; - process.once("SIGTERM", shutdown); - process.once("SIGINT", shutdown); - - runtime.start().catch((err: unknown) => { - // EADDRINUSE means the gateway is already running (e.g. this is a CLI invocation). - // Any other error is a real problem worth logging. - const code = (err as NodeJS.ErrnoException | undefined)?.code; - if (code !== "EADDRINUSE") { - console.error("[yonexus-server] failed to start:", err); - } }); } -export default createYonexusServerPlugin; +export default definePluginEntry({ + id: "yonexus-server", + name: "Yonexus.Server", + description: "Yonexus central hub plugin for cross-instance OpenClaw communication", + register: createYonexusServerPlugin, +}); export { createServerTransport, YonexusServerTransport, diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index bd7afeb..bcdcc29 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -1,10 +1,13 @@ { "id": "yonexus-server", "name": "Yonexus.Server", - "version": "0.1.0", "description": "Yonexus central hub plugin for cross-instance OpenClaw communication", - "entry": "./dist/Yonexus.Server/plugin/index.js", - "permissions": [], + "activation": { + "onStartup": true + }, + "commandAliases": [ + { "name": "yonexus-server" } + ], "configSchema": { "type": "object", "additionalProperties": false,