zhi 3229fbd024 fix: end-to-end working after laptop integration test
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.
2026-05-14 14:00:32 +00:00

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:

  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

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
No description provided
Readme 58 KiB
Languages
TypeScript 100%