feat(acp): add conversation binds for message channels

This commit is contained in:
Peter Steinberger
2026-03-28 01:53:53 +00:00
parent 067f8db4c9
commit c42ec81e37
30 changed files with 1922 additions and 80 deletions

View File

@@ -212,6 +212,60 @@ Per-group configuration:
- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
- Authorized senders can run control commands even without mentioning in groups.
## ACP conversation bindings
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer.
Fast operator flow:
- Run `/acp spawn codex --bind here` inside the DM or allowed group chat.
- Future messages in that same BlueBubbles conversation route to the spawned ACP session.
- `/new` and `/reset` reset the same bound ACP session in place.
- `/acp close` closes the ACP session and removes the binding.
Configured persistent bindings are also supported through top-level `bindings[]` entries with `type: "acp"` and `match.channel: "bluebubbles"`.
`match.peer.id` can use any supported BlueBubbles target form:
- normalized DM handle such as `+15555550123` or `user@example.com`
- `chat_id:<id>`
- `chat_guid:<guid>`
- `chat_identifier:<identifier>`
For stable group bindings, prefer `chat_id:*` or `chat_identifier:*`.
Example:
```json5
{
agents: {
list: [
{
id: "codex",
runtime: {
type: "acp",
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
},
},
],
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "bluebubbles",
accountId: "default",
peer: { kind: "dm", id: "+15555550123" },
},
acp: { label: "codex-imessage" },
},
],
}
```
See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
## Typing + read receipts
- **Typing indicators**: Sent automatically before and during response generation.

View File

@@ -750,9 +750,13 @@ Default slash command settings:
Notes:
- `/acp spawn codex --bind here` binds the current Discord channel or thread in place and keeps future messages routed to the same ACP session.
- That can still mean "start a fresh Codex ACP session", but it does not create a new Discord thread by itself. The existing channel stays the chat surface.
- Codex may still run in its own `cwd` or backend workspace on disk. That workspace is runtime state, not a Discord thread.
- Thread messages can inherit the parent channel ACP binding.
- In a bound channel or thread, `/new` and `/reset` reset the same ACP session in place.
- Temporary thread bindings still work and can override target resolution while active.
- `spawnAcpSessions` is only required when OpenClaw needs to create/bind a child thread via `--thread auto|here`. It is not required for `/acp spawn ... --bind here` in the current channel.
See [ACP Agents](/tools/acp-agents) for binding behavior details.

View File

@@ -184,6 +184,58 @@ imsg send <handle> "test"
</Tab>
</Tabs>
## ACP conversation bindings
Legacy iMessage chats can also be bound to ACP sessions.
Fast operator flow:
- Run `/acp spawn codex --bind here` inside the DM or allowed group chat.
- Future messages in that same iMessage conversation route to the spawned ACP session.
- `/new` and `/reset` reset the same bound ACP session in place.
- `/acp close` closes the ACP session and removes the binding.
Configured persistent bindings are supported through top-level `bindings[]` entries with `type: "acp"` and `match.channel: "imessage"`.
`match.peer.id` can use:
- normalized DM handle such as `+15555550123` or `user@example.com`
- `chat_id:<id>` (recommended for stable group bindings)
- `chat_guid:<guid>`
- `chat_identifier:<identifier>`
Example:
```json5
{
agents: {
list: [
{
id: "codex",
runtime: {
type: "acp",
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
},
},
],
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "imessage",
accountId: "default",
peer: { kind: "group", id: "chat_id:123" },
},
acp: { label: "codex-group" },
},
],
}
```
See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
## Deployment patterns
<AccordionGroup>

View File

@@ -523,6 +523,7 @@ BlueBubbles is the recommended iMessage path (plugin-backed, configured under `c
- Core key paths covered here: `channels.bluebubbles`, `channels.bluebubbles.dmPolicy`.
- Optional `channels.bluebubbles.defaultAccount` overrides default account selection when it matches a configured account id.
- Top-level `bindings[]` entries with `type: "acp"` can bind BlueBubbles conversations to persistent ACP sessions. Use a BlueBubbles handle or target string (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`) in `match.peer.id`. Shared field semantics: [ACP Agents](/tools/acp-agents#channel-specific-settings).
- Full BlueBubbles channel configuration is documented in [BlueBubbles](/channels/bluebubbles).
### iMessage
@@ -559,6 +560,7 @@ OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
- `attachmentRoots` and `remoteAttachmentRoots` restrict inbound attachment paths (default: `/Users/*/Library/Messages/Attachments`).
- SCP uses strict host-key checking, so ensure the relay host key already exists in `~/.ssh/known_hosts`.
- `channels.imessage.configWrites`: allow or deny iMessage-initiated config writes.
- Top-level `bindings[]` entries with `type: "acp"` can bind iMessage conversations to persistent ACP sessions. Use a normalized handle or explicit chat target (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`) in `match.peer.id`. Shared field semantics: [ACP Agents](/tools/acp-agents#channel-specific-settings).
<Accordion title="iMessage SSH wrapper example">

View File

@@ -2,8 +2,8 @@
summary: "Use ACP runtime sessions for Codex, Claude Code, Cursor, Gemini CLI, OpenClaw ACP, and other harness agents"
read_when:
- Running coding harnesses through ACP
- Setting up thread-bound ACP sessions on thread-capable channels
- Binding Discord channels or Telegram forum topics to persistent ACP sessions
- Setting up conversation-bound ACP sessions on messaging channels
- Binding Discord channels, Telegram topics, BlueBubbles chats, or iMessage chats to persistent ACP sessions
- Troubleshooting ACP backend and plugin wiring
- Operating /acp commands from chat
title: "ACP Agents"
@@ -20,8 +20,9 @@ If you ask OpenClaw in plain language to "run this in Codex" or "start Claude Co
Use this when you want a practical `/acp` runbook:
1. Spawn a session:
- `/acp spawn codex --bind here`
- `/acp spawn codex --mode persistent --thread auto`
2. Work in the bound thread (or target that session key explicitly).
2. Work in the bound conversation or thread (or target that session key explicitly).
3. Check runtime state:
- `/acp status`
4. Tune runtime options as needed:
@@ -38,16 +39,19 @@ Use this when you want a practical `/acp` runbook:
Examples of natural requests:
- "Bind this Discord channel to Codex."
- "Start a persistent Codex session in a thread here and keep it focused."
- "Run this as a one-shot Claude Code ACP session and summarize the result."
- "Bind this iMessage chat to Codex and keep follow-ups in the same workspace."
- "Use Gemini CLI for this task in a thread, then keep follow-ups in that same thread."
What OpenClaw should do:
1. Pick `runtime: "acp"`.
2. Resolve the requested harness target (`agentId`, for example `codex`).
3. If thread binding is requested and the current channel supports it, bind the ACP session to the thread.
4. Route follow-up thread messages to that same ACP session until unfocused/closed/expired.
3. If current-conversation binding is requested and the active channel supports it, bind the ACP session to that conversation.
4. Otherwise, if thread binding is requested and the current channel supports it, bind the ACP session to the thread.
5. Route follow-up bound messages to that same ACP session until unfocused/closed/expired.
## ACP versus sub-agents
@@ -62,7 +66,55 @@ Use ACP when you want an external harness runtime. Use sub-agents when you want
See also [Sub-agents](/tools/subagents).
## Thread-bound sessions (channel-agnostic)
## Bound sessions
### Current-conversation binds
Use `/acp spawn <harness> --bind here` when you want the current conversation to become a durable ACP workspace without creating a child thread.
Behavior:
- OpenClaw keeps owning the channel transport, auth, safety, and delivery.
- The current conversation is pinned to the spawned ACP session key.
- Follow-up messages in that conversation route to the same ACP session.
- `/new` and `/reset` reset the same bound ACP session in place.
- `/acp close` closes the session and removes the current-conversation binding.
What this means in practice:
- `--bind here` keeps the same chat surface. On Discord, the current channel stays the current channel.
- `--bind here` can still create a new ACP session if you are spawning fresh work. The bind attaches that session to the current conversation.
- `--bind here` does not create a child Discord thread or Telegram topic by itself.
- The ACP runtime can still have its own working directory (`cwd`) or backend-managed workspace on disk. That runtime workspace is separate from the chat surface and does not imply a new messaging thread.
Mental model:
- chat surface: where people keep talking (`Discord channel`, `Telegram topic`, `iMessage chat`)
- ACP session: the durable Codex/Claude/Gemini runtime state OpenClaw routes to
- child thread/topic: an optional extra messaging surface created only by `--thread ...`
- runtime workspace: the filesystem location where the harness runs (`cwd`, repo checkout, backend workspace)
Examples:
- `/acp spawn codex --bind here`: keep this chat, spawn or attach a Codex ACP session, and route future messages here to it
- `/acp spawn codex --thread auto`: OpenClaw may create a child thread/topic and bind the ACP session there
- `/acp spawn codex --bind here --cwd /workspace/repo`: same chat binding as above, but Codex runs in `/workspace/repo`
Built-in current-conversation binding support:
- Discord current channel or current thread
- Telegram current chat or current topic
- BlueBubbles DM or group chat
- iMessage DM or group chat
Notes:
- `--bind here` and `--thread ...` are mutually exclusive on `/acp spawn`.
- On Discord, `--bind here` binds the current channel or thread in place. `spawnAcpSessions` is only required when OpenClaw needs to create a child thread for `--thread auto|here`.
- If the active channel does not expose current-conversation ACP bindings, OpenClaw returns a clear unsupported message.
- `resume` and "new session" questions are ACP-session questions, not channel questions. You can reuse or replace runtime state without changing the current chat surface.
### Thread-bound sessions
When thread bindings are enabled for a channel adapter, ACP sessions can be bound to threads:
@@ -99,6 +151,10 @@ For non-ephemeral workflows, configure persistent ACP bindings in top-level `bin
- `bindings[].match` identifies the target conversation:
- Discord channel or thread: `match.channel="discord"` + `match.peer.id="<channelOrThreadId>"`
- Telegram forum topic: `match.channel="telegram"` + `match.peer.id="<chatId>:topic:<topicId>"`
- BlueBubbles DM/group chat: `match.channel="bluebubbles"` + `match.peer.id="<handle|chat_id:*|chat_guid:*|chat_identifier:*>"`
Prefer `chat_id:*` or `chat_identifier:*` for stable group bindings.
- iMessage DM/group chat: `match.channel="imessage"` + `match.peer.id="<handle|chat_id:*|chat_guid:*|chat_identifier:*>"`
Prefer `chat_id:*` for stable group bindings.
- `bindings[].agentId` is the owning OpenClaw agent id.
- Optional ACP overrides live under `bindings[].acp`:
- `mode` (`persistent` or `oneshot`)
@@ -333,12 +389,14 @@ Use `/acp spawn` for explicit operator control from chat when needed.
```text
/acp spawn codex --mode persistent --thread auto
/acp spawn codex --mode oneshot --thread off
/acp spawn codex --bind here
/acp spawn codex --thread here
```
Key flags:
- `--mode persistent|oneshot`
- `--bind here|off`
- `--thread auto|here|off`
- `--cwd <absolute-path>`
- `--label <name>`
@@ -358,8 +416,26 @@ Resolution order:
2. Current thread binding (if this conversation/thread is bound to an ACP session)
3. Current requester session fallback
Current-conversation bindings and thread bindings both participate in step 2.
If no target resolves, OpenClaw returns a clear error (`Unable to resolve session target: ...`).
## Spawn bind modes
`/acp spawn` supports `--bind here|off`.
| Mode | Behavior |
| ------ | ---------------------------------------------------------------------- |
| `here` | Bind the current active conversation in place; fail if none is active. |
| `off` | Do not create a current-conversation binding. |
Notes:
- `--bind here` is the simplest operator path for "make this channel or chat Codex-backed."
- `--bind here` does not create a child thread.
- `--bind here` is only available on adapters that expose current-conversation ACP bindings.
- `--bind` and `--thread` cannot be combined in the same `/acp spawn` call.
## Spawn thread modes
`/acp spawn` supports `--thread auto|here|off`.
@@ -376,6 +452,7 @@ Notes:
- Thread-bound spawn requires channel policy support:
- Discord: `channels.discord.threadBindings.spawnAcpSessions=true`
- Telegram: `channels.telegram.threadBindings.spawnAcpSessions=true`
- Use `--bind here` when you want to pin the current conversation without creating a child thread.
## ACP controls
@@ -403,23 +480,23 @@ Some controls depend on backend capabilities. If a backend does not support a co
## ACP command cookbook
| Command | What it does | Example |
| -------------------- | --------------------------------------------------------- | -------------------------------------------------------------- |
| `/acp spawn` | Create ACP session; optional thread bind. | `/acp spawn codex --mode persistent --thread auto --cwd /repo` |
| `/acp cancel` | Cancel in-flight turn for target session. | `/acp cancel agent:codex:acp:<uuid>` |
| `/acp steer` | Send steer instruction to running session. | `/acp steer --session support inbox prioritize failing tests` |
| `/acp close` | Close session and unbind thread targets. | `/acp close` |
| `/acp status` | Show backend, mode, state, runtime options, capabilities. | `/acp status` |
| `/acp set-mode` | Set runtime mode for target session. | `/acp set-mode plan` |
| `/acp set` | Generic runtime config option write. | `/acp set model openai/gpt-5.2` |
| `/acp cwd` | Set runtime working directory override. | `/acp cwd /Users/user/Projects/repo` |
| `/acp permissions` | Set approval policy profile. | `/acp permissions strict` |
| `/acp timeout` | Set runtime timeout (seconds). | `/acp timeout 120` |
| `/acp model` | Set runtime model override. | `/acp model anthropic/claude-opus-4-6` |
| `/acp reset-options` | Remove session runtime option overrides. | `/acp reset-options` |
| `/acp sessions` | List recent ACP sessions from store. | `/acp sessions` |
| `/acp doctor` | Backend health, capabilities, actionable fixes. | `/acp doctor` |
| `/acp install` | Print deterministic install and enable steps. | `/acp install` |
| Command | What it does | Example |
| -------------------- | --------------------------------------------------------- | ------------------------------------------------------------- |
| `/acp spawn` | Create ACP session; optional current bind or thread bind. | `/acp spawn codex --bind here --cwd /repo` |
| `/acp cancel` | Cancel in-flight turn for target session. | `/acp cancel agent:codex:acp:<uuid>` |
| `/acp steer` | Send steer instruction to running session. | `/acp steer --session support inbox prioritize failing tests` |
| `/acp close` | Close session and unbind thread targets. | `/acp close` |
| `/acp status` | Show backend, mode, state, runtime options, capabilities. | `/acp status` |
| `/acp set-mode` | Set runtime mode for target session. | `/acp set-mode plan` |
| `/acp set` | Generic runtime config option write. | `/acp set model openai/gpt-5.2` |
| `/acp cwd` | Set runtime working directory override. | `/acp cwd /Users/user/Projects/repo` |
| `/acp permissions` | Set approval policy profile. | `/acp permissions strict` |
| `/acp timeout` | Set runtime timeout (seconds). | `/acp timeout 120` |
| `/acp model` | Set runtime model override. | `/acp model anthropic/claude-opus-4-6` |
| `/acp reset-options` | Remove session runtime option overrides. | `/acp reset-options` |
| `/acp sessions` | List recent ACP sessions from store. | `/acp sessions` |
| `/acp doctor` | Backend health, capabilities, actionable fixes. | `/acp doctor` |
| `/acp install` | Print deterministic install and enable steps. | `/acp install` |
`/acp sessions` reads the store for the current bound or requester session. Commands that accept `session-key`, `session-id`, or `session-label` tokens resolve targets through gateway session discovery, including custom per-agent `session.store` roots.
@@ -527,6 +604,8 @@ If thread-bound ACP spawn does not work, verify the adapter feature flag first:
- Discord: `channels.discord.threadBindings.spawnAcpSessions=true`
Current-conversation binds do not require child-thread creation. They require an active conversation context and a channel adapter that exposes ACP conversation bindings.
See [Configuration Reference](/gateway/configuration-reference).
## Plugin setup for acpx backend
@@ -629,19 +708,21 @@ Restart the gateway after changing these values.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACP runtime backend is not configured` | Backend plugin missing or disabled. | Install and enable backend plugin, then run `/acp doctor`. |
| `ACP is disabled by policy (acp.enabled=false)` | ACP globally disabled. | Set `acp.enabled=true`. |
| `ACP dispatch is disabled by policy (acp.dispatch.enabled=false)` | Dispatch from normal thread messages disabled. | Set `acp.dispatch.enabled=true`. |
| `ACP agent "<id>" is not allowed by policy` | Agent not in allowlist. | Use allowed `agentId` or update `acp.allowedAgents`. |
| `Unable to resolve session target: ...` | Bad key/id/label token. | Run `/acp sessions`, copy exact key/label, retry. |
| `--thread here requires running /acp spawn inside an active ... thread` | `--thread here` used outside a thread context. | Move to target thread or use `--thread auto`/`off`. |
| `Only <user-id> can rebind this thread.` | Another user owns thread binding. | Rebind as owner or use a different thread. |
| `Thread bindings are unavailable for <channel>.` | Adapter lacks thread binding capability. | Use `--thread off` or move to supported adapter/channel. |
| `Sandboxed sessions cannot spawn ACP sessions ...` | ACP runtime is host-side; requester session is sandboxed. | Use `runtime="subagent"` from sandboxed sessions, or run ACP spawn from a non-sandboxed session. |
| `sessions_spawn sandbox="require" is unsupported for runtime="acp" ...` | `sandbox="require"` requested for ACP runtime. | Use `runtime="subagent"` for required sandboxing, or use ACP with `sandbox="inherit"` from a non-sandboxed session. |
| Missing ACP metadata for bound session | Stale/deleted ACP session metadata. | Recreate with `/acp spawn`, then rebind/focus thread. |
| `AcpRuntimeError: Permission prompt unavailable in non-interactive mode` | `permissionMode` blocks writes/exec in non-interactive ACP session. | Set `plugins.entries.acpx.config.permissionMode` to `approve-all` and restart gateway. See [Permission configuration](#permission-configuration). |
| ACP session fails early with little output | Permission prompts are blocked by `permissionMode`/`nonInteractivePermissions`. | Check gateway logs for `AcpRuntimeError`. For full permissions, set `permissionMode=approve-all`; for graceful degradation, set `nonInteractivePermissions=deny`. |
| ACP session stalls indefinitely after completing work | Harness process finished but ACP session did not report completion. | Monitor with `ps aux \| grep acpx`; kill stale processes manually. |
| Symptom | Likely cause | Fix |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACP runtime backend is not configured` | Backend plugin missing or disabled. | Install and enable backend plugin, then run `/acp doctor`. |
| `ACP is disabled by policy (acp.enabled=false)` | ACP globally disabled. | Set `acp.enabled=true`. |
| `ACP dispatch is disabled by policy (acp.dispatch.enabled=false)` | Dispatch from normal thread messages disabled. | Set `acp.dispatch.enabled=true`. |
| `ACP agent "<id>" is not allowed by policy` | Agent not in allowlist. | Use allowed `agentId` or update `acp.allowedAgents`. |
| `Unable to resolve session target: ...` | Bad key/id/label token. | Run `/acp sessions`, copy exact key/label, retry. |
| `--bind here requires running /acp spawn inside an active ... conversation` | `--bind here` used without an active bindable conversation. | Move to the target chat/channel and retry, or use unbound spawn. |
| `Conversation bindings are unavailable for <channel>.` | Adapter lacks current-conversation ACP binding capability. | Use `/acp spawn ... --thread ...` where supported, configure top-level `bindings[]`, or move to a supported channel. |
| `--thread here requires running /acp spawn inside an active ... thread` | `--thread here` used outside a thread context. | Move to target thread or use `--thread auto`/`off`. |
| `Only <user-id> can rebind this channel/conversation/thread.` | Another user owns the active binding target. | Rebind as owner or use a different conversation or thread. |
| `Thread bindings are unavailable for <channel>.` | Adapter lacks thread binding capability. | Use `--thread off` or move to supported adapter/channel. |
| `Sandboxed sessions cannot spawn ACP sessions ...` | ACP runtime is host-side; requester session is sandboxed. | Use `runtime="subagent"` from sandboxed sessions, or run ACP spawn from a non-sandboxed session. |
| `sessions_spawn sandbox="require" is unsupported for runtime="acp" ...` | `sandbox="require"` requested for ACP runtime. | Use `runtime="subagent"` for required sandboxing, or use ACP with `sandbox="inherit"` from a non-sandboxed session. |
| Missing ACP metadata for bound session | Stale/deleted ACP session metadata. | Recreate with `/acp spawn`, then rebind/focus thread. |
| `AcpRuntimeError: Permission prompt unavailable in non-interactive mode` | `permissionMode` blocks writes/exec in non-interactive ACP session. | Set `plugins.entries.acpx.config.permissionMode` to `approve-all` and restart gateway. See [Permission configuration](#permission-configuration). |
| ACP session fails early with little output | Permission prompts are blocked by `permissionMode`/`nonInteractivePermissions`. | Check gateway logs for `AcpRuntimeError`. For full permissions, set `permissionMode=approve-all`; for graceful degradation, set `nonInteractivePermissions=deny`. |
| ACP session stalls indefinitely after completing work | Harness process finished but ACP session did not report completion. | Monitor with `ps aux \| grep acpx`; kill stale processes manually. |