chore: bump submodules + update wire protocol + STATUS
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).
This commit is contained in:
91
STATUS.md
91
STATUS.md
@@ -1,68 +1,39 @@
|
|||||||
# SynthesisAgent — Status (paused 2026-05-14)
|
# SynthesisAgent — Status (in development, 2026-05-14)
|
||||||
|
|
||||||
## Where we left off
|
## Architecture decisions (locked)
|
||||||
|
|
||||||
Scaffolding complete. Two submodules wired up with their core skeletons:
|
- **HTTP provider pattern**: OpenclawPlugin registers as model provider `synthesis-claude-bridge`. OpenClaw routes agent turns via `POST /v1/chat/completions`. (Mirrors contractor-agent.)
|
||||||
|
- **WebSocket bridge**: ClaudePlugin (in spawned claude) dials back into OpenclawPlugin's WS. Single long-lived connection per Claude process.
|
||||||
|
- **Session granularity**: `agent_id::chat_id` (matches contractor-agent's `buildSessionKey`).
|
||||||
|
- **Reply channel**: ClaudePlugin exposes a `reply(text, final?)` MCP tool. Model calls it. ClaudePlugin → WS → OpenclawPlugin → SSE → OpenClaw.
|
||||||
|
- **Channel dev mode**: `--dangerously-load-development-channels server:synthesis`. Process-manager pipes "1\n" to stdin to auto-confirm the dev dialog.
|
||||||
|
- **No auth, no permission_request reverse**: same-machine deployment, full perms.
|
||||||
|
- **Tool catalog v1**: ClaudePlugin only exposes `reply`. Claude Code's built-in tools (Read/Edit/Bash) do everything else. OpenClaw tool proxy deferred to v2.
|
||||||
|
|
||||||
- `SynthesisAgent.ClaudePlugin/server.ts` — stdio MCP server + bridge WebSocket client. Reads 4 env vars, forwards `tool_call` / `permission_request` to OpenClaw side, translates `inbound_message` frames to `notifications/claude/channel`.
|
## Implementation status
|
||||||
- `SynthesisAgent.OpenclawPlugin` — process manager (spawn/resume/idle-GC), session mapping (JSON persist), bridge WS server, OpenClaw plugin entry. Tool-dispatch and inbound-event-subscription are stubbed.
|
|
||||||
|
|
||||||
Wire protocol fully specified in `docs/wire-protocol.md`.
|
- ✅ Scaffold for both submodules + parent repo
|
||||||
|
- ✅ Submodules are separate gitea repos, referenced by SHA from parent
|
||||||
|
- ✅ Capability declaration uses `experimental.claude/channel` (validated bug fix)
|
||||||
|
- ✅ OpenclawPlugin: HTTP + WS server, process manager, session mapping, FIFO queue, SSE heartbeat, `--channels` spawn with dev-confirm auto-pipe
|
||||||
|
- ✅ ClaudePlugin: WS client, MCP server, `reply` tool, `notifications/claude/channel` emission, reconnect
|
||||||
|
- ✅ Wire protocol doc reflects current behavior
|
||||||
|
- ⏳ Not yet tested end-to-end on laptop — next step
|
||||||
|
|
||||||
## Open blockers (in priority order)
|
## Laptop smoke test plan
|
||||||
|
|
||||||
### Done / validated 2026-05-14 on laptop (SSH via T2)
|
See `SynthesisAgent.OpenclawPlugin/README.md` for the exact commands. Summary:
|
||||||
|
1. `claude mcp add --scope user synthesis -- bun run <path>/server.ts`
|
||||||
|
2. `bun index.ts` in OpenclawPlugin (standalone mode)
|
||||||
|
3. `curl POST /v1/chat/completions` with a "say READY" prompt
|
||||||
|
4. Watch for "READY" in the SSE stream
|
||||||
|
|
||||||
- ✅ `--channels` flag exists in Claude Code 2.1.140/141 (hidden from `--help`)
|
## Deferred to v2
|
||||||
- ✅ Syntax: `plugin:<name>@<marketplace>` or `server:<name>`
|
|
||||||
- ✅ `--plugin-dir <path>` IS documented and works for local plugin loading
|
|
||||||
- ✅ Capability declaration must be `experimental.claude/channel` not top-level — **scaffolding patched**
|
|
||||||
- ✅ `-p` mode does NOT trigger turns from channel notifications — interactive only
|
|
||||||
- ✅ Full gating chain decoded from binary (see `memory/claude_channels_protocol.md`)
|
|
||||||
- ❌ Did NOT capture an end-to-end "claude responds to channel notification" trace — got stuck at `--dangerously-load-development-channels` allowlist gate in script-PTY runs (interactive confirm may not have flowed through). Indirect evidence is overwhelming via the Discord plugin's working production pattern; remaining doubt is small.
|
|
||||||
|
|
||||||
### Remaining
|
- OpenClaw tool proxy via MCP (read/edit/etc. forwarded as MCP tools instead of using Claude Code's built-ins) — only matters if we want OpenClaw-specific tools (memory_*, wiki_*, canvas) to be accessible from the bridged session
|
||||||
|
- Full session-key extraction from OpenClaw "Conversation info" block (currently using HTTP headers)
|
||||||
1. **`.mcp.json` not written** — sandbox flagged it as sensitive on Write. Five-line file, need to drop in by hand or approve the write.
|
- `plugin:` channel form for marketplace distribution (currently `server:` + dev flag)
|
||||||
2. **Confirm channel notification → new turn empirically** — Have user run interactively (no PTY tricks):
|
- Permission_request reverse channel (skipped per design)
|
||||||
```
|
- Install / uninstall script (mirrors contractor-agent's `scripts/install.mjs`)
|
||||||
claude --mcp-config /tmp/synthesis-mcp.json \
|
- Better stop-hook integration for graceful shutdown
|
||||||
--channels server:synthesis-stub \
|
- Multi-machine deployment (currently same-host only)
|
||||||
--dangerously-load-development-channels server:synthesis-stub
|
|
||||||
```
|
|
||||||
(in a trusted dir), confirm dev mode at the prompt, watch for the stub's 1.5s-delayed channel-test ping triggering a turn.
|
|
||||||
3. **Decide deployment path**:
|
|
||||||
- `server:` channels are flagged "dangerous dev only" — fine for our own use, awkward for shipping
|
|
||||||
- `plugin:` channels need to be in marketplace allowlist (org `allowedChannelPlugins`) — requires distribution mechanism
|
|
||||||
- Easiest dev path: local marketplace pointing at our repo
|
|
||||||
4. **OpenClaw plugin-sdk inbound subscription API** — `index.ts` has a TODO where `api.channels.onInbound(...)` should live. Need to read `contractor-agent/index.ts` carefully and copy the actual pattern.
|
|
||||||
5. **Tool catalog source** — `web/bridge-server.ts` sends an empty `tools: []` in `hello_ack`. Must enumerate OpenClaw's real tool surface and forward.
|
|
||||||
6. **`tool_call` dispatch** — currently returns `not implemented`. Need to wire to OpenClaw's tool execution path.
|
|
||||||
7. **Permission routing back to source channel** — bridge auto-denies any `permission_request` today.
|
|
||||||
|
|
||||||
## Quick-start tests to run when resuming
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 0. write .mcp.json (blocked last time)
|
|
||||||
|
|
||||||
# 1. verify --channels accepts local path or local marketplace
|
|
||||||
claude --channels plugin:synthesis-claude@/root/.openclaw/workspace/workspace-developer/SynthesisAgent/SynthesisAgent.ClaudePlugin 2>&1 | head -20
|
|
||||||
|
|
||||||
# 2. once #1 works: smoke-test notifications/claude/channel from a stub server
|
|
||||||
# (build a 30-line MCP server that fakes inbound_message after 5s)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files to start from when picking up
|
|
||||||
|
|
||||||
- `README.md` — context refresher
|
|
||||||
- `docs/wire-protocol.md` — protocol contract
|
|
||||||
- `SynthesisAgent.OpenclawPlugin/README.md` — TODO list for that side
|
|
||||||
- `SynthesisAgent.ClaudePlugin/README.md` — TODO list for that side
|
|
||||||
|
|
||||||
Reference implementations to crib from:
|
|
||||||
- `~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/discord/server.ts` — channel protocol model
|
|
||||||
- `/root/.openclaw/plugins/contractor-agent/index.ts` — OpenClaw plugin lifecycle model
|
|
||||||
|
|
||||||
## Why paused
|
|
||||||
|
|
||||||
User pivoted to investigate a recurrence of the Discord duplicate-messages bug ([[discord_duplicate_messages]]). SynthesisAgent is the longer-term fix for that whole class of problem; the duplicate-messages bug is the short-term hotfix path.
|
|
||||||
|
|||||||
Submodule SynthesisAgent.ClaudePlugin updated: cb94464060...813f81af9a
Submodule SynthesisAgent.OpenclawPlugin updated: 38ac6d20b7...0324a47d13
@@ -1,122 +1,99 @@
|
|||||||
# Wire Protocol — OpenclawPlugin ↔ ClaudePlugin
|
# Wire Protocol
|
||||||
|
|
||||||
A WebSocket connection from each spawned ClaudePlugin instance back to OpenclawPlugin's bridge server. JSON messages, one per frame.
|
Two channels between **SynthesisAgent.OpenclawPlugin** (OpenClaw side) and **SynthesisAgent.ClaudePlugin** (Claude side):
|
||||||
|
|
||||||
## Connection bootstrap
|
1. **HTTP** — `OpenClaw → OpenclawPlugin`. OpenAI-compatible `/v1/chat/completions` SSE. This is the public boundary; OpenClaw routes agent turns here.
|
||||||
|
2. **WebSocket** — `ClaudePlugin → OpenclawPlugin`. Single long-lived per Claude process. Carries the channel-notification protocol and the reply path.
|
||||||
|
|
||||||
On spawn, OpenclawPlugin sets these env vars on the Claude process so ClaudePlugin can find its way home:
|
Same-machine deployment — no auth, no permission_request reverse channel.
|
||||||
|
|
||||||
| Env var | Purpose |
|
## HTTP (OpenClaw → OpenclawPlugin)
|
||||||
|--------------------------------|---------------------------------------------------------------|
|
|
||||||
| `SYNTHESIS_BRIDGE_URL` | WebSocket URL, e.g. `ws://127.0.0.1:18801/bridge` |
|
|
||||||
| `SYNTHESIS_BRIDGE_TOKEN` | Shared secret, validated server-side |
|
|
||||||
| `SYNTHESIS_OPENCLAW_SESSION` | The OpenClaw session ID this Claude process serves |
|
|
||||||
| `SYNTHESIS_CLAUDE_SESSION` | The Claude session UUID (for traceability) |
|
|
||||||
|
|
||||||
ClaudePlugin connects on startup and sends:
|
```
|
||||||
|
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 `ClaudePlugin` reports `reply` with `final: 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:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "hello",
|
{ "type": "hello",
|
||||||
"openclaw_session": "<sid>",
|
"openclaw_session": "<session_key>",
|
||||||
"claude_session": "<uuid>",
|
"claude_session": "<uuid>",
|
||||||
"pid": 12345,
|
"pid": 12345 }
|
||||||
"token": "<bridge_token>" }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenclawPlugin responds with:
|
OpenclawPlugin replies:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "hello_ack", "tools": [ /* tool catalog */ ], "session_meta": { ... } }
|
{ "type": "hello_ack" }
|
||||||
```
|
```
|
||||||
|
|
||||||
If `hello` is rejected (bad token, unknown session), the server closes the socket.
|
### `OpenclawPlugin → ClaudePlugin`
|
||||||
|
|
||||||
## Direction: OpenClaw → Claude (inbound events)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "inbound_message",
|
{ "type": "inbound",
|
||||||
"request_id": "msg_abc123",
|
"content": "<user message text>",
|
||||||
"content": "user message text",
|
"meta": { "chat_id":"…", "message_id":"…", "ts":"…" } }
|
||||||
"meta": {
|
|
||||||
"chat_id": "<openclaw_session_id>",
|
|
||||||
"message_id": "...",
|
|
||||||
"user": "alice",
|
|
||||||
"user_id": "...",
|
|
||||||
"ts": "2026-05-14T...Z",
|
|
||||||
"source_channel": "discord",
|
|
||||||
"attachments": [ ... ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
ClaudePlugin translates each `inbound_message` into:
|
ClaudePlugin translates this to an MCP notification on its stdio:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "method": "notifications/claude/channel",
|
{ "method": "notifications/claude/channel",
|
||||||
"params": { "content": ..., "meta": ... } }
|
"params": { "content": "…", "meta": {…} } }
|
||||||
```
|
```
|
||||||
|
|
||||||
…and emits it on its MCP stdio. Claude Code reacts by starting a new turn.
|
Claude Code reacts by starting a new turn.
|
||||||
|
|
||||||
## Direction: OpenClaw → Claude (permission reply)
|
### `ClaudePlugin → OpenclawPlugin`
|
||||||
|
|
||||||
When the user answers a permission prompt out of band (e.g. "yes abxyz" in Discord), OpenClaw forwards:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "permission_reply",
|
{ "type": "reply",
|
||||||
"request_id": "abxyz",
|
"content": "<text chunk>",
|
||||||
"behavior": "allow" }
|
"final": true }
|
||||||
```
|
```
|
||||||
|
|
||||||
ClaudePlugin emits an MCP `notifications/claude/channel/permission` for that request_id.
|
`final: false` lets the model stream progress chunks. OpenclawPlugin buffers them and completes the pending SSE stream when a final `reply` arrives.
|
||||||
|
|
||||||
## Direction: Claude → OpenClaw (tool calls)
|
### Heartbeat
|
||||||
|
|
||||||
Each `tools/call` Claude makes on ClaudePlugin (excluding internal ones the plugin owns) is forwarded over the bridge:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "tool_call",
|
{ "type": "ping" }
|
||||||
"call_id": "tc_001",
|
{ "type": "pong" }
|
||||||
"tool": "pcexec",
|
|
||||||
"args": { ... } }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenclawPlugin invokes the corresponding OpenClaw tool and responds:
|
OpenclawPlugin sends `ping` every 30s. ClaudePlugin missing 2 in a row → terminate the connection (it will reconnect with exponential backoff).
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "tool_result",
|
|
||||||
"call_id": "tc_001",
|
|
||||||
"ok": true,
|
|
||||||
"result": { ... } }
|
|
||||||
```
|
|
||||||
|
|
||||||
…or with `"ok": false, "error": "..."`.
|
|
||||||
|
|
||||||
## Direction: Claude → OpenClaw (permission request)
|
|
||||||
|
|
||||||
When Claude Code asks for a permission via `notifications/claude/channel/permission_request`, ClaudePlugin forwards:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "permission_request",
|
|
||||||
"request_id": "<5 lowercase letters minus 'l'>",
|
|
||||||
"tool": "Bash",
|
|
||||||
"args": { ... },
|
|
||||||
"rationale": "..." }
|
|
||||||
```
|
|
||||||
|
|
||||||
OpenclawPlugin routes this to the user via the source channel (e.g. posts a Discord message: "Approve? Reply `yes <code>`").
|
|
||||||
|
|
||||||
## Direction: bidirectional health
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ping", "ts": ... }
|
|
||||||
{ "type": "pong", "ts": ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
OpenclawPlugin sends `ping` every 30s. ClaudePlugin missing 2 consecutive pings → server marks the connection dead, allows reconnect.
|
|
||||||
|
|
||||||
## Reconnect semantics
|
## Reconnect semantics
|
||||||
|
|
||||||
- ClaudePlugin reconnects on socket close with exponential backoff (1s, 2s, 4s, max 30s).
|
- ClaudePlugin reconnects with exponential backoff (1s → 30s cap).
|
||||||
- During disconnect, OpenclawPlugin buffers up to N inbound messages per session (configurable). Buffer overflow drops the oldest with a warning.
|
- During disconnect, OpenclawPlugin rejects any in-flight dispatch with an error (the HTTP caller sees an SSE error chunk + `[DONE]`).
|
||||||
- On reconnect, the `hello` message re-establishes session binding.
|
- `hello` on reconnect re-establishes routing.
|
||||||
|
- No queued message buffering yet (v1 simplification — add later if needed).
|
||||||
|
|||||||
Reference in New Issue
Block a user