Submodules now have real implementations of the HTTP+WS bridge and the channel-side MCP server. STATUS reflects locked architecture decisions and the laptop smoke-test plan. wire-protocol.md updated to drop the auth token and permission_request descriptions (no longer in scope).
3.1 KiB
Wire Protocol
Two channels between SynthesisAgent.OpenclawPlugin (OpenClaw side) and SynthesisAgent.ClaudePlugin (Claude side):
- HTTP —
OpenClaw → OpenclawPlugin. OpenAI-compatible/v1/chat/completionsSSE. This is the public boundary; OpenClaw routes agent turns here. - WebSocket —
ClaudePlugin → OpenclawPlugin. Single long-lived per Claude process. Carries the channel-notification protocol and the reply path.
Same-machine deployment — no auth, no permission_request reverse channel.
HTTP (OpenClaw → OpenclawPlugin)
POST /v1/chat/completions
Headers (v1, transitional — read in this order, first wins):
X-Openclaw-Agent-Id agent identifier
X-Openclaw-Chat-Id channel/chat identifier (DM, group, …)
X-Openclaw-Workspace absolute workspace dir
Body:
OpenAI-format chat completion request (messages[], tools[], …)
Session key = ${agentId}::${chatId} (matches contractor-agent's buildSessionKey).
Response: SSE stream, OpenAI chat.completion.chunk events:
- empty content delta every 30s (heartbeat — empty delta counts as model progress for OpenClaw's idle watchdog; SSE comments do not)
- single content delta with the reply text once
ClaudePluginreportsreplywithfinal: true - final stop chunk +
data: [DONE]
WebSocket bridge
OpenclawPlugin listens on ws://127.0.0.1:<channelWsPort>/bridge. Each spawned Claude process dials in once.
Connection bootstrap
Spawn env vars set by OpenclawPlugin's process-manager:
| Env var | Example |
|---|---|
SYNTHESIS_WS_URL |
ws://127.0.0.1:18901/bridge |
SYNTHESIS_OPENCLAW_SESSION |
developer::discord:channel:123 |
SYNTHESIS_CLAUDE_SESSION |
UUID for traceability |
ClaudePlugin sends:
{ "type": "hello",
"openclaw_session": "<session_key>",
"claude_session": "<uuid>",
"pid": 12345 }
OpenclawPlugin replies:
{ "type": "hello_ack" }
OpenclawPlugin → ClaudePlugin
{ "type": "inbound",
"content": "<user message text>",
"meta": { "chat_id":"…", "message_id":"…", "ts":"…" } }
ClaudePlugin translates this to an MCP notification on its stdio:
{ "method": "notifications/claude/channel",
"params": { "content": "…", "meta": {…} } }
Claude Code reacts by starting a new turn.
ClaudePlugin → OpenclawPlugin
{ "type": "reply",
"content": "<text chunk>",
"final": true }
final: false lets the model stream progress chunks. OpenclawPlugin buffers them and completes the pending SSE stream when a final reply arrives.
Heartbeat
{ "type": "ping" }
{ "type": "pong" }
OpenclawPlugin sends ping every 30s. ClaudePlugin missing 2 in a row → terminate the connection (it will reconnect with exponential backoff).
Reconnect semantics
- ClaudePlugin reconnects with exponential backoff (1s → 30s cap).
- During disconnect, OpenclawPlugin rejects any in-flight dispatch with an error (the HTTP caller sees an SSE error chunk +
[DONE]). helloon reconnect re-establishes routing.- No queued message buffering yet (v1 simplification — add later if needed).