feat: HTTP /v1/chat/completions + WS bridge + process manager
Real first cut. OpenClaw routes agent turns via /v1/chat/completions (OpenAI-compatible SSE) into our bridge. Bridge ensures a long-lived claude process per session-key, pushes the user message as notifications/claude/channel into the running Claude Code, awaits a reply via the WS connection, streams the reply back as SSE deltas. - core/process-manager: spawn / track / reap claude processes, auto-confirm the --dangerously-load-development-channels dev-mode prompt by piping "1\n" to stdin shortly after spawn - web/bridge-server: unified HTTP + WS server, per-session FIFO queue, SSE heartbeat (empty content delta — SSE comments don't reset OpenClaw's idle watchdog), reply buffering for streaming progress chunks - index.ts: definePluginEntry for OpenClaw runtime + standalone-mode main for laptop smoke testing (just `bun index.ts`) Same-machine simplifications: no bridge token, no permission_request reverse channel. Session key = agent_id::chat_id (contractor-agent convention).
This commit is contained in:
129
README.md
129
README.md
@@ -1,73 +1,88 @@
|
||||
# SynthesisAgent.OpenclawPlugin
|
||||
|
||||
The **OpenClaw side** of SynthesisAgent. Lives inside the OpenClaw process; spawns and manages one long-lived interactive `claude` per OpenClaw session.
|
||||
OpenClaw plugin that routes agent turns through long-lived interactive Claude Code processes. Replaces the contractor-agent `claude -p` spawn pattern.
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
index.ts entry: definePluginEntry, wires up the others
|
||||
core/config.ts plugin config schema + defaults
|
||||
core/session-mapping.ts persistent openclaw_session ↔ claude_session_uuid
|
||||
core/process-manager.ts spawn/resume/kill claude processes
|
||||
core/cli.ts admin commands (`openclaw synthesis ...`)
|
||||
web/bridge-server.ts WebSocket server that ClaudePlugins connect into
|
||||
index.ts plugin entry + standalone-run main
|
||||
core/config.ts config + defaults
|
||||
core/session-mapping.ts openclaw_session ↔ claude_session_uuid (JSON file)
|
||||
core/process-manager.ts spawn / track / reap claude processes
|
||||
web/bridge-server.ts HTTP server + WS bridge server in one module
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
## Two ways to run
|
||||
|
||||
```
|
||||
plugin loaded
|
||||
↓
|
||||
SessionMapping reads ~/.openclaw/synthesis/sessions.json
|
||||
ProcessManager idle, no spawns yet
|
||||
BridgeServer binds 127.0.0.1:18801
|
||||
↓
|
||||
inbound Discord msg for session=alice
|
||||
↓
|
||||
processManager.ensure('alice')
|
||||
- mapping.ensure → claude_session_uuid (new or existing)
|
||||
- spawn `claude --channels plugin:synthesis-claude@local --resume <uuid> ...`
|
||||
- inject env: SYNTHESIS_BRIDGE_URL / TOKEN / OPENCLAW_SESSION / CLAUDE_SESSION
|
||||
↓
|
||||
ClaudePlugin (in spawned claude) dials ws://127.0.0.1:18801/bridge
|
||||
- sends hello frame
|
||||
- server validates token, marks process ready, sends hello_ack with tool catalog
|
||||
↓
|
||||
processManager.ensure resolves
|
||||
inbound message pushed → notifications/claude/channel → Claude starts new turn
|
||||
↓
|
||||
Claude calls tools/call → forwarded to OpenClaw tool surface
|
||||
Claude finishes → process stays alive, idle timer reset
|
||||
↓
|
||||
1 hour idle → idleSweeper SIGTERMs the process
|
||||
mapping is preserved → next message resumes via --resume
|
||||
### As OpenClaw plugin (production)
|
||||
|
||||
OpenClaw loads `index.ts` via `definePluginEntry`. On `gateway_start`, the bridge server boots on the configured ports.
|
||||
|
||||
### Standalone (development / testing)
|
||||
|
||||
```bash
|
||||
bun index.ts
|
||||
```
|
||||
|
||||
## Config
|
||||
Same code path, but no `definePluginEntry` registration — just boots the bridge with default config.
|
||||
|
||||
See `openclaw.plugin.json` `configSchema`. Override in `~/.openclaw/openclaw.json`:
|
||||
## Config (`openclaw.plugin.json` configSchema)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"synthesis-agent": {
|
||||
"bridgePort": 18801,
|
||||
"bridgeToken": "<random secret>",
|
||||
"idleKillMs": 1800000,
|
||||
"maxProcesses": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `bridgePort` | 18900 | HTTP port for `/v1/chat/completions` |
|
||||
| `channelWsPort` | 18901 | WebSocket port for ClaudePlugin connections |
|
||||
| `permissionMode` | `bypassPermissions` | Spawned Claude's permission mode |
|
||||
| `idleKillMs` | 3 600 000 | Idle TTL before SIGTERM |
|
||||
| `maxProcesses` | 16 | Process pool cap |
|
||||
| `mappingDbPath` | `~/.openclaw/synthesis/sessions.json` | Persistent session map |
|
||||
| `channelName` | `synthesis` | Used in `--channels server:<channelName>` |
|
||||
|
||||
## Laptop smoke test (no real OpenClaw needed)
|
||||
|
||||
```bash
|
||||
# 0. Globally register the ClaudePlugin as an MCP server (one-time)
|
||||
claude mcp add --scope user synthesis -- bun run \
|
||||
/path/to/SynthesisAgent.ClaudePlugin/server.ts
|
||||
|
||||
# 1. Start OpenclawPlugin standalone (this terminal)
|
||||
cd SynthesisAgent.OpenclawPlugin
|
||||
bun install
|
||||
bun index.ts
|
||||
# → HTTP listening on 127.0.0.1:18900
|
||||
# → WS listening on 127.0.0.1:18901/bridge
|
||||
|
||||
# 2. POST a chat completion (another terminal)
|
||||
curl -N -X POST http://127.0.0.1:18900/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-Openclaw-Agent-Id: dev' \
|
||||
-H 'X-Openclaw-Chat-Id: test-1' \
|
||||
-H "X-Openclaw-Workspace: $HOME/some-trusted-dir" \
|
||||
-d '{
|
||||
"model":"synthesis-claude-bridge",
|
||||
"messages":[
|
||||
{"role":"user","content":"Reply with exactly the word READY"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## TODO
|
||||
Expected flow:
|
||||
1. OpenclawPlugin receives request, ensures a claude process for `dev::test-1`
|
||||
2. Spawns `claude --channels server:synthesis --dangerously-load-development-channels server:synthesis --resume <new-uuid> ...`
|
||||
3. ClaudePlugin (inside the claude process) dials `ws://127.0.0.1:18901/bridge`, sends `hello`
|
||||
4. OpenclawPlugin pushes `inbound` frame, ClaudePlugin emits `notifications/claude/channel`
|
||||
5. Claude reacts, eventually calls `reply(text)` tool
|
||||
6. ClaudePlugin sends `reply` WS frame, OpenclawPlugin streams it as SSE delta
|
||||
7. curl sees the reply text
|
||||
|
||||
- [ ] Discover exact OpenClaw plugin-sdk API for: inbound event subscription, CLI command registration, tool catalog enumeration. See `contractor-agent/index.ts` for current best example.
|
||||
- [ ] Wire `api.channels.onInbound(...)` to `pm.ensure()` + `bridgeServer.pushInbound()`.
|
||||
- [ ] Implement tool dispatch in `web/bridge-server.ts` (current `tool_call` handler returns not-implemented).
|
||||
- [ ] Implement permission routing back to source channel.
|
||||
- [ ] CLI: `openclaw synthesis list`, `push`, `kill`, `forget`.
|
||||
- [ ] Settle path conflict: this plugin needs to live at `/root/.openclaw/plugins/synthesis-agent/` to be auto-loaded. Either symlink from this repo or document copy-install.
|
||||
- [ ] Decide policy on `--no-session-persistence` flag (currently we rely on default persistence so `--resume` works).
|
||||
## Known v1 simplifications (documented punch list)
|
||||
|
||||
- Session-key extraction reads headers only — production OpenClaw routing will need the contractor-agent style "Conversation info" parser
|
||||
- No tool-catalog proxy: ClaudePlugin only exposes `reply`; the model uses Claude Code's built-in tools (Read/Edit/Bash/Grep) for everything else
|
||||
- No permission_request reverse channel (full perms by config)
|
||||
- No bridge token / auth handshake (same-machine assumption)
|
||||
- Standalone mode boots with defaults only; no config flags
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
Reference in New Issue
Block a user