@aspalagin/openclaw-max 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +412 -344
  3. package/README_EN.md +492 -0
  4. package/SKILL.md +45 -37
  5. package/assets/icon.png +0 -0
  6. package/dist/secret-contract-api.js +6 -0
  7. package/dist/src/access-policy.js +123 -0
  8. package/dist/src/accounts.js +170 -23
  9. package/dist/src/action-scope.js +213 -0
  10. package/dist/src/actions.js +159 -31
  11. package/dist/src/api.js +99 -83
  12. package/dist/src/callbacks.js +27 -7
  13. package/dist/src/channel-agent-prompt.js +8 -19
  14. package/dist/src/channel-config.js +25 -3
  15. package/dist/src/channel-lifecycle.js +28 -5
  16. package/dist/src/channel-outbound.js +43 -9
  17. package/dist/src/channel.js +16 -0
  18. package/dist/src/command-menu.js +199 -0
  19. package/dist/src/config-schema.js +106 -10
  20. package/dist/src/deliver.js +144 -48
  21. package/dist/src/dispatch.js +12 -11
  22. package/dist/src/gateway-drain.js +40 -0
  23. package/dist/src/group-mention.js +129 -0
  24. package/dist/src/inbound-attachments.js +162 -20
  25. package/dist/src/inbound.js +388 -134
  26. package/dist/src/media-temp.js +31 -20
  27. package/dist/src/network.js +216 -0
  28. package/dist/src/onboarding.js +4 -2
  29. package/dist/src/polling.js +180 -12
  30. package/dist/src/presentation.js +11 -0
  31. package/dist/src/progress-draft.js +168 -0
  32. package/dist/src/runtime.js +15 -0
  33. package/dist/src/secret-contract.js +149 -0
  34. package/dist/src/send.js +250 -46
  35. package/dist/src/stream-draft.js +83 -41
  36. package/dist/src/turn-adoption.js +24 -0
  37. package/dist/src/typing.js +55 -0
  38. package/dist/src/update-journal.js +267 -0
  39. package/dist/src/webhook-queue.js +151 -26
  40. package/dist/src/webhook-runner.js +61 -9
  41. package/dist/src/webhook.js +34 -15
  42. package/openclaw.plugin.json +986 -13
  43. package/package.json +25 -5
  44. package/src/access-policy.ts +154 -0
  45. package/src/accounts.ts +260 -25
  46. package/src/action-scope.ts +274 -0
  47. package/src/actions.ts +188 -35
  48. package/src/api.ts +158 -101
  49. package/src/callbacks.ts +26 -7
  50. package/src/channel-agent-prompt.ts +8 -19
  51. package/src/channel-config.ts +32 -3
  52. package/src/channel-lifecycle.ts +33 -6
  53. package/src/channel-outbound.ts +50 -7
  54. package/src/channel.ts +16 -0
  55. package/src/command-menu.ts +277 -0
  56. package/src/config-schema.ts +111 -10
  57. package/src/deliver.ts +174 -55
  58. package/src/dispatch.ts +12 -11
  59. package/src/gateway-drain.ts +45 -0
  60. package/src/group-mention.ts +191 -0
  61. package/src/inbound-attachments.ts +173 -22
  62. package/src/inbound.ts +443 -130
  63. package/src/media-temp.ts +35 -23
  64. package/src/network.ts +233 -0
  65. package/src/onboarding.ts +4 -2
  66. package/src/polling.ts +221 -15
  67. package/src/presentation.ts +14 -1
  68. package/src/progress-draft.ts +226 -0
  69. package/src/runtime.ts +23 -0
  70. package/src/secret-contract.ts +183 -0
  71. package/src/send.ts +346 -59
  72. package/src/stream-draft.ts +102 -41
  73. package/src/turn-adoption.ts +33 -0
  74. package/src/types.ts +9 -3
  75. package/src/typing.ts +77 -0
  76. package/src/update-journal.ts +302 -0
  77. package/src/webhook-queue.ts +205 -33
  78. package/src/webhook-runner.ts +65 -8
  79. package/src/webhook.ts +39 -18
  80. package/scripts/check-cycles.mjs +0 -38
  81. package/scripts/test-api.mjs +0 -169
  82. package/scripts/test-send.mjs +0 -162
  83. package/scripts/update-schema.mjs +0 -48
package/README_EN.md ADDED
@@ -0,0 +1,492 @@
1
+ # openclaw-max — MAX messenger channel for OpenClaw
2
+
3
+ [Русская версия](README.md)
4
+
5
+ A channel plugin that connects an [OpenClaw](https://openclaw.ai) assistant to the [MAX](https://max.ru) messenger through the MAX Bot API (`platform-api2.max.ru`). People write to your bot in MAX — in a private dialog or in a group — and the OpenClaw agent answers there, with media, buttons and voice.
6
+
7
+ It is for OpenClaw users who need their assistant reachable in MAX. You need a MAX bot token (bots are created by organisations on [business.max.ru](https://business.max.ru/self)) and an OpenClaw gateway you run yourself.
8
+
9
+ Version 0.8.0. Changes: [CHANGELOG.md](https://github.com/aspalagin/openclaw-max/blob/main/CHANGELOG.md) (in Russian).
10
+
11
+ ## Contents
12
+
13
+ - [Supported message types](#supported-message-types)
14
+ - [Requirements and compatibility](#requirements-and-compatibility)
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [Transports: webhook and long polling](#transports-webhook-and-long-polling)
18
+ - [Configuration reference](#configuration-reference)
19
+ - [Access and policies](#access-and-policies)
20
+ - [`message` tool actions and their scope](#message-tool-actions-and-their-scope)
21
+ - [Media and local files](#media-and-local-files)
22
+ - [Voice](#voice)
23
+ - [Commands and menus](#commands-and-menus)
24
+ - [Turn status](#turn-status)
25
+ - [Multiple accounts and inheritance](#multiple-accounts-and-inheritance)
26
+ - [Proxy, API address and the Russian Trusted CA](#proxy-api-address-and-the-russian-trusted-ca)
27
+ - [Secrets and SecretRef](#secrets-and-secretref)
28
+ - [Reliability](#reliability)
29
+ - [MAX API limits](#max-api-limits)
30
+ - [Security](#security)
31
+ - [Privacy](#privacy)
32
+ - [Troubleshooting](#troubleshooting)
33
+ - [Upgrading from 0.7](#upgrading-from-07)
34
+ - [Development](#development)
35
+ - [Acknowledgements, authors and license](#acknowledgements-authors-and-license)
36
+
37
+ ## Supported message types
38
+
39
+ | Type | Incoming (MAX → agent) | Outgoing (agent → MAX) |
40
+ |---|---|---|
41
+ | Text | Yes; mentions detected from MAX markup, `@username` and core mention patterns | Yes; markdown converted to the MAX dialect (`++underline++`), split at 4000 characters; if MAX rejects the markup, resent once as plain text |
42
+ | Image | Downloaded and passed to the agent as media | Yes; several images and videos go as albums of up to 12; a public https image URL is passed to MAX as a link |
43
+ | Video | Downloaded (size limit `mediaMaxMb`) | Yes |
44
+ | Audio and voice | Downloaded; the MAX transcript is used when present, otherwise OpenClaw core transcribes | Yes; `asVoice` / `audioAsVoice` sends a MAX `audio` attachment |
45
+ | File | Downloaded | Yes, from an allowed local path, a URL or inline `buffer` |
46
+ | Sticker | Sticker code and image | `sticker` action by code |
47
+ | Contact | Name and phones parsed from the VCard, MAX profile | `sendAttachment` with `type="contact"` |
48
+ | Location | Coordinates and a map link | `sendAttachment` with `type="location"` |
49
+ | Share card | Title, description and link | — |
50
+ | Forwarded message | Text and attachments with the original author; the sender's own comment comes first | — |
51
+ | Reply | Quote of the replied-to message (up to 1000 characters) and its author | Yes, as a MAX reply (`replyTo`) |
52
+ | Inline keyboard | Button presses: callbacks, approvals, command menus | 7 button types (callback, link, message, clipboard, open_app, request_contact, request_geo_location); OpenClaw `presentation` blocks rendered as text and keyboards |
53
+ | Edited message | Processed again as a new message | `edit` action; draft streaming edits one message |
54
+ | Deleted message | Ignored (logged at debug level) | `delete` action |
55
+ | Pin | — | `pin` / `unpin` in groups and channels; `delivery.pin` |
56
+ | Bot start (deep link) | Delivered to the agent as `/start` or `/start <payload>`; like messages, skipped when older than `maxEventAgeMinutes` | — |
57
+ | Typing indicator, read receipt | — | `typing_on` for the whole turn; `mark_seen` on incoming messages (`markSeen`) |
58
+
59
+ Reactions and polls are not supported.
60
+
61
+ ## Requirements and compatibility
62
+
63
+ - OpenClaw ≥ 2026.9.6 (`peerDependencies.openclaw`, `openclaw.compat.minGatewayVersion`). Older releases do not work, checked against 2026.9.3–2026.9.5 by typecheck and the test suite: 2026.9.5 lacks `createLivePreviewLifecycle` in `openclaw/plugin-sdk/channel-outbound`, so the plugin module fails to load (turn status is built on it); 2026.9.3 and 2026.9.4 also lack the sender check for answering agent questions with buttons (`authorize` in `resolveOption`), the guarded heartbeat typing hook and the plan-explanation format. Upgrade OpenClaw to use this plugin.
64
+ - Node.js ≥ 22.
65
+ - MAX Bot API as described by schema 0.0.33 and [dev.max.ru/docs-api](https://dev.max.ru/docs-api).
66
+ - For webhook mode: a public HTTPS address on port 443 with a trusted certificate that reaches the gateway's HTTP server.
67
+
68
+ **Verification status.** Version 0.8.0 has been in production use since 27 September 2026 with OpenClaw 2026.9.6 in webhook mode. Verified live: start and webhook subscription, channel status (`mode`, `tokenStatus`), text in a direct chat, long answers in parts, core commands, sends through the `message` tool, including a file from the agent workspace and the refusal of a file of a type that is not allowed, and recovery of the durable queue after a real gateway restart — a message core refused while the gateway was shutting down waited for the next start and was answered. Versions 0.7.x were in production use before that with text, media, buttons, both transports and incoming voice transcription over webhook. The rest of what is new in 0.8.0 is covered by the automated test suite and has passed an independent code review, but has not been checked live yet — among it the HTTP proxy and `apiBaseUrl`, SecretRef resolution at gateway start, turn status, command menus, voice replies, forwarded messages, groups and long polling.
69
+
70
+ ## Installation
71
+
72
+ From ClawHub:
73
+
74
+ ```bash
75
+ openclaw plugins install clawhub:@aspalagin/openclaw-max
76
+ ```
77
+
78
+ From npm:
79
+
80
+ ```bash
81
+ openclaw plugins install npm:@aspalagin/openclaw-max
82
+ ```
83
+
84
+ > Use the scoped name `@aspalagin/openclaw-max`. The unscoped npm package `openclaw-max` is a different project (see [Acknowledgements](#acknowledgements-authors-and-license)).
85
+
86
+ Restart the gateway after installing so it loads the plugin. The plugin id in OpenClaw is `openclaw-max`, the channel id is `max`.
87
+
88
+ To update: `openclaw plugins update openclaw-max` for an npm install, or `openclaw plugins install clawhub:@aspalagin/openclaw-max --force` for a ClawHub install; then restart the gateway. Read [Upgrading from 0.7](#upgrading-from-07) first.
89
+
90
+ ## Quick start
91
+
92
+ 1. **Create a bot.** On [business.max.ru](https://business.max.ru/self) (a registered organisation or sole trader is required) open **Chat bots → Create**, wait for moderation, then copy the token under **Chat bots → Integration**.
93
+ 2. **Store the token** in a file readable by the gateway user, for example `/home/you/.openclaw/secrets/max-bot-token` (mode `0600`). The path is used as given: use an absolute path; `~` is not expanded. It must be a regular file: a symbolic link is not read, as if the file were missing.
94
+ 3. **Configure** `~/.openclaw/openclaw.json`:
95
+
96
+ ```jsonc
97
+ {
98
+ "channels": {
99
+ "max": {
100
+ "enabled": true,
101
+ "tokenFile": "/home/you/.openclaw/secrets/max-bot-token",
102
+ "dmPolicy": "pairing",
103
+ // Webhook (recommended by MAX for production); omit both keys for long polling
104
+ "webhookUrl": "https://bot.example.com/max/webhook",
105
+ "webhookSecretFile": "/home/you/.openclaw/secrets/max-webhook-secret"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ 4. **Restart the gateway**: `openclaw gateway restart`.
112
+ 5. **Check the account**: `openclaw channels status` shows the account, `tokenSource`, `tokenStatus` and `mode`; `openclaw channels status --probe` also calls the MAX API.
113
+ 6. **Write to the bot** in MAX. With `dmPolicy: "pairing"` you get a pairing code; approve it with `openclaw pairing approve max <code>` (`openclaw pairing list max` lists pending requests with the sender's user id). Put your user id into `allowFrom` to skip pairing.
114
+
115
+ The token can also come from `botToken` (a string or a [SecretRef](#secrets-and-secretref)) or from the `MAX_BOT_TOKEN` environment variable (top-level account only). Order: `botToken`, then `tokenFile`, then `MAX_BOT_TOKEN`.
116
+
117
+ ## Transports: webhook and long polling
118
+
119
+ **Use webhook mode in production.** MAX documents long polling as a development and testing mode, limited in speed and event retention, and asks for webhooks in production ([GET /updates](https://dev.max.ru/docs-api/methods/GET/updates)). Other plugins also report that voice messages may arrive empty or not at all over long polling (notably from Android); MAX does not document this and we have not confirmed it yet — over webhook, voice messages arrive and are transcribed.
120
+
121
+ `transport` selects the mode: `webhook` when `webhookUrl` is set, otherwise `polling`. `transport: "webhook"` without `webhookUrl` is a config error.
122
+
123
+ ### Webhook
124
+
125
+ - **MAX requirements:** HTTPS on port 443 with a certificate from a trusted CA; a `200` answer within 30 seconds; MAX retries failed deliveries and removes the subscription after 8 hours of failures. Each request carries the `X-Max-Bot-Api-Secret` header. While a webhook subscription is active, long polling does not work.
126
+ - **Route:** the plugin serves the webhook on the gateway's own HTTP server (no extra port). Path: `webhookPath`, otherwise the path of `webhookUrl`, otherwise `/max/webhook`. Put a reverse proxy or tunnel with a valid certificate in front of the gateway and forward only this path.
127
+ - **Secret:** `webhookSecret`, else `webhookSecretFile`, else generated once and kept in the account state file. Format required by MAX: 5–256 characters `A–Z a–z 0–9 _ -`. The secret is compared in constant time before the body is read; a mismatch gets `401`.
128
+ - **Subscription:** at start the plugin removes this bot's subscriptions to other URLs and subscribes `webhookUrl`. Every 12 minutes it checks that the subscription still exists and recreates it if MAX removed it. On stop the subscription is kept, so events that arrive during a restart are redelivered by MAX.
129
+ - **Processing:** the HTTP handler checks the secret, the body and duplicates, records the event (see [Reliability](#reliability)) and answers; the agent runs in the account task — in order within a chat, up to 4 chats in parallel.
130
+ - **Turning it on:** deploy the plugin and restart the gateway first, then add `webhookUrl` and restart again; check that the log shows `MAX webhook subscribed` and that a message reaches the agent.
131
+ - **Rolling back:** set `transport: "polling"` (or remove `webhookUrl`) and restart; the polling start removes the subscription. If the plugin cannot start, remove it by hand: `curl -X DELETE "<api>/subscriptions?url=<webhookUrl>" -H "Authorization: <token>"`, where `<api>` is your `apiBaseUrl` or, if it is not set, `https://platform-api2.max.ru`.
132
+
133
+ ### Long polling
134
+
135
+ The plugin polls `GET /updates` and stores the marker in the account state file after each batch, so a restart continues where it stopped. After an error it waits 2 s, doubling up to 60 s with jitter, honours `Retry-After`, and after `401` retries every 5 minutes. At start, an active webhook subscription of the bot is removed with a warning (MAX does not serve `GET /updates` while it exists).
136
+
137
+ ## Configuration reference
138
+
139
+ All options live under `channels.max`; the same keys (except `accounts` and `commands`) are accepted under `channels.max.accounts.<id>`. "Own" options belong to one bot and are not inherited by named accounts; everything else is inherited from the channel level (see [Multiple accounts](#multiple-accounts-and-inheritance)).
140
+
141
+ | Option | Type | Default | Description |
142
+ |---|---|---|---|
143
+ | `enabled` | boolean | `true` | `false` at channel level disables all accounts |
144
+ | `botToken` | string or SecretRef | — | Bot token. Own |
145
+ | `tokenFile` | string | — | Absolute path to a regular file with the token. Own |
146
+ | `name` | string | — | Display name of the account. Own |
147
+ | `transport` | `polling` \| `webhook` | `webhook` if `webhookUrl` is set, else `polling` | Own |
148
+ | `webhookUrl` | string | — | Public HTTPS URL MAX posts to. Own |
149
+ | `webhookSecret` | string or SecretRef | generated | Webhook secret. Own |
150
+ | `webhookSecretFile` | string | — | File with the webhook secret (regular file, not a symlink). Own |
151
+ | `webhookPath` | string | path of `webhookUrl`, else `/max/webhook` | Gateway route path. Own |
152
+ | `webhookQueue.mode` | `durable` \| `memory` | `durable` | Journal accepted webhook events on disk or keep them in memory only |
153
+ | `webhookQueue.maxPending` | integer | `5000` | Accepted but unprocessed events |
154
+ | `webhookQueue.overflow` | `reject` \| `drop` | `reject` | When full: answer `503` so MAX retries, or acknowledge and drop |
155
+ | `maxEventAgeMinutes` | integer ≥ 0 | `60` | Skip messages, edits, button presses and bot starts older than this (by MAX event time); `0` — no limit |
156
+ | `dmPolicy` | `pairing` \| `allowlist` \| `open` \| `disabled` | `pairing` | Who may write in private dialogs |
157
+ | `allowFrom` | array of user ids | `[]` | Allowed private senders; `"*"` — anyone (required with `open`) |
158
+ | `groupPolicy` | `allowlist` \| `open` \| `disabled` | `allowlist` (or core `channels.defaults.groupPolicy`) | Which groups the bot answers in |
159
+ | `groups` | object keyed by chat id or `"*"` | `{}` | Allowed groups and per-group settings, see below |
160
+ | `groupAllowFrom` | array of user ids | — | Senders allowed in admitted groups (when the group has no own `allowFrom`) |
161
+ | `mentionPatterns` | `{ mode: "allow" \| "deny", allowIn, denyIn }` | allow | Where core mention patterns apply in MAX groups |
162
+ | `actionScope` | `admitted` \| `current` \| `off` | `admitted` | Chats the `message` tool may act in |
163
+ | `notify` | boolean | MAX default (notify) | Push notifications for sends; `false` — silent (not possible in channels) |
164
+ | `disableLinkPreview` | boolean | MAX default (previews on), off for tool summaries | Link previews for sends; a set value applies to tool summaries too |
165
+ | `markSeen` | boolean | `true` | Mark incoming messages as read |
166
+ | `mediaMaxMb` | number | `20` | Size limit for downloaded and uploaded media, MB |
167
+ | `mediaMaxCount` | integer | `12` | Media attachments downloaded per incoming message, forwards included |
168
+ | `streamMode` | `off` \| `partial` \| `block` | `off` | `partial` — one draft message edited while the answer streams; `block` — core block replies |
169
+ | `streaming` | core streaming config | off | `streaming.mode` (`off`, `partial`, `block`, `progress`) wins over `streamMode`; `streaming.progress.*` configures [turn status](#turn-status) |
170
+ | `textChunkLimit` | integer | `4000` | Characters per outgoing message, account value first; values above MAX's 4000 are capped |
171
+ | `responsePrefix` | string | — | Prefix of agent replies, applied by OpenClaw core (`"auto"` — agent name) |
172
+ | `historyLimit` | integer ≥ 0 | core default | Read by OpenClaw core: recent turns of a group session the embedded agent runtime keeps in the prompt (native CLI runtimes keep their own history) |
173
+ | `dmHistoryLimit`, `dms.<userId>.historyLimit` | integer ≥ 0 | no limit | Read by OpenClaw core: the same for private dialogs with a per-channel session (`session.dmScope`), per user first |
174
+ | `actions` | object `{ <action>: boolean }` | all on | `false` turns a `message` tool action off (`send`, `edit`, `delete`, `sticker`, `sendAttachment`, `pin`, `unpin`): hidden from the tool and refused |
175
+ | `apiBaseUrl` | string | `https://platform-api2.max.ru` | Bot API base URL; `https` only, `http` just for loopback |
176
+ | `httpProxy` | string or SecretRef | — | HTTP(S) proxy for all MAX traffic; `""` in an account turns an inherited proxy off |
177
+ | `logMessagePreview` | boolean | `false` | Add a 50-character text preview to debug logs |
178
+ | `commands` | array of `{ name, description }` | — | Channel level only: bot commands registered in MAX (up to 32; name ≤ 64 characters without `/`, description ≤ 128) |
179
+ | `accounts` | object | — | Channel level only: named accounts |
180
+
181
+ Per-group settings (`groups.<chatId>` or `groups["*"]`):
182
+
183
+ | Key | Default | Description |
184
+ |---|---|---|
185
+ | `requireMention` | `true` | Answer only when the bot is mentioned, replied to or its button is pressed |
186
+ | `allowFrom` | — | Senders allowed in this group (overrides `groupAllowFrom`) |
187
+ | `tools` | — | Core tool policy for this group |
188
+ | `disableAudioPreflight` | `false` | Do not transcribe captionless voice messages to look for a mention |
189
+ | `enabled` | `true` | `false` — ignore this group under any `groupPolicy` |
190
+ | `systemPrompt` | — | Extra system prompt for turns in this group |
191
+ | `skills` | all | Skills a turn in this group may load; `[]` — none |
192
+
193
+ Accepted by the schema for compatibility with the common channel config shape but **without effect in MAX** — neither the plugin nor OpenClaw core reads them for this channel: `markdown` (table rendering), `blockStreaming` (use `streamMode` or `streaming.mode: "block"`), `blockStreamingCoalesce` (core reads `streaming.block.coalesce`). They stay in the schema so existing configs keep validating.
194
+
195
+ ## Access and policies
196
+
197
+ **Private dialogs** (`dmPolicy`):
198
+
199
+ - `pairing` (default) — unknown senders get a pairing code; the owner approves it with `openclaw pairing approve max <code>`. Senders in `allowFrom` need no pairing.
200
+ - `allowlist` — only user ids in `allowFrom`.
201
+ - `open` — anyone; requires `allowFrom: ["*"]`.
202
+ - `disabled` — no private messages.
203
+
204
+ **Groups** (`groupPolicy`):
205
+
206
+ - `allowlist` (default) — only chats listed in `groups` (or any chat if `groups` has a `"*"` entry).
207
+ - `open` — any group the bot is in.
208
+ - `disabled` — no group messages.
209
+
210
+ In an admitted group, a non-empty `groups.<id>.allowFrom` (else `groupAllowFrom`) restricts who can talk to the bot: messages, button presses, commands and voice checks from other members are ignored, and their attachments are not downloaded. `"*"` allows anyone; the `max:` prefix is accepted in ids. The private `allowFrom` list does **not** apply to groups.
211
+
212
+ **Mentions.** With `requireMention` (default `true`) the bot answers in a group when it is mentioned (`@username` or a MAX mention), when someone replies to its message or presses its button, or when the text matches mention patterns configured in OpenClaw core (`messages.groupChat.mentionPatterns` or the agent's `groupChat.mentionPatterns`). Patterns that core derives from the agent's name are not used in MAX — only explicitly configured ones. `channels.max.mentionPatterns` limits where patterns apply: `{ "mode": "deny" }` turns them off for MAX, `allowIn` / `denyIn` list group ids. A captionless voice message in such a group is checked against the patterns using the MAX transcript or one transcription by core (only for admitted groups and senders).
213
+
214
+ **Approvals and questions.** Approval buttons can be pressed only by senders listed explicitly in `allowFrom`; for `ask_user` questions a `"*"` entry is enough — in a group such an answer also needs the group admitted and the sender on its sender list (`groups.<id>.allowFrom` / `groupAllowFrom`), if it has one.
215
+
216
+ **Finding a user id.** `openclaw pairing list max` shows the id of a sender waiting for pairing. Group chat ids are negative numbers.
217
+
218
+ A named account that inherits an `open` policy logs a warning at start with the option path to set. Mind that inheritance when adding a second bot.
219
+
220
+ ## `message` tool actions and their scope
221
+
222
+ Actions: `send`, `sendAttachment`, `sticker`, `edit`, `delete`, `pin`, `unpin`.
223
+
224
+ Targets: `user:<id>` for a private dialog, a numeric chat id for a group or channel (for example `-70000000000001`). `@username` and max.ru links are not supported by the MAX Bot API and are rejected with a clear error. A bare positive number that MAX answers with `dialog.not.found` is retried once as a user id.
225
+
226
+ ```
227
+ message(action="send", target="user:12345678", message="Hello")
228
+ message(action="send", target="CHAT_ID", message="Choose:",
229
+ buttons=[[{"text":"Yes","type":"callback","payload":"yes"},
230
+ {"text":"More","type":"message"}]])
231
+ message(action="sendAttachment", target="CHAT_ID", path="report.pdf", caption="Report")
232
+ message(action="sendAttachment", target="CHAT_ID", buffer="data:text/plain;base64,SGVsbG8=", filename="hello.txt")
233
+ message(action="send", target="CHAT_ID", path="reply.ogg", asVoice=true)
234
+ message(action="send", target="CHAT_ID", message="Quiet", silent=true)
235
+ message(action="sendAttachment", target="CHAT_ID", type="location", latitude="55.75", longitude="37.62")
236
+ message(action="sendAttachment", target="CHAT_ID", type="contact", contactName="Name", vcfPhone="+70000000000")
237
+ message(action="sticker", target="CHAT_ID", stickerId="CODE")
238
+ message(action="pin", target="CHAT_ID", messageId="MID")
239
+ ```
240
+
241
+ - `attachments=[…]` sends several files; images and videos go as albums of up to 12, audio and files one by one. The result has `messageIds`; failed items are listed in `mediaErrors`.
242
+ - `silent=true` sends without a push notification; MAX channels always notify.
243
+ - `pin=true` or `delivery.pin` pins the sent message. MAX has no pinning in private dialogs: the plugin skips the call and returns `pinned: false` with a reason.
244
+
245
+ **Scope (`actionScope`).** In a turn started by someone other than the owner, actions work only in the current chat and in chats the inbound policy admits (dialogs with senders in `allowFrom` or paired, groups in `groups` and, when a group has a sender list, only if the requester is on it). Only calls OpenClaw marks as the owner's (`senderIsOwner: true`) are not limited: the owner's own turns, `openclaw message` from the CLI, an admin's chat in the Control UI. Every other call — including runs without a conversation whose owner mark is missing or false, as heartbeat, subagent, scheduled and plugin- or hook-started runs may be — is limited to the admitted chats; in a group with a sender list it is refused, as there is no requester to check. Core deliveries (agent replies, reminders, automation announcements) do not go through the `message` tool and are not affected. A `send` with `presentation` (cards, buttons), which core delivers without the plugin's action handler, is checked the same way, and `actions.send: false` applies to it too. The current chat counts only for the account the turn came through: an action in it through another MAX account (`accountId`) is checked against that account's policy. A refused action is a tool error raised before anything changes in MAX; if the plugin cannot determine the chat of a message, it refuses. Outside the chat of the current turn, `edit` and `delete` work only on messages the bot sent itself (the author comes from the same lookup as the chat); in the current chat and for the owner they work as before. A `send`, `sendAttachment` or `sticker` with `replyTo` (and a `send` with `presentation`) may reply only to a message of the chat it goes to (checked with one lookup, made only when `replyTo` is set); a reply to a message of another chat, or one whose chat cannot be determined, is refused.
246
+
247
+ - `admitted` (default) — as above.
248
+ - `current` — non-owner turns act only in their own chat.
249
+ - `off` — no check by the plugin (0.7 behaviour).
250
+
251
+ This stops a prompt injection in one chat from editing, deleting or posting in another.
252
+
253
+ ## Media and local files
254
+
255
+ - **Incoming** media is downloaded after the access checks, within `mediaMaxMb` (default 20 MB) and `mediaMaxCount` (default 12 per message); the agent gets a note about media that was not loaded. Files are stored by OpenClaw core's media store.
256
+ - **Outgoing local files** are read only through the OpenClaw SDK loader from the directories core allows for the agent: its workspace, the gateway media directories and what the core filesystem policy permits. Paths outside them — including via `..` or symbolic links — fail with `Local media path is not under an allowed directory: …`. To send a file from elsewhere, copy it into the agent workspace first. When core policy grants the agent host reads, the directory is not limited, but core checks the type by content: images, audio, video, PDF, Office documents, archives and plain-text documents (`.txt`, `.md`, `.csv`, `.json`, `.yaml`) are sent, anything else fails with `Host-local media sends only allow …`. There is no option to turn these checks off.
257
+ - **Inline content:** `buffer` (base64 or a data URL) with `filename` and `contentType`; the size limit is checked before decoding. If both a path and `buffer` are given, the path is used.
258
+ - **Remote URLs** are fetched through core's SSRF-guarded downloader into memory. An image URL is passed to MAX as a link only when its host is public https; otherwise the image is downloaded and uploaded.
259
+
260
+ ## Voice
261
+
262
+ **Incoming voice messages** are passed to OpenClaw core as audio media. If MAX supplied a transcript, the agent gets it as `[Voice transcript: …]` and core does not transcribe again; otherwise core transcribes the audio with the provider configured in `tools.media.audio`. A voice message that could not be downloaded and has no transcript reaches the agent as `[Voice message: audio unavailable, no transcript]`.
263
+
264
+ **Voice replies.** The channel tells core it accepts audio files (`mp3`, `m4a`, `wav`, `ogg`, `opus`) for TTS. When a reply is marked as voice — core TTS (for example `/tts`), the `[[audio_as_voice]]` directive or `asVoice=true` on `send` / `sendAttachment` — the audio goes out as a MAX `audio` attachment; the Bot API has no separate voice-note type. The reply text is delivered once, as a separate message. If MAX rejects the audio, the same bytes are sent as a file.
265
+
266
+ ## Commands and menus
267
+
268
+ - `channels.max.commands` registers the bot's command list in MAX (`PATCH /me/commands`) at account start: up to 32 commands.
269
+ - A message starting with `/` is handled as an OpenClaw command.
270
+ - Commands for which core defines choices — `/think`, `/fast`, `/reasoning`, `/verbose`, `/usage`, `/elevated`, `/trace`, `/activation`, `/send`, `/tts`, `/session`, `/subagents`, `/acp`, `/tools` — sent without an argument answer with a menu of buttons; the current choice is marked ✓. A press applies the command exactly like typing `/think high`. Menus and presses are available only to senders allowed to run commands.
271
+ - A bot start from a deep link (`max.ru/<bot>?start=<payload>`) reaches the agent as `/start <payload>`.
272
+
273
+ ## Turn status
274
+
275
+ With core's progress streaming, one status message shows what the agent is doing (status line, plan, approval requests and, with `toolProgress`, tool lines). It is edited at most once per second, sent without a notification and deleted after the answer is delivered. It is off by default.
276
+
277
+ ```jsonc
278
+ "channels": { "max": { "streaming": { "mode": "progress", "progress": { "toolProgress": true } } } }
279
+ ```
280
+
281
+ Other `streaming.progress` keys (`label`, `labels`, `maxLines`, `maxLineChars`, `commandText`) are core settings. If status sending fails, only the status stops; the turn is not affected.
282
+
283
+ ## Multiple accounts and inheritance
284
+
285
+ Several MAX bots can run from one gateway. The top-level `channels.max` is the default account; named accounts live under `accounts`:
286
+
287
+ ```jsonc
288
+ {
289
+ "channels": {
290
+ "max": {
291
+ "tokenFile": "/home/you/.openclaw/secrets/max-main-token",
292
+ "dmPolicy": "allowlist",
293
+ "allowFrom": ["12345678"],
294
+ "maxEventAgeMinutes": 30,
295
+ "accounts": {
296
+ "support": {
297
+ "tokenFile": "/home/you/.openclaw/secrets/max-support-token",
298
+ "dmPolicy": "open",
299
+ "allowFrom": ["*"]
300
+ // inherits maxEventAgeMinutes, groupPolicy, groups, … from channels.max
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ ```
307
+
308
+ A named account takes every option it does not set from the channel level, including access policies and lists; its own value wins, and lists and `groups` are replaced as a whole. Own options are not inherited: `botToken`, `tokenFile`, `name`, `transport`, `webhookUrl`, `webhookSecret`, `webhookSecretFile`, `webhookPath`. `channels.max.enabled: false` disables every account; `accounts.<id>.enabled: false` disables one. `MAX_BOT_TOKEN` applies to the top-level account only.
309
+
310
+ Select an account from the CLI with `--account`: `openclaw message send --channel max --account support --target user:87654321 --message "Hi"`.
311
+
312
+ ## Proxy, API address and the Russian Trusted CA
313
+
314
+ - **`httpProxy`** (for example `http://proxy.example.com:3128`, credentials allowed) routes all MAX traffic of the account through an HTTP(S) proxy: Bot API calls, uploads, and downloads of incoming attachments and remote media. Loopback addresses bypass it. Proxy credentials never appear in logs or errors; the start log shows `http://***@host:port`.
315
+ - **`apiBaseUrl`** points the plugin at another Bot API address, such as a test stand; the path prefix is kept. Only `https` is accepted (`http` only for loopback), so the token is never sent in clear text.
316
+ - An invalid value of either option stops the account at start with an error naming the option.
317
+ - **Russian Trusted CA.** Since July 2026 the Bot API at `platform-api2.max.ru` uses a certificate issued by the Russian Trusted Sub CA of the Ministry of Digital Development (Минцифры), which is not in Node.js's default trust store. The plugin ships the Russian Trusted Root CA and Sub CA certificates (`src/russian-trusted-ca.ts`) and adds them, next to the system CAs, only to its own connections to MAX — also inside a proxy tunnel. The process-wide trust store is not changed and `NODE_EXTRA_CA_CERTS` is not needed.
318
+
319
+ ## Secrets and SecretRef
320
+
321
+ `botToken`, `webhookSecret` and `httpProxy` accept a plain string or an OpenClaw SecretRef:
322
+
323
+ ```jsonc
324
+ "botToken": { "source": "env", "provider": "default", "id": "MAX_BOT_TOKEN" }
325
+ ```
326
+
327
+ The provider must exist in core's `secrets.providers`. The paths `channels.max.botToken`, `webhookSecret`, `httpProxy` (and their `accounts.<id>` forms) are visible to `openclaw secrets configure`, `apply` and `audit`. The gateway resolves references before the account starts. An account whose reference does not resolve does not start — the error names the option and the reference, without its value and without falling back to `tokenFile` or `MAX_BOT_TOKEN`; other accounts keep running. `openclaw secrets audit` reports a token, webhook secret or proxy written as a plain string in `openclaw.json`.
328
+
329
+ `tokenFile` and `webhookSecretFile` remain supported. `openclaw channels status` shows `tokenSource` (`config`, `file`, `env`, `none`) and `tokenStatus` (`available`, `configured_unavailable`, `missing`) without the token.
330
+
331
+ ## Reliability
332
+
333
+ - **Durable webhook queue.** Each accepted webhook event is written to `<stateDir>/max/inbox-<account>/` before MAX gets `200`. After a restart or crash, unprocessed events are handled first, in the original order within each chat. An event is finished once core has accepted the agent turn (core resumes an interrupted turn itself). If the event cannot be written or the queue is full (`webhookQueue.maxPending`), MAX gets `503` and retries later (`overflow: "drop"` acknowledges and drops instead). If the state directory is not writable, the queue falls back to memory with a warning. The SDK's own ingress queue is available only to bundled and official plugins in OpenClaw 2026.9.x, so the plugin keeps its own journal with the same contract.
334
+ - **Deduplication.** Keys of handled events (`update_type:timestamp:mid`; for events without a message — the chat and user instead of `mid`) are kept for 24 hours, up to 5000, and survive restarts, so MAX redeliveries are dropped. Long polling uses the same keys and, like the webhook queue, marks an event handled once core has accepted its turn: after a restart or crash in the middle of a batch — even in the middle of a turn — handled events are not repeated, and core resumes an interrupted turn instead of it being started again.
335
+ - **Gateway shutdown.** While the gateway stops or restarts gracefully, core accepts no new agent turns, but the webhook and long polling keep receiving events. Such an event is not lost: the plugin keeps it queued and retries after 2, 5, 10 and then every 30 seconds, with the chat's later events waiting behind it. If the gateway stops first, the event stays in the journal on disk and is handled after the start (long polling also does not save the batch marker). A refusal after core has accepted the turn and other processing errors are not retried. With the queue in memory (`webhookQueue.mode: "memory"` or an unwritable state directory) such an event is still skipped with an error in the log.
336
+ - **Event age.** Messages, edits, button presses and bot starts older than `maxEventAgeMinutes` (default 60) are skipped with a warning, so after a long outage the bot does not answer hours-old messages. Chat registry events are always processed.
337
+ - **Delivery errors** are reported to core: nothing sent → the reply is not dispatched; part visible → partial delivery with the visible message ids. After a failed text chunk the rest is not sent.
338
+ - **Long answers** are split at 4000 characters (or a lower `textChunkLimit`); with `streamMode: "partial"` the first chunk replaces the draft and the rest follow as new messages, buttons on the last one.
339
+ - **Answers in several parts.** When core delivers an answer as several parts, only the first goes into the streaming draft or answers the button press (`POST /answers`); the other parts, and the blocks of `streamMode: "block"`, follow as new messages, so no part overwrites another.
340
+ - **Streaming draft** (`streamMode: "partial"`). Draft updates are sent one at a time, so there is never a second draft; the final answer waits for a draft send already in flight. Tool output (verbose mode) goes as separate messages while the draft keeps streaming. A draft that did not become the answer — `NO_REPLY`, `/stop`, a reply via the `message` tool, or an answer of media only — is deleted at the end of the turn; after a processing error it stays.
341
+ - **Retries:** `429` and network errors are retried; `attachment.not.ready` after an upload is retried; text sends are throttled to MAX's 2 messages per second per chat.
342
+
343
+ ## MAX API limits
344
+
345
+ From the MAX Bot API documentation:
346
+
347
+ | Limit | Value |
348
+ |---|---|
349
+ | Message text | 4000 characters (the plugin splits longer text) |
350
+ | Sending | 2 messages per second per dialog, group or channel (the plugin queues sends) |
351
+ | Editing | Only the bot's own messages. In dialogs: messages with an inline keyboard at any age, others within 7 days. In group chats and channels: any age. At most 2 edits per second per chat |
352
+ | Deleting | The bot needs admin rights with permission to delete. In dialogs only the bot's own messages, in groups and channels any. At most 2 deletions per second per chat |
353
+ | Album | Up to 12 images and videos per message; files and audio go separately |
354
+ | Buttons | Text ≤ 128 characters, payload ≤ 1024 bytes, ≤ 30 rows, ≤ 210 buttons |
355
+ | Bot commands | Up to 32 |
356
+ | Webhook | HTTPS on port 443, trusted certificate, `200` within 30 s; subscription removed after 8 hours of failed deliveries |
357
+ | Long polling | Not for production; unavailable while a webhook subscription exists |
358
+
359
+ ## Security
360
+
361
+ - **Access control by default:** private messages need pairing, groups need an allowlist, group sender lists are enforced.
362
+ - **Action scope:** the `message` tool cannot act in chats the inbound policy does not admit (see [above](#message-tool-actions-and-their-scope)).
363
+ - **Local files:** only from directories OpenClaw allows for the agent; `..` and symbolic links cannot escape them. For an agent with host reads core allows any path, but only file types verified by content.
364
+ - **Remote media:** fetched through core's SSRF guard; private, loopback, link-local and metadata addresses are not passed to MAX as links.
365
+ - **Webhook:** the secret is checked in constant time before the body is read.
366
+ - **Secrets:** SecretRef support; token, webhook secret and proxy credentials are not logged.
367
+ - **TLS:** the extra Russian CAs are trusted only for the plugin's own connections.
368
+ - **Logs:** no message text by default.
369
+
370
+ **Proxy and internal addresses.** With `httpProxy` set, host names of the media the plugin downloads are resolved by the proxy, not by the plugin: literal private addresses and blocked host names are still refused, but a name that the proxy's DNS resolves to an internal address is fetched from the proxy's network. A proxy that sits inside an internal network therefore gives a path to that network's addresses. Use a proxy that has no access to internal resources.
371
+
372
+ **Known limitations.**
373
+
374
+ - If the disk fills up or becomes read-only while the gateway runs, the webhook answers `503` to every event and the bot does not answer until space is freed.
375
+ - In an admitted group, a captionless voice message that MAX has not transcribed is sent to speech-to-text to look for a mention of the bot. This is paid and not rate-limited; it happens only when mention patterns are configured (`groups.<id>.disableAudioPreflight` turns it off for a group).
376
+ - A button whose payload starts with `/` runs, when pressed, as a command from the person who pressed it, with that person's rights — as in core channels.
377
+ - An event whose processing crashes the gateway process (rather than raising an error the plugin catches) is processed again after every start until it is older than `maxEventAgeMinutes`; with `0`, without limit.
378
+ - `pin` and `unpin` check the chat but not the message author: in an admitted chat where the bot is an administrator they pin and unpin any message (unlike `edit` and `delete` outside the current chat).
379
+
380
+ Report vulnerabilities privately as described in [SECURITY.md](https://github.com/aspalagin/openclaw-max/blob/main/SECURITY.md). Supported version: 0.8.x.
381
+
382
+ ## Privacy
383
+
384
+ What the plugin stores on disk (`<stateDir>` is OpenClaw's state directory, by default `~/.openclaw`):
385
+
386
+ | Path | Contents | Lifetime |
387
+ |---|---|---|
388
+ | `<stateDir>/max/state-<account>.json` | Long-polling marker; chat registry (chat id, type, title, when the bot was added, removed or stopped); the generated webhook secret, if any | Until deleted |
389
+ | `<stateDir>/max/inbox-<account>/` (directory `0700`, files `0600`) | Webhook events waiting to be processed, including message text and attachment metadata | Deleted once processed |
390
+ | `<stateDir>/max/inbox-<account>/completed.json` | Keys of handled events (`update_type:timestamp:mid`), no content | 24 hours, up to 5000 keys |
391
+
392
+ `webhookQueue.mode: "memory"` keeps pending webhook events in memory only (the key file stays). Downloaded incoming media and conversation history are stored by OpenClaw core, not by the plugin.
393
+
394
+ Logs: message type, text length, chat, message and user ids, errors and timings. Message text, captions and transcripts are not logged unless `logMessagePreview: true` (50-character preview at debug level). Tokens, secrets and proxy credentials are never logged.
395
+
396
+ Network access by the plugin: the MAX Bot API and its upload and CDN hosts (through `httpProxy` if set) and the hosts of remote media URLs the agent sends. When a voice message has no MAX transcript, OpenClaw core sends the audio to the speech-to-text provider configured in `tools.media.audio`.
397
+
398
+ ## Troubleshooting
399
+
400
+ | Symptom | Likely cause and fix |
401
+ |---|---|
402
+ | The bot does not answer in private messages | `dmPolicy` is `pairing` and the sender is not approved (`openclaw pairing list max`), or `allowlist` without the sender in `allowFrom` |
403
+ | The bot does not answer in a group | The chat is not in `groups` (`groupPolicy: allowlist`); the bot was not mentioned (`requireMention`); the sender is not in `groups.<id>.allowFrom` / `groupAllowFrom`; the bot is not a group administrator — MAX then does not deliver group messages to it (observed with long polling) |
404
+ | `MAX bot token not configured: …` in status | No `botToken`, `tokenFile` or `MAX_BOT_TOKEN`; the message names the option path. A `tokenFile` that is missing, empty or a symbolic link is not read (`tokenFile is missing, empty or not a regular file`); the default account then uses `MAX_BOT_TOKEN`, if set |
405
+ | Account does not start, error mentions a SecretRef | The reference did not resolve; check `secrets.providers` and the referenced variable, file or command |
406
+ | Webhook: no events arrive | Check the public URL, certificate and that the reverse proxy forwards the path; after 8 hours of failures MAX removes the subscription — the plugin recreates it within 12 minutes; long polling does not work while a subscription exists |
407
+ | MAX gets `503` from the webhook | The journal could not be written (disk full, permissions) or `webhookQueue.maxPending` was reached |
408
+ | Old messages get no answer after downtime | They are older than `maxEventAgeMinutes` (default 60); set `0` to answer any age |
409
+ | `Local media path is not under an allowed directory` | The file is outside the agent's allowed directories; copy it into the workspace |
410
+ | `Host-local media sends only allow …` | The agent has host reads and core did not recognize or does not allow the file type; send a file of an allowed type or pass the content as `buffer` |
411
+ | The `message` tool is refused in another chat | `actionScope`: the chat is not admitted by the inbound policy; add it to the policy or change `actionScope` |
412
+ | `MAX API does not resolve @username …` | Use `user:<id>` or a numeric chat id |
413
+ | `pinned: false` in a private dialog | MAX has no pinning in dialogs |
414
+ | Raw `**` or `_` in a message | MAX rejected the markup and the plugin resent the text as plain text |
415
+ | Account stops at start with a proxy or API URL error | `httpProxy` or `apiBaseUrl` is invalid, or `apiBaseUrl` uses `http` for a non-loopback host |
416
+ | `/think` answers with buttons instead of a text reply | Command menus: send `/think high` to set a value directly |
417
+ | Warning that an account inherits an open policy | A named account without its own `dmPolicy` / `groupPolicy` inherits `open`; set the policy in `accounts.<id>` |
418
+
419
+ ## Upgrading from 0.7
420
+
421
+ A 0.7 config works without changes. Behaviour that changes and how to get the 0.7 behaviour back:
422
+
423
+ | Change in 0.8 | 0.7 behaviour |
424
+ |---|---|
425
+ | In a non-owner turn the `message` tool acts only in the current chat and in chats the inbound policy admits | `actionScope: "off"` (stricter: `"current"`) |
426
+ | A call without `senderIsOwner: true` (heartbeat, subagent, scheduled run without message authority) is limited to the admitted chats; in a group with a sender list it is refused | `actionScope: "off"` |
427
+ | Outside the chat of the current turn `edit` and `delete` touch only the bot's own messages | `actionScope: "off"` |
428
+ | `replyTo` of a send must point to a message of the chat the send goes to; otherwise it is refused | `actionScope: "off"` |
429
+ | Local files are read by core's guarded loader: from the agent's allowed directories, and with host reads only verified file types | No switch: copy files into the agent workspace |
430
+ | Messages, edits, button presses and bot starts older than 60 minutes — including MAX redeliveries after downtime — get no answer | `maxEventAgeMinutes: 0` |
431
+ | The webhook answers `200` after writing the event to disk; `503` when that fails or the queue is full | `webhookQueue.overflow: "drop"`, `webhookQueue.mode: "memory"` |
432
+ | Pending webhook events, with message text, are kept in `<stateDir>/max/inbox-<account>/` until processed | `webhookQueue.mode: "memory"` (the key file stays) |
433
+ | Named accounts inherit every channel-level option, including `dmPolicy`, `allowFrom`, `groupPolicy`, `groups` | Set the policies in each account |
434
+ | `channels.max.enabled: false` also disables named accounts | Keep the channel enabled; to run only named accounts, set no token at the top level (`botToken`, `tokenFile`, `MAX_BOT_TOKEN`) |
435
+ | A non-empty `groupAllowFrom` or `groups.<id>.allowFrom` limits who can talk to the bot in an admitted group | Remove the lists or add `"*"` |
436
+ | At most 12 media are downloaded from one incoming message | Raise `mediaMaxCount` |
437
+ | Forwarded messages start an agent turn (they were dropped) | — |
438
+ | Tool summaries (verbose mode) come without link previews | `disableLinkPreview: false` |
439
+ | `/think`, `/fast`, `/reasoning` and the other menu commands without an argument answer with buttons | — (with an argument they work as before) |
440
+ | Core `messages.groupChat.mentionPatterns`, if configured, now apply in MAX groups | `mentionPatterns: { "mode": "deny" }` |
441
+ | Core `silent` is honoured; the typing indicator follows core `typingMode` and lasts the whole turn | Core settings |
442
+ | After MAX rejects the markup, the answer arrives as plain text (visible `**`, `_`) | — |
443
+ | Debug logs carry no message text | `logMessagePreview: true` |
444
+ | `openclaw secrets audit` flags `botToken`, `webhookSecret` and `httpProxy` written as strings | Move them to a SecretRef, `tokenFile` or `webhookSecretFile` |
445
+ | `streaming.mode` wins over `streamMode` | Keep only one of them |
446
+ | Polling waits 2–60 s after an error (was 3 s), 5 minutes after `401` | — |
447
+ | The published package no longer contains `scripts/` | The scripts stay in the repository |
448
+ | `groups.<id>.enabled`, `groups.<id>.systemPrompt`, `groups.<id>.skills`, `actions` and `textChunkLimit` take effect (0.7 accepted them in the schema but did not read them) | Remove these keys from the config |
449
+
450
+ The full list is in the [changelog](https://github.com/aspalagin/openclaw-max/blob/main/CHANGELOG.md) (in Russian).
451
+
452
+ ## Development
453
+
454
+ ```bash
455
+ git clone https://github.com/aspalagin/openclaw-max.git
456
+ cd openclaw-max
457
+ npm ci
458
+ npm run typecheck # run before build: build also emits on type errors
459
+ npm run build # dist/index.js, dist/setup-entry.js, dist/secret-contract-api.js, dist/src/*.js
460
+ ```
461
+
462
+ Checks, as in CI:
463
+
464
+ ```bash
465
+ npm run format:check # prettier
466
+ npm run lint # eslint
467
+ npm run check:cycles # import cycles between src/ modules
468
+ npm run typecheck
469
+ npm test # vitest, including conformance with the MAX schema snapshot
470
+ npm audit --omit=dev --omit=peer # runtime dependencies shipped with the package
471
+ ```
472
+
473
+ CI also runs `typecheck` and the tests against `openclaw@latest` (with the locked and the minimum supported zod) and `openclaw@beta`; a failure on `beta` is reported but does not fail the build.
474
+
475
+ - `src/__fixtures__/max-schema-0.0.33.yaml` is a snapshot of the MAX Bot API schema; `npm run schema:update [ref]` refreshes it.
476
+ - `npm run test:api` calls the live API with `MAX_BOT_TOKEN` (`GET /me`, `GET /updates`); the helper scripts in `scripts/` are in the repository only, not in the package.
477
+ - Tests must use made-up tokens and ids.
478
+
479
+ Questions and proposals: [GitHub Issues](https://github.com/aspalagin/openclaw-max/issues). Contribution rules: [CONTRIBUTING.md](https://github.com/aspalagin/openclaw-max/blob/main/CONTRIBUTING.md).
480
+
481
+ ## Acknowledgements, authors and license
482
+
483
+ **Acknowledgements.** Since 28 May 2026 (version 0.5.0 sync, commit `d8d24bc`) this plugin contains code derived from the `openclaw-max` 0.5.0 npm package by Evgeniy Bystrov, published under the MIT License (Copyright (c) 2026 Evgeniy Bystrov): [github.com/evgeniyvbystrov/openclaw-max](https://github.com/evgeniyvbystrov/openclaw-max). His copyright notice is kept in [LICENSE](LICENSE). Thank you.
484
+
485
+ **Authors.**
486
+
487
+ - [Petlevoy](https://github.com/petlevoy) — project founder.
488
+ - [Arseniy Palagin](https://github.com/aspalagin) — maintainer.
489
+
490
+ Development is carried out with the help of AI agents.
491
+
492
+ **License:** MIT, see [LICENSE](LICENSE).