import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry' import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core' import { normalizeConfig, type SynthesisConfig } from './core/config.js' import { ProcessManager } from './core/process-manager.js' import { SessionMapping } from './core/session-mapping.js' import { startBridgeServer } from './web/bridge-server.js' import { registerCli } from './core/cli.js' // All long-lived state lives on globalThis so OpenClaw hot-reloads don't kill // running Claude processes (mirrors contractor-agent's convention). const _G = globalThis as Record const PM_KEY = '_synthesisProcessManager' const SERVER_KEY = '_synthesisBridgeServer' const MAPPING_KEY = '_synthesisSessionMapping' export default definePluginEntry({ id: 'synthesis-agent', name: 'SynthesisAgent', description: 'Manages long-lived Claude Code processes per OpenClaw session', register(api: OpenClawPluginApi): void { const config: SynthesisConfig = normalizeConfig(api.pluginConfig) // ── Reuse existing state across hot reload ───────────────────────────── let mapping = _G[MAPPING_KEY] as SessionMapping | undefined if (!mapping) { mapping = new SessionMapping(config.mappingDbPath) _G[MAPPING_KEY] = mapping } let pm = _G[PM_KEY] as ProcessManager | undefined if (!pm) { pm = new ProcessManager({ config, mapping }) _G[PM_KEY] = pm } // Existing bridge server: leave it alone. if (!_G[SERVER_KEY]) { startBridgeServer({ config, mapping, processManager: pm }) .then(server => { _G[SERVER_KEY] = server }) .catch(err => { api.logger?.error?.('synthesis: bridge server failed to start', err) }) } // ── Wire to OpenClaw inbound event bus ───────────────────────────────── // TODO: subscribe to api.events / api.channels.onInbound (exact API TBD). // Each inbound message arrives with an openclaw_session_id; we forward to // the process manager, which spawns/resumes claude as needed and pushes // the message into its plugin's bridge socket. // // api.channels.onInbound((evt) => { // pm.deliverInbound(evt.openclawSessionId, evt) // }) // // Similarly for permission replies. registerCli(api, { processManager: pm, mapping, config }) api.lifecycle?.onShutdown?.(async () => { const server = _G[SERVER_KEY] as Awaited> | undefined await server?.close?.() delete _G[SERVER_KEY] await pm!.shutdown() delete _G[PM_KEY] }) }, })