3229fbd024ff83762ba52011f3d678419c041a54
Discovered during smoke-testing on hzhang's laptop: 1. `--channels server:X --dangerously-load-development-channels server:X` makes Claude Code list the channel twice and the second copy never inherits dev-mode, leaving "server: entries need --dangerously-load-development-channels" stuck in the status panel. Fix: pass channel ONLY via --dangerously-load-development-channels. 2. Without a controlling TTY, Claude Code's dev-mode confirmation dialog blocks forever waiting for keystrokes that never arrive. Fix: spawn claude wrapped in `script -q -c CMD PTYLOG` so it gets a PTY, then write "\r" to stdin at several timeouts (cheap to over-send). 3. process-manager.markReady was matching on PID, but the PID in the bridge hello frame is the ClaudePlugin (bun) process's pid, not the script-wrapped claude process's pid we tracked. Fix: match on openclaw-session-key, which is consistent on both sides. 4. First spawn for a new session can't use --resume (no transcript exists yet) — claude errors out. Fix: probe ~/.claude/projects/<workspace-slug>/<uuid>.jsonl for existence and use --session-id on fresh sessions, --resume after a process restart. 5. Add --debug-file per session so future debugging has the gating logs. 6. Local definePluginEntry shim (no openclaw runtime dependency) so `bun index.ts` works standalone for laptop smoke tests. End-to-end verified twice on laptop: curl POST -> SSE delta with the exact reply text. Average cold-start ~10s, hot path 2-3s.
SynthesisAgent.OpenclawPlugin
OpenClaw plugin that routes agent turns through long-lived interactive Claude Code processes. Replaces the contractor-agent claude -p spawn pattern.
Components
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
Two ways to run
As OpenClaw plugin (production)
OpenClaw loads index.ts via definePluginEntry. On gateway_start, the bridge server boots on the configured ports.
Standalone (development / testing)
bun index.ts
Same code path, but no definePluginEntry registration — just boots the bridge with default config.
Config (openclaw.plugin.json configSchema)
| 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)
# 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"}
]
}'
Expected flow:
- OpenclawPlugin receives request, ensures a claude process for
dev::test-1 - Spawns
claude --channels server:synthesis --dangerously-load-development-channels server:synthesis --resume <new-uuid> ... - ClaudePlugin (inside the claude process) dials
ws://127.0.0.1:18901/bridge, sendshello - OpenclawPlugin pushes
inboundframe, ClaudePlugin emitsnotifications/claude/channel - Claude reacts, eventually calls
reply(text)tool - ClaudePlugin sends
replyWS frame, OpenclawPlugin streams it as SSE delta - curl sees the reply text
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
Description
Languages
TypeScript
100%