OpenClaw plugin that manages long-lived interactive Claude Code processes per OpenClaw session. Process manager + session-mapping persistence + bridge WebSocket server skeleton. Will be rewritten to follow the contractor-agent HTTP model-provider pattern (register as `synthesis-claude-bridge` provider, receive /v1/chat/completions, dispatch to channel notification on the bound Claude process). See parent repo's STATUS.md for the punch list.
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
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<string, unknown>
|
|
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<ReturnType<typeof startBridgeServer>> | undefined
|
|
await server?.close?.()
|
|
delete _G[SERVER_KEY]
|
|
await pm!.shutdown()
|
|
delete _G[PM_KEY]
|
|
})
|
|
},
|
|
})
|