diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index b1ee7acc31cc..5d01a2786947 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -42a33b67a94f8dd3c5ffd61d439cb6128ea8347f9f98d0f71647898756952ee1 config-baseline.json -c8d9553463c613cd5e62eb3567add11fbf1733165aed22bdd2e947cd1a157ffe config-baseline.core.json -552f5ae69ac13628d754e796bace6e800242d09cacbe762593c17ef3693ba754 config-baseline.channel.json +061d4bc0ba8f8aad8ee1593521d65edc3e947b6ff7e4149c9783ce6ef806d448 config-baseline.json +c12619ab274c0ce34d858a705a6ef542a4c0f71c9d58977b7949bb452b62f9eb config-baseline.core.json +dd317647cf5ccf8d23774dcc7208d5ca749c08486137c6e5e6c8b7114592356f config-baseline.channel.json 4bcc2364924c80f38139f0508945b6d28b33b70dd2973f0685a8f221d672ac94 config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 79b43401faf0..bbb5a1e4fe54 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"f0a3282bbb9dd2ff22df32d5f86a27a488e6f7f6b007548d2026712b4e59a6c5","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"3197bbee115b315bb7ffaec2f495abba5bc9981a90751760da6c430be3ee5445","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index 6422474cc573..f33b64194546 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"60140db5b14eb3811a0398983df4729a0c4c2036f49a85622c1ed4a62b2e232a","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"bc2750304f9959144c04143e8f5b14ca56bae3f8ec1bdfb27d05ddbe77e01d53","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json index f6d820607f46..86df5f87e0e0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-runtime.json @@ -1 +1 @@ -{"contentHash":"a5fd8c9fbf19c5a9eb6dca252cd551616571ddf344cf8e2963135e4b418cf063","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"} +{"contentHash":"adc174fdd6ceeaa664ecf483d76aec22ecb111a7a8f6534cb78463b4347abb5e","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index 65f9f377cf7c..64f43a589af0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"e78d0b185d0a718fc12254b09e8432265614c8cdddae804a383fd0da3093968a","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"362c2fc4b9048470c4857d542f7593bb5a8a8484d4c2176f4c9e560af642db7e","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index ddceba0e8d03..c6e101d3c8ef 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"f1804d094895fffe929f32fbda11ddbef26be518eb310efff3b337cc069feb53","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"bf8fa1e5fbb43f0a5fc3b891b93a848684414d78cb88c8f75b73491e4e46b0c7","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json index b7c60e6c080d..84dc84757f9c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json @@ -1 +1 @@ -{"contentHash":"96daa5866ecf95c20d81bab20d27289a6bb97019e94899e33b431846ba537208","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} +{"contentHash":"4cd42b2807c10408d0067265697f27a18bce6a1de9a4988e1a2e879f87c74598","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index 6713a0ee44f9..e0e3b05b70d3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"9e96e620155dbc97b8cc74b1eeaf8987fc535d23e9f3c86450a9a75cc202964d","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"b3dc5322d5837138fa4b857d5ca0f18cef4c1bcebb10874bef30f42c91e04465","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index f778eec0d129..da36845c984a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"febc6854cb89d5c26debeec50008526d0a8d97738f727011c3942509bb538fcc","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"ad3ff6b5e1b3c11bf4e03599ec3724697869e5d342f5597c835ae1dd45b7feff","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index 53d1642409ae..9243511fe10e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"b3806b628ae37ac8242ab0f0a8387cd209c78ff9233486269e279677afda4c1b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"d2bfeec50ac2ee0abbf987a58b004820060f1d3d5634834e781d125da7e7a930","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-streaming.json b/docs/.generated/plugin-sdk-api-baseline/channel-streaming.json index fb136c246ffd..f8d490b91aaa 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-streaming.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-streaming.json @@ -1 +1 @@ -{"contentHash":"de23263a833b2d80eac0f7d7f41225ca0350a8918ae9b605d554a9400a9cfa2a","entrypoint":"channel-streaming","importSpecifier":"openclaw/plugin-sdk/channel-streaming"} +{"contentHash":"2e6f9a8e8a07de789f6673813b5acc17dd08a11420c9de1b3ac509d37786adc6","entrypoint":"channel-streaming","importSpecifier":"openclaw/plugin-sdk/channel-streaming"} diff --git a/docs/.generated/plugin-sdk-api-baseline/config-contracts.json b/docs/.generated/plugin-sdk-api-baseline/config-contracts.json index 1e35cca4f5ec..d00412999dd3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/config-contracts.json +++ b/docs/.generated/plugin-sdk-api-baseline/config-contracts.json @@ -1 +1 @@ -{"contentHash":"424adb55a599d61ed8a334b9df45398864aa105d47d06bcf2a59dd644e6c0188","entrypoint":"config-contracts","importSpecifier":"openclaw/plugin-sdk/config-contracts"} +{"contentHash":"02217a87f4bf0e081b406641519df90da15acd8aa430a9049f0c1584aabd2e3a","entrypoint":"config-contracts","importSpecifier":"openclaw/plugin-sdk/config-contracts"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 9bb152e18153..d30de5ad0c39 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"4ba3f7cf748c5ba26f22d626499c4bce05ef8e17f69bf864a87c1df3ab2dc5d4","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"d24c93b08546f841c3f60d03383214671a3bd8e479ff2c1aa3d7a934ef1e0d28","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 7b6cd8e8a977..c72db5a25f8c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"518f095d39488d9c6fcfff4cf68043bca7308e4e807020d718f6a05f139d1b6b","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"a910e8c530f0dc757c11a62e9b6898dffb5d0274de0311728badfdae657eb1bc","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index 2f0f84ec93cb..60f036854df5 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"8b35ab0994d669d6bb95dd64bcd1d2c721fb5716d8c997d048775ab7d9cd0153","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"4ba70d6eaf72efab607d4b2f9783df5bafdf21ab43a1f4a2947a48a829ed0337","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index 66692d9749a9..58d2af41d2a0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"3447ab3b4f9d00dd164e97954dbccb7cd53d2298aa5204c2017f390f15588437","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"db01fbcef6813a72f89535130a49036afdf998d88de3bb29e2e5d7c62d6c95eb","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 820d9db55bb7..4a48fd4bae0f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"6dea61641b09e01cdf88055074dceebfb16545da28b6dfefed6b1d85e66dbfeb","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"80ebabbad4a9bc7b8b6eacc806fe3d94d468f3650dee8b97e4cf2883d025d08f","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 23c5c3504bbf..a2cddc7a8164 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"c9e9acdc7f66b6020b738d7a26657a09edce6f21c5ab5acd65e0dac8a7c3bb6f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"7766933d97e904bf753203045d712d8423cd33f8e018aedb39b5cdc8ac0b8a5f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index de87e1b12933..45a9548396b7 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"a45abfd8f04f808a9b7bd2703b903be221b0bdb55fb3782f7e0e9d4ee71871a0","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"17b9e593a0f1aebd5e985c34cbfc6a6df8963f8e73b25cfb9ca756a0e0918b77","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index f51e190f6ab5..d29e6c73f8e9 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"dffc96ff312adfa28713a43b47d8ffc70e44c65be64c65e129d775a8fd77728b","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"86096091c7e63bac10baec86c07cf7d9256e1ba1433a5dbb60b86df81e53ca11","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 6de1ff4fea80..e51795802cb5 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"4a6a6b7e93f7169d0aed68b74121ff4676d120d4a88e1b06ced1f2434e030677","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"ee451a45f1687cede02fb776e87a630ed21fe485e1e5af1e36281161351d8278","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 41cee93e5d7b..7d7d14e2c717 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -1492,9 +1492,9 @@ The default scope (`"group-mentions"`) does not fire ack reactions in direct mes `channels.slack.streaming` controls live preview behavior: - `off`: disable live preview streaming. -- `partial` (default): replace preview text with the latest partial output. +- `partial`: replace preview text with the latest partial output. Set this to restore the previous default behavior. - `block`: append chunked preview updates. -- `progress`: show progress status text while generating, then send final text. +- `progress` (default): maintain one live Block Kit session card in the thread while work runs, finalize that card in place, and send the assistant's final text as a separate message. - `streaming.preview.toolProgress`: when draft preview is active, route tool/progress updates into the same edited preview message (default: `true`). Set `false` to keep separate tool/progress messages. - `streaming.preview.commandText` / `streaming.progress.commandText`: `status` keeps compact tool-progress lines while hiding raw command/exec text (default); set `raw` to opt into command text. @@ -1518,12 +1518,14 @@ Hide raw command/exec text while keeping compact progress lines: `channels.slack.streaming.nativeTransport` controls Slack native text streaming when `channels.slack.streaming.mode` is `partial` (default: `true`). -Slack native progress task cards are opt-in for progress mode. Set `channels.slack.streaming.progress.nativeTaskCards` to `true` with `channels.slack.streaming.mode="progress"` to send a Slack-native plan/task card while work is running, then update the same task card at completion. Without this flag, progress mode keeps the portable draft-preview behavior. +The default session card shows the current title, optional narration, plan checklist, recent activity, tool/file totals, and elapsed time. Completion changes the header to success or error while preserving the last plan and activity. When `gateway.publicOrigin` is configured, terminal cards include an **Open in OpenClaw** button linked to that session. If the Control UI is served below a path prefix, also set `gateway.controlUi.basePath`. + +Slack native progress task cards remain a separate opt-in path. Set `channels.slack.streaming.progress.nativeTaskCards` to `true` with `channels.slack.streaming.mode="progress"` to use Slack's native plan/task stream instead of the Block Kit session card. This setting is unchanged. - A reply thread must be available for native text streaming and Slack assistant thread status to appear. Thread selection still follows `replyToMode`. - Channel, group-chat, and top-level DM roots can still use the normal draft preview when native streaming is unavailable or no reply thread exists. - Top-level Slack DMs stay off-thread by default, so they do not show Slack's thread-style native stream/status preview; OpenClaw posts and edits a draft preview in the DM instead. -- Custom outbound username/icon settings keep portable previews enabled. OpenClaw keeps the preview app-authored so partial/block previews can be removed before a separately customized final; progress mode may instead collapse the app-authored draft into a receipt. Slack does not allow impersonated messages to be deleted. +- Custom outbound username/icon settings keep portable previews enabled. OpenClaw keeps the preview or session card app-authored and delivers the customized final separately. Slack does not allow impersonated messages to be deleted. - Media and non-text payloads fall back to normal delivery. - Media/error finals cancel pending preview edits; eligible text/block finals flush only when they can edit the preview in place. - If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads. @@ -1553,7 +1555,6 @@ Opt in to Slack native progress task cards: mode: "progress", progress: { nativeTaskCards: true, - render: "rich", }, }, }, diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index 8a13a15de2d3..938ece582020 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -183,14 +183,14 @@ instead of being overwritten in one editable draft. ### Channel mapping -Discord defaults to `off` when `streaming` is unset, Telegram defaults to -`progress`, and Slack, Mattermost, and MS Teams default to `partial`. +Discord defaults to `off` when `streaming` is unset, Telegram and Slack default +to `progress`, and Mattermost and MS Teams default to `partial`. | Channel | `off` | `partial` | `block` | `progress` | | ---------- | ------------- | --------- | ------- | --------------------------------- | | Telegram | Yes | Yes | Yes | editable progress draft (default) | | Discord | Yes (default) | Yes | Yes | editable progress draft (opt-in) | -| Slack | Yes | Yes | Yes | Yes | +| Slack | Yes | Yes | Yes | Block Kit session card (default) | | Mattermost | Yes | Yes | Yes | Yes | | MS Teams | Yes | Yes | Yes | native progress stream | @@ -203,7 +203,7 @@ Slack-only: - `channels.slack.streaming.nativeTransport` toggles Slack native streaming API calls (`chat.startStream`/`chat.appendStream`/`chat.stopStream`) when - `channels.slack.streaming.mode="partial"` (default: `true`). + `channels.slack.streaming.mode="partial"` (`nativeTransport` defaults to `true`). - Slack native streaming and Slack assistant thread status require a reply thread target. Top-level DMs do not show that thread-style preview, but can still use Slack draft preview posts and edits. @@ -270,14 +270,17 @@ Slack-only: - `partial` can use Slack native streaming (`chat.startStream`/`append`/`stop`) when available. - `block` uses append-style draft previews. -- `progress` uses status preview text, then the final answer. +- `progress` maintains one live Block Kit session card, finalizes it to success + or error, and always posts the assistant's final text as a separate message. +- Terminal cards include **Open in OpenClaw** when `gateway.publicOrigin` is set. + Slack's native plan/task stream remains opt-in through + `streaming.progress.nativeTaskCards: true`. - Top-level DMs without a reply thread use draft preview posts and edits instead of Slack native streaming. - Native and draft preview streaming suppress block replies for that turn, so a Slack reply is streamed by one delivery path only. -- Final media/error payloads and progress finals do not create throwaway draft - messages; only text/block finals that can edit the preview flush pending - draft text. +- A successful turn with no visible reply still deletes its draft card. A + failed no-reply turn retains the card in its error state. ### Mattermost @@ -341,7 +344,7 @@ Supported surfaces: `"status"` (the default). Set either option to `"raw"` to opt into command text. This policy is shared by draft/progress channels that use OpenClaw's compact progress renderer, including Discord, Matrix, - Microsoft Teams, Mattermost, Slack draft previews, and Telegram. To disable + Microsoft Teams, Mattermost, Slack session cards, and Telegram. To disable preview edits entirely, set `streaming.mode` to `off`. ## Progress draft rendering @@ -356,6 +359,9 @@ channel: | `streaming.progress.label` | `"auto"` | Draft title; a custom string, or `false` to hide it | | `streaming.progress.labels` | built-in pool | Candidate labels used when `label: "auto"` | +Slack always renders progress mode as its fixed session-card layout; these +limits still bound the activity rows and plan text inside that card. + ### Commentary progress lane Beyond tool-progress, the compact progress renderer can surface one more lane diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index e37dd9daf942..c2f3670707e2 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -722,6 +722,9 @@ it on other network interfaces according to macOS Sharing settings. loopback hosts (`localhost`, `127.0.0.1`, or `[::1]`) during local development. Per-requester MCP OAuth requires this value and uses `/oauth/mcp/callback` as its callback URL. + Slack session-card actions and plugin-generated viewer links also use this + origin. Set `gateway.controlUi.basePath` separately when the Control UI is + served below a reverse-proxy path prefix. - `bind`: `auto`, `loopback` (default), `lan` (`0.0.0.0`), `tailnet` (Tailscale IPv4 when available, otherwise loopback), or `custom` (one IPv4 address). A resolved `tailnet` address and any `custom` address other than `127.0.0.1` or `0.0.0.0` require `127.0.0.1` on the same port for same-host clients; startup fails if either listener cannot bind. Non-loopback exposure remains limited to the selected interface. - **Legacy bind aliases**: use bind mode values in `gateway.bind` (`auto`, `loopback`, `lan`, `tailnet`, `custom`), not host aliases (`0.0.0.0`, `127.0.0.1`, `localhost`, `::`, `::1`). - **Docker note**: the default `loopback` bind listens on `127.0.0.1` inside the container. With Docker bridge networking (`-p 18789:18789`), traffic arrives on `eth0`, so the gateway is unreachable. Use `--network host`, or set `bind: "lan"` (or `bind: "custom"` with `customBindHost: "0.0.0.0"`) to listen on all interfaces. diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 4384064b2f18..f70479a060a7 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -237,7 +237,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It | `plugin-sdk/tts-runtime` | Private-local after July 2026; Supported facade for text-to-speech config schemas and runtime helpers | | `plugin-sdk/gateway-method-runtime` | Reserved Gateway method dispatch helper for plugin HTTP routes that declare `contracts.gatewayMethodDispatch: ["authenticated-request"]` | | `plugin-sdk/gateway-runtime` | Gateway client, event-loop-ready client start helper, gateway CLI RPC, gateway protocol errors, advertised LAN host resolution, and channel-status patch helpers | - | `plugin-sdk/config-contracts` | Focused type-only config surface for plugin config shapes such as `OpenClawConfig` and channel/provider config types | + | `plugin-sdk/config-contracts` | Focused config surface for plugin config shapes such as `OpenClawConfig` and channel/provider config types, plus the dependency-light runtime helper `resolveGatewayPublicOrigin(cfg)` which returns the normalized `gateway.publicOrigin` (bare http(s) origin, optional reverse-proxy path, no query/hash) or `undefined` when unset, for building links back to the Gateway | | `plugin-sdk/plugin-config-runtime` | Deprecated compatibility facade for runtime plugin-config helpers; new plugins use `api.pluginConfig` plus focused config contracts, snapshots, and mutation helpers | | `plugin-sdk/config-mutation` | Transactional config mutation helpers such as `mutateConfigFile`, `replaceConfigFile`, and `logConfigUpdated` | | `plugin-sdk/message-tool-delivery-hints` | Private-local after July 2026; Shared message-tool delivery metadata hint strings | diff --git a/docs/tools/diffs.md b/docs/tools/diffs.md index 0296de9a0dc6..5f97f1e252bd 100644 --- a/docs/tools/diffs.md +++ b/docs/tools/diffs.md @@ -307,7 +307,7 @@ Viewer assets: The viewer document resolves these assets relative to the viewer URL, so an optional `baseUrl` path prefix carries through to asset requests too. -URL resolution order: tool-call `baseUrl` (after strict validation) -> plugin `viewerBaseUrl` -> loopback `127.0.0.1` default. If gateway bind mode is `custom` and `gateway.customBindHost` is set, that host is used instead of loopback. +URL resolution order: tool-call `baseUrl` (after strict validation) -> plugin `viewerBaseUrl` -> `gateway.publicOrigin` -> the existing bind-aware Gateway fallback. `baseUrl` rules: must be `http://` or `https://`; query and hash are rejected; origin plus optional base path is allowed. @@ -365,7 +365,7 @@ Common failure text: `Diff PNG/PDF rendering requires a Chromium-compatible brow - Viewer URL resolves to `127.0.0.1` by default. - - For remote access, either set plugin `viewerBaseUrl`, pass `baseUrl` per call, or use `gateway.bind=custom` with `gateway.customBindHost`. + - For remote access, set `gateway.publicOrigin`, set plugin `viewerBaseUrl`, or pass `baseUrl` per call. - If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw loopback viewer requests without forwarded client-IP headers fail closed by design. - For that proxy topology, prefer `mode: "file"`/`"both"` for an attachment, or intentionally enable `security.allowRemoteViewer` plus plugin `viewerBaseUrl`/a proxy `baseUrl` for a shareable viewer link. - Enable `security.allowRemoteViewer` only when external viewer access is intended. diff --git a/extensions/diffs/README.md b/extensions/diffs/README.md index 15fbfcd95047..264c4a55abe3 100644 --- a/extensions/diffs/README.md +++ b/extensions/diffs/README.md @@ -222,7 +222,7 @@ diff --git a/src/example.ts b/src/example.ts - The viewer is hosted locally through the gateway under `/plugins/diffs/...`. - Viewer HTML and metadata are ephemeral SQLite plugin blobs. The URL token is returned to the caller while SQLite stores only its SHA-256 hash. - Rendered PNG/PDF files remain temporary materializations in `$TMPDIR/openclaw-diffs` because delivery APIs require a file path. No JSON metadata sidecars are written or imported. -- Default viewer URLs use loopback (`127.0.0.1`) unless you set plugin `viewerBaseUrl`, pass `baseUrl`, or use `gateway.bind=custom` + `gateway.customBindHost`. +- Default viewer URLs use `gateway.publicOrigin` when configured, then the existing bind-aware Gateway fallback. Plugin `viewerBaseUrl` and per-call `baseUrl` take precedence. - If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw `127.0.0.1` viewer requests without forwarded client-IP headers fail closed by design. - In that topology, prefer `mode=file` / `mode=both` for attachments, or intentionally enable remote viewers and set plugin `viewerBaseUrl` (or pass a proxy/public `baseUrl`) when you need a shareable viewer URL. - Remote viewer misses are throttled to reduce token-guess abuse. diff --git a/extensions/diffs/src/config.test.ts b/extensions/diffs/src/config.test.ts index 4b58ee64e3c6..df2961dcc200 100644 --- a/extensions/diffs/src/config.test.ts +++ b/extensions/diffs/src/config.test.ts @@ -418,7 +418,28 @@ describe("diffs viewer URL helpers", () => { ).toBe("http://127.0.0.1:24444/plugins/diffs/view/id/token"); }); - it("uses custom bind host when provided", () => { + it("resolves explicit, plugin, public, then bind-aware viewer bases", () => { + expect( + buildViewerUrl({ + config: { gateway: { publicOrigin: "https://public.example.com" } }, + baseUrl: "https://explicit.example.com/review", + viewerBaseUrl: "https://plugin.example.com/viewer", + viewerPath: "/plugins/diffs/view/id/token", + }), + ).toBe("https://explicit.example.com/review/plugins/diffs/view/id/token"); + expect( + buildViewerUrl({ + config: { gateway: { publicOrigin: "https://public.example.com" } }, + viewerBaseUrl: "https://plugin.example.com/viewer", + viewerPath: "/plugins/diffs/view/id/token", + }), + ).toBe("https://plugin.example.com/viewer/plugins/diffs/view/id/token"); + expect( + buildViewerUrl({ + config: { gateway: { publicOrigin: "https://public.example.com" } }, + viewerPath: "/plugins/diffs/view/id/token", + }), + ).toBe("https://public.example.com/plugins/diffs/view/id/token"); expect( buildViewerUrl({ config: { diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index 65cee3c98aa0..cf6f68dd6563 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -240,7 +240,8 @@ export function createDiffsTool(params: { const viewerUrl = buildViewerUrl({ config: params.api.config, viewerPath: artifact.viewerPath, - baseUrl: normalizeBaseUrl(toolParams.baseUrl) ?? params.viewerBaseUrl, + baseUrl: normalizeBaseUrl(toolParams.baseUrl), + viewerBaseUrl: params.viewerBaseUrl, }); const baseDetails = { diff --git a/extensions/diffs/src/url.ts b/extensions/diffs/src/url.ts index c842ebb33c6e..6bf3ab7c1034 100644 --- a/extensions/diffs/src/url.ts +++ b/extensions/diffs/src/url.ts @@ -1,5 +1,8 @@ // Diffs plugin module implements url behavior. -import type { OpenClawConfig } from "../api.js"; +import { + resolveGatewayPublicOrigin, + type OpenClawConfig, +} from "openclaw/plugin-sdk/config-contracts"; const DEFAULT_GATEWAY_PORT = 18789; type ViewerBaseUrlFieldName = "baseUrl" | "viewerBaseUrl"; @@ -8,8 +11,13 @@ export function buildViewerUrl(params: { config: OpenClawConfig; viewerPath: string; baseUrl?: string; + viewerBaseUrl?: string; }): string { - const baseUrl = params.baseUrl?.trim() || resolveGatewayBaseUrl(params.config); + const baseUrl = + params.baseUrl?.trim() || + params.viewerBaseUrl?.trim() || + resolveGatewayPublicOrigin(params.config) || + resolveGatewayBaseUrl(params.config); const normalizedBase = normalizeViewerBaseUrl(baseUrl); const viewerPath = params.viewerPath.startsWith("/") ? params.viewerPath diff --git a/extensions/slack/src/__traces__/progress-session-card.trace.jsonl b/extensions/slack/src/__traces__/progress-session-card.trace.jsonl new file mode 100644 index 000000000000..743315f34999 --- /dev/null +++ b/extensions/slack/src/__traces__/progress-session-card.trace.jsonl @@ -0,0 +1,9 @@ +{"seq":1,"at":0,"dir":"in","kind":"reply-start"} +{"seq":2,"at":0,"dir":"out","kind":"assistant.threads.setStatus","data":{"payload":{"channel_id":"C0TRACE","status":"is typing...","thread_ts":"ts#1"},"result":{"ok":true},"target":"C0TRACE/ts#1"}} +{"seq":3,"at":0,"dir":"in","kind":"tool-progress","data":{"name":"read","phase":"start"}} +{"seq":4,"at":1500,"dir":"out","kind":"chat.postMessage","data":{"payload":{"blocks":[{"text":{"text":"๐Ÿ”„ *Working*","type":"mrkdwn"},"type":"section"},{"text":{"text":"๐Ÿ“– *Read* โ€” โ€”","type":"mrkdwn"},"type":"section"},{"elements":[{"text":"๐Ÿ› ๏ธ 1 tools ยท โฑ 2s","type":"mrkdwn"}],"type":"context"}],"channel":"C0TRACE","text":"Working\n\n๐Ÿ“– Read\n\n๐Ÿ”„ *Working*\n\n๐Ÿ“– *Read* โ€” โ€”\n\n๐Ÿ› ๏ธ 1 tools ยท โฑ 2s","thread_ts":"ts#1","unfurl_links":false},"result":{"ts":"ts#2"},"target":"C0TRACE"}} +{"seq":5,"at":2000,"dir":"in","kind":"final","data":{"text":"The session card is complete."}} +{"seq":6,"at":2000,"dir":"out","kind":"chat.postMessage","data":{"payload":{"channel":"C0TRACE","text":"The session card is complete.","thread_ts":"ts#1","unfurl_links":false},"result":{"ts":"ts#3"},"target":"C0TRACE"}} +{"seq":7,"at":2000,"dir":"out","kind":"chat.update","data":{"payload":{"blocks":[{"text":{"text":"โœ… *Working*","type":"mrkdwn"},"type":"section"},{"text":{"text":"๐Ÿ“– *Read* โ€” โ€”","type":"mrkdwn"},"type":"section"},{"elements":[{"text":"๐Ÿ› ๏ธ 1 tool call ยท โฑ๏ธ 2s","type":"mrkdwn"}],"type":"context"},{"elements":[{"action_id":"openclaw:session_link","text":{"text":"Open in OpenClaw","type":"plain_text"},"type":"button","url":"https://team.openclaw.ai/openclaw/chat/trace-agent/slack/channel/c0trace"}],"type":"actions"}],"channel":"C0TRACE","text":"Working\n\n๐Ÿ“– Read\n\nโœ… *Working*\n\n๐Ÿ“– *Read* โ€” โ€”\n\n๐Ÿ› ๏ธ 1 tool call ยท โฑ๏ธ 2s\n\nOpen in OpenClaw","ts":"ts#2"},"result":{"ok":true},"target":"ts#2"}} +{"seq":8,"at":2000,"dir":"in","kind":"idle"} +{"seq":9,"at":2000,"dir":"out","kind":"assistant.threads.setStatus","data":{"payload":{"channel_id":"C0TRACE","status":"","thread_ts":"ts#1"},"result":{"ok":true},"target":"C0TRACE/ts#1"}} diff --git a/extensions/slack/src/config-ui-hints.ts b/extensions/slack/src/config-ui-hints.ts index 038a0258fae2..4de7a997e32f 100644 --- a/extensions/slack/src/config-ui-hints.ts +++ b/extensions/slack/src/config-ui-hints.ts @@ -22,8 +22,8 @@ export const slackChannelConfigUiHints = { nativeCommands: true, implicitMentions: true, streaming: { - "": 'Unified Slack stream preview mode: "off" | "partial" | "block" | "progress". Legacy boolean/streamMode keys are auto-mapped.', - mode: 'Canonical Slack preview mode: "off" | "partial" | "block" | "progress".', + "": 'Unified Slack stream preview mode: "off" | "partial" | "block" | "progress" (default). Legacy boolean/streamMode keys are auto-mapped.', + mode: 'Canonical Slack preview mode: "off" | "partial" | "block" | "progress" (default).', chunkMode: 'Chunking mode for outbound Slack text delivery: "length" (default) or "newline".', "block.enabled": 'Enable chunked block-style Slack preview delivery when channels.slack.streaming.mode="block".', @@ -34,8 +34,6 @@ export const slackChannelConfigUiHints = { "Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active.", "preview.commandText": 'Command/exec detail in preview tool-progress lines: "status" is the safe default; "raw" opts into command text.', - "progress.render": - 'Progress draft renderer: "text" uses one portable text body; "rich" renders structured Slack Block Kit fields with the same text fallback.', "progress.nativeTaskCards": 'Opt in to Slack native task-card progress updates when channels.slack.streaming.mode="progress" and streaming.nativeTransport is enabled. Default: false.', }, diff --git a/extensions/slack/src/delivery-trace.test.ts b/extensions/slack/src/delivery-trace.test.ts index de6c38c73c8d..fbf576c0471d 100644 --- a/extensions/slack/src/delivery-trace.test.ts +++ b/extensions/slack/src/delivery-trace.test.ts @@ -164,7 +164,8 @@ type SlackTraceScenarioName = | "stream-stop-first-network-call" | "final-blocks-and-text" | "cancel-mid-stream" - | "preview-edit-fallback"; + | "preview-edit-fallback" + | "progress-session-card"; const NATIVE_SCENARIOS = new Set([ "streaming-happy-native", @@ -254,6 +255,13 @@ const slackTraceScenarios: Record { } function createPreparedTraceMessage(scenario: SlackTraceScenarioName): PreparedSlackMessage { - const cfg = { channels: { slack: { enabled: true } } } as OpenClawConfig; + const progressCard = scenario === "progress-session-card"; + const cfg = { + channels: { slack: { enabled: true } }, + ...(progressCard + ? { + gateway: { + publicOrigin: "https://team.openclaw.ai", + controlUi: { basePath: "/openclaw" }, + }, + } + : {}), + } as OpenClawConfig; const client = traceState.client; if (!client) { throw new Error("trace Slack client not initialized"); @@ -469,9 +488,14 @@ function createPreparedTraceMessage(scenario: SlackTraceScenarioName): PreparedS }, account: { accountId: "default", - config: { - streaming: { mode: "partial", nativeTransport: NATIVE_SCENARIOS.has(scenario) }, - }, + config: progressCard + ? {} + : { + streaming: { + mode: "partial", + nativeTransport: NATIVE_SCENARIOS.has(scenario), + }, + }, }, message: { type: "message", diff --git a/extensions/slack/src/draft-stream.test.ts b/extensions/slack/src/draft-stream.test.ts index 036e9ee8d428..39846441d838 100644 --- a/extensions/slack/src/draft-stream.test.ts +++ b/extensions/slack/src/draft-stream.test.ts @@ -197,6 +197,25 @@ describe("createSlackDraftStream", () => { expect(stream.messageId()).toBe("333.444"); }); + it("rearms updates after sealing and finalizing the previous message", async () => { + const send = vi + .fn() + .mockResolvedValueOnce(slackDraftSendResult("111.222")) + .mockResolvedValueOnce(slackDraftSendResult("333.444")); + const { stream } = createDraftStreamHarness({ send }); + + stream.update("first card"); + await stream.flush(); + await stream.seal(); + await expect(stream.finalizeMessage("111.222", async () => {})).resolves.toBe(true); + stream.forceNewMessage(); + stream.update("second card"); + await stream.flush(); + + expect(send).toHaveBeenCalledTimes(2); + expect(stream.messageId()).toBe("333.444"); + }); + it("continues below a human message that interrupts an in-progress Slack reply", async () => { const accountId = "interrupted-reply"; const send = vi diff --git a/extensions/slack/src/draft-stream.ts b/extensions/slack/src/draft-stream.ts index cd09f2ee9c21..b024278d1e73 100644 --- a/extensions/slack/src/draft-stream.ts +++ b/extensions/slack/src/draft-stream.ts @@ -196,6 +196,8 @@ export function createSlackDraftStream(params: { const forceNewMessage = () => { stopTrackingConversationBoundary(); + streamState.stopped = false; + streamState.final = false; streamMessageId = undefined; streamChannelId = undefined; lastVisibleUpdate = undefined; diff --git a/extensions/slack/src/monitor/events/interactions.block-actions.ts b/extensions/slack/src/monitor/events/interactions.block-actions.ts index e74e421d4ba1..68ae02f081d6 100644 --- a/extensions/slack/src/monitor/events/interactions.block-actions.ts +++ b/extensions/slack/src/monitor/events/interactions.block-actions.ts @@ -28,6 +28,7 @@ import { SLACK_REPLY_BUTTON_ACTION_ID, SLACK_REPLY_LINK_ACTION_ID, SLACK_REPLY_SELECT_ACTION_ID, + SLACK_SESSION_LINK_ACTION_ID, } from "../../reply-action-ids.js"; import { truncateSlackText } from "../../truncate.js"; import { @@ -387,6 +388,9 @@ function readSlackApprovalAction(parsed: ParsedSlackBlockAction): SlackApprovalA } function isSlackReplyLinkAction(parsed: ParsedSlackBlockAction): boolean { + if (parsed.actionId === SLACK_SESSION_LINK_ACTION_ID) { + return true; + } if ( parsed.actionId === SLACK_REPLY_LINK_ACTION_ID || parsed.actionId.startsWith(`${SLACK_REPLY_LINK_ACTION_ID}:`) diff --git a/extensions/slack/src/monitor/events/interactions.test.ts b/extensions/slack/src/monitor/events/interactions.test.ts index bb6a92aeb3db..bc15316744d4 100644 --- a/extensions/slack/src/monitor/events/interactions.test.ts +++ b/extensions/slack/src/monitor/events/interactions.test.ts @@ -2142,6 +2142,7 @@ describe("registerSlackInteractionEvents", () => { it.each([ { name: "current", actionId: "openclaw:reply_link:1:1", value: undefined }, + { name: "session", actionId: "openclaw:session_link", value: undefined }, { name: "legacy", actionId: "openclaw:reply_button:1:1", diff --git a/extensions/slack/src/monitor/message-handler/dispatch-progress-card.ts b/extensions/slack/src/monitor/message-handler/dispatch-progress-card.ts new file mode 100644 index 000000000000..ea88aa24e9f3 --- /dev/null +++ b/extensions/slack/src/monitor/message-handler/dispatch-progress-card.ts @@ -0,0 +1,179 @@ +import { + createChannelProgressReceiptTracker, + formatChannelProgressDraftText, + type ChannelProgressDraftCompositorSnapshot, +} from "openclaw/plugin-sdk/channel-outbound"; +import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts"; +import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { buildControlUiSessionPath } from "openclaw/plugin-sdk/session-discussion"; +import { createSlackDraftStream } from "../../draft-stream.js"; +import { formatSlackError } from "../../errors.js"; +import { buildSlackProgressCardBlocks } from "../../progress-blocks.js"; +import { escapeSlackMrkdwn } from "../mrkdwn.js"; +import { + combineProgressHeadlineAndExplanation, + resolveStructuredProgressLines, +} from "./dispatch-progress-render.js"; +import type { SlackDispatchSetup } from "./dispatch-setup.js"; +import { finalizeSlackPreviewEdit } from "./preview-finalize.js"; + +type DraftProgressCardState = "working" | "success" | "error"; + +export function createSlackDraftProgressCardRuntime(params: { + setup: Pick; + draftStream: ReturnType | undefined; + enabled: boolean; + progressReceipt: ReturnType; + progressSeed: string; + explicitTitle: string | undefined; + maxLineChars: number; + getSnapshot: () => ChannelProgressDraftCompositorSnapshot; + getThreadTs: () => string | undefined; +}) { + const { account, cfg, ctx, prepared, slackClient } = params.setup; + let latestFallbackText = ""; + let finalStatus: Exclude | undefined; + + const resolveSessionUrl = () => { + const publicOrigin = resolveGatewayPublicOrigin(cfg); + if (!publicOrigin) { + return undefined; + } + const url = new URL(publicOrigin); + const path = buildControlUiSessionPath({ + namespace: "chat", + sessionKey: prepared.route.sessionKey, + fallbackAgentId: prepared.route.agentId, + basePath: cfg.gateway?.controlUi?.basePath, + }); + if (!path) { + return undefined; + } + url.pathname = path; + return url.toString(); + }; + + const resolveText = (snapshot: ChannelProgressDraftCompositorSnapshot) => + latestFallbackText || + formatChannelProgressDraftText({ + entry: account.config, + lines: [...snapshot.lines], + seed: params.progressSeed, + formatLine: formatSlackProgressDraftLine, + narration: snapshot.statusHeadline, + plan: snapshot.plan, + }); + + const resolvePresentation = ( + snapshot: ChannelProgressDraftCompositorSnapshot, + state: DraftProgressCardState, + ) => { + const title = params.explicitTitle ?? snapshot.statusHeadline ?? "Working"; + const narration = params.explicitTitle + ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) + : snapshot.planExplanation && snapshot.planExplanation !== title + ? snapshot.planExplanation + : undefined; + return buildSlackProgressCardBlocks({ + state, + title, + narration, + plan: snapshot.plan, + lines: resolveStructuredProgressLines(snapshot.lines), + maxLineChars: params.maxLineChars, + diffStat: snapshot.diffStat, + ...(state === "working" + ? { + toolCalls: params.progressReceipt.toolCalls, + elapsedSeconds: params.progressReceipt.elapsedSeconds, + } + : { + receiptSummary: params.progressReceipt.buildSummaryLine(), + sessionUrl: resolveSessionUrl(), + }), + }); + }; + + const finalize = async ( + status: Exclude, + snapshot = params.getSnapshot(), + fallbackText = resolveText(snapshot), + ): Promise => { + if (!params.draftStream || !params.enabled) { + return false; + } + const terminalStatus = finalStatus === "error" || status === "error" ? "error" : "success"; + if (finalStatus === terminalStatus) { + return true; + } + await params.draftStream.flush(); + const channelId = params.draftStream.channelId(); + const messageId = params.draftStream.messageId(); + if (!channelId || !messageId) { + return false; + } + await params.draftStream.seal(); + try { + const finalized = await params.draftStream.finalizeMessage(messageId, async () => { + await finalizeSlackPreviewEdit({ + client: slackClient, + token: ctx.botToken, + accountId: account.accountId, + channelId, + messageId, + text: fallbackText, + blocks: resolvePresentation(snapshot, terminalStatus), + threadTs: params.getThreadTs(), + }); + }); + if (finalized) { + finalStatus = terminalStatus; + } + return finalized; + } catch (err) { + logVerbose(`slack: progress card final edit failed (${formatSlackError(err)})`); + return false; + } + }; + + return { + resolveText, + resolvePresentation, + finalize, + setFallbackText(text: string) { + latestFallbackText = text; + }, + reset() { + latestFallbackText = ""; + finalStatus = undefined; + }, + }; +} + +export function formatSlackProgressDraftLine(line: string): string { + if (/^(?:๐Ÿง |๐Ÿ’ฌ)\s/u.test(line)) { + return line; + } + + const italicCommentary = /^_(.*)_$/su.exec(line); + if (!italicCommentary) { + return escapeSlackMrkdwn(line); + } + + const content = italicCommentary[1]! + .split(/(`[^`\n]+`)/u) + .map((segment, index) => { + if (index % 2 === 0) { + return escapeSlackMrkdwn(segment); + } + const code = segment + .slice(1, -1) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + return `\`${code}\``; + }) + .join(""); + + return `_${content}_`; +} diff --git a/extensions/slack/src/monitor/message-handler/dispatch-progress.ts b/extensions/slack/src/monitor/message-handler/dispatch-progress.ts index fa623f1ce542..bf75c315bfa4 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch-progress.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch-progress.ts @@ -7,7 +7,6 @@ import { formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, resolveChannelProgressDraftMaxLineChars, - resolveChannelProgressDraftRender, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages, type ChannelProgressDraftCompositorSnapshot, @@ -18,7 +17,6 @@ import { createSlackDraftStream } from "../../draft-stream.js"; import { formatSlackError } from "../../errors.js"; import { SLACK_TEXT_LIMIT } from "../../limits.js"; import { - buildSlackProgressDraftBlocks, buildSlackProgressStreamCompletionChunks, buildSlackProgressStreamStartChunks, buildSlackProgressStreamUpdateChunks, @@ -32,18 +30,20 @@ import { stopSlackStream, type SlackStreamSession, } from "../../streaming.js"; -import { escapeSlackMrkdwn } from "../mrkdwn.js"; import { resolveExplicitSlackProgressTitle, resolveSlackStreamRecipientTeamId, } from "./dispatch-helpers.js"; +import { + createSlackDraftProgressCardRuntime, + formatSlackProgressDraftLine, +} from "./dispatch-progress-card.js"; import { collapseSlackProgressReceipt } from "./dispatch-progress-io.js"; import { buildNativeProgressChunks as buildRenderedNativeProgressChunks, combineProgressHeadlineAndExplanation, resolveNativeProgressLines, resolveNativeProgressPlan, - resolveStructuredProgressLines, } from "./dispatch-progress-render.js"; import type { SlackDispatchSetup } from "./dispatch-setup.js"; import type { SlackStreamingDeliveryRuntime } from "./dispatch-streaming.js"; @@ -107,6 +107,7 @@ export function createSlackProgressRuntime(runtimeParams: { const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, { draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming, + mode: slackStreaming.mode, previewToolProgressEnabled, previewStreamingEnabled, }); @@ -125,10 +126,20 @@ export function createSlackProgressRuntime(runtimeParams: { let progressReceiptCollapsed = false; let pendingNativeProgressReceipt: string | undefined; const progressSeed = `${account.accountId}:${message.channel}`; - const useRichProgressDraft = - streamMode === "status_final" && resolveChannelProgressDraftRender(account.config) === "rich"; + const useDraftProgressCard = Boolean(draftStream) && streamMode === "status_final"; const explicitProgressTitle = resolveExplicitSlackProgressTitle(account.config); const progressDraftMaxLineChars = resolveChannelProgressDraftMaxLineChars(account.config); + const progressCard = createSlackDraftProgressCardRuntime({ + setup: { account, cfg, ctx, prepared, slackClient }, + draftStream, + enabled: useDraftProgressCard, + progressReceipt, + progressSeed, + explicitTitle: explicitProgressTitle, + maxLineChars: progressDraftMaxLineChars, + getSnapshot: () => progressDraft.getSnapshot(), + getThreadTs: () => delivery.usedReplyThreadTs, + }); const waitForNativeProgressStreamStart = async (): Promise => { if (delivery.streamSession || !delivery.nativeProgressStreamStartPromise) { @@ -337,7 +348,7 @@ export function createSlackProgressRuntime(runtimeParams: { input.event === "tool" || input.event === "item" || input.event === "command-output" ? buildChannelProgressDraftLineForEntry(account.config, input, options) : buildChannelProgressDraftLine(input, options), - updateOnLineChange: useNativeProgressStreaming || useRichProgressDraft, + updateOnLineChange: useNativeProgressStreaming || useDraftProgressCard, update: async (previewText, options) => { if (useNativeProgressStreaming) { return await updateNativeProgressStream(); @@ -346,23 +357,13 @@ export function createSlackProgressRuntime(runtimeParams: { return false; } const snapshot = progressDraft.getSnapshot(); - const structuredLines = resolveStructuredProgressLines(options?.lines ?? snapshot.lines); - const richNarration = combineProgressHeadlineAndExplanation( - snapshot.statusHeadline, - snapshot.planExplanation, - ); - const richProgressBlocks = useRichProgressDraft - ? buildSlackProgressDraftBlocks({ - title: explicitProgressTitle, - lines: structuredLines, - plan: snapshot.plan, - narration: richNarration, - maxLineChars: progressDraftMaxLineChars, - }) - : undefined; + progressCard.setFallbackText(previewText); draftStream.update( - useRichProgressDraft && richProgressBlocks - ? { text: previewText, blocks: richProgressBlocks } + useDraftProgressCard + ? { + text: previewText, + blocks: progressCard.resolvePresentation(snapshot, "working"), + } : previewText, ); hasStreamedMessage = true; @@ -557,6 +558,8 @@ export function createSlackProgressRuntime(runtimeParams: { progressDraft.reset(); }; const beginNewProgressTurn = async (options?: { force?: boolean }) => { + const priorSnapshot = progressDraft.getSnapshot(); + const priorFallbackText = progressCard.resolveText(priorSnapshot); const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent ? buildNativeProgressCompletionChunks(nativeProgressTerminalStatus) @@ -569,6 +572,7 @@ export function createSlackProgressRuntime(runtimeParams: { if (useNativeProgressStreaming) { await finishNativeProgressTurn(completionChunks); } else { + await progressCard.finalize("success", priorSnapshot, priorFallbackText); draftStream?.forceNewMessage(); } resetProgressTurnState(); @@ -576,6 +580,7 @@ export function createSlackProgressRuntime(runtimeParams: { nativeProgressCompletionSent = false; nativeProgressTerminalStatus = "complete"; nativeProgressChunkKey = undefined; + progressCard.reset(); // A re-armed turn is a new visible reply: it must not dedupe against or // inherit delivery state from the settled turn (mirrors queued admission). resetPreviewDeliveryState(); @@ -619,6 +624,7 @@ export function createSlackProgressRuntime(runtimeParams: { return { draftStream, streamMode, + useDraftProgressCard, useNativeProgressStreaming, progressDraftActive, previewToolProgressEnabled, @@ -648,6 +654,7 @@ export function createSlackProgressRuntime(runtimeParams: { beginNewProgressTurn, buildNativeProgressCompletionChunks, collapseProgressReceipt, + finalizeDraftProgressCard: progressCard.finalize, onDraftBoundary, onQueuedFollowupAdmitted, pushPlanProgress, @@ -661,31 +668,3 @@ export function createSlackProgressRuntime(runtimeParams: { shouldYieldDraftProgress: () => shouldYieldDraftProgress(), }; } - -function formatSlackProgressDraftLine(line: string): string { - if (/^(?:๐Ÿง |๐Ÿ’ฌ)\s/u.test(line)) { - return line; - } - - const italicCommentary = /^_(.*)_$/su.exec(line); - if (!italicCommentary) { - return escapeSlackMrkdwn(line); - } - - const content = italicCommentary[1]! - .split(/(`[^`\n]+`)/u) - .map((segment, index) => { - if (index % 2 === 0) { - return escapeSlackMrkdwn(segment); - } - const code = segment - .slice(1, -1) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">"); - return `\`${code}\``; - }) - .join(""); - - return `_${content}_`; -} diff --git a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts index 115aff7a7355..bc53f8ff9ded 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts @@ -12,7 +12,7 @@ const createSlackDraftStreamMock = vi.fn(); const deliverRepliesMock = vi.fn( async () => undefined as { messageId?: string; channelId?: string } | undefined, ); -const finalizeSlackPreviewEditMock = vi.fn(async () => {}); +const finalizeSlackPreviewEditMock = vi.fn(async (_input: { blocks?: unknown }) => {}); const normalizeSlackOutboundTextMock = vi.fn((value: string) => value.trim()); const postMessageMock = vi.fn(async () => ({ ok: true, ts: "171234.999" })); const chatUpdateMock = vi.fn(async () => ({ ok: true, ts: "171234.999" })); @@ -93,6 +93,7 @@ let mockedDispatchSequence: TestDispatchSequenceEntry[] = []; let mockedQueuedDispatchCounts: TestDispatchCounts = { tool: 0, block: 0, final: 0 }; let mockedDispatcherCapturesDeliveryErrors = false; let mockedAgentRunTerminalOutcome: "completed" | "failed" | undefined; +let mockedDispatchError: Error | undefined; let mockedProgressEvents: string[] = []; let mockedEmptyProgressToolName: string | undefined; @@ -288,6 +289,22 @@ function createDraftStreamStub() { }; } +function draftUpdateTexts(draftStream: ReturnType): string[] { + return draftStream.update.mock.calls.map(([update]) => { + if (typeof update === "string") { + return update; + } + return requireRecord(update, "draft update").text as string; + }); +} + +function expectLastDraftUpdateText( + draftStream: ReturnType, + expected: string, +) { + expect(draftUpdateTexts(draftStream).at(-1)).toBe(expected); +} + function createPreparedSlackMessage(params?: { cfg?: Record; accountConfig?: Record; @@ -490,25 +507,8 @@ vi.mock("openclaw/plugin-sdk/channel-outbound", async (importOriginal) => { onModelSelected: undefined, }; }, - resolveChannelMessageSourceReplyDeliveryMode: (params: { - cfg?: { messages?: { groupChat?: { visibleReplies?: string } } }; - ctx?: { ChatType?: string; InboundEventKind?: string }; - requested?: "automatic" | "message_tool_only"; - }) => { - if (params.requested) { - return params.requested; - } - if (params.ctx?.InboundEventKind === "room_event") { - return "message_tool_only"; - } - const chatType = params.ctx?.ChatType; - if (chatType === "group" || chatType === "channel") { - return params.cfg?.messages?.groupChat?.visibleReplies === "automatic" - ? "automatic" - : "message_tool_only"; - } - return "automatic"; - }, + resolveChannelMessageSourceReplyDeliveryMode: + actual.resolveChannelMessageSourceReplyDeliveryMode, resolveAgentOutboundIdentity: () => undefined, buildChannelProgressDraftLine: (params: { event?: string; @@ -753,9 +753,6 @@ vi.mock("openclaw/plugin-sdk/channel-outbound", async (importOriginal) => { const previousText = typeof previous === "string" ? previous.trim() : previous?.text.trim(); return previousText === normalized ? lines : [...lines, line].slice(-params.maxLines); }, - resolveChannelProgressDraftRender: (entry?: { - streaming?: { progress?: { render?: "text" | "rich" } }; - }) => entry?.streaming?.progress?.render ?? "text", resolveChannelStreamingBlockEnabled: () => mockedBlockStreamingEnabled, resolveChannelStreamingNativeTransport: () => mockedNativeStreaming, resolveChannelStreamingPreviewToolProgress: (entry?: { @@ -1061,6 +1058,9 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { await params.replyOptions?.onItemEvent?.({ progressText }); } } + if (mockedDispatchError) { + throw mockedDispatchError; + } for (const entry of mockedDispatchSequence) { if (entry.kind === "queued_followup") { await params.replyOptions?.onQueuedFollowupAdmitted?.(); @@ -1153,6 +1153,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { mockedQueuedDispatchCounts = { tool: 0, block: 0, final: 0 }; mockedDispatcherCapturesDeliveryErrors = false; mockedAgentRunTerminalOutcome = undefined; + mockedDispatchError = undefined; mockedProgressEvents = []; mockedEmptyProgressToolName = undefined; mockedReplyOptionEvents = []; @@ -2065,7 +2066,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenCalledWith( + expect(draftUpdateTexts(draftStream)).toContain( "Shelling\n\nโ€ข ran <!here> <@U123> \\*bold\\* \\`code\\` & done", ); }); @@ -2089,10 +2090,11 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( + expectLastDraftUpdateText( + draftStream, ["Shelling", "", "โ€ข exec", "๐Ÿง  _Reading the Slack handler_"].join("\n"), ); - const updates = draftStream.update.mock.calls.map((call) => String(call[0])); + const updates = draftUpdateTexts(draftStream); expect(updates.join("\n")).not.toContain("Reasoning"); }); @@ -2118,10 +2120,11 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( + expectLastDraftUpdateText( + draftStream, ["Shelling", "", "โ€ข exec", "๐Ÿง  _Reading Checking_"].join("\n"), ); - const updates = draftStream.update.mock.calls.map((call) => String(call[0])); + const updates = draftUpdateTexts(draftStream); expect(updates.join("\n")).not.toContain("Checking Reading"); }); @@ -2145,10 +2148,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "๐Ÿง  _Reading Checking_"].join("\n"), - ); - const updates = draftStream.update.mock.calls.map((call) => String(call[0])); + expectLastDraftUpdateText(draftStream, ["Shelling", "", "๐Ÿง  _Reading Checking_"].join("\n")); + const updates = draftUpdateTexts(draftStream); expect(updates.join("\n")).toContain("Reading Checking"); }); @@ -2172,7 +2173,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( + expectLastDraftUpdateText( + draftStream, ["Shelling", "", "๐Ÿง  _Thinking about Slack preview state_"].join("\n"), ); }); @@ -2189,7 +2191,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( + expectLastDraftUpdateText( + draftStream, [ "โ€ข step 1", "โ€ข step 2", @@ -2223,12 +2226,10 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), - ); + expectLastDraftUpdateText(draftStream, ["Shelling", "", "โ€ข tool one", "โ€ข tool two"].join("\n")); }); - it("renders rich status-final progress drafts as legacy Slack section blocks and finalizes once", async () => { + it("renders and finalizes one Slack session card while delivering final text separately", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); @@ -2243,43 +2244,67 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await dispatchPreparedSlackMessage( createPreparedSlackMessage({ - accountConfig: { streaming: { progress: { label: "Shelling", render: "rich" } } }, + cfg: { + gateway: { + publicOrigin: "https://team.openclaw.ai", + controlUi: { basePath: "/openclaw" }, + }, + }, + accountConfig: { streaming: { progress: { label: "Shelling" } } }, }), ); - expect(draftStream.update).toHaveBeenLastCalledWith({ - text: ["Shelling", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), - blocks: [ - { - type: "section", - text: { type: "mrkdwn", text: "*Shelling*" }, - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - ], - }); - expectMockCallArgFields(finalizeSlackPreviewEditMock, 0, "preview edit params", { + expect(draftStream.update).toHaveBeenLastCalledWith( + expect.objectContaining({ + text: ["Shelling", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), + blocks: expect.arrayContaining([ + { type: "section", text: { type: "mrkdwn", text: "๐Ÿ”„ *Shelling*" } }, + ]), + }), + ); + expectMockCallArgFields(finalizeSlackPreviewEditMock, 0, "session card final edit", { channelId: "C123", messageId: "171234.567", - text: FINAL_REPLY_TEXT, }); - expect(deliverRepliesMock).not.toHaveBeenCalled(); + const finalEdit = requireRecord( + requireMockCall(finalizeSlackPreviewEditMock, 0, "session card final edit")[0], + "session card final edit", + ); + expect(JSON.stringify(finalEdit.blocks)).toContain("โœ… *Shelling*"); + expect(JSON.stringify(finalEdit.blocks)).toContain("Open in OpenClaw"); + expect(JSON.stringify(finalEdit.blocks)).toContain( + "https://team.openclaw.ai/openclaw/chat/agent-1/slack/C123", + ); + expect(deliverRepliesMock).toHaveBeenCalledTimes(1); + expectDeliverReplyCall(0, FINAL_REPLY_TEXT); expect(draftStream.clear).not.toHaveBeenCalled(); }); - it("keeps plan explanation in rich blocks with a fresh preamble", async () => { + it("clears the stale session card when the terminal edit fails after final delivery", async () => { + const draftStream = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + // Final reply lands, but terminalizing the card into its โœ… state fails. + finalizeSlackPreviewEditMock.mockRejectedValueOnce(new Error("card edit failed")); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedDispatchSequence = [{ kind: "final", payload: { text: FINAL_REPLY_TEXT } }]; + mockedReplyOptionEvents = [{ kind: "item", progressText: "working" }]; + + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { streaming: { mode: "progress", progress: { label: "Working" } } }, + }), + ); + + expect(deliverRepliesMock).toHaveBeenCalledTimes(1); + expectDeliverReplyCall(0, FINAL_REPLY_TEXT); + // A card left in its Working state would misrepresent a finished turn; the + // failed terminalization must drop it instead of leaving it stranded. + expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); + expect(draftStream.clear).toHaveBeenCalledTimes(1); + }); + + it("keeps plan explanation in the session card with a fresh preamble", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); mockedSlackStreamingMode = "progress"; @@ -2303,7 +2328,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await dispatchPreparedSlackMessage( createPreparedSlackMessage({ accountConfig: { - streaming: { mode: "progress", progress: { label: "Shelling", render: "rich" } }, + streaming: { mode: "progress", progress: { label: "Shelling" } }, }, }), ); @@ -2313,7 +2338,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { blocks: [ { type: "section", - text: { type: "mrkdwn", text: "*Shelling*" }, + text: { type: "mrkdwn", text: "๐Ÿ”„ *Shelling*" }, }, { type: "section", @@ -2326,11 +2351,15 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { type: "section", text: { type: "mrkdwn", text: "โ–ธ Patch" }, }, + { + type: "context", + elements: [{ type: "mrkdwn", text: "โฑ 1s" }], + }, ], }); }); - it("keeps unlabeled rich Slack progress drafts as legacy section blocks", async () => { + it("uses the default card title when no Slack progress label is configured", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); @@ -2345,38 +2374,25 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await dispatchPreparedSlackMessage( createPreparedSlackMessage({ - accountConfig: { streaming: { progress: { render: "rich" } } }, + accountConfig: { streaming: { mode: "progress" } }, }), ); - expect(draftStream.update).toHaveBeenLastCalledWith({ - text: ["Working", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), - blocks: [ - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - ], - }); + expect(draftStream.update).toHaveBeenLastCalledWith( + expect.objectContaining({ + blocks: expect.arrayContaining([ + { type: "section", text: { type: "mrkdwn", text: "๐Ÿ”„ *Working*" } }, + ]), + }), + ); expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); - expect(deliverRepliesMock).not.toHaveBeenCalled(); + expect(deliverRepliesMock).toHaveBeenCalledTimes(1); }); - it("replaces the progress draft with the final answer without posting a receipt", async () => { + it("delivers the final answer separately from the progress draft", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); - mockedReplyThreadTsSequence = [undefined]; mockedSlackStreamingMode = "progress"; mockedSlackDraftMode = "status_final"; mockedDispatchSequence = [{ kind: "final", payload: { text: FINAL_REPLY_TEXT } }]; @@ -2396,12 +2412,18 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(deliverRepliesMock).not.toHaveBeenCalled(); + expect(deliverRepliesMock).toHaveBeenCalledTimes(1); + expectDeliverReplyCall(0, FINAL_REPLY_TEXT); expectMockCallArgFields(finalizeSlackPreviewEditMock, 0, "progress final edit", { channelId: "C123", messageId: "171234.567", - text: FINAL_REPLY_TEXT, }); + const finalEdit = requireRecord( + requireMockCall(finalizeSlackPreviewEditMock, 0, "progress final edit")[0], + "progress final edit", + ); + expect(finalEdit.text).not.toBe(FINAL_REPLY_TEXT); + expect(JSON.stringify(finalEdit.blocks)).not.toContain("Open in OpenClaw"); expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); expect(draftStream.clear).not.toHaveBeenCalled(); }); @@ -2414,6 +2436,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { async ({ finalText }) => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); mockedSlackStreamingMode = "progress"; mockedSlackDraftMode = "status_final"; mockedDispatchSequence = [{ kind: "final", payload: { text: finalText } }]; @@ -2437,14 +2460,14 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(finalizeSlackPreviewEditMock).not.toHaveBeenCalled(); + expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); expect(deliverRepliesMock).toHaveBeenCalledTimes(1); expectDeliverReplyCall(0, finalText); - expect(draftStream.clear).toHaveBeenCalledTimes(1); + expect(draftStream.clear).not.toHaveBeenCalled(); }, ); - it("retains the progress draft when both the final edit and fallback send fail", async () => { + it("terminalizes the progress card as failed when final delivery fails", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); deliverRepliesMock.mockRejectedValueOnce(new Error("final send failed")); @@ -2464,14 +2487,19 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(draftStream.update).toHaveBeenCalled(); expectMockCallArgFields(finalizeSlackPreviewEditMock, 0, "progress final edit", { messageId: "171234.567", - text: FINAL_REPLY_TEXT, }); + const finalEdit = requireRecord( + requireMockCall(finalizeSlackPreviewEditMock, 0, "failed progress card edit")[0], + "failed progress card edit", + ); + expect(JSON.stringify(finalEdit.blocks)).toContain("โŒ *Working*"); expect(draftStream.clear).not.toHaveBeenCalled(); }); - it("replaces a progress draft with an error final without creating a receipt", async () => { + it("keeps and terminalizes the progress card when the final reply is an error", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); mockedSlackStreamingMode = "progress"; mockedSlackDraftMode = "status_final"; mockedDispatchSequence = [{ kind: "final", payload: { text: "tool failed", isError: true } }]; @@ -2484,8 +2512,70 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(deliverRepliesMock).toHaveBeenCalledTimes(1); + expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); + const finalEdit = requireRecord( + requireMockCall(finalizeSlackPreviewEditMock, 0, "error session card edit")[0], + "error session card edit", + ); + expect(JSON.stringify(finalEdit.blocks)).toContain("โŒ *Working*"); + expect(draftStream.clear).not.toHaveBeenCalled(); + }); + + it("terminalizes the progress card on a dispatch error", async () => { + const draftStream = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedDispatchSequence = []; + mockedReplyOptionEvents = [{ kind: "item", progressText: "working" }]; + mockedDispatchError = new Error("agent dispatch failed"); + + await expect( + dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { streaming: { mode: "progress", progress: { label: "Working" } } }, + }), + ), + ).rejects.toThrow("agent dispatch failed"); + + const finalEdit = requireRecord( + requireMockCall(finalizeSlackPreviewEditMock, 0, "dispatch error card edit")[0], + "dispatch error card edit", + ); + expect(JSON.stringify(finalEdit.blocks)).toContain("โŒ *Working*"); + expect(draftStream.clear).not.toHaveBeenCalled(); + }); + + it("keeps a failed no-reply card but deletes a silent successful card", async () => { + const failedDraft = createDraftStreamStub(); + const silentDraft = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(failedDraft).mockReturnValueOnce(silentDraft); + finalizeSlackPreviewEditMock.mockResolvedValue(undefined); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedDispatchSequence = []; + mockedReplyOptionEvents = [{ kind: "item", progressText: "working" }]; + mockedAgentRunTerminalOutcome = "failed"; + + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { streaming: { mode: "progress", progress: { label: "Working" } } }, + }), + ); + expect(failedDraft.clear).not.toHaveBeenCalled(); + expect(JSON.stringify(finalizeSlackPreviewEditMock.mock.calls[0]?.[0]?.blocks)).toContain( + "โŒ *Working*", + ); + + finalizeSlackPreviewEditMock.mockClear(); + mockedAgentRunTerminalOutcome = "completed"; + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { streaming: { mode: "progress", progress: { label: "Working" } } }, + }), + ); + expect(silentDraft.clear).toHaveBeenCalledTimes(1); expect(finalizeSlackPreviewEditMock).not.toHaveBeenCalled(); - expect(draftStream.clear).toHaveBeenCalledTimes(1); }); it("mandatory E2E: streams native Slack progress with the newest meaningful plan title when no explicit label exists", async () => { @@ -3676,54 +3766,6 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ]); }); - it("preserves the last rich Slack progress lines after a draft boundary status update", async () => { - const draftStream = createDraftStreamStub(); - createSlackDraftStreamMock.mockReturnValueOnce(draftStream); - finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); - mockedSlackStreamingMode = "progress"; - mockedSlackDraftMode = "status_final"; - mockedDispatchSequence = [{ kind: "final", payload: { text: FINAL_REPLY_TEXT } }]; - mockedReplyOptionEvents = [ - { kind: "item", progressText: "tool one" }, - { kind: "item", progressText: "tool two" }, - { kind: "assistant_start" }, - { kind: "partial", text: "partial answer" }, - ]; - - await dispatchPreparedSlackMessage( - createPreparedSlackMessage({ - accountConfig: { streaming: { progress: { label: "Shelling", render: "rich" } } }, - }), - ); - - expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); - expect(draftStream.update).toHaveBeenLastCalledWith({ - text: ["Shelling", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), - blocks: [ - { - type: "section", - text: { type: "mrkdwn", text: "*Shelling*" }, - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - ], - }); - expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); - expect(deliverRepliesMock).not.toHaveBeenCalled(); - }); - it("preserves text Slack progress lines after a draft boundary status update", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); @@ -3744,9 +3786,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); - expect(draftStream.update).toHaveBeenLastCalledWith( - ["Working", "", "โ€ข tool one", "โ€ข tool two"].join("\n"), - ); + expectLastDraftUpdateText(draftStream, ["Working", "", "โ€ข tool one", "โ€ข tool two"].join("\n")); }); it("re-arms an isolated progress draft on an assistant boundary after final delivery", async () => { @@ -3766,7 +3806,10 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await requireCapturedItemEventHandler()({ progressText: "second turn" }); expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1); - expect(draftStream.update).toHaveBeenLastCalledWith("Working\n\nโ€ข second turn"); + expect(finalizeSlackPreviewEditMock.mock.invocationCallOrder.at(-1)).toBeLessThan( + draftStream.forceNewMessage.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, + ); + expectLastDraftUpdateText(draftStream, "Working\n\nโ€ข second turn"); }); it("re-arms an isolated progress draft when a queued followup is admitted", async () => { @@ -3786,7 +3829,36 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await requireCapturedItemEventHandler()({ progressText: "queued turn" }); expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1); - expect(draftStream.update).toHaveBeenLastCalledWith("Working\n\nโ€ข queued turn"); + expect(finalizeSlackPreviewEditMock.mock.invocationCallOrder.at(-1)).toBeLessThan( + draftStream.forceNewMessage.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, + ); + expectLastDraftUpdateText(draftStream, "Working\n\nโ€ข queued turn"); + }); + + it("finalizes a queued turn card before rotating to the admitted followup", async () => { + const draftStream = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + finalizeSlackPreviewEditMock.mockResolvedValue(undefined); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedReplyOptionEvents = [{ kind: "item", progressText: "first turn" }]; + mockedDispatchSequence = [ + { kind: "queued_followup" }, + { kind: "item", progressText: "queued turn" }, + { kind: "final", payload: { text: "queued answer" } }, + ]; + + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { streaming: { mode: "progress", progress: { label: "Working" } } }, + }), + ); + + expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(2); + expect(finalizeSlackPreviewEditMock.mock.invocationCallOrder[0]).toBeLessThan( + draftStream.forceNewMessage.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, + ); + expectDeliverReplyCall(0, "queued answer"); }); it("re-arms queued progress after a silent turn without a final delivery", async () => { @@ -3806,7 +3878,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { await requireCapturedItemEventHandler()({ progressText: "queued turn" }); expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1); - expect(draftStream.update).toHaveBeenLastCalledWith("Working\n\nโ€ข queued turn"); + expectLastDraftUpdateText(draftStream, "Working\n\nโ€ข queued turn"); }); it("forces a new draft message on assistant boundaries in partial mode", async () => { @@ -3865,8 +3937,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenCalledWith("Shelling\n\n๐Ÿ› ๏ธ Exec\nโ€ข done"); - expect(draftStream.update.mock.calls.flat().join("\n")).not.toContain("pnpm test"); + expect(draftUpdateTexts(draftStream)).toContain("Shelling\n\n๐Ÿ› ๏ธ Exec\nโ€ข done"); + expect(draftUpdateTexts(draftStream).join("\n")).not.toContain("pnpm test"); }); it("preserves command output text when raw Slack progress is configured", async () => { @@ -3896,7 +3968,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update.mock.calls.flat().join("\n")).toContain("pnpm test -- --watch=false"); + expect(draftUpdateTexts(draftStream).join("\n")).toContain("pnpm test -- --watch=false"); }); it("suppresses standalone Slack tool progress when progress lines are disabled", async () => { @@ -3955,8 +4027,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(capturedReplyOptions?.commentaryProgressEnabled).toBe(true); expect(capturedReplyOptions?.suppressDefaultToolProgressMessages).toBe(true); - expect(draftStream.update).toHaveBeenLastCalledWith("_Preparing the smallest fix_"); - expect(draftStream.update.mock.calls.flat().join("\n")).not.toContain("pnpm test"); + expectLastDraftUpdateText(draftStream, "_Preparing the smallest fix_"); + expect(draftUpdateTexts(draftStream).join("\n")).not.toContain("pnpm test"); const updateCount = draftStream.update.mock.calls.length; capturedReplyOptions?.onVerboseProgressVisibility?.(() => true); @@ -3994,9 +4066,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( - "_Iโ€™m using the `monorepo` skill on Linux x86\\_64._", - ); + expectLastDraftUpdateText(draftStream, "_Iโ€™m using the `monorepo` skill on Linux x86\\_64._"); }); it("escapes Slack mentions and formatting in commentary without losing outer italics or inline code", async () => { @@ -4026,12 +4096,13 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenLastCalledWith( + expectLastDraftUpdateText( + draftStream, "_checking <@U123> in <#C123> and <!channel> with \\*urgent\\* \\_context\\_ `src/one.ts`_", ); }); - it("keeps the full latest preamble and turns the same Slack message into the final answer", async () => { + it("keeps the full latest preamble in the card and posts the final answer separately", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); @@ -4073,16 +4144,16 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - expect(draftStream.update).toHaveBeenCalledWith(`_${firstPreamble}_`); - expect(draftStream.update).toHaveBeenLastCalledWith(`_${latestPreamble}_`); + expect(draftUpdateTexts(draftStream)).toContain(`_${firstPreamble}_`); + expectLastDraftUpdateText(draftStream, `_${latestPreamble}_`); expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); expectMockCallArgFields(finalizeSlackPreviewEditMock, 0, "progress final edit", { channelId: "C123", messageId: "171234.567", - text: FINAL_REPLY_TEXT, }); - expect(deliverRepliesMock).not.toHaveBeenCalled(); - expect(draftStream.update.mock.calls.flat().join("\n")).not.toMatch(/Working|๐Ÿ’ฌ|โ€ข|โฑ๏ธ/u); + expect(deliverRepliesMock).toHaveBeenCalledTimes(1); + expectDeliverReplyCall(0, FINAL_REPLY_TEXT); + expect(draftUpdateTexts(draftStream).join("\n")).not.toMatch(/Working|๐Ÿ’ฌ|โ€ข|โฑ๏ธ/u); }); it("uses the enterprise event client for Slack commentary drafts", async () => { @@ -4128,7 +4199,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(createSlackDraftStreamMock).toHaveBeenCalledWith( expect.objectContaining({ eventScope }), ); - expect(draftStream.update).toHaveBeenLastCalledWith("_Using the scoped listener client_"); + expectLastDraftUpdateText(draftStream, "_Using the scoped listener client_"); }); it("renders the latest Slack preamble as the status headline by default", async () => { @@ -4170,9 +4241,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(capturedReplyOptions?.commentaryProgressEnabled).toBeUndefined(); expect(capturedReplyOptions?.onVerboseProgressVisibility).toBeUndefined(); expect(capturedReplyOptions?.progressPreambleEnabled).toBe(true); - expect(draftStream.update).toHaveBeenLastCalledWith( - "Keeping the released behavior\n\nโ€ข pnpm test", - ); + expectLastDraftUpdateText(draftStream, "Keeping the released behavior\n\nโ€ข pnpm test"); }); it("preserves Slack preamble previews outside progress mode", async () => { @@ -4257,7 +4326,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { }), ); - const updates = draftStream.update.mock.calls.flat().join("\n"); + const updates = draftUpdateTexts(draftStream).join("\n"); expect(capturedReplyOptions?.commentaryProgressEnabled).toBeUndefined(); expect(updates).toContain("pnpm test"); expect(updates).toContain("Hidden commentary"); @@ -4287,48 +4356,6 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(draftStream.update).not.toHaveBeenCalled(); }); - it("preserves hidden-title rich Slack progress drafts when the label is hidden", async () => { - const draftStream = createDraftStreamStub(); - createSlackDraftStreamMock.mockReturnValueOnce(draftStream); - finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined); - mockedSlackStreamingMode = "progress"; - mockedSlackDraftMode = "status_final"; - mockedDispatchSequence = [{ kind: "final", payload: { text: FINAL_REPLY_TEXT } }]; - mockedReplyOptionEvents = [ - { kind: "item", progressText: "tool one" }, - { kind: "partial", text: "partial answer" }, - { kind: "item", progressText: "tool two" }, - ]; - - await dispatchPreparedSlackMessage( - createPreparedSlackMessage({ - accountConfig: { streaming: { progress: { label: false, render: "rich" } } }, - }), - ); - - expect(draftStream.update).toHaveBeenLastCalledWith({ - text: ["โ€ข tool one", "โ€ข tool two"].join("\n"), - blocks: [ - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - { - type: "section", - fields: [ - { type: "mrkdwn", text: "โ€ข *Update*" }, - { type: "mrkdwn", text: "โ€”" }, - ], - }, - ], - }); - expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1); - expect(deliverRepliesMock).not.toHaveBeenCalled(); - }); - it("suppresses standalone Slack tool progress when partial preview lines are disabled", async () => { mockedSlackStreamingMode = "partial"; mockedSlackDraftMode = "replace"; diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 513a98b1f6ca..7335a9329087 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -111,6 +111,24 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag } return; } + if (progress.useDraftProgressCard) { + await delivery.deliverNormally({ + payload, + kind: info.kind, + forcedThreadTs: delivery.usedReplyThreadTs, + }); + const finalized = await progress.finalizeDraftProgressCard( + payload.isError === true ? "error" : "success", + ); + // The final reply already landed separately. A card that could not be + // terminalized would linger in its Working state and misrepresent an + // in-progress turn, so drop it (mirrors the pre-card preview cleanup). + if (!finalized) { + await draftStream?.clear(); + } + progress.progressDraft.markFinalReplyDelivered(); + return; + } } if (progress.useNativeProgressStreaming) { await delivery.deliverNormally({ @@ -498,7 +516,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag dispatchError = err; } finally { progress.progressDraft.cancel(); - await draftStream?.discardPending(); + if (!progress.useDraftProgressCard) { + await draftStream?.discardPending(); + } } // ----------------------------------------------------------------------- @@ -569,6 +589,10 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }, ); + if (dispatchError || agentRunFailed) { + await progress.finalizeDraftProgressCard("error"); + } + if (statusReactionsEnabled) { if (dispatchError || agentRunFailed) { await statusReactions.setError(); @@ -596,7 +620,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag if (dispatchError) { throw toErrorObject(dispatchError, "Slack dispatch failed"); } - if (!anyReplyDelivered && !draftPreviewCommitted.value) { + if ( + !anyReplyDelivered && + !draftPreviewCommitted.value && + !(agentRunFailed && progress.useDraftProgressCard) + ) { await draftStream?.clear(); return; } diff --git a/extensions/slack/src/progress-blocks.test.ts b/extensions/slack/src/progress-blocks.test.ts index 02f9e82b0770..a0ddb8c585af 100644 --- a/extensions/slack/src/progress-blocks.test.ts +++ b/extensions/slack/src/progress-blocks.test.ts @@ -1,8 +1,7 @@ -import { formatChannelProgressDraftText } from "openclaw/plugin-sdk/channel-outbound"; // Slack tests cover progress blocks plugin behavior. import { describe, expect, it } from "vitest"; import { - buildSlackProgressDraftBlocks, + buildSlackProgressCardBlocks, buildSlackProgressStreamCompletionChunks, buildSlackProgressStreamStartChunks, buildSlackProgressStreamUpdateChunks, @@ -50,27 +49,6 @@ function contentTaskId(prefix: string) { return expect.stringMatching(new RegExp(`^${prefix}_[a-f0-9]{8}_1$`, "u")); } -function legacyHeadingBlock(text: string) { - return { - type: "section", - text: { type: "mrkdwn", text }, - }; -} - -function legacyLineBlock(title: string, detail: string) { - return { - type: "section", - fields: [ - { type: "mrkdwn", text: title }, - { type: "mrkdwn", text: detail }, - ], - }; -} - -function expectLegacyLineBlock(block: unknown, title: string, detail: string) { - expect(block).toEqual(legacyLineBlock(title, detail)); -} - function expectTaskUpdate(task: unknown, fields: { id: unknown; title: string; status: string }) { expect(task).toEqual({ type: "task_update", @@ -80,158 +58,96 @@ function expectTaskUpdate(task: unknown, fields: { id: unknown; title: string; s }); } -describe("buildSlackProgressDraftBlocks", () => { - it("keeps a typed checklist below Slack status draft text and work lines", () => { - expect( - formatChannelProgressDraftText({ - entry: { streaming: { mode: "progress", progress: { label: "Shelling" } } }, - lines: [toolLine("read the config")], - narration: "Implementing the change.", - plan: [ - { step: "Inspect", status: "completed" }, - { step: "Patch", status: "in_progress" }, - { step: "Test", status: "pending" }, - ], - }), - ).toBe( - "Shelling\n\nImplementing the change.\n\n๐Ÿ› ๏ธ read the config\nโœ… Inspect\nโ–ธ Patch\nโ–ข Test", - ); - }); - - it("keeps legacy rich draft rendering as section field blocks", () => { - expect( - buildSlackProgressDraftBlocks({ - label: "Shelling...", - lines: [toolLine("run tests")], - }), - ).toEqual([legacyHeadingBlock("*Shelling...*"), legacyLineBlock("๐Ÿ› ๏ธ *Exec*", "run tests")]); - }); - - it("uses title as the legacy rich draft heading when label is absent", () => { - expect( - buildSlackProgressDraftBlocks({ - title: "Shelling...", - lines: [toolLine("run tests")], - }), - ).toEqual([legacyHeadingBlock("*Shelling...*"), legacyLineBlock("๐Ÿ› ๏ธ *Exec*", "run tests")]); - }); - - it("uses configured max line chars for legacy rich draft details", () => { - const blocks = buildSlackProgressDraftBlocks({ - title: "Shelling...", - maxLineChars: 64, - lines: [ - { - kind: "tool", - icon: "๐Ÿ› ๏ธ", - label: "Exec", - detail: "run tests in /Users/example/Projects/openclaw/packages/very/deep/path/example", - text: "๐Ÿ› ๏ธ Exec: run tests in /Users/example/Projects/openclaw/packages/very/deep/path/example", - }, +describe("buildSlackProgressCardBlocks", () => { + it("renders the working card with narration, plan, one activity block, and live footer", () => { + const blocks = buildSlackProgressCardBlocks({ + state: "working", + title: "Implementing", + narration: "Checking the workspace.", + plan: [ + { step: "Inspect", status: "completed" }, + { step: "Patch", status: "in_progress" }, ], + lines: [toolLine("run tests"), itemLine("prepare the workspace", "Preamble")], + toolCalls: 3, + elapsedSeconds: 12, + diffStat: { files: 4, added: 2, removed: 1 }, }); - expectLegacyLineBlock( - blocks?.[1], - "๐Ÿ› ๏ธ *Exec*", - "run tests in /Users/example/Pโ€ฆaw/packages/very/deep/path/example", - ); - }); - - it("keeps completed and failed statuses in legacy rich draft details", () => { - const blocks = buildSlackProgressDraftBlocks({ - title: "Shelling...", - lines: [ - { - kind: "command-output", - label: "Exec", - detail: "command finished", - status: "completed", - text: "๐Ÿ› ๏ธ Exec: completed", - toolName: "exec", - }, - { - kind: "command-output", - label: "Exec", - detail: "command failed", - status: "exit 1", - text: "๐Ÿ› ๏ธ Exec: exit 1", - toolName: "exec", - }, - ], - }); - - expectLegacyLineBlock(blocks?.[1], "โ€ข *Exec*", "command finished"); - expectLegacyLineBlock(blocks?.[2], "โ€ข *Exec*", "command failed ยท exit 1"); - }); - - it("keeps newest rich progress lines when capping legacy draft blocks", () => { - const blocksWithLabel = buildSlackProgressDraftBlocks({ - title: "Shelling...", - lines: Array.from({ length: 60 }, (_value, index) => progressLine(index)), - }); - expect(blocksWithLabel).toHaveLength(50); - // The label block survives capping; tool lines yield the remaining budget. - expect(JSON.stringify(blocksWithLabel?.[0])).toContain("Shelling..."); - expectLegacyLineBlock(blocksWithLabel?.[1], "๐Ÿ› ๏ธ *Exec 11*", "run 11"); - expectLegacyLineBlock(blocksWithLabel?.at(-1), "๐Ÿ› ๏ธ *Exec 59*", "run 59"); - - const blocksWithoutTitle = buildSlackProgressDraftBlocks({ - lines: Array.from({ length: 60 }, (_value, index) => progressLine(index)), - }); - expect(blocksWithoutTitle).toHaveLength(50); - expectLegacyLineBlock(blocksWithoutTitle?.[0], "๐Ÿ› ๏ธ *Exec 10*", "run 10"); - expectLegacyLineBlock(blocksWithoutTitle?.at(-1), "๐Ÿ› ๏ธ *Exec 59*", "run 59"); - }); - - it("renders legacy rich draft lines without a heading when no label or title is provided", () => { - expect( - buildSlackProgressDraftBlocks({ - lines: [toolLine("run tests")], - }), - ).toEqual([legacyLineBlock("๐Ÿ› ๏ธ *Exec*", "run tests")]); - }); - - it("uses a blank legacy rich draft detail when structured detail is absent", () => { - expect( - buildSlackProgressDraftBlocks({ - lines: [itemLine("prepare the workspace", "Preamble"), toolLine("run tests")], - }), - ).toEqual([legacyLineBlock("โ€ข *Preamble*", "โ€”"), legacyLineBlock("๐Ÿ› ๏ธ *Exec*", "run tests")]); - }); - - it("renders authored commentary Markdown in legacy rich draft details", () => { - expect( - buildSlackProgressDraftBlocks({ - lines: [ - { - id: "commentary:preamble-1", - kind: "item", - label: "Commentary", - text: "๐Ÿ’ฌ Rendering the `sample-widget` fixture on **example.test**.", - prefix: false, - }, - { - id: "reasoning", - kind: "item", - label: "Reasoning", - text: "_Reading the Slack handler_", - prefix: false, - }, - ], - }), - ).toEqual([ - legacyLineBlock("โ€ข *Commentary*", "Rendering the `sample-widget` fixture on *example.test*."), - legacyLineBlock("โ€ข *Reasoning*", "_Reading the Slack handler_"), + expect(blocks).toEqual([ + { type: "section", text: { type: "mrkdwn", text: "๐Ÿ”„ *Implementing*" } }, + { + type: "section", + text: { type: "mrkdwn", text: "_Checking the workspace._" }, + }, + { type: "section", text: { type: "mrkdwn", text: "โœ… Inspect\nโ–ธ Patch" } }, + { + type: "section", + text: { type: "mrkdwn", text: "๐Ÿ› ๏ธ *Exec* โ€” run tests\nโ€ข *Preamble* โ€” โ€”" }, + }, + { + type: "context", + elements: [{ type: "mrkdwn", text: "๐Ÿ› ๏ธ 3 tools ยท ๐Ÿ“ 4 files +2 โˆ’1 ยท โฑ 12s" }], + }, ]); }); - it("does not emit legacy rich draft blocks when there are no lines or heading", () => { - expect( - buildSlackProgressDraftBlocks({ - lines: [], - }), - ).toBeUndefined(); + it.each([ + { state: "success" as const, icon: "โœ…" }, + { state: "error" as const, icon: "โŒ" }, + ])( + "renders $state terminal cards and gates the session action on public URL", + ({ state, icon }) => { + const blocks = buildSlackProgressCardBlocks({ + state, + title: "Implementing", + lines: [toolLine("run tests")], + receiptSummary: "๐Ÿ› ๏ธ 1 tool call ยท โฑ๏ธ 8s", + diffStat: { files: 2, added: 1, removed: 1 }, + sessionUrl: "https://team.openclaw.ai/openclaw/chat/main", + }); + + expect(blocks[0]).toEqual({ + type: "section", + text: { type: "mrkdwn", text: `${icon} *Implementing*` }, + }); + expect(blocks).toContainEqual({ + type: "context", + elements: [{ type: "mrkdwn", text: "๐Ÿ› ๏ธ 1 tool call ยท โฑ๏ธ 8s ยท ๐Ÿ“ 2 files +1 โˆ’1" }], + }); + expect(blocks.at(-1)).toEqual({ + type: "actions", + elements: [ + { + type: "button", + action_id: "openclaw:session_link", + text: { type: "plain_text", text: "Open in OpenClaw" }, + url: "https://team.openclaw.ai/openclaw/chat/main", + }, + ], + }); + + expect( + buildSlackProgressCardBlocks({ state, title: "Implementing", lines: [] }), + ).toHaveLength(1); + }, + ); + + it("keeps the newest activity rows inside one section and the Slack block budget", () => { + const blocks = buildSlackProgressCardBlocks({ + state: "working", + title: "Working", + lines: Array.from({ length: 60 }, (_value, index) => progressLine(index)), + elapsedSeconds: 1, + }); + const activity = blocks.find( + (block) => block.type === "section" && JSON.stringify(block).includes("Exec 59"), + ); + + expect(blocks.length).toBeLessThanOrEqual(50); + expect(activity).toBeDefined(); + expect(JSON.stringify(activity)).toContain("๐Ÿ› ๏ธ *Exec 59* โ€” run 59"); + expect(JSON.stringify(activity)).not.toContain("Exec 0"); }); }); @@ -287,20 +203,6 @@ describe("native Slack progress stream chunks", () => { ]); }); - it("renders the plan checklist in rich draft blocks", () => { - const blocks = buildSlackProgressDraftBlocks({ - title: "Implementation", - lines: [], - plan: [ - { step: "Inspect code", status: "completed" }, - { step: "Run tests", status: "in_progress" }, - ], - }); - - expect(JSON.stringify(blocks)).toContain("โœ… Inspect code"); - expect(JSON.stringify(blocks)).toContain("โ–ธ Run tests"); - }); - it("terminalizes orphaned rows when a plan snapshot shrinks", () => { const first = reconcileSlackNativeTaskChunks({ previousTasks: new Map(), diff --git a/extensions/slack/src/progress-blocks.ts b/extensions/slack/src/progress-blocks.ts index 4123e664c9a3..64a3e4b87045 100644 --- a/extensions/slack/src/progress-blocks.ts +++ b/extensions/slack/src/progress-blocks.ts @@ -4,12 +4,14 @@ import type { AnyChunk } from "@slack/types"; import type { Block, KnownBlock } from "@slack/web-api"; import { type AgentPlanStep, + type ChannelProgressDraftCompositorSnapshot, type ChannelProgressDraftLine, formatPlanChecklistLines, } from "openclaw/plugin-sdk/channel-outbound"; import { SLACK_MAX_BLOCKS } from "./blocks-input.js"; import { normalizeSlackOutboundText } from "./format.js"; import { escapeSlackMrkdwn } from "./monitor/mrkdwn.js"; +import { SLACK_SESSION_LINK_ACTION_ID } from "./reply-action-ids.js"; import { truncateSlackText } from "./truncate.js"; const SLACK_PROGRESS_FIELD_MAX = 1800; @@ -254,15 +256,48 @@ function buildSlackProgressStreamChunks(params: { return chunks; } -export function buildSlackProgressDraftBlocks(params: { - label?: string; - title?: string; +type SlackProgressCardState = "working" | "success" | "error"; +type SlackProgressDiffStat = NonNullable; + +function formatDiffStat(diffStat: SlackProgressDiffStat | undefined): string | undefined { + if (!diffStat || (diffStat.files === 0 && diffStat.added === 0 && diffStat.removed === 0)) { + return undefined; + } + return [ + `๐Ÿ“ ${diffStat.files} files`, + ...(diffStat.added > 0 ? [`+${diffStat.added}`] : []), + ...(diffStat.removed > 0 ? [`โˆ’${diffStat.removed}`] : []), + ].join(" "); +} + +function buildActivityText(lines: readonly ChannelProgressDraftLine[], maxLineChars: number) { + const rendered: string[] = []; + let length = 0; + for (const line of lines.slice(-SLACK_MAX_BLOCKS).toReversed()) { + const row = `${legacyLineTitle(line)} โ€” ${legacyLineDetail(line, maxLineChars)}`; + const nextLength = length + row.length + (rendered.length > 0 ? 1 : 0); + if (nextLength > SLACK_PROGRESS_FIELD_MAX) { + break; + } + rendered.push(row); + length = nextLength; + } + return rendered.toReversed().join("\n"); +} + +export function buildSlackProgressCardBlocks(params: { + state: SlackProgressCardState; + title: string; lines: readonly ChannelProgressDraftLine[]; plan?: readonly AgentPlanStep[]; narration?: string; maxLineChars?: number; -}): (Block | KnownBlock)[] | undefined { - const label = params.label?.trim() || params.title?.trim(); + toolCalls?: number; + elapsedSeconds?: number; + diffStat?: SlackProgressDiffStat; + receiptSummary?: string; + sessionUrl?: string; +}): (Block | KnownBlock)[] { const maxLineChars = resolveMaxLineChars( params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS, @@ -272,18 +307,21 @@ export function buildSlackProgressDraftBlocks(params: { maxLineChars, }); const narration = params.narration?.replace(/\s+/g, " ").trim(); - // Status blocks (label, narration, checklist) take priority over rolling - // tool lines inside Slack's 50-block budget; the tail slice would otherwise - // silently drop the checklist first. - const headBlocks: (Block | KnownBlock)[] = [ - ...(label - ? [ - { - type: "section" as const, - text: field(`*${escapeSlackMrkdwn(label)}*`), - }, - ] - : []), + const activityText = buildActivityText(params.lines, maxLineChars); + const diffStat = formatDiffStat(params.diffStat); + const workingFooter = [ + ...(params.toolCalls && params.toolCalls > 0 ? [`๐Ÿ› ๏ธ ${params.toolCalls} tools`] : []), + ...(diffStat ? [diffStat] : []), + ...(params.elapsedSeconds && params.elapsedSeconds > 0 ? [`โฑ ${params.elapsedSeconds}s`] : []), + ].join(" ยท "); + const terminalFooter = [params.receiptSummary?.trim(), diffStat].filter(Boolean).join(" ยท "); + const footer = params.state === "working" ? workingFooter : terminalFooter; + const icon = params.state === "working" ? "๐Ÿ”„" : params.state === "success" ? "โœ…" : "โŒ"; + const blocks: (Block | KnownBlock)[] = [ + { + type: "section" as const, + text: field(`${icon} *${escapeSlackMrkdwn(params.title.trim() || "Working")}*`), + }, ...(narration ? [ { @@ -300,16 +338,39 @@ export function buildSlackProgressDraftBlocks(params: { }, ] : []), - ].slice(0, SLACK_MAX_BLOCKS); - const lineBudget = Math.max(0, SLACK_MAX_BLOCKS - headBlocks.length); - const renderedBlocks: (Block | KnownBlock)[] = [ - ...headBlocks, - ...params.lines.slice(-lineBudget).map((line) => ({ - type: "section" as const, - fields: [field(legacyLineTitle(line)), field(legacyLineDetail(line, maxLineChars))], - })), + ...(activityText + ? [ + { + type: "section" as const, + text: field(activityText), + }, + ] + : []), + ...(footer + ? [ + { + type: "context" as const, + elements: [field(footer)], + }, + ] + : []), + ...(params.state !== "working" && params.sessionUrl + ? [ + { + type: "actions" as const, + elements: [ + { + type: "button" as const, + action_id: SLACK_SESSION_LINK_ACTION_ID, + text: { type: "plain_text" as const, text: "Open in OpenClaw" }, + url: params.sessionUrl, + }, + ], + }, + ] + : []), ]; - return renderedBlocks.length ? renderedBlocks : undefined; + return blocks.slice(0, SLACK_MAX_BLOCKS); } export type SlackNativeTaskSnapshot = ReadonlyMap< diff --git a/extensions/slack/src/reply-action-ids.ts b/extensions/slack/src/reply-action-ids.ts index 169033d27094..e01e037d3e09 100644 --- a/extensions/slack/src/reply-action-ids.ts +++ b/extensions/slack/src/reply-action-ids.ts @@ -3,6 +3,7 @@ import type { Block, KnownBlock } from "@slack/web-api"; export const SLACK_REPLY_BUTTON_ACTION_ID = "openclaw:reply_button"; export const SLACK_REPLY_LINK_ACTION_ID = "openclaw:reply_link"; +export const SLACK_SESSION_LINK_ACTION_ID = "openclaw:session_link"; export const SLACK_REPLY_SELECT_ACTION_ID = "openclaw:reply_select"; export const SLACK_CALLBACK_BUTTON_ACTION_ID = "openclaw:callback_button"; export const SLACK_CALLBACK_SELECT_ACTION_ID = "openclaw:callback_select"; diff --git a/extensions/slack/src/stream-mode.test.ts b/extensions/slack/src/stream-mode.test.ts index 43ce3ededb23..5cf475092e9e 100644 --- a/extensions/slack/src/stream-mode.test.ts +++ b/extensions/slack/src/stream-mode.test.ts @@ -3,8 +3,16 @@ import { describe, expect, it } from "vitest"; import { applyAppendOnlyStreamUpdate, resolveSlackStreamingConfig } from "./stream-mode.js"; describe("resolveSlackStreamingConfig", () => { - it("defaults to partial mode with native streaming enabled", () => { + it("defaults to progress mode with native streaming enabled", () => { expect(resolveSlackStreamingConfig({})).toEqual({ + mode: "progress", + nativeStreaming: true, + draftMode: "status_final", + }); + }); + + it("keeps explicit partial mode on the replace draft path", () => { + expect(resolveSlackStreamingConfig({ streaming: { mode: "partial" } })).toEqual({ mode: "partial", nativeStreaming: true, draftMode: "replace", diff --git a/extensions/slack/src/streaming-compat.ts b/extensions/slack/src/streaming-compat.ts index f3dcc39bb2d0..ee9ebffde8cf 100644 --- a/extensions/slack/src/streaming-compat.ts +++ b/extensions/slack/src/streaming-compat.ts @@ -82,7 +82,7 @@ export function resolveSlackStreamingMode( if (typeof params.streaming === "boolean") { return params.streaming ? "partial" : "off"; } - return "partial"; + return "progress"; } export function resolveSlackNativeStreaming( diff --git a/scripts/plugin-sdk-surface-report.mts b/scripts/plugin-sdk-surface-report.mts index d27f7f199981..4a9dd26ad799 100644 --- a/scripts/plugin-sdk-surface-report.mts +++ b/scripts/plugin-sdk-surface-report.mts @@ -274,7 +274,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: QQBot 2.0.1 operator-approval Gateway client compatibility export. // +2: narrow channel agent-run terminal reader and outcome contract. // +5: narrow string, record, and error coercion helpers. - 4878, + // +1: normalized Gateway public origin resolver for plugin-generated links. + 4879, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -340,7 +341,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: QQBot 2.0.1 operator-approval Gateway client compatibility export. // +1: narrow channel agent-run terminal reader. // +5: narrow string, record, and error coercion helpers. - 2931, + // +1: normalized Gateway public origin resolver for plugin-generated links. + 2932, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/channels/progress-draft-compositor.test.ts b/src/channels/progress-draft-compositor.test.ts index db69fad8e570..7c538ab3da86 100644 --- a/src/channels/progress-draft-compositor.test.ts +++ b/src/channels/progress-draft-compositor.test.ts @@ -33,6 +33,8 @@ describe("createChannelProgressDraftCompositor", () => { receipt.noteReasoning(); now = 43_000; + expect(receipt.toolCalls).toBe(1); + expect(receipt.elapsedSeconds).toBe(42); expect(receipt.buildSummaryLine()).toBe("๐Ÿง  2 thoughts ยท ๐Ÿ’ฌ 1 note ยท ๐Ÿ› ๏ธ 1 tool call ยท โฑ๏ธ 42s"); receipt.reset(); @@ -963,6 +965,30 @@ describe("createChannelProgressDraftCompositor", () => { expect.objectContaining({ id: "command-1", kind: "command-output", status: "completed" }), expect.objectContaining({ id: "patch-1", kind: "patch", toolName: "apply_patch" }), ]); + expect(progress.getSnapshot().diffStat).toBeUndefined(); + }); + + it("wires successful mutation completions into the snapshot diff stat", async () => { + const progress = createTestProgressDraftCompositor({ + entry: { streaming: { mode: "progress", progress: { label: "Working" } } }, + update: vi.fn(), + updateOnLineChange: true, + }); + + await progress.pushToolEvent({ + toolCallId: "write-1", + name: "write", + phase: "start", + args: { path: "src/example.ts", content: "one\ntwo" }, + }); + expect(progress.getSnapshot().diffStat).toBeUndefined(); + await progress.pushItemEvent({ + toolCallId: "write-1", + kind: "tool", + phase: "end", + status: "completed", + }); + expect(progress.getSnapshot().diffStat).toEqual({ files: 1, added: 2, removed: 0 }); }); it("ignores status updates once the final reply started and clears both per turn", async () => { diff --git a/src/channels/progress-draft-compositor.ts b/src/channels/progress-draft-compositor.ts index e305452c453d..d08b1c36ae9f 100644 --- a/src/channels/progress-draft-compositor.ts +++ b/src/channels/progress-draft-compositor.ts @@ -1,3 +1,7 @@ +import { + createProgressDraftDiffStatTracker, + type ChannelProgressDraftDiffStat, +} from "./progress-draft-diffstat.js"; import { createChannelProgressDraftEventHandlers, type ChannelProgressDraftEventLineBuilder, @@ -46,6 +50,7 @@ export type ChannelProgressDraftCompositorSnapshot = Readonly<{ statusHeadline?: string; plan?: readonly AgentPlanStep[]; planExplanation?: string; + diffStat?: ChannelProgressDraftDiffStat; }>; type ChannelProgressDraftUpdateOptions = { @@ -106,12 +111,14 @@ export function createChannelProgressDraftCompositor(params: { params.active && resolveChannelStreamingSuppressDefaultToolProgressMessages(params.entry, { draftStreamActive: true, + mode: params.mode, previewToolProgressEnabled, }); let progressSuppressed = false; let lines: ChannelProgressDraftCompositorLine[] = []; let lastRenderedText = ""; let lastRenderedLines = lines; + let lastRenderedDiffStatKey = ""; let reasoningRawText = ""; let lastReasoningLine: string | undefined; // Id-less commentary streams as cumulative snapshots ("Checking" โ†’ "Checking @@ -129,6 +136,14 @@ export function createChannelProgressDraftCompositor(params: { let planExplanation = ""; let finalReplyStarted = false; let finalReplyDelivered = false; + const diffStatTracker = createProgressDraftDiffStatTracker({ + canStage: () => + params.active && + params.mode === "progress" && + !progressSuppressed && + !finalReplyStarted && + !finalReplyDelivered, + }); let preambleExpiryTimer: ReturnType | undefined; let lastStartRendered = false; @@ -174,13 +189,17 @@ export function createChannelProgressDraftCompositor(params: { }); }; + const resolveDiffStat = diffStatTracker.resolve; + const getSnapshot = (): ChannelProgressDraftCompositorSnapshot => { const statusHeadline = resolveStatusText(); + const diffStat = resolveDiffStat(); return { lines: lines.map((line) => (typeof line === "string" ? line : { ...line })), ...(statusHeadline ? { statusHeadline } : {}), ...(planSteps ? { plan: planSteps.map((entry) => ({ ...entry })) } : {}), ...(planExplanation ? { planExplanation } : {}), + ...(diffStat ? { diffStat } : {}), }; }; @@ -190,6 +209,7 @@ export function createChannelProgressDraftCompositor(params: { lines = []; lastRenderedText = ""; lastRenderedLines = lines; + lastRenderedDiffStatKey = ""; reasoningRawText = ""; lastReasoningLine = undefined; lastIdLessCommentaryId = undefined; @@ -200,13 +220,17 @@ export function createChannelProgressDraftCompositor(params: { narrationText = ""; planSteps = undefined; planExplanation = ""; + diffStatTracker.reset(); lastStartRendered = false; }; const publish = async (options?: { flush?: boolean }): Promise => { const text = formatDraftText(); - const linesChanged = params.updateOnLineChange === true && lines !== lastRenderedLines; - if (!text || (text === lastRenderedText && !linesChanged)) { + const diffStatKey = JSON.stringify(resolveDiffStat() ?? null); + const structuredStateChanged = + params.updateOnLineChange === true && + (lines !== lastRenderedLines || diffStatKey !== lastRenderedDiffStatKey); + if (!text || (text === lastRenderedText && !structuredStateChanged)) { return false; } const observed = await settleProgressVisibilityCallbackResult( @@ -218,6 +242,7 @@ export function createChannelProgressDraftCompositor(params: { // Only accepted renders become the dedupe baseline; pending sends remain retryable. lastRenderedText = text; lastRenderedLines = lines; + lastRenderedDiffStatKey = diffStatKey; return true; }; @@ -337,7 +362,10 @@ export function createChannelProgressDraftCompositor(params: { : lines; const lineChanged = nextLines !== lines; const hasUnconfirmedRender = formatDraftText(nextLines) !== lastRenderedText; - if (shouldStoreLine && !lineChanged && !hasUnconfirmedRender) { + const diffStatChanged = + params.updateOnLineChange === true && + JSON.stringify(resolveDiffStat() ?? null) !== lastRenderedDiffStatKey; + if (shouldStoreLine && !lineChanged && !hasUnconfirmedRender && !diffStatChanged) { return false; } // A work line lands between reasoning bursts: commit the current thinking @@ -384,6 +412,8 @@ export function createChannelProgressDraftCompositor(params: { const progressEventHandlers = createChannelProgressDraftEventHandlers({ entry: params.entry, pushLine: noteProgress, + onTool: diffStatTracker.stageToolEvent, + onItem: diffStatTracker.commitItemEvent, ...(params.buildProgressEventLine ? { buildLine: params.buildProgressEventLine } : {}), }); diff --git a/src/channels/progress-draft-diffstat.test.ts b/src/channels/progress-draft-diffstat.test.ts new file mode 100644 index 000000000000..9437ace47680 --- /dev/null +++ b/src/channels/progress-draft-diffstat.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest"; +import { createProgressDraftDiffStatTracker } from "./progress-draft-diffstat.js"; + +type DiffStatTracker = ReturnType; + +function stageMutation( + tracker: DiffStatTracker, + toolCallId: string, + name: string, + args: Record, + phase = "start", +) { + tracker.stageToolEvent({ toolCallId, name, phase, args }); +} + +function completeMutation(tracker: DiffStatTracker, toolCallId: string, status = "completed") { + tracker.commitItemEvent({ toolCallId, phase: "end", status }); +} + +describe("createProgressDraftDiffStatTracker", () => { + it("stages starts and commits successful terminal items additively", () => { + const tracker = createProgressDraftDiffStatTracker({ canStage: () => true }); + + stageMutation(tracker, "write-1", "write", { + path: "src/example.ts", + content: "one\ntwo", + }); + expect(tracker.resolve()).toBeUndefined(); + stageMutation( + tracker, + "write-1", + "write", + { path: "src/example.ts", content: "ignored\npartial\nargs" }, + "update", + ); + expect(tracker.resolve()).toBeUndefined(); + + completeMutation(tracker, "write-1"); + expect(tracker.resolve()).toEqual({ files: 1, added: 2, removed: 0 }); + + stageMutation(tracker, "edit-1", "edit", { + path: "src/example.ts", + edits: [{ oldText: "one\ntwo", newText: "three" }], + }); + completeMutation(tracker, "edit-1"); + expect(tracker.resolve()).toEqual({ files: 1, added: 3, removed: 2 }); + + for (const status of ["failed", "error"]) { + const toolCallId = `failed-${status}`; + stageMutation(tracker, toolCallId, "write", { + path: `src/${toolCallId}.ts`, + content: "ignored", + }); + completeMutation(tracker, toolCallId, status); + } + expect(tracker.resolve()).toEqual({ files: 1, added: 3, removed: 2 }); + + stageMutation(tracker, "patch-1", "apply_patch", { + input: [ + "*** Begin Patch", + "*** Update File: src/example.ts", + "@@", + "-three", + "+four", + "+five", + "*** Add File: src/new.ts", + "+new", + "+line", + "*** End Patch", + ].join("\n"), + }); + completeMutation(tracker, "patch-1"); + expect(tracker.resolve()).toEqual({ files: 2, added: 7, removed: 3 }); + + stageMutation(tracker, "codex-patch-1", "apply_patch", { + changes: [ + { path: "src/example.ts", stat: { added: 7, removed: 3 } }, + { path: "src/third.ts", stat: { added: 5, removed: 2 } }, + ], + }); + completeMutation(tracker, "codex-patch-1"); + expect(tracker.resolve()).toEqual({ files: 3, added: 19, removed: 8 }); + + stageMutation(tracker, "pending-reset", "write", { + path: "src/pending-reset.ts", + content: "pending", + }); + tracker.reset(); + expect(tracker.resolve()).toBeUndefined(); + completeMutation(tracker, "pending-reset"); + expect(tracker.resolve()).toBeUndefined(); + }); + + it("bounds pending staging and distinct committed file tracking", () => { + const tracker = createProgressDraftDiffStatTracker({ canStage: () => true }); + + for (let index = 0; index < 65; index += 1) { + stageMutation(tracker, `write-${index}`, "write", { + path: `src/file-${index}.ts`, + content: "line", + }); + } + expect(tracker.resolve()).toBeUndefined(); + for (let index = 0; index < 65; index += 1) { + completeMutation(tracker, `write-${index}`); + } + expect(tracker.resolve()).toEqual({ files: 64, added: 64, removed: 0 }); + + for (let index = 64; index < 257; index += 1) { + const toolCallId = `restaged-write-${index}`; + stageMutation(tracker, toolCallId, "write", { + path: `src/file-${index}.ts`, + content: "line", + }); + completeMutation(tracker, toolCallId); + } + expect(tracker.resolve()).toEqual({ files: 257, added: 257, removed: 0 }); + + stageMutation(tracker, "edit-known", "edit", { + path: "src/file-0.ts", + oldText: "one\ntwo", + newText: "one\ntwo\nthree", + }); + completeMutation(tracker, "edit-known"); + expect(tracker.resolve()).toEqual({ files: 257, added: 260, removed: 2 }); + }); +}); diff --git a/src/channels/progress-draft-diffstat.ts b/src/channels/progress-draft-diffstat.ts new file mode 100644 index 000000000000..66177efef9e1 --- /dev/null +++ b/src/channels/progress-draft-diffstat.ts @@ -0,0 +1,105 @@ +import { readCompletedFileMutationDelta } from "../agents/file-mutation-args.js"; +import { resolveFileMutationToolName } from "../agents/tool-mutation-names.js"; + +const MAX_TRACKED_MUTATION_FILES = 256; +const MAX_PENDING_MUTATION_DIFFS = 64; + +type PendingMutationDelta = NonNullable>; + +export type ChannelProgressDraftDiffStat = Readonly<{ + files: number; + added: number; + removed: number; +}>; + +export function createProgressDraftDiffStatTracker(params: { canStage: () => boolean }) { + let hasCommittedDiff = false; + let mutationFiles = new Set(); + let mutationOverflowFiles = 0; + let mutationAdded = 0; + let mutationRemoved = 0; + let pendingMutationDiffs = new Map(); + + const reset = () => { + hasCommittedDiff = false; + mutationFiles = new Set(); + mutationOverflowFiles = 0; + mutationAdded = 0; + mutationRemoved = 0; + pendingMutationDiffs = new Map(); + }; + + const stageToolEvent = (payload: { + toolCallId?: string; + name?: string; + phase?: string; + args?: Record; + }) => { + if (!params.canStage()) { + return; + } + const toolCallId = payload.toolCallId?.trim(); + if (payload.phase !== "start" || !toolCallId || !payload.name || !payload.args) { + return; + } + const kind = resolveFileMutationToolName(payload.name); + const delta = kind ? readCompletedFileMutationDelta(kind, payload.args) : undefined; + if (!delta) { + return; + } + if ( + !pendingMutationDiffs.has(toolCallId) && + pendingMutationDiffs.size >= MAX_PENDING_MUTATION_DIFFS + ) { + return; + } + pendingMutationDiffs.set(toolCallId, delta); + }; + + const commitItemEvent = (payload: { toolCallId?: string; phase?: string; status?: string }) => { + const toolCallId = payload.toolCallId?.trim(); + if (!toolCallId || payload.phase !== "end") { + return; + } + const delta = pendingMutationDiffs.get(toolCallId); + if (!delta) { + return; + } + pendingMutationDiffs.delete(toolCallId); + const status = payload.status?.trim().toLowerCase(); + if (status === "failed" || status === "error") { + return; + } + hasCommittedDiff = true; + mutationAdded += delta.added; + mutationRemoved += delta.removed; + for (const file of delta.files) { + if (mutationFiles.has(file)) { + continue; + } + if (mutationFiles.size < MAX_TRACKED_MUTATION_FILES) { + mutationFiles.add(file); + continue; + } + // Overflow keeps file-count memory bounded. Repeated paths beyond the + // tracked window may count again, while line totals remain authoritative. + mutationOverflowFiles += 1; + } + }; + + const resolve = (): ChannelProgressDraftDiffStat | undefined => + hasCommittedDiff + ? { + files: mutationFiles.size + mutationOverflowFiles, + added: mutationAdded, + removed: mutationRemoved, + } + : undefined; + + return { + stageToolEvent, + commitItemEvent, + resolve, + reset, + }; +} diff --git a/src/channels/progress-draft-events.ts b/src/channels/progress-draft-events.ts index 9d00277cc084..6134588e96e3 100644 --- a/src/channels/progress-draft-events.ts +++ b/src/channels/progress-draft-events.ts @@ -22,6 +22,8 @@ export type ChannelProgressDraftEventLineBuilder = ( export function createChannelProgressDraftEventHandlers(params: { entry: StreamingCompatEntry | null | undefined; buildLine?: ChannelProgressDraftEventLineBuilder; + onTool?: (payload: ToolProgressPayload) => void; + onItem?: (payload: ItemProgressPayload) => void; pushLine: ( line: ChannelProgressDraftEventLine | undefined, options?: { toolName?: string; startImmediately?: boolean }, @@ -41,10 +43,12 @@ export function createChannelProgressDraftEventHandlers(params: { return { pushToolEvent: (payload: ToolProgressPayload) => { const { detailMode, ...input } = payload; + params.onTool?.(payload); return pushEvent({ event: "tool", ...input }, detailMode); }, pushItemEvent: (payload: ItemProgressPayload) => { const { kind: itemKind, ...input } = payload; + params.onItem?.(payload); return pushEvent({ event: "item", ...input, itemKind }); }, pushApprovalEvent: (payload: ProgressPayload<"approval">) => { diff --git a/src/channels/progress-receipt-tracker.ts b/src/channels/progress-receipt-tracker.ts index a6264b1a3034..fa2fa7b27fbd 100644 --- a/src/channels/progress-receipt-tracker.ts +++ b/src/channels/progress-receipt-tracker.ts @@ -29,6 +29,8 @@ export function createChannelProgressReceiptTracker(params?: { now?: () => numbe lastCommentaryText = ""; }; + const elapsedSeconds = () => Math.max(1, Math.round((now() - startedAt) / 1000)); + return { noteReasoning() { reasoningOpen = true; @@ -58,9 +60,15 @@ export function createChannelProgressReceiptTracker(params?: { now?: () => numbe } }, reset, + get toolCalls() { + return toolCalls; + }, + get elapsedSeconds() { + return elapsedSeconds(); + }, buildSummaryLine() { closeReasoning(); - const seconds = Math.max(1, Math.round((now() - startedAt) / 1000)); + const seconds = elapsedSeconds(); return [ ...(reasoningSteps > 0 ? [`๐Ÿง  ${reasoningSteps} thought${reasoningSteps === 1 ? "" : "s"}`] diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index 1be9ced42d5d..43d4dfa25a27 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -820,7 +820,7 @@ export function resolveChannelStreamingPreviewToolProgress( /** * The channel's resolved stream mode. Only the caller knows it: channels pick * their own default when `streaming.mode` is unset (Telegram uses "progress", - * Discord uses "off", and Slack uses "partial"), and this helper has no + * Discord uses "off", and Slack uses "progress"), and this helper has no * channel identity to guess with. Omitting it reads the configured mode and * treats unset as "partial". */ @@ -885,6 +885,7 @@ export function resolveChannelStreamingSuppressDefaultToolProgressMessages( entry: StreamingCompatEntry | null | undefined, options?: { draftStreamActive?: boolean; + mode?: StreamingMode; previewToolProgressEnabled?: boolean; previewStreamingEnabled?: boolean; }, @@ -892,7 +893,7 @@ export function resolveChannelStreamingSuppressDefaultToolProgressMessages( if (options?.draftStreamActive === false || options?.previewStreamingEnabled === false) { return false; } - const mode = resolveChannelPreviewStreamMode(entry, "off"); + const mode = options?.mode ?? resolveChannelPreviewStreamMode(entry, "off"); if (mode === "off") { return false; } diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index d640a14e6888..d2aa92a5586f 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -28,12 +28,12 @@ const RAW_BUNDLED_CHANNEL_CONFIG_METADATA = [ '":9007199254740991},"webhookHost":{"type":"string"},"webhookPath":{"type":"string"},"webhookPublicUrl":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"rooms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"nostr","channelId":"nostr","order":55,"channelEnvVars":["NOSTR_PRIVATE_KEY"],"label":"Nostr","description":"Decentralized protocol; encrypted DMs via NIP-04.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"defaultAccount":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"privateKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"relays":{"type":"array","items":{"type":"string"}},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"profile":{"type":"object","properties":{"name":{"type":"string","maxLength":256},"displayName":{"type":"string","maxLength":256},"about":{"type":"string","maxLength":2000},"picture":{"type":"string","format":"uri"},"banner":{"type":"string","format":"uri"},"website":{"type":"string","format":"uri"},"nip05":{"type":"string"},"lud16":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false}},{"pluginId":"qa-channel","channelId":"qa-channel","order":999,"configurable":false,"label":"QA Channel","description":"Synthetic Slack-class transport for automated OpenClaw QA scenarios.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"raft","channelId":"raft","order":72,"channelEnvVars":["RAFT_PROFILE"],"label":"Raft","description":"Raft CLI wake bridge for human and agent collaboration.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"profile":{"type":"string","minLength":1},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"profile":{"type":"string","minLength":1}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"reef","channelId":"reef","label":"Reef","description":"Guarded end-to-end encrypted claw messaging.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"default":true,"type":"boolean"},"configWrites":{"type":"boolean"},"relayUrl":{"default":"https://reefwire.ai","type":"string","format":"uri","pattern":"^[hH][tT][tT][pP][sS]?:\\\\/\\\\/[^\\\\\\\\/?#@]+\\\\/?$"},"handle":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,62}$"},"email":{"type":"string","format":"email","pattern":"^(?!\\\\.)(?!.*\\\\.\\\\.)([A-Za-z0-9_\'+\\\\-\\\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\\\-]*\\\\.)+[A-Za-z]{2,}$"},"guard":{"type":"object","properties":{"provider":{"type":"string","enum":["anthropic","openai"]},"pinnedModel":{"type":"string","minLength":1},"apiKeyEnv":{"type":"string","pattern":"^[A-Z_][A-Z0-9_]*$"},"policyVersion":{"type":"string","minLength":1},"timeoutMs":{"type":"integer","minimum":100,"maximum":120000}},"required":["provider","pinnedModel","apiKeyEnv","policyVersion","timeoutMs"],"additionalProperties":false},"stateDir":{"type":"string","minLength":1},"requestPolicy":{"default":"code-only","type":"string","enum":["code-only","friends-of-friends","open"]},"friends":{}},"required":["enabled","relayUrl","requestPolicy"],"additionalProperties":false}},{"pluginId":"signal","channelId":"signal","label":"Signal","description":"signal-cli linked device with additional setup for the local REST bridge.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"account":{"type":"string"},"accountUuid":{"type":"string"},"transport":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"managed-native"},"configPath":{"type":"string"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","minimum":1,"maximum":65535},"cliPath":{"type":"string"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreStories":{"type":"boolean"}},"required":["kind"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"external-native"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"}},"required":["kind","url"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"container"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"}},"required":["kind","url"],"additionalProperties":false}]},"ignoreAttachments":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"ingest":{"type":"boolean"}},"additionalProperties":false}},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"t', 'ype":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"account":{"type":"string"},"accountUuid":{"type":"string"},"transport":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"managed-native"},"configPath":{"type":"string"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","minimum":1,"maximum":65535},"cliPath":{"type":"string"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreStories":{"type":"boolean"}},"required":["kind"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"external-native"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"}},"required":["kind","url"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"container"},"url":{"type":"string","pattern":"^[Hh][Tt][Tt][Pp][Ss]?:\\\\/\\\\/(?![^/?#]*@)"}},"required":["kind","url"],"additionalProperties":false}]},"ignoreAttachments":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"ingest":{"type":"boolean"}},"additionalProperties":false}},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Signal","help":"Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups."},"dmPolicy":{"label":"Signal DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.signal.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Signal Config Writes","help":"Allow Signal to write config in response to channel events/commands (default: true)."},"account":{"label":"Signal Account","help":"Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state.","presentation":"phone-number"},"allowFrom":{"presentation":"phone-number"},"defaultTo":{"presentation":"phone-number"},"groupAllowFrom":{"presentation":"phone-number"},"reactionAllowlist":{"presentation":"phone-number"},"accounts.*.account":{"presentation":"phone-number"},"accounts.*.allowFrom.*":{"presentation":"phone-number"},"accounts.*.defaultTo":{"presentation":"phone-number"},"accounts.*.groupAllowFrom.*":{"presentation":"phone-number"},"accounts.*.reactionAllowlist.*":{"presentation":"phone-number"},"transport":{"label":"Signal Transport","help":"Account-owned native process or external endpoint configuration. Named accounts do not inherit this value."},"transport.kind":{"label":"Signal Transport Kind","help":"Use managed-native to let OpenClaw start signal-cli, external-native for an existing native daemon, or container for signal-cli-rest-api."},"transport.configPath":{"label":"Signal CLI Config Path","help":"Optional directory passed to signal-cli via --config when the service needs a non-default signal-cli data path."},"transport.url":{"label":"Signal Transport URL","help":"Base URL for an external-native or container transport, or the connection endpoint for a managed-native daemon when it differs from the bind address."}}},{"pluginId":"slack","channelId":"slack","channelEnvVars":["SLACK_APP_TOKEN","SLACK_BOT_TOKEN","SLACK_USER_TOKEN"],"label":"Slack","description":"supported (Socket Mode).","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"postAs":{"default":"bot","type":"string","enum":["bot","user"]},"mode":{"default":"socket","type":"string","enum":["socket","http","relay"]},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"default":"/slack/events","type":"string"},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"a', 'llowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"ackReaction":{"type":"string"},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"typingReaction":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"postAs":{"type":"string","enum":["bot","user"]},"mode":{"type":"string","enum":["socket","http","relay"]},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"ackReaction":{"type":"string"},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"', - 'boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"typingReaction":{"type":"string"}},"required":["userTokenReadOnly"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy","postAs","mode","webhookPath","userTokenReadOnly"],"additionalProperties":false},"uiHints":{"":{"label":"Slack","help":"Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions."},"postAs":{"label":"Slack Identity","help":"Select \\"bot\\" (default) for the classic Slack app/bot identity or \\"user\\" to post as the authorizing human through a user token while the app carries event transport."},"dmPolicy":{"label":"Slack DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.slack.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Slack Config Writes","help":"Allow Slack to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Slack Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Slack channel IDs. Native Slack @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Slack Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Slack Mention Pattern Allowlist","help":"Slack channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Slack Mention Pattern Denylist","help":"Slack channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"commands.native":{"label":"Slack Native Commands","help":"Override native commands for Slack (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Slack Native Skill Commands","help":"Override native skill commands for Slack (bool or \\"auto\\")."},"implicitMentions":{"label":"Slack Implicit Mentions","help":"Control which Slack reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Slack Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Slack Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Slack Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Slack Streaming Mode","help":"Unified Slack stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Slack Streaming Mode","help":"Canonical Slack preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Slack Chunk Mode","help":"Chunking mode for outbound Slack text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Slack Block Streaming Enabled","help":"Enable chunked block-style Slack preview delivery when channels.slack.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Slack Block Streaming Coalesce","help":"Merge streamed Slack block replies before final delivery."},"streaming.nativeTransport":{"label":"Slack Native Streaming","help":"Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming."},"streaming.preview.toolProgress":{"label":"Slack Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Slack Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.render":{"label":"Slack Progress Renderer","help":"Progress draft renderer: \\"text\\" uses one portable text body; \\"rich\\" renders structured Slack Block Kit fields with the same text fallback."},"streaming.progress.nativeTaskCards":{"label":"Slack Native Progress Task Cards","help":"Opt in to Slack native task-card progress updates when channels.slack.streaming.mode=\\"progress\\" and streaming.nativeTransport is enabled. Default: false."},"streaming.progress.label":{"label":"Slack Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Slack Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"Slack Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Slack Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Slack Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Slack Progress Command Text","help":"Command/exec detail in progress draft lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"allowBots":{"label":"Slack Allow Bot Messages","help":"Allow bot-authored messages to trigger Slack replies (default: false)."},"botLoopProtection":{"label":"Slack Bot Loop Protection","help":"Sliding-window guard for Slack bot-to-bot loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Slack Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Slack Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Slack Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Slack Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"relay":{"label":"Slack Relay Mode","help":"Relay-delivered Slack events. Use with mode=\\"relay\\" when openclaw-slack-router owns the Slack Socket Mode connection."},"relay.url":{"label":"Slack Relay URL","help":"Full websocket URL for openclaw-slack-router. Include the route path, for example ws://127.0.0.1:8081/gateway/ws."},"relay.authToken":{"label":"Slack Relay Auth Token","help":"Bearer token used by this gateway to authenticate its reverse websocket connection to openclaw-slack-router."},"relay.gatewayId":{"label":"Slack Relay Gateway ID","help":"Destination id that openclaw-slack-router uses when routing user-group mentions to this gateway."},"botToken":{"label":"Slack Bot Token","help":"Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change."},"appToken":{"label":"Slack App Token","help":"Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret."},"userToken":{"label":"Slack User Token","help":"Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority."},"userTokenReadOnly":{"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes."},"execApprovals":{"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this Slack account."},"presenceEvents":{"label":"Slack Presence Events","help":"Poll observed human participants and wake the routed agent on away-to-active transitions. Default: \\"off\\"."},"presenceEvents.mode":{"label":"Slack Presence Event Mode","help":"\\"off\\" disables polling; \\"auto\\" covers DMs, MPIMs, and recent threads with up to 8 observed people; \\"on\\" also covers larger threads and top-level channels."},"channels.*.presenceEvents.mode":{"label":"Slack Channel Presence Event Mode","help":"Override presence events for one Slack channel. Use \\"on\\" to include large threads or top-level channel sessions."},"execApprovals.enabled":{"label":"Slack Exec Approvals Enabled","help":"Controls Slack native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack."},"execApprovals.sessionFilter":{"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions."},"execApprovals.target":{"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Slack chat/thread, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels."},"thread.historyScope":{"label":"Slack Thread History Scope","help":"Scope for Slack thread history context (\\"thread\\" isolates per thread; \\"channel\\" reuses channel history)."},"thread.inheritParent":{"label":"Slack Thread Parent Inheritance","help":"If true, Slack thread sessions inherit the parent channel transcript (default: false)."},"thread.initialHistoryLimit":{"label":"Slack Thread Initial History Limit","help":"Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable)."}}},{"pluginId":"sms","channelId":"sms","order":88,"channelEnvVars":["SMS_ALLOWED_USERS","SMS_PUBLIC_WEBHOOK_URL","SMS_WEBHOOK_PATH","TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_MESSAGING_SERVICE_SID","TWILIO_PHONE_NUMBER","TWILIO_SMS_FROM"],"label":"SMS","description":"Twilio-backed SMS/MMS with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"p', - 'rovider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["dmPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"SMS","help":"Twilio SMS/MMS channel configuration for inbound webhooks and outbound replies."},"accountSid":{"label":"Twilio Account SID","help":"Twilio Account SID used for SMS outbound API calls."},"authToken":{"label":"Twilio Auth Token","help":"Twilio Auth Token used to sign webhook validation and SMS outbound API calls."},"fromNumber":{"label":"SMS From Number","help":"Twilio SMS-capable phone number in E.164 format; outbound attachments also require MMS capability.","presentation":"phone-number"},"messagingServiceSid":{"label":"Twilio Messaging Service SID","help":"Twilio Messaging Service SID to use instead of a dedicated fromNumber."},"defaultTo":{"label":"SMS Default To Number","help":"Optional default outbound phone number used when a send flow omits an explicit SMS target.","presentation":"phone-number"},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly; outbound MMS also requires this same path to be reachable over HTTPS."},"webhookPath":{"label":"SMS Webhook Path","help":"Gateway HTTP path that receives Twilio incoming-message webhooks. Use a distinct path per account."},"dmPolicy":{"label":"SMS DM Policy","help":"Direct SMS access control (\\"pairing\\" recommended). \\"open\\" requires channels.sms.allowFrom=[\\"*\\"]."},"allowFrom":{"label":"SMS Allow From","help":"Allowed sender phone numbers in E.164 format, or * when dmPolicy is open.","presentation":"phone-number"},"accounts.*.fromNumber":{"presentation":"phone-number"},"accounts.*.defaultTo":{"presentation":"phone-number"},"accounts.*.allowFrom.*":{"presentation":"phone-number"},"textChunkLimit":{"label":"SMS Text Chunk Limit","help":"Maximum characters per outbound SMS chunk before OpenClaw splits long replies."}}},{"pluginId":"synology-chat","channelId":"synology-chat","order":90,"channelEnvVars":["OPENCLAW_BOT_NAME","SYNOLOGY_ALLOWED_USER_IDS","SYNOLOGY_CHAT_INCOMING_URL","SYNOLOGY_CHAT_TOKEN","SYNOLOGY_NAS_HOST","SYNOLOGY_RATE_LIMIT"],"label":"Synology Chat","description":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"dangerouslyAllowNameMatching":{"type":"boolean"},"dangerouslyAllowInheritedWebhookPath":{"type":"boolean"}},"additionalProperties":{}}},{"pluginId":"telegram","channelId":"telegram","channelEnvVars":["TELEGRAM_BOT_TOKEN"],"label":"Telegram","description":"simplest way to get started โ€” register a bot with @BotFather and get going.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"t', - 'ype":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Telegram","help":"Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics."},"customCommands":{"label":"Telegram Custom Commands","help":"Additional Telegram bot menu commands (merged with native; conflicts ignored)."},"botToken":{"label":"Telegram Bot Token","help":"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected."},"dmPolicy":{"label":"Telegram DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.telegram.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Telegram Config Writes","help":"Allow Telegram to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Telegram Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Telegram group chat IDs or chatId:topic:threadId topic IDs. Native Telegram bot mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Telegram Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Telegram Mention Pattern Allowlist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are enabled when mode i', - 's deny."},"mentionPatterns.denyIn":{"label":"Telegram Mention Pattern Denylist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are disabled. Native bot mentions still trigger."},"commands.native":{"label":"Telegram Native Commands","help":"Override native commands for Telegram (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Telegram Native Skill Commands","help":"Override native skill commands for Telegram (bool or \\"auto\\")."},"streaming":{"label":"Telegram Streaming Mode","help":"Unified Telegram stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"progress\\"). \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate."},"streaming.mode":{"label":"Telegram Streaming Mode","help":"Canonical Telegram preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"progress\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable normal Telegram block replies. This takes precedence over editable preview delivery."},"streaming.block.coalesce":{"label":"Telegram Block Streaming Coalesce","help":"Merge streamed Telegram block replies before sending final delivery."},"streaming.preview.chunk.minChars":{"label":"Telegram Draft Chunk Min Chars","help":"Minimum chars before emitting a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.maxChars":{"label":"Telegram Draft Chunk Max Chars","help":"Target max size for a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.breakPreference":{"label":"Telegram Draft Chunk Break Preference","help":"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence)."},"streaming.preview.toolProgress":{"label":"Telegram Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true when preview streaming is active). Set false to keep tool updates out of the edited Telegram preview."},"streaming.preview.commandText":{"label":"Telegram Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.label":{"label":"Telegram Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Telegram Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Telegram Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Telegram Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Telegram Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Telegram Progress Command Text","help":"Command/exec detail in progress draft lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.2 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"network.autoSelectFamily":{"label":"Telegram autoSelectFamily","help":"Override Node autoSelectFamily for Telegram (true=enable, false=disable)."},"network.dangerouslyAllowPrivateNetwork":{"label":"Telegram Dangerously Allow Private Network","help":"Dangerous opt-in for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve api.telegram.org to private/internal/special-use addresses."},"silentErrorReplies":{"label":"Telegram Silent Error Replies","help":"When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false."},"apiRoot":{"label":"Telegram API Root URL","help":"Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked."},"trustedLocalFileRoots":{"label":"Telegram Trusted Local File Roots","help":"Trusted local filesystem roots for self-hosted Telegram Bot API file_path values. Exact in-root paths are read directly; container paths under /var/lib/telegram-bot-api can map into a host volume mount. Other absolute paths are rejected."},"autoTopicLabel":{"label":"Telegram Auto Topic Label","help":"Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: \'...\' } for custom prompt."},"autoTopicLabel.enabled":{"label":"Telegram Auto Topic Label Enabled","help":"Whether auto topic labeling is enabled. Default: true."},"autoTopicLabel.prompt":{"label":"Telegram Auto Topic Label Prompt","help":"Custom prompt for LLM-based topic naming. The user message is appended after the prompt."},"capabilities.inlineButtons":{"label":"Telegram Inline Buttons","help":"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior."},"execApprovals":{"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account."},"execApprovals.enabled":{"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram."},"execApprovals.sessionFilter":{"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions."},"execApprovals.target":{"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Telegram chat/topic, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics."},"threadBindings.enabled":{"label":"Telegram Thread Binding Enabled","help":"Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Telegram Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Telegram Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Telegram Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-bind Telegram current conversations when supported."},"threadBindings.defaultSpawnContext":{"label":"Telegram Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."}}},{"pluginId":"tlon","channelId":"tlon","order":90,"label":"Tlon","description":"decentralized messaging on Urbit; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1},"authorization":{"type":"object","properties":{"channelRules":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"mode":{"type":"string","enum":["restricted","open"]},"allowedShips":{"type":"array","items":{"type":"string","minLength":1}}},"additionalProperties":false}}},"additionalProperties":false},"defaultAuthorizedShips":{"type":"array","items":{"type":"string","minLength":1}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false},"uiHints":{"implicitMentions":{"label":"Tlon Implicit Mentions","help":"Control which Tlon reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Tlon Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Tlon Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Tlon Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."}}},{"pluginId":"twitch","channelId":"twitch","aliases":["twitch-chat"],"channelEnvVars":["OPENCLAW_TWITCH_ACCESS_TOKEN"],"label":"Twitch","description":"Twitch chat integration","schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false},{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false}]}},{"pluginId":"whatsapp","channelId":"whatsapp","label":"WhatsApp","description":"works with your own number; recommend a separate phone + eSIM.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},', - '"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"default":50,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"name":{"type":"string"},"authDir":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"polls":{"type":"boolean"},"calls":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy","mediaMaxMb"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and direct-message routing safety."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"allowFrom":{"presentation":"phone-number"},"defaultTo":{"presentation":"phone-number"},"groupAllowFrom":{"presentation":"phone-number"},"accounts.*.allowFrom.*":{"presentation":"phone-number"},"accounts.*.defaultTo":{"presentation":"phone-number"},"accounts.*.groupAllowFrom.*":{"presentation":"phone-number"},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"direct":{"label":"WhatsApp Direct Chat Overrides","help":"Per-conversation overrides keyed by WhatsApp DM id. Applied after a DM is already admitted by dmPolicy; \\"*\\" supplies a default without admitting anyone."},"pluginHooks":{"label":"WhatsApp Plugin Hooks","help":"Opt in to broadcasting inbound WhatsApp events to plugins. Payloads carry personal content, so only enable it for plugins you trust."},"configWrites":{"label":"WhatsApp Config Writes","help":"Allow WhatsApp to write config in response to channel events/commands (default: true)."},"actions.calls":{"label":"WhatsApp Voice Calls","help":"Expose the experimental requester-bound WhatsApp voice-call tool. Default: false. Requires a separately paired MeowCaller CLI."},"mentionPatterns":{"label":"WhatsApp Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected WhatsApp conversation IDs such as 123@g.us."},"mentionPatterns.mode":{"label":"WhatsApp Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"WhatsApp Mention Pattern Allowlist","help":"WhatsApp conversation IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"WhatsApp Mention Pattern Denylist","help":"WhatsApp conversation IDs where configured regex mention patterns are disabled."}},"unsupportedSecretRefSurfacePatterns":["channels.whatsapp.accounts.*.creds.json","channels.whatsapp.creds.json"]},{"pluginId":"zalo","channelId":"zalo","aliases":["zl"],"order":80,"channelEnvVars":["ZALO_BOT_TOKEN","ZALO_WEBHOOK_SECRET"],"label":"Zalo","description":"Vietnam-focused messaging platform with Bot API.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"zalouser","channelId":"zalouser","aliases":["zlu"],"order":85,"channelEnvVars":["ZALOUSER_PROFILE","ZCA_PROFILE"],"label":"Zalo Personal","description":"Zalo personal account via QR code login.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additi', - 'onalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}}]', + 'boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"typingReaction":{"type":"string"}},"required":["userTokenReadOnly"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy","postAs","mode","webhookPath","userTokenReadOnly"],"additionalProperties":false},"uiHints":{"":{"label":"Slack","help":"Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions."},"postAs":{"label":"Slack Identity","help":"Select \\"bot\\" (default) for the classic Slack app/bot identity or \\"user\\" to post as the authorizing human through a user token while the app carries event transport."},"dmPolicy":{"label":"Slack DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.slack.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Slack Config Writes","help":"Allow Slack to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Slack Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Slack channel IDs. Native Slack @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Slack Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Slack Mention Pattern Allowlist","help":"Slack channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Slack Mention Pattern Denylist","help":"Slack channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"commands.native":{"label":"Slack Native Commands","help":"Override native commands for Slack (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Slack Native Skill Commands","help":"Override native skill commands for Slack (bool or \\"auto\\")."},"implicitMentions":{"label":"Slack Implicit Mentions","help":"Control which Slack reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Slack Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Slack Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Slack Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Slack Streaming Mode","help":"Unified Slack stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default). Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Slack Streaming Mode","help":"Canonical Slack preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default)."},"streaming.chunkMode":{"label":"Slack Chunk Mode","help":"Chunking mode for outbound Slack text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Slack Block Streaming Enabled","help":"Enable chunked block-style Slack preview delivery when channels.slack.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Slack Block Streaming Coalesce","help":"Merge streamed Slack block replies before final delivery."},"streaming.nativeTransport":{"label":"Slack Native Streaming","help":"Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming."},"streaming.preview.toolProgress":{"label":"Slack Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Slack Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.nativeTaskCards":{"label":"Slack Native Progress Task Cards","help":"Opt in to Slack native task-card progress updates when channels.slack.streaming.mode=\\"progress\\" and streaming.nativeTransport is enabled. Default: false."},"streaming.progress.label":{"label":"Slack Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Slack Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"Slack Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Slack Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Slack Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Slack Progress Command Text","help":"Command/exec detail in progress draft lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"allowBots":{"label":"Slack Allow Bot Messages","help":"Allow bot-authored messages to trigger Slack replies (default: false)."},"botLoopProtection":{"label":"Slack Bot Loop Protection","help":"Sliding-window guard for Slack bot-to-bot loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Slack Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Slack Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Slack Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Slack Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"relay":{"label":"Slack Relay Mode","help":"Relay-delivered Slack events. Use with mode=\\"relay\\" when openclaw-slack-router owns the Slack Socket Mode connection."},"relay.url":{"label":"Slack Relay URL","help":"Full websocket URL for openclaw-slack-router. Include the route path, for example ws://127.0.0.1:8081/gateway/ws."},"relay.authToken":{"label":"Slack Relay Auth Token","help":"Bearer token used by this gateway to authenticate its reverse websocket connection to openclaw-slack-router."},"relay.gatewayId":{"label":"Slack Relay Gateway ID","help":"Destination id that openclaw-slack-router uses when routing user-group mentions to this gateway."},"botToken":{"label":"Slack Bot Token","help":"Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change."},"appToken":{"label":"Slack App Token","help":"Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret."},"userToken":{"label":"Slack User Token","help":"Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority."},"userTokenReadOnly":{"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes."},"execApprovals":{"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this Slack account."},"presenceEvents":{"label":"Slack Presence Events","help":"Poll observed human participants and wake the routed agent on away-to-active transitions. Default: \\"off\\"."},"presenceEvents.mode":{"label":"Slack Presence Event Mode","help":"\\"off\\" disables polling; \\"auto\\" covers DMs, MPIMs, and recent threads with up to 8 observed people; \\"on\\" also covers larger threads and top-level channels."},"channels.*.presenceEvents.mode":{"label":"Slack Channel Presence Event Mode","help":"Override presence events for one Slack channel. Use \\"on\\" to include large threads or top-level channel sessions."},"execApprovals.enabled":{"label":"Slack Exec Approvals Enabled","help":"Controls Slack native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack."},"execApprovals.sessionFilter":{"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions."},"execApprovals.target":{"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Slack chat/thread, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels."},"thread.historyScope":{"label":"Slack Thread History Scope","help":"Scope for Slack thread history context (\\"thread\\" isolates per thread; \\"channel\\" reuses channel history)."},"thread.inheritParent":{"label":"Slack Thread Parent Inheritance","help":"If true, Slack thread sessions inherit the parent channel transcript (default: false)."},"thread.initialHistoryLimit":{"label":"Slack Thread Initial History Limit","help":"Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable)."}}},{"pluginId":"sms","channelId":"sms","order":88,"channelEnvVars":["SMS_ALLOWED_USERS","SMS_PUBLIC_WEBHOOK_URL","SMS_WEBHOOK_PATH","TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_MESSAGING_SERVICE_SID","TWILIO_PHONE_NUMBER","TWILIO_SMS_FROM"],"label":"SMS","description":"Twilio-backed SMS/MMS with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"typ', + 'e":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["dmPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"SMS","help":"Twilio SMS/MMS channel configuration for inbound webhooks and outbound replies."},"accountSid":{"label":"Twilio Account SID","help":"Twilio Account SID used for SMS outbound API calls."},"authToken":{"label":"Twilio Auth Token","help":"Twilio Auth Token used to sign webhook validation and SMS outbound API calls."},"fromNumber":{"label":"SMS From Number","help":"Twilio SMS-capable phone number in E.164 format; outbound attachments also require MMS capability.","presentation":"phone-number"},"messagingServiceSid":{"label":"Twilio Messaging Service SID","help":"Twilio Messaging Service SID to use instead of a dedicated fromNumber."},"defaultTo":{"label":"SMS Default To Number","help":"Optional default outbound phone number used when a send flow omits an explicit SMS target.","presentation":"phone-number"},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly; outbound MMS also requires this same path to be reachable over HTTPS."},"webhookPath":{"label":"SMS Webhook Path","help":"Gateway HTTP path that receives Twilio incoming-message webhooks. Use a distinct path per account."},"dmPolicy":{"label":"SMS DM Policy","help":"Direct SMS access control (\\"pairing\\" recommended). \\"open\\" requires channels.sms.allowFrom=[\\"*\\"]."},"allowFrom":{"label":"SMS Allow From","help":"Allowed sender phone numbers in E.164 format, or * when dmPolicy is open.","presentation":"phone-number"},"accounts.*.fromNumber":{"presentation":"phone-number"},"accounts.*.defaultTo":{"presentation":"phone-number"},"accounts.*.allowFrom.*":{"presentation":"phone-number"},"textChunkLimit":{"label":"SMS Text Chunk Limit","help":"Maximum characters per outbound SMS chunk before OpenClaw splits long replies."}}},{"pluginId":"synology-chat","channelId":"synology-chat","order":90,"channelEnvVars":["OPENCLAW_BOT_NAME","SYNOLOGY_ALLOWED_USER_IDS","SYNOLOGY_CHAT_INCOMING_URL","SYNOLOGY_CHAT_TOKEN","SYNOLOGY_NAS_HOST","SYNOLOGY_RATE_LIMIT"],"label":"Synology Chat","description":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"dangerouslyAllowNameMatching":{"type":"boolean"},"dangerouslyAllowInheritedWebhookPath":{"type":"boolean"}},"additionalProperties":{}}},{"pluginId":"telegram","channelId":"telegram","channelEnvVars":["TELEGRAM_BOT_TOKEN"],"label":"Telegram","description":"simplest way to get started โ€” register a bot with @BotFather and get going.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnConte', + 'xt":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Telegram","help":"Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics."},"customCommands":{"label":"Telegram Custom Commands","help":"Additional Telegram bot menu commands (merged with native; conflicts ignored)."},"botToken":{"label":"Telegram Bot Token","help":"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected."},"dmPolicy":{"label":"Telegram DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.telegram.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Telegram Config Writes","help":"Allow Telegram to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Telegram Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Telegram group chat IDs or chatId:topic:threadId topic IDs. Native Telegram bot mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Telegram Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Telegram Mention Pattern Allowlist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Telegram Mention Pattern Denylist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are disab', + 'led. Native bot mentions still trigger."},"commands.native":{"label":"Telegram Native Commands","help":"Override native commands for Telegram (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Telegram Native Skill Commands","help":"Override native skill commands for Telegram (bool or \\"auto\\")."},"streaming":{"label":"Telegram Streaming Mode","help":"Unified Telegram stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"progress\\"). \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate."},"streaming.mode":{"label":"Telegram Streaming Mode","help":"Canonical Telegram preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"progress\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable normal Telegram block replies. This takes precedence over editable preview delivery."},"streaming.block.coalesce":{"label":"Telegram Block Streaming Coalesce","help":"Merge streamed Telegram block replies before sending final delivery."},"streaming.preview.chunk.minChars":{"label":"Telegram Draft Chunk Min Chars","help":"Minimum chars before emitting a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.maxChars":{"label":"Telegram Draft Chunk Max Chars","help":"Target max size for a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.breakPreference":{"label":"Telegram Draft Chunk Break Preference","help":"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence)."},"streaming.preview.toolProgress":{"label":"Telegram Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true when preview streaming is active). Set false to keep tool updates out of the edited Telegram preview."},"streaming.preview.commandText":{"label":"Telegram Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.label":{"label":"Telegram Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Telegram Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Telegram Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Telegram Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Telegram Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Telegram Progress Command Text","help":"Command/exec detail in progress draft lines: \\"status\\" is the safe default; \\"raw\\" opts into command text."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.2 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"network.autoSelectFamily":{"label":"Telegram autoSelectFamily","help":"Override Node autoSelectFamily for Telegram (true=enable, false=disable)."},"network.dangerouslyAllowPrivateNetwork":{"label":"Telegram Dangerously Allow Private Network","help":"Dangerous opt-in for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve api.telegram.org to private/internal/special-use addresses."},"silentErrorReplies":{"label":"Telegram Silent Error Replies","help":"When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false."},"apiRoot":{"label":"Telegram API Root URL","help":"Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked."},"trustedLocalFileRoots":{"label":"Telegram Trusted Local File Roots","help":"Trusted local filesystem roots for self-hosted Telegram Bot API file_path values. Exact in-root paths are read directly; container paths under /var/lib/telegram-bot-api can map into a host volume mount. Other absolute paths are rejected."},"autoTopicLabel":{"label":"Telegram Auto Topic Label","help":"Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: \'...\' } for custom prompt."},"autoTopicLabel.enabled":{"label":"Telegram Auto Topic Label Enabled","help":"Whether auto topic labeling is enabled. Default: true."},"autoTopicLabel.prompt":{"label":"Telegram Auto Topic Label Prompt","help":"Custom prompt for LLM-based topic naming. The user message is appended after the prompt."},"capabilities.inlineButtons":{"label":"Telegram Inline Buttons","help":"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior."},"execApprovals":{"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account."},"execApprovals.enabled":{"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram."},"execApprovals.sessionFilter":{"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions."},"execApprovals.target":{"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Telegram chat/topic, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics."},"threadBindings.enabled":{"label":"Telegram Thread Binding Enabled","help":"Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Telegram Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Telegram Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Telegram Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-bind Telegram current conversations when supported."},"threadBindings.defaultSpawnContext":{"label":"Telegram Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."}}},{"pluginId":"tlon","channelId":"tlon","order":90,"label":"Tlon","description":"decentralized messaging on Urbit; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1},"authorization":{"type":"object","properties":{"channelRules":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"mode":{"type":"string","enum":["restricted","open"]},"allowedShips":{"type":"array","items":{"type":"string","minLength":1}}},"additionalProperties":false}}},"additionalProperties":false},"defaultAuthorizedShips":{"type":"array","items":{"type":"string","minLength":1}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false},"uiHints":{"implicitMentions":{"label":"Tlon Implicit Mentions","help":"Control which Tlon reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Tlon Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Tlon Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Tlon Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."}}},{"pluginId":"twitch","channelId":"twitch","aliases":["twitch-chat"],"channelEnvVars":["OPENCLAW_TWITCH_ACCESS_TOKEN"],"label":"Twitch","description":"Twitch chat integration","schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false},{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false}]}},{"pluginId":"whatsapp","channelId":"whatsapp","label":"WhatsApp","description":"works with your own number; recommend a separate phone + eSIM.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"t', + 'ype":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"default":50,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeatVisibility":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"name":{"type":"string"},"authDir":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"polls":{"type":"boolean"},"calls":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy","mediaMaxMb"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and direct-message routing safety."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"allowFrom":{"presentation":"phone-number"},"defaultTo":{"presentation":"phone-number"},"groupAllowFrom":{"presentation":"phone-number"},"accounts.*.allowFrom.*":{"presentation":"phone-number"},"accounts.*.defaultTo":{"presentation":"phone-number"},"accounts.*.groupAllowFrom.*":{"presentation":"phone-number"},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"direct":{"label":"WhatsApp Direct Chat Overrides","help":"Per-conversation overrides keyed by WhatsApp DM id. Applied after a DM is already admitted by dmPolicy; \\"*\\" supplies a default without admitting anyone."},"pluginHooks":{"label":"WhatsApp Plugin Hooks","help":"Opt in to broadcasting inbound WhatsApp events to plugins. Payloads carry personal content, so only enable it for plugins you trust."},"configWrites":{"label":"WhatsApp Config Writes","help":"Allow WhatsApp to write config in response to channel events/commands (default: true)."},"actions.calls":{"label":"WhatsApp Voice Calls","help":"Expose the experimental requester-bound WhatsApp voice-call tool. Default: false. Requires a separately paired MeowCaller CLI."},"mentionPatterns":{"label":"WhatsApp Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected WhatsApp conversation IDs such as 123@g.us."},"mentionPatterns.mode":{"label":"WhatsApp Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"WhatsApp Mention Pattern Allowlist","help":"WhatsApp conversation IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"WhatsApp Mention Pattern Denylist","help":"WhatsApp conversation IDs where configured regex mention patterns are disabled."}},"unsupportedSecretRefSurfacePatterns":["channels.whatsapp.accounts.*.creds.json","channels.whatsapp.creds.json"]},{"pluginId":"zalo","channelId":"zalo","aliases":["zl"],"order":80,"channelEnvVars":["ZALO_BOT_TOKEN","ZALO_WEBHOOK_SECRET"],"label":"Zalo","description":"Vietnam-focused messaging platform with Bot API.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"store"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"zalouser","channelId":"zalouser","aliases":["zlu"],"order":85,"channelEnvVars":["ZALOUSER_PROFILE","ZCA_PROFILE"],"label":"Zalo Personal","description":"Zalo personal account via QR code login.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name', + '":{"type":"string"},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}}]', ].join(""); export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = JSON.parse( diff --git a/src/config/gateway-public-origin.ts b/src/config/gateway-public-origin.ts new file mode 100644 index 000000000000..958e84d3c167 --- /dev/null +++ b/src/config/gateway-public-origin.ts @@ -0,0 +1,16 @@ +import type { OpenClawConfig } from "./types.js"; + +export function resolveGatewayPublicOrigin( + config: Pick | null | undefined, +): string | undefined { + const raw = config?.gateway?.publicOrigin?.trim(); + if (!raw) { + return undefined; + } + try { + const parsed = new URL(raw); + return parsed.pathname === "/" && !parsed.search && !parsed.hash ? parsed.origin : undefined; + } catch { + return undefined; + } +} diff --git a/src/config/schema.help.runtime.ts b/src/config/schema.help.runtime.ts index d1c581232e34..57e794f453ee 100644 --- a/src/config/schema.help.runtime.ts +++ b/src/config/schema.help.runtime.ts @@ -199,7 +199,7 @@ export const RUNTIME_FIELD_HELP: Record = { "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": "DANGEROUS toggle that enables Host-header based origin fallback for Control UI/WebChat websocket checks. This mode is supported when your deployment intentionally relies on Host-header origin policy; explicit gateway.controlUi.allowedOrigins remains the recommended hardened default.", "gateway.publicOrigin": - "Externally reachable HTTPS origin of the Gateway. HTTP is allowed only for localhost, 127.0.0.1, or [::1]. Per-requester MCP OAuth uses it to build the callback URL at /oauth/mcp/callback; configure the reverse proxy or tunnel to route that path to the Gateway.", + "Externally reachable HTTPS origin of the Gateway. HTTP is allowed only for localhost, 127.0.0.1, or [::1]. Per-requester MCP OAuth uses it to build the callback URL at /oauth/mcp/callback; channel session links and plugin-generated viewer links use it to reach the Control UI and Gateway routes.", "mcp.apps": "MCP Apps UI support. When enabled, configured MCP servers may provide interactive HTML views for their tool results.", "mcp.apps.enabled": diff --git a/src/plugin-sdk/config-contracts.ts b/src/plugin-sdk/config-contracts.ts index 24e82147224c..a5366a696305 100644 --- a/src/plugin-sdk/config-contracts.ts +++ b/src/plugin-sdk/config-contracts.ts @@ -1,5 +1,7 @@ // Focused public config shape types used by bundled and third-party plugins. +export { resolveGatewayPublicOrigin } from "../config/gateway-public-origin.js"; + export type { ChannelGroupPolicy } from "../config/group-policy.js"; export type { SessionScope } from "../config/sessions/types.js"; export type {