diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index eb97433f1eba..1a1b431c99d2 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -19,7 +19,6 @@ extensions/browser/src/browser/chrome-mcp.ts extensions/browser/src/browser/chrome.executables.ts extensions/browser/src/browser/chrome.internal.test.ts extensions/browser/src/browser/chrome.ts -extensions/browser/src/browser/config.test.ts extensions/browser/src/browser/extension-relay/relay-bridge.ts extensions/browser/src/browser/pw-session.create-page.navigation-guard.test.ts extensions/browser/src/browser/pw-session.ts @@ -545,7 +544,6 @@ src/agents/system-prompt.test.ts src/agents/system-prompt.ts src/agents/tool-display-common.ts src/agents/tool-loop-detection.test.ts -src/agents/tool-loop-detection.ts src/agents/tool-search.test.ts src/agents/tool-search.ts src/agents/tools/computer-tool.ts diff --git a/docs/.generated/config-baseline.counts.json b/docs/.generated/config-baseline.counts.json index 6fc707b0cc0f..04bcf195f0b6 100644 --- a/docs/.generated/config-baseline.counts.json +++ b/docs/.generated/config-baseline.counts.json @@ -1,5 +1,5 @@ { - "core": 3007, - "channel": 3732, + "core": 2851, + "channel": 3680, "plugin": 3537 } diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 6323b7b24715..10c54aab6747 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -2f4ba51e354995c0d28052d7c61210ace881145af0db7f5409f8eb7dff167a4f config-baseline.json -0e9d04a669b853f8b58397d2dfc8913b5a3dd70af20ebd43e25b08be4cd3d292 config-baseline.core.json -c624c55cb46a67db4c836dcc04d295db966579885a00778d1a32241632de3475 config-baseline.channel.json +a8e315e49d62b95c54c752041cc27628260686f0a99de646c24505edc977a3c3 config-baseline.json +d3da432d372bc652d432be978495a9f28b3478da1034cbe888550c8bf6cb9dd4 config-baseline.core.json +dbf05e8852c873d48288336dbbb05d7a7b3a1bcbee8cb8ece5e40f204d2ab9db config-baseline.channel.json 0e0592b95e4958477e539abd5ccb336c63735c7d69133199953c48ff3c0845a8 config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index e1936e2fe941..01dade52c799 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -13,7 +13,7 @@ e5e67ddf3cab38fcbf9220bc3160715897e2709d9a9ff6ff36f1ecc9453c2367 module/agent-c 30452bae2a689fb75dcb6dba9ef11e5b95f9cbe0c62eb190a0fcb82d0a19ae52 module/agent-core 74daa746deb548379d3f0d6eac3c4d082df1034c4360cc03bf51fee0f10a2e4d module/agent-harness e09226afd443cee02ed648f032f53bfeb60585617bb523a649995764d3c38da9 module/agent-harness-exec-review-runtime -ca9810b66aff3c8b60278b80d634c9c280322034964c05cb727d84252080aabf module/agent-harness-runtime +ffacec9f51060b81c597a9d169e7ee0fa463fcbd18daffdce129d144897b2c5b module/agent-harness-runtime ec22d7a039fb58d0b8343ad149322960d3d8ca58b3f4c70f2fa8a099f8186d0c module/agent-harness-task-runtime 5f63bf587bf3547d59d0dc5d0dc2fee54745aa6edaab4aa3ae700dba03443edb module/agent-harness-tool-runtime 5168648cd946abad8a92822889f13ceacc87ed502314a66190d0b1eb8ebe76ea module/agent-media-payload @@ -35,13 +35,13 @@ a5e9215460bc99fb6e6cdd4bc67eb6466a5e99f28279e13d1769599c18dfe0b3 module/approva 6c81b9122ab0a5c3190700c0a234047274a8d48edddcc8f26d3859b886696068 module/async-lock-runtime ad60ccc4fe9084d47f0477e02d9296bacad32f26d7456e2a84be8d25a53a25c2 module/boolean-param 333a906d4ad9d3e89102ab7eb9059a7f9c7277328041223ee3b0713442f56a29 module/browser-config -0de3c53c3ba6ff739d4b047b440588ac25fdea3b795e92b37cab90bfd1520a8c module/bundled-channel-config-schema +c117222faacb1c68bf6df6594fbf47c3ea0240fd1403f913ecb72e08730d32cf module/bundled-channel-config-schema b6e53fb9c69840d71785325cd9a244809ffd6d3f0dd08ed0cccf816dd993f210 module/channel-actions fcfd76dbfe818e8e8574a425f340fee935acf53d4484e961f54c3ec760fb15fe module/channel-activity-runtime 24c53c9cefacd8c1bec2aa91ba2b01e606f8b8477bc5cb2f99567225a4dcf67c module/channel-config-helpers 16dc9d32e8ca3ef78fc63e0fc4b20e6b943633e9572de1a49d24a880b6ffc66c module/channel-config-primitives 5da2caccf30780a4cf86ac710e2ca42cc9576cb1c49291c0929889fec0cc2257 module/channel-config-schema -080d51451ad15a2787eada3dcc04985154cc394058dfbf2194c6560801f3a1c1 module/channel-config-schema-legacy +e6c20ca52397a90297c49928e9b8d98a48059ed107d21f8cec6a7ec9c9e2d1ab module/channel-config-schema-legacy 8740f7cc786a380043e41aaf710ac47931b3587c122bb152d5f0231f98fd5351 module/channel-config-writes 2dd98659d9600e755f09ef00dd91c36692b8562ed3700461937e94f6cd1e640a module/channel-contract 3db1a968e5b8aa97623a48a9ee76393d578d4bb90634fa59f499a2fd87b9da72 module/channel-core @@ -94,8 +94,8 @@ e26e0f75b43c5bbadd34401b21d8c76406ca0b87cc997be5abd100462d318f1a module/compat 235a9e4d983042c3db156efcab0e333984cbbf13ebdfcc44a3a5ab40cf3edb4f module/config-contracts 20f3f8042de53e4eee61b64de9102c8c202b9299e6a29235647a4729f70145f2 module/config-mutation 316949815affe623ac63951a5db580527f02663576dffa084f02768f612c0c1c module/config-runtime -adb5fa3476c1f6b4e6c6d2d170254ed781949e7b609be1679661003911635de3 module/config-schema -1531347939d51c528b7230b951da194faafdfc3d8284765e7e9a0373d1ce5ef8 module/config-types +a8ce8d7a8df9d427956a3d6f2daf00b0234be473e8cec2d01b84f34c6d7c78bb module/config-schema +b317e9810e57d07ddd720d68c1ce93172e5bc331d9c095a332edc52f565d5ce4 module/config-types 42d15153981cfe3adc1d5f91621434c56f07a9bd48c15ce34742f72dd040c142 module/context-visibility-runtime 03636897fb99cb73e4d8620c8a0e0d72b4d52fc32bf94f525af2aa88c489c6c2 module/conversation-binding-runtime c1ea9510dfda047609a99d5d2cd1f1560f5d469a36e6b695766213d695c25b0f module/conversation-runtime @@ -111,7 +111,7 @@ f70c93d28053ca2e8353e45e6515ce7acef188097c6117d1545965d0699c8004 module/device- 371ee1fd78810526745c93c24c4b85562c981676adcc33aaa0c627f5d2d45810 module/direct-dm-guard-policy 91278c800e0f87d7111f226e1cfcb6f29552936fec7215b3b69be4da7e2ee8fb module/directory-config-runtime ea81ef06956c1bc0853fa00afbbc2b5a4019116aaf8a436e1b27d06f7a2c9e88 module/directory-runtime -c443b68d232f28b7241e1be10d1604def0d52d99ce3ae1ca8e2be3c6fd8b2d2f module/discord +7e953d227d71e4e07c412b21f6db0604279cb6360eb6e025aceb79576212afd0 module/discord f41f9b34ab771c894293453bcdf072860c7cd509dd4e0172156634a816b8d727 module/document-extractor 3ac20ebba52de5a2f18807c0c8ade6e61f59e260a35fc1d077c9dedb9960dabd module/embedding-providers 46c05a90b66032d1d7ad08445840a4bf81aa2bd325348f87710ad4538daf38f6 module/error-runtime @@ -299,12 +299,12 @@ eb9a25321eaa2bbea1721f7d4b8b218bed398379494e09a21a621f264435b39a module/string- 32f031eb75c887b24b8eaa693cd0aa1a4648dca85eab7bfdfb3d08136861c274 module/system-event-runtime 65361dc9a23578787c1ccf98f7df99e443574614e7ce95889f9a295725e12fae module/talk-config-runtime 9efd666b8c2cc8a9abf816751fd9420f3c048d158e48570cae8b7a4cbc52a83f module/target-resolver-runtime -da05dc1506e226fc25285a2963bd8d96dd0e882d6baa24b8073a0c53fef9fc81 module/telegram-account +ec6df323a2dbd1f837df25c0290289031bf60e0b0250f47172f3eaa316667c90 module/telegram-account 7729f9f201c08f114925da75ee86a5a8deb6677e5d1b5ee86bc60aacaa01f63b module/telegram-command-config 110944726884fca94f38c9c329b5950629438b9a719f4782c4beeade8bd67746 module/temp-path a65f17db3d04c2ca1b34f9a4ebe8748952bbcb00318b741adbe3f227d2e2de20 module/text-autolink-runtime e24c49c1c7b35e8b4403d45eb76bb0f33ec2f23f714bda322e1363251737d6cd module/text-chunking -948694f6da0f85fb2e9af0d66cd49b8ff0648b6dc96ed4165b03736a3e8f2439 module/text-runtime +1df5a33be5dbf611e301c01c6b8bcf502c3101aae10cd14a79f1bbb58b116497 module/text-runtime 4bdc79e3b42814a30c3c84401f1f0e4620706092787a1f5d2b6c4148b202fe02 module/text-utility-runtime dac9afb3833e237e3edcc9d687240caceb8288abd0c6b757dcc3c16a819eb029 module/thread-bindings-runtime 65c9b0b77c0c19140765ec282f9a6424f6ce8a55d0e318b30485d52fd41968e9 module/thread-bindings-session-runtime diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index e65db6324fc0..4f586034c244 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -82,7 +82,7 @@ Recurring jobs can set `pacing.min` and/or `pacing.max` to duration strings such During an isolated run, a paced job can call the `cron` tool with `action: "next_check"` and `in: "30m"`. The proposal applies only to that currently running job and is measured from successful run completion. OpenClaw silently clamps it to the configured bounds. -Pacing without a proposal leaves the normal schedule unchanged. Failed, timed-out, and skipped runs discard the proposal, so existing retry and error-backoff behavior takes precedence. Manually forcing a recurring job is out-of-band and preserves its pending natural or paced slot. For condition-triggered jobs, `cron.triggers.minIntervalMs` remains a lower bound even when a proposal requests an earlier check. +Pacing without a proposal leaves the normal schedule unchanged. Failed, timed-out, and skipped runs discard the proposal, so existing retry and error-backoff behavior takes precedence. Manually forcing a recurring job is out-of-band and preserves its pending natural or paced slot. For condition-triggered jobs, the built-in minimum interval remains a lower bound even when a proposal requests an earlier check. ### Day-of-month and day-of-week use OR logic @@ -583,15 +583,8 @@ Use the latest-generation, best-tier model available from your provider for untr cron: { enabled: true, store: "~/.openclaw/cron/jobs.json", - maxConcurrentRuns: 8, triggers: { enabled: false, - minIntervalMs: 30000, - }, - retry: { - maxAttempts: 3, - backoffMs: [30000, 60000, 300000], - retryOn: ["rate_limit", "overloaded", "network", "timeout", "server_error"], }, webhookToken: "replace-with-dedicated-webhook-token", sessionRetention: "24h", @@ -599,9 +592,7 @@ Use the latest-generation, best-tier model available from your provider for untr } ``` -The `retry` values above are the defaults: up to 3 retries with `30s/60s/5m` backoff, retrying all five transient categories. `webhookToken` is sent as `Authorization: Bearer ` on cron webhook POSTs. - -`maxConcurrentRuns` limits both scheduled cron dispatch and isolated agent-turn execution, and defaults to 8. Isolated cron agent turns use the queue's dedicated `cron-nested` execution lane internally, so raising this value lets independent cron LLM runs progress in parallel instead of only starting their outer cron wrappers. The shared non-cron `nested` lane is not widened by this setting. +`webhookToken` is sent as `Authorization: Bearer ` on cron webhook POSTs. `cron.store` is a logical store key and doctor migration path, not a live JSON file to hand-edit. Job data lives in SQLite; use the CLI or Gateway API for changes. @@ -609,7 +600,7 @@ Disable cron: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`. - **One-shot retry**: transient errors (rate limit, overload, network, timeout, server error) retry up to `retry.maxAttempts` times (default 3) using `retry.backoffMs` (default 30s, 60s, 5m). Permanent errors disable the job immediately. + **One-shot retry**: transient errors (rate limit, overload, network, timeout, server error) use a built-in retry schedule. Permanent errors disable the job immediately. **Recurring retry**: consecutive execution errors back off on an extended schedule (30s, 60s, 5m, 15m, 60m). Backoff resets after the next successful run. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 45f969bd6a4e..e975903b13ac 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1580,57 +1580,21 @@ openclaw logs --follow - `Slow listener detected ...` - `stuck session: sessionKey=agent:...:discord:... state=processing ...` - Discord gateway queue knobs: - - - single-account: `channels.discord.eventQueue.listenerTimeout` - - multi-account: `channels.discord.accounts..eventQueue.listenerTimeout` - - this only controls Discord gateway listener work, not agent turn lifetime - Discord does not apply a channel-owned timeout to queued agent turns. Message listeners hand off immediately, and queued Discord runs preserve per-session ordering until the session/tool/runtime lifecycle completes or aborts the work. -```json5 -{ - channels: { - discord: { - accounts: { - default: { - eventQueue: { - listenerTimeout: 120000, - }, - }, - }, - }, - }, -} -``` - OpenClaw fetches Discord `/gateway/bot` metadata before connecting. Transient failures fall back to Discord's default gateway URL and are rate-limited in logs. - Metadata timeout knobs: - - - single-account: `channels.discord.gatewayInfoTimeoutMs` - - multi-account: `channels.discord.accounts..gatewayInfoTimeoutMs` - - env fallback when config is unset: `OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS` - - default: `30000` (30 seconds), max: `120000` + The metadata timeout defaults to 30 seconds. `OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS` can override it for unusual host environments. OpenClaw waits for Discord's gateway `READY` event during startup and after runtime reconnects. Multi-account setups with startup staggering can need a longer startup READY window than the default. - READY timeout knobs: - - - startup single-account: `channels.discord.gatewayReadyTimeoutMs` - - startup multi-account: `channels.discord.accounts..gatewayReadyTimeoutMs` - - startup env fallback when config is unset: `OPENCLAW_DISCORD_READY_TIMEOUT_MS` - - startup default: `15000` (15 seconds), max: `120000` - - runtime single-account: `channels.discord.gatewayRuntimeReadyTimeoutMs` - - runtime multi-account: `channels.discord.accounts..gatewayRuntimeReadyTimeoutMs` - - runtime env fallback when config is unset: `OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS` - - runtime default: `30000` (30 seconds), max: `120000` + Startup waits 15 seconds and runtime reconnects wait 30 seconds. `OPENCLAW_DISCORD_READY_TIMEOUT_MS` and `OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS` remain available for unusual host environments. @@ -1737,12 +1701,11 @@ Primary reference: [Configuration reference - Discord](/gateway/config-channels# - startup/auth: `enabled`, `token`, `applicationId`, `accounts.*`, `allowBots` - policy: `groupPolicy`, `dmPolicy`, `allowFrom`, `dm.*`, `guilds.*`, `guilds.*.channels.*` - command: `commands.native`, `commands.useAccessGroups` (global), `configWrites`, `slashCommand.ephemeral` -- event queue: `eventQueue.listenerTimeout` (listener budget, default `120000`), `eventQueue.maxQueueSize` (default `10000`), `eventQueue.maxConcurrency` (default `50`) -- gateway: `proxy`, `gatewayInfoTimeoutMs`, `gatewayReadyTimeoutMs`, `gatewayRuntimeReadyTimeoutMs` +- gateway: `proxy` - reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` - delivery: `textChunkLimit` (default `2000`), `maxLinesPerMessage` (default `17`) - streaming: `streaming.mode`, `streaming.chunkMode`, `streaming.preview.*`, `streaming.progress.*`, `streaming.block.*` (legacy flat `streamMode`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce`, `chunkMode` keys are migrated into `streaming.*` by `openclaw doctor --fix`) -- media/retry: `mediaMaxMb` (caps outbound Discord uploads, default `100`), `retry` +- media: `mediaMaxMb` (caps outbound Discord uploads, default `100`) - actions: `actions.*` - presence: `activity`, `status`, `activityType`, `activityUrl`, `autoPresence.*` - UI: `ui.components.accentColor` diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index cb9cd0df50f8..2dddc0db885e 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -283,7 +283,7 @@ curl "https://api.telegram.org/bot/getUpdates" - Long polling uses the grammY runner with per-chat/per-thread sequencing. Runner sink concurrency uses `agents.defaults.maxConcurrent`. - Multi-account startup bounds concurrent `getMe` probes so large bot fleets do not fan out every account probe at once. - Each gateway process guards long polling so only one active poller can use a bot token at a time. Persistent `getUpdates` 409 conflicts point to another OpenClaw gateway, script, or external poller using the same token. -- The polling watchdog restarts after 120 seconds without completed `getUpdates` liveness by default. Raise `channels.telegram.pollingStallThresholdMs` (30000-600000, per-account overrides supported) only if your deployment sees false polling-stall restarts during long-running work. +- The polling watchdog restarts after 120 seconds without completed `getUpdates` liveness. - Telegram Bot API has no read-receipt support (`sendReadReceipts` does not apply). @@ -743,17 +743,13 @@ curl "https://api.telegram.org/bot/getUpdates" - + - `channels.telegram.textChunkLimit` default 4000; `streaming.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. - `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound media size. - - `channels.telegram.mediaGroupFlushMs` (default 500, range 10-60000) controls how long albums/media groups are buffered before OpenClaw dispatches them as one inbound message. Increase it if album parts arrive late; decrease it to reduce album reply latency. - - `channels.telegram.timeoutSeconds` overrides the API client timeout (grammY default applies if unset). Bot clients clamp configured values below the 60-second outbound text/typing request guard so grammY does not abort visible reply delivery before OpenClaw's transport guard and fallback can run. Long polling still uses a 45-second `getUpdates` request guard so idle polls are not abandoned indefinitely. - - `channels.telegram.pollingStallThresholdMs` defaults to 120000; tune between 30000 and 600000 only for false-positive polling-stall restarts. - group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables. - reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload. - Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary. - DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms[""].historyLimit`. - - `channels.telegram.retry` applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery uses a bounded safe-send retry for pre-connect failures, but does not retry ambiguous post-send network envelopes that could duplicate visible messages. CLI and message-tool send targets accept a numeric chat ID, username, or forum topic target: @@ -804,10 +800,9 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \ When the agent hits a delivery or provider error, the error policy controls whether error messages reach the Telegram chat: -| Key | Values | Default | Description | -| ----------------------------------- | -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `channels.telegram.errorPolicy` | `always`, `once`, `silent` | `always` | `always` sends every error message to the chat. `once` sends each unique error message once per cooldown window (suppresses repeated identical errors). `silent` never sends error messages to the chat. | -| `channels.telegram.errorCooldownMs` | number (ms) | `14400000` (4h) | Cooldown window for the `once` policy. After an error is sent, the same message is suppressed until this interval elapses. Prevents error spam during outages. | +| Key | Values | Default | Description | +| ------------------------------- | -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `channels.telegram.errorPolicy` | `always`, `once`, `silent` | `always` | `always` sends every error message to the chat. `once` sends each unique error message once per built-in cooldown window. `silent` never sends error messages to the chat. | Per-account, per-group, and per-topic overrides are supported (same inheritance as other Telegram config keys). @@ -816,7 +811,6 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance channels: { telegram: { errorPolicy: "always", - errorCooldownMs: 120000, groups: { "-1001234567890": { errorPolicy: "silent", // suppress errors in this group @@ -869,10 +863,8 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance - Logs with `TypeError: fetch failed` or `Network request for 'getUpdates' failed!` are retried as recoverable network errors. - During polling startup, OpenClaw reuses the successful startup `getMe` probe for grammY so the runner does not need a second `getMe` before the first `getUpdates`. - If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw continues into long polling instead of making another pre-poll control-plane call. A still-active webhook then surfaces as a `getUpdates` conflict; OpenClaw rebuilds the transport and retries webhook cleanup. - - If Telegram sockets recycle on a short fixed cadence, check for a low `channels.telegram.timeoutSeconds` — bot clients clamp configured values below the outbound and `getUpdates` request guards, but older releases could abort every poll or reply when this was set below those guards. - `Polling stall detected` in logs means OpenClaw restarts polling and rebuilds the transport after 120 seconds without completed long-poll liveness by default. - `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, a running webhook account has not completed `setWebhook` after startup grace, or the last successful polling transport activity is stale. - - Raise `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues to `api.telegram.org`. - Telegram honors process proxy env for Bot API transport: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`. - If `OPENCLAW_PROXY_URL` is set for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too. - On VPS hosts with unstable direct egress/TLS, route Telegram API calls through a proxy: @@ -936,12 +928,12 @@ Primary reference: [Configuration reference - Telegram](/gateway/config-channels - threading/replies: `replyToMode`, `threadBindings` - streaming: `streaming` (modes `off | partial | block | progress`), `streaming.preview.toolProgress` - formatting/delivery: `textChunkLimit`, `streaming.chunkMode`, `richMessages`, `markdown.tables` (`off | bullets | code | block`), `linkPreview`, `responsePrefix` -- media/network: `mediaMaxMb`, `mediaGroupFlushMs`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy` +- media/network: `mediaMaxMb`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy` - custom API root: `apiRoot` (Bot API root only; do not include `/bot`), `trustedLocalFileRoots` (self-hosted Bot API absolute `file_path` roots) - webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`, `webhookPort`, `webhookCertPath` - actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker|createForumTopic|editForumTopic` - reactions: `reactionNotifications`, `reactionLevel` -- errors: `errorPolicy`, `errorCooldownMs`, `silentErrorReplies` +- errors: `errorPolicy`, `silentErrorReplies` - writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` diff --git a/docs/channels/troubleshooting.md b/docs/channels/troubleshooting.md index 875751d30969..657fe524eb6d 100644 --- a/docs/channels/troubleshooting.md +++ b/docs/channels/troubleshooting.md @@ -64,15 +64,15 @@ Full troubleshooting: [WhatsApp troubleshooting](/channels/whatsapp#troubleshoot ### Telegram failure signatures -| Symptom | Fastest check | Fix | -| ------------------------------------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -| `/start` but no usable reply flow | `openclaw pairing list telegram` | Approve pairing or change DM policy. | -| Bot online but group stays silent | Verify mention requirement and bot privacy mode | Disable privacy mode for group visibility or mention bot. | -| Send failures with network errors | Inspect logs for Telegram API call failures | Fix DNS/IPv6/proxy routing to `api.telegram.org`. | -| Startup reports `getMe returned 401` | Check configured token source | Re-copy or regenerate the BotFather token and update `botToken`, `tokenFile`, or default-account `TELEGRAM_BOT_TOKEN`. | -| Polling stalls or reconnects slowly | `openclaw logs --follow` for polling diagnostics | Upgrade; if restarts are false positives, tune `pollingStallThresholdMs`. Persistent stalls still point to proxy/DNS/IPv6. | -| `setMyCommands` rejected at startup | Inspect logs for `BOT_COMMANDS_TOO_MUCH` | Reduce plugin/skill/custom Telegram commands or disable native menus. | -| Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. | +| Symptom | Fastest check | Fix | +| ------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `/start` but no usable reply flow | `openclaw pairing list telegram` | Approve pairing or change DM policy. | +| Bot online but group stays silent | Verify mention requirement and bot privacy mode | Disable privacy mode for group visibility or mention bot. | +| Send failures with network errors | Inspect logs for Telegram API call failures | Fix DNS/IPv6/proxy routing to `api.telegram.org`. | +| Startup reports `getMe returned 401` | Check configured token source | Re-copy or regenerate the BotFather token and update `botToken`, `tokenFile`, or default-account `TELEGRAM_BOT_TOKEN`. | +| Polling stalls or reconnects slowly | `openclaw logs --follow` for polling diagnostics | Upgrade; persistent stalls usually point to proxy/DNS/IPv6. | +| `setMyCommands` rejected at startup | Inspect logs for `BOT_COMMANDS_TOO_MUCH` | Reduce plugin/skill/custom Telegram commands or disable native menus. | +| Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. | Full troubleshooting: [Telegram troubleshooting](/channels/telegram#troubleshooting) diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 257e15e219c7..0d9c548497fe 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -126,7 +126,6 @@ A separate WhatsApp number is recommended (setup and metadata are optimized for - The gateway owns the WhatsApp socket and reconnect loop. - A watchdog tracks two signals independently: raw WhatsApp Web transport activity and application-message activity. A quiet-but-connected session is not restarted just because no message arrived recently; it forces reconnect only when transport frames stop arriving for a fixed internal window (not user-configurable) or application messages stay silent past 4x the normal message timeout. Right after a reconnect for a recently active session, that first window uses the shorter normal message timeout instead of the 4x window. OpenClaw can auto-reply to offline messages that Baileys delivers early in that reconnect, bounded by the inbound message-ID dedupe lifetime; initial startup keeps the short stale-history guard. -- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` (application ping interval), `connectTimeoutMs` (opening handshake timeout), `defaultQueryTimeoutMs` (Baileys query waits, plus OpenClaw's outbound send/presence and inbound read-receipt timeouts). - Outbound sends require an active WhatsApp listener for the target account; sends fail fast otherwise. - Group sends attach native mention metadata for `@+` and `@` tokens (in text and media captions) when the token matches current participant metadata, including LID-backed groups. - Status and broadcast chats (`@status`, `@broadcast`) are ignored. @@ -540,20 +539,6 @@ openclaw channels status Quiet accounts can stay connected past the normal message timeout; the watchdog restarts only when WhatsApp Web transport activity stops, the socket closes, or application-level activity stays silent beyond the longer safety window (see Runtime model above). - If logs show repeated `status=408 Request Time-out Connection was lost`, tune Baileys socket timings under `web.whatsapp`. Start by shortening `keepAliveIntervalMs` below your network's idle timeout and increasing `connectTimeoutMs` on slow or lossy links: - - ```json5 - { - web: { - whatsapp: { - keepAliveIntervalMs: 15000, - connectTimeoutMs: 60000, - defaultQueryTimeoutMs: 60000, - }, - }, - } - ``` - Fix: ```bash @@ -683,7 +668,7 @@ Primary reference: [Configuration reference - WhatsApp](/gateway/config-channels | Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` | | Delivery | `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` | | Multi-account | `accounts..enabled`, `accounts..authDir`, and other per-account overrides | -| Operations | `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*` | +| Operations | `configWrites`, `debounceMs`, `web.enabled` | | Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms..historyLimit` | | Prompts | `groups..systemPrompt`, `groups["*"].systemPrompt`, `direct..systemPrompt`, `direct["*"].systemPrompt` | diff --git a/docs/cli/browser.md b/docs/cli/browser.md index fd17ed5046d0..355855eb7f8a 100644 --- a/docs/cli/browser.md +++ b/docs/cli/browser.md @@ -255,7 +255,7 @@ The default existing-session path is host-only Chrome MCP auto-connect. If the b Current existing-session limits: - Snapshot-driven actions use refs, not CSS selectors. -- `browser.actionTimeoutMs` defaults supported `act` requests to 60000 ms when callers omit `timeoutMs`; per-call `timeoutMs` still wins. +- Supported `act` requests use a built-in 60000 ms default when callers omit `timeoutMs`; per-call `timeoutMs` still wins. - `click` is left-click only. - `type` does not support `slowly=true`. - `press` does not support `delayMs`. diff --git a/docs/cli/daemon.md b/docs/cli/daemon.md index fbce36beed29..787b7d384c8f 100644 --- a/docs/cli/daemon.md +++ b/docs/cli/daemon.md @@ -34,7 +34,7 @@ openclaw daemon uninstall - `status`: shows service install state (launchd/systemd/schtasks) and probes Gateway health. - `install`: installs the service; `--force` reinstalls/overwrites an existing install. -- `restart --safe`: asks the running Gateway to preflight active work and schedule one coalesced restart after work drains, bounded by `gateway.reload.deferralTimeoutMs` (default 300000ms/5 minutes; set to `0` to wait indefinitely). When that budget expires, the restart is forced anyway. Plain `restart` uses the service manager directly; `--force` is the immediate override. +- `restart --safe`: asks the running Gateway to preflight active work and schedule one coalesced restart after work drains, bounded to 5 minutes. When that budget expires, the restart is forced anyway. Plain `restart` uses the service manager directly; `--force` is the immediate override. - `restart --safe --skip-deferral`: bypasses the active-work deferral gate so the Gateway restarts immediately even when blockers are reported. Requires `--safe`. ## Notes diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index 4f0bc8516926..a8cf817976be 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -114,7 +114,7 @@ openclaw gateway restart --force openclaw gateway restart --wait 30s ``` -`--safe` asks the running Gateway to preflight active work and schedule one coalesced restart after that work drains. The wait is bounded by `gateway.reload.deferralTimeoutMs` (default: 5 minutes / `300000`); when the budget expires the restart is forced. Set `deferralTimeoutMs: 0` to wait indefinitely (with periodic still-pending warnings) instead of forcing. `--safe` cannot combine with `--force` or `--wait`. +`--safe` asks the running Gateway to preflight active work and schedule one coalesced restart after that work drains. The wait is bounded to 5 minutes; when the budget expires the restart is forced. `--safe` cannot combine with `--force` or `--wait`. `--skip-deferral` bypasses the active-work deferral gate on a safe restart, so the Gateway restarts immediately even with reported blockers. It requires `--safe` — use it when a deferral is stuck on a runaway task. diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index 5feb8e3ba38d..9951e698ea33 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -376,7 +376,7 @@ Those saved definitions are for runtimes that OpenClaw launches or configures la - servers that advertise resources or prompts also expose utility tools for listing/reading resources and listing/fetching prompts; those generated utility names (`resources_list`, `resources_read`, `prompts_list`, `prompts_get`) use the same include/exclude filter - dynamic MCP tool-list changes invalidate the cached catalog for that session; the next discovery/use refreshes from the server - repeated MCP tool request/protocol failures pause that server briefly so one broken server does not consume the whole turn - - session-scoped bundled MCP runtimes are reaped after `mcp.sessionIdleTtlMs` milliseconds of idle time (default 10 minutes; set `0` to disable) and one-shot embedded runs clean them up at run end + - session-scoped bundled MCP runtimes are reaped after 10 minutes of idle time and one-shot embedded runs clean them up at run end diff --git a/docs/cli/transcripts.md b/docs/cli/transcripts.md index c58598b3e00a..9877a87dc745 100644 --- a/docs/cli/transcripts.md +++ b/docs/cli/transcripts.md @@ -111,20 +111,16 @@ with: ```json { "transcripts": { - "enabled": true, - "maxUtterances": 2000 + "enabled": true } } ``` - `enabled` (default `false`): turn the tool on. -- `maxUtterances` (default `2000`, clamped 1-10000): utterance buffer size per - session. - -Configure auto-start sources with `transcripts.autoStart`. Each entry is -enabled by being present; omit an entry to disable that source. `discord-voice` -is the bundled auto-start-capable source and requires `guildId` and -`channelId`: + Configure auto-start sources with `transcripts.autoStart`. Each entry is + enabled by being present; omit an entry to disable that source. `discord-voice` + is the bundled auto-start-capable source and requires `guildId` and + `channelId`: ```json { diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index 297697e39de3..ba67fb7ff88c 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -27,7 +27,7 @@ execution, streaming, persistence. Runs are serialized per session key (session lane) and optionally through a global lane, preventing tool/session races. Messaging channels choose a queue mode (steer/followup/collect/interrupt) that feeds this lane system; see [Command Queue](/concepts/queue). -Transcript writes are additionally protected by a session write lock on the session file. The lock is process-aware and file-based, so it catches writers that bypass the in-process queue or come from another process. Writers wait up to `session.writeLock.acquireTimeoutMs` (default `60000` ms; env override `OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS`) before reporting the session as busy. +Transcript writes are additionally protected by a session write lock on the session file. The lock is process-aware and file-based, so it catches writers that bypass the in-process queue or come from another process. Writers wait up to 60 seconds by default (env override `OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS`) before reporting the session as busy. Session write locks are non-reentrant by default. A helper that intentionally nests acquisition of the same lock while preserving one logical writer must opt in with `allowReentrant: true`. @@ -138,13 +138,13 @@ Assistant deltas buffer into chat `delta` messages. A chat `final` is emitted on ### Stuck session diagnostics -With diagnostics enabled, `diagnostics.stuckSessionWarnMs` (default `120000` ms) classifies long `processing` sessions with no observed reply, tool, status, block, or ACP progress: +With diagnostics enabled, a built-in two-minute threshold classifies long `processing` sessions with no observed reply, tool, status, block, or ACP progress: -- Active embedded runs, model calls, and tool calls report as `session.long_running`. Owned silent model calls stay `session.long_running` until `diagnostics.stuckSessionAbortMs` so slow or non-streaming providers are not flagged as stalled too early. +- Active embedded runs, model calls, and tool calls report as `session.long_running`. Owned silent model calls stay `session.long_running` until the abort threshold so slow or non-streaming providers are not flagged as stalled too early. - Active work with no recent progress reports as `session.stalled`. Owned model calls switch to `session.stalled` at or after the abort threshold; ownerless stale model/tool activity is not hidden as long-running. - `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity. -`diagnostics.stuckSessionAbortMs` defaults to at least 5 minutes and 3x the warn threshold. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after the abort threshold, so queued work resumes without cutting off merely slow runs. Recovery emits structured requested/completed outcomes; diagnostic state is marked idle only if the same processing generation is still current, and repeated `session.stuck` diagnostics back off while the session stays unchanged. +The abort threshold is at least 5 minutes and 3x the warning threshold. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after the abort threshold, so queued work resumes without cutting off merely slow runs. Recovery emits structured requested/completed outcomes; diagnostic state is marked idle only if the same processing generation is still current, and repeated `session.stuck` diagnostics back off while the session stays unchanged. ## Where things can end early diff --git a/docs/concepts/managed-worktrees.md b/docs/concepts/managed-worktrees.md index 07b0ed059f37..3d68a663fda0 100644 --- a/docs/concepts/managed-worktrees.md +++ b/docs/concepts/managed-worktrees.md @@ -65,7 +65,6 @@ OpenClaw applies these cleanup rules: - At run end, it removes a worktree only when `git status --porcelain` is empty and `git log HEAD --not --remotes --oneline` finds no unpushed commits. Otherwise it only releases the activity lock. - Hourly cleanup snapshots and removes unlocked Workboard- and session-owned worktrees idle for more than 7 days, even when dirty. Manual worktrees are never automatically removed. -- When `worktrees.cleanup.maxCount` or `worktrees.cleanup.maxTotalSizeGb` is configured, cleanup also snapshots and removes the least recently active Workboard- and session-owned worktrees until the total count and disk size fit the limits. All managed worktrees count toward the totals, but manual and otherwise protected worktrees are never limit-evicted, so a limit can remain exceeded until eligible worktrees exist. 0 or unset disables a limit. - Snapshot records remain restorable for 30 days. Cleanup then deletes the snapshot ref and registry row. - A live OpenClaw process lock and any foreign or unrecognized git worktree lock protect a worktree from garbage collection. @@ -81,7 +80,7 @@ openclaw worktrees restore [--json] openclaw worktrees gc [--json] ``` -The Control UI **Worktrees** page under Settings provides the same actions plus creation with a base-branch picker, shows each worktree's owner (manual, Workboard, or the owning session with a link into its chat), and offers a force retry when a removal reports a failed snapshot. Its **Cleanup** section edits the `worktrees.cleanup` retention limits described in the [configuration reference](/gateway/configuration-reference#worktrees). +The Control UI **Worktrees** page under Settings provides the same actions plus creation with a base-branch picker, shows each worktree's owner (manual, Workboard, or the owning session with a link into its chat), and offers a force retry when a removal reports a failed snapshot. ## Gateway methods diff --git a/docs/concepts/messages.md b/docs/concepts/messages.md index 9b293c0f50d2..cc27ac8ca7b0 100644 --- a/docs/concepts/messages.md +++ b/docs/concepts/messages.md @@ -108,7 +108,7 @@ When a run is already active, inbound messages steer into it by default. `messag | `collect` | Batch compatible messages into one later turn. | | `interrupt` | Abort the active run, then start the newest prompt. | -Defaults: `messages.queue.debounceMs` is 500ms (applies to steer, followup, and collect batching alike), `messages.queue.cap` is 20 queued messages, and `messages.queue.drop` is `summarize` (`old` and `new` are also available). Configure per-channel overrides via `messages.queue.byChannel` and `messages.queue.debounceMsByChannel`. +The queue uses a built-in 500ms debounce for steer, followup, and collect batching. `messages.queue.cap` defaults to 20 queued messages, and `messages.queue.drop` defaults to `summarize` (`old` and `new` are also available). Configure per-channel overrides via `messages.queue.byChannel` and `messages.queue.debounceMsByChannel`. Details: [Command queue](/concepts/queue) and [Steering queue](/concepts/queue-steering). diff --git a/docs/concepts/model-failover.md b/docs/concepts/model-failover.md index 41ee6b9c44df..ff18d231c50c 100644 --- a/docs/concepts/model-failover.md +++ b/docs/concepts/model-failover.md @@ -203,7 +203,7 @@ Regular (non-billing, non-auth-permanent) cooldowns scale with the profile's rec - 2nd failure: 1 minute - 3rd+ failure: 5 minutes (cap) -Counters reset once the profile's failure window has passed (`auth.cooldowns.failureWindowHours`, default 24). +Counters reset once the profile's built-in failure window has passed. State is stored in the per-agent SQLite auth state under `usageStats`: @@ -244,19 +244,6 @@ State is stored in the per-agent SQLite auth state: } ``` -Defaults (`auth.cooldowns.*`): - -| Key | Default | Purpose | -| ----------------------------- | ------- | --------------------------------------------------------------------------- | -| `billingBackoffHours` | 5 | Base billing backoff, doubles per billing failure | -| `billingMaxHours` | 24 | Billing backoff cap | -| `authPermanentBackoffMinutes` | 10 | Base backoff for high-confidence permanent-auth failures | -| `authPermanentMaxMinutes` | 60 | Cap for that backoff | -| `failureWindowHours` | 24 | Failure counters reset if no failures occur in this window | -| `overloadedProfileRotations` | 1 | Same-provider profile rotations allowed before model fallback on overload | -| `overloadedBackoffMs` | 0 | Fixed delay before an overloaded rotation retry | -| `rateLimitedProfileRotations` | 1 | Same-provider profile rotations allowed before model fallback on rate limit | - Overloaded and rate-limit errors are handled more aggressively than billing cooldowns: by default, OpenClaw allows one same-provider auth-profile retry, then switches to the next configured model fallback without waiting. ## Model fallback @@ -363,11 +350,6 @@ That cooldown summary is model-aware: See [Gateway configuration](/gateway/configuration) for: - `auth.profiles` / `auth.order` -- `auth.cooldowns.billingBackoffHours` / `auth.cooldowns.billingBackoffHoursByProvider` -- `auth.cooldowns.billingMaxHours` / `auth.cooldowns.failureWindowHours` -- `auth.cooldowns.authPermanentBackoffMinutes` / `auth.cooldowns.authPermanentMaxMinutes` -- `auth.cooldowns.overloadedProfileRotations` / `auth.cooldowns.overloadedBackoffMs` -- `auth.cooldowns.rateLimitedProfileRotations` - `agents.defaults.model.primary` / `agents.defaults.model.fallbacks` - `agents.defaults.imageModel` routing diff --git a/docs/concepts/queue-steering.md b/docs/concepts/queue-steering.md index 34783cafe964..fc4a486074cf 100644 --- a/docs/concepts/queue-steering.md +++ b/docs/concepts/queue-steering.md @@ -52,7 +52,7 @@ Use `followup` or `collect` when you want messages to queue by default instead o ## Debounce -`messages.queue.debounceMs` applies to queued `followup` and `collect` delivery. In `steer` mode with the native Codex harness, it also sets the quiet window before sending batched `turn/steer`. For OpenClaw, active steering itself does not use the debounce timer because OpenClaw naturally batches messages until the next model boundary. +The built-in queue debounce applies to queued `followup` and `collect` delivery. In `steer` mode with the native Codex harness, it also sets the quiet window before sending batched `turn/steer`. For OpenClaw, active steering itself does not use the debounce timer because OpenClaw naturally batches messages until the next model boundary. ## Related diff --git a/docs/concepts/queue.md b/docs/concepts/queue.md index 27720420ace1..392c6dd846de 100644 --- a/docs/concepts/queue.md +++ b/docs/concepts/queue.md @@ -131,7 +131,7 @@ not a local-mode command. - Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.). - Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel. -- Additional lanes may exist (e.g. `cron`, `cron-nested`, `nested`, `subagent`) so background jobs can run in parallel without blocking inbound replies. Isolated cron agent turns hold a `cron` slot while their inner agent execution uses `cron-nested`; both use `cron.maxConcurrentRuns`. Shared non-cron `nested` flows keep their own lane behavior. These detached runs are tracked as [background tasks](/automation/tasks). +- Additional lanes may exist (e.g. `cron`, `cron-nested`, `nested`, `subagent`) so background jobs can run in parallel without blocking inbound replies. Isolated cron agent turns hold a `cron` slot while their inner agent execution uses `cron-nested`. Shared non-cron `nested` flows keep their own lane behavior. These detached runs are tracked as [background tasks](/automation/tasks). - Per-session lanes guarantee that only one agent run touches a given session at a time. - No external dependencies or background worker threads; pure TypeScript + promises. @@ -139,11 +139,11 @@ not a local-mode command. - If commands seem stuck, enable verbose logs and look for "queued for ...ms" lines to confirm the queue is draining. - Codex app-server runs that accept a turn and then stop emitting progress are interrupted by the Codex adapter so the active session lane can release instead of waiting for the outer run timeout. -- When diagnostics are enabled, sessions that remain in `processing` past `diagnostics.stuckSessionWarnMs` with no observed reply, tool, status, block, or ACP progress are classified by current activity: - - Active work with recent progress logs as `session.long_running`. Owned silent model calls also stay `session.long_running` until `diagnostics.stuckSessionAbortMs` so slow or non-streaming providers are not reported as stalled too early. +- When diagnostics are enabled, sessions that remain in `processing` past the built-in warning threshold with no observed reply, tool, status, block, or ACP progress are classified by current activity: + - Active work with recent progress logs as `session.long_running`. Owned silent model calls also stay `session.long_running` until the built-in abort threshold so slow or non-streaming providers are not reported as stalled too early. - Active work with no recent progress logs as `session.stalled`; owned model calls, blocked tool calls, and stalled embedded runs switch to `session.stalled` at or after the abort threshold. Ownerless stale model/tool activity is not hidden as long-running. - `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity. - - `session.stuck` always triggers recovery that can release the affected session lane. A `session.stalled` classification past `diagnostics.stuckSessionAbortMs` (blocked tool call, stalled model call, or stalled embedded run) can also trigger active-abort recovery, so both classifications can unstick a queue, not only `session.stuck`. + - `session.stuck` always triggers recovery that can release the affected session lane. A `session.stalled` classification past the abort threshold (blocked tool call, stalled model call, or stalled embedded run) can also trigger active-abort recovery, so both classifications can unstick a queue, not only `session.stuck`. - Repeated `session.stuck` and `session.long_running` warning log lines back off exponentially while the session remains unchanged; recovery attempts still run on every heartbeat tick regardless of that backoff. ## Related diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index d0349c31305d..6f5566ae2c4c 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -86,7 +86,7 @@ Thread-scoped chat sessions, such as keys ending in `:thread:`, are not vali Messages and A2A follow-up replies are marked as inter-session data in the receiving prompt (`[Inter-session message ... isUser=false]`) and in transcript provenance. The receiving agent should treat them as tool-routed data, not as a direct end-user-authored instruction. -After the target responds, OpenClaw can run a **reply-back loop** where the agents alternate messages (up to `session.agentToAgent.maxPingPongTurns`, range 0-20, default 5). The target agent can reply `REPLY_SKIP` to stop early. +After the target responds, OpenClaw can run a **reply-back loop** where the agents alternate messages up to the built-in limit. The target agent can reply `REPLY_SKIP` to stop early. Pass `watch: true` to also register the sender as a state-change watcher of the target: when another actor later sends the target a direct human message or changes its goal, the sender receives a system notice pointing at `session_status` `changesSince`. Registration happens after successful dispatch, targets the session that actually received the message, and starts at its current state version, so only later changes produce notices. The result reports `watched: true` when registration succeeded. See [Session state awareness](/concepts/session-state). diff --git a/docs/concepts/typing-indicators.md b/docs/concepts/typing-indicators.md index 090820153306..b2d8d70cfe20 100644 --- a/docs/concepts/typing-indicators.md +++ b/docs/concepts/typing-indicators.md @@ -42,13 +42,12 @@ Set the agent-level default: } ``` -Override mode or cadence per session: +Override the mode per session: ```json5 { session: { typingMode: "message", - typingIntervalSeconds: 4, }, } ``` @@ -59,7 +58,7 @@ Override mode or cadence per session: - `thinking` still reacts to streamed reasoning (`reasoningLevel: "stream"`), and can also start from active execution before reasoning deltas arrive. - Heartbeat typing is a liveness signal for the resolved delivery target. It starts at heartbeat run start instead of following `message` or `thinking` stream timing. Set `typingMode: "never"` to disable it. - Heartbeats do not show typing when the heartbeat target is `"none"`, when the target cannot be resolved, when chat delivery is disabled for the heartbeat, or when the channel does not support typing. -- `typingIntervalSeconds` controls the **refresh cadence**, not the start time. Default: 6 seconds. +- `agents.defaults.typingIntervalSeconds` controls the **refresh cadence**, not the start time. Default: 6 seconds. ## Related diff --git a/docs/docs_map.md b/docs/docs_map.md index 136d5594c8c3..2be084007910 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -3290,7 +3290,6 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H3: agents.defaults.promptOverlays - H3: agents.defaults.heartbeat - H3: agents.defaults.compaction - - H3: agents.defaults.runRetries - H3: agents.defaults.contextPruning - H3: Block streaming - H3: Typing indicators @@ -3421,7 +3420,6 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H3: Supported credential surface - H3: Secret providers config - H2: Auth storage - - H3: auth.cooldowns - H2: Audit - H2: Logging - H2: Diagnostics @@ -3432,10 +3430,8 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H2: Identity - H2: Bridge (legacy, removed) - H2: Cron - - H3: cron.retry - H3: cron.failureAlert - H3: cron.failureDestination - - H2: Worktrees - H2: Media model template variables - H2: Config includes ($include) - H2: Related @@ -3666,7 +3662,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H2: When to use this endpoint - H2: Agent-first model contract - H2: Session behavior - - H2: Request limits (config) + - H2: Request limits - H2: Chat tool contract - H3: Supported request fields - H3: Unsupported variants @@ -3692,7 +3688,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H2: Tools (client-side function tools) - H2: Images (inputimage) - H2: Files (inputfile) - - H2: File + image limits (config) + - H2: File + image limits - H2: Streaming (SSE) - H2: Usage - H2: Errors diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index c86eeaca41ee..054e8c971042 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -138,27 +138,6 @@ openclaw config unset agents.defaults.timeoutSeconds openclaw config set agents.defaults.timeoutSeconds 43200 ``` -For a CLI that legitimately emits no output for long periods, tune the relevant watchdog profile instead of the overall turn timeout: - -```json5 -{ - agents: { - defaults: { - cliBackends: { - "claude-cli": { - reliability: { - watchdog: { - fresh: { noOutputTimeoutMs: 1800000 }, - resume: { noOutputTimeoutMs: 1800000 }, - }, - }, - }, - }, - }, - }, -} -``` - Background work started inside a CLI is still part of that CLI subprocess. If the parent turn reaches its overall limit, OpenClaw stops the subprocess and its CLI-internal background tasks together. For durable long work, use a detached OpenClaw [sub-agent](/tools/subagents) or [ACP agent](/tools/acp-agents); detached sub-agents have no run timeout by default. The `openclaw agent` command also has its own request deadline. Its 600-second fallback default applies to that command invocation, not to ordinary Gateway turns; see [`openclaw agent`](/cli/agent). @@ -192,7 +171,7 @@ Set `agents.defaults.cliBackends.claude-cli.command` only when the `claude` bina - `existing`: only send a session id if one was stored before. - `none`: never send a session id. - `claude-cli` defaults to `liveSession: "claude-stdio"`, `output: "jsonl"`, and `input: "stdin"`, so follow-up turns reuse the live Claude process while it is active, including for custom configs that omit transport fields. If the gateway restarts or the idle process exits, OpenClaw resumes from the stored Claude session id. Stored session ids are verified against a readable project transcript before resume; a missing transcript clears the binding (logged as `reason=transcript-missing`) instead of silently starting a fresh session under `--resume`. -- Claude live sessions keep bounded JSONL output guards: 8 MiB and 20,000 raw JSONL lines per turn by default. Raise them per backend with `agents.defaults.cliBackends.claude-cli.reliability.outputLimits.maxTurnRawChars` and `maxTurnLines`; OpenClaw clamps those settings to 64 MiB and 100,000 lines. +- Claude live sessions keep bounded JSONL output guards: 8 MiB and 20,000 raw JSONL lines per turn. - Stored CLI sessions are provider-owned continuity. Automatic reset is disabled by default; `/reset` and explicit daily or idle `session.reset` policies still cut them. - Fresh CLI sessions normally reseed only from OpenClaw's compaction summary plus the post-compaction tail. To recover short sessions invalidated before compaction, a backend can opt in with `reseedFromRawTranscriptWhenUncompacted: true`. Raw transcript reseed stays bounded and limited to safe invalidations, such as a missing CLI transcript, an orphaned tool-use tail, message-policy/system-prompt/cwd/MCP changes, or a session-expired retry; auth profile or credential-epoch changes never reseed raw transcript history. @@ -326,13 +305,13 @@ When bundle MCP is enabled, OpenClaw: If no MCP servers are enabled, OpenClaw still injects a strict config when a backend opts into bundle MCP, so background runs stay isolated. -Session-scoped bundled MCP runtimes are cached for reuse within a session, then reaped after `mcp.sessionIdleTtlMs` milliseconds of idle time (default 10 minutes; set `0` to disable). One-shot embedded runs such as auth probes, slug generation, and active-memory recall request cleanup at run end so stdio children and Streamable HTTP/SSE streams do not outlive the run. +Session-scoped bundled MCP runtimes are cached for reuse within a session, then reaped after 10 minutes of idle time. One-shot embedded runs such as auth probes, slug generation, and active-memory recall request cleanup at run end so stdio children and Streamable HTTP/SSE streams do not outlive the run. ## Reseed history cap When a fresh CLI session is seeded from a prior OpenClaw transcript (for example after a `session_expired` retry), the rendered `` block is capped to keep reseed prompts from exploding. The default is 12,288 characters (about 3,000 tokens). -Claude CLI backends scale this cap with the resolved Claude context window instead: larger context windows get a larger prior-history slice, up to a fixed ceiling; other CLI backends keep the conservative default. This cap only governs the reseed prompt's prior-history block — live-session output limits are tuned separately under `reliability.outputLimits` (see [Sessions](#sessions)). +Claude CLI backends scale this cap with the resolved Claude context window instead: larger context windows get a larger prior-history slice, up to a fixed ceiling; other CLI backends keep the conservative default. This cap only governs the reseed prompt's prior-history block. ## Limitations diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index fd1029792531..b35f779ab9c2 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -620,10 +620,8 @@ Periodic heartbeat runs. provider: "my-provider", // id of a registered compaction provider plugin (optional) thinkingLevel: "low", // optional compaction-only thinking override timeoutSeconds: 180, - reserveTokensFloor: 24000, keepRecentTokens: 50000, recentTurnsPreserve: 3, - maxHistoryShare: 0.7, identifierPolicy: "strict", // strict | off | custom identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom qualityGuard: { enabled: true, maxRetries: 1 }, @@ -652,11 +650,8 @@ Periodic heartbeat runs. - `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction). - `thinkingLevel`: optional thinking level used only for embedded OpenClaw compaction summaries (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `adaptive`, `max`, or `ultra`). It overrides the session's current thinking level and is clamped to the selected compaction model/runtime. Leave unset to inherit the session level. Native Codex app-server compaction ignores this setting because the native compact request has no per-operation thinking override; OpenClaw logs a warning when configured. - `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `180`. -- `reserveTokens`: token headroom kept available for model output and future tool results after compaction. When the model context window is known, OpenClaw caps the effective reserve so it cannot consume the prompt budget. -- `reserveTokensFloor`: minimum reserve enforced by the embedded runtime. Set `0` to disable the floor. The floor remains subject to the active context-window cap. - `keepRecentTokens`: agent cut-point budget for keeping the most recent transcript tail verbatim. Manual `/compact` honors this when explicitly set; otherwise manual compaction is a hard checkpoint. - `recentTurnsPreserve`: number of most recent user/assistant turns kept verbatim outside safeguard summarization. Default: `3`. -- `maxHistoryShare`: maximum fraction of the total context budget allowed for retained history after compaction (range `0.1`-`0.9`). - `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization. - `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`. - `qualityGuard`: retry-on-malformed-output checks for safeguard summaries. Enabled by default in safeguard mode; set `enabled: false` to skip the audit. @@ -669,36 +664,6 @@ Periodic heartbeat runs. - `notifyUser`: when `true`, sends brief context-maintenance notices to the user: when compaction starts and completes (for example, "Compacting context..." and "Compaction complete"), and when a pre-compaction memory flush is exhausted so the reply continues in a degraded state (for example, "Memory maintenance temporarily failed; continuing your reply."). Disabled by default to keep these notices silent. - `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Set `model` to an exact provider/model such as `ollama/qwen3:8b` when this housekeeping turn should stay on a local model; the override does not inherit the active session fallback chain. `forceFlushTranscriptBytes` forces the flush when transcript size reaches the threshold even if token counters are stale. Skipped when workspace is read-only. -### `agents.defaults.runRetries` - -Outer run loop retry iteration boundaries for the embedded agent runtime to prevent infinite execution loops during failure recovery. This setting only applies to the embedded agent runtime, not ACP or CLI runtimes. - -```json5 -{ - agents: { - defaults: { - runRetries: { - base: 24, - perProfile: 8, - min: 32, - max: 160, - }, - }, - list: [ - { - id: "main", - runRetries: { max: 50 }, // optional per-agent overrides - }, - ], - }, -} -``` - -- `base`: base number of run retry iterations for the outer run loop. Default: `24`. -- `perProfile`: additional run retry iterations granted per fallback profile candidate. Default: `8`. -- `min`: minimum absolute limit for run retry iterations. Default: `32`. -- `max`: maximum absolute limit for run retry iterations to prevent runaway execution. Default: `160`. - ### `agents.defaults.contextPruning` Prunes **old tool results** from in-memory context before sending to the LLM. Does **not** modify session history on disk. Disabled by default; set `mode: "cache-ttl"` to enable. @@ -709,14 +674,6 @@ Prunes **old tool results** from in-memory context before sending to the LLM. Do defaults: { contextPruning: { mode: "cache-ttl", // off (default) | cache-ttl - ttl: "1h", // duration (ms/s/m/h), default unit: minutes; default: 5m - keepLastAssistants: 3, - softTrimRatio: 0.3, - hardClearRatio: 0.5, - minPrunableToolChars: 50000, - softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 }, - hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" }, - tools: { deny: ["browser", "canvas"] }, }, }, }, @@ -726,9 +683,7 @@ Prunes **old tool results** from in-memory context before sending to the LLM. Do - `mode: "cache-ttl"` enables pruning passes. -- `ttl` controls how often pruning can run again (after the last cache touch). Default: `5m`. - Pruning soft-trims oversized tool results first, then hard-clears older tool results if needed. -- `softTrimRatio` and `hardClearRatio` accept values from `0.0` through `1.0`; config validation rejects values outside that range. **Soft-trim** keeps beginning + end and inserts `...` in the middle. @@ -738,7 +693,7 @@ Notes: - Image blocks are never trimmed/cleared. - Ratios are character-based (approximate), not exact token counts. -- If fewer than `keepLastAssistants` assistant messages exist, pruning is skipped. +- The most recent assistant messages are preserved. @@ -782,7 +737,7 @@ See [Streaming](/concepts/streaming) for behavior + chunking details. - Defaults: `instant` for direct chats/mentions, `message` for unmentioned group chats. - `typingIntervalSeconds` default: `6`. -- Per-session overrides: `session.typingMode`, `session.typingIntervalSeconds`. +- Per-session overrides: `session.typingMode`. See [Typing Indicators](/concepts/typing-indicators). @@ -1288,18 +1243,12 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden maxDiskBytes: "500mb", // optional hard budget highWaterBytes: "400mb", // optional cleanup target }, - writeLock: { - acquireTimeoutMs: 60000, - staleMs: 1800000, - maxHoldMs: 300000, - }, threadBindings: { enabled: true, idleHours: 24, // default inactivity auto-unfocus in hours (`0` disables) maxAgeHours: 0, // default hard max age in hours (`0` disables) }, mainKey: "main", // legacy (runtime always uses "main") - agentToAgent: { maxPingPongTurns: 5 }, sendPolicy: { rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }], default: "allow", @@ -1323,7 +1272,6 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden - **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`. - **`resetByChannel`**: per-channel reset overrides keyed by provider/channel id. When the session's channel has a matching entry, it wins outright over `resetByType`/`reset` for that session. Use only when one channel needs reset behavior different from the type-level policy. - **`mainKey`**: legacy field. Runtime always uses `"main"` for the main direct-chat bucket. -- **`agentToAgent.maxPingPongTurns`**: maximum reply-back turns between agents during agent-to-agent exchanges (integer, range: `0`-`20`, default: `5`). `0` disables ping-pong chaining. - **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins. - **`maintenance`**: session-store cleanup + retention controls. - `mode`: `enforce` applies cleanup and is the default; `warn` emits warnings only. @@ -1334,10 +1282,6 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden - `resetArchiveRetention`: age-based retention for reset/deleted transcript archives. By default, archives remain until disk-budget eviction; set a duration to opt into wall-clock deletion, or `false` to disable it explicitly. - `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first. - `highWaterBytes`: optional target after budget cleanup. Defaults to `80%` of `maxDiskBytes`. -- **`writeLock`**: session transcript write-lock controls. Tune only when legitimate transcript prep, cleanup, compaction, or mirror work contends longer than the default policies. - - `acquireTimeoutMs`: milliseconds to wait while acquiring a lock before reporting the session as busy. Default: `60000`; env override `OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS`. - - `staleMs`: milliseconds before an existing lock is treated as stale and reclaimed. Default: `1800000`; env override `OPENCLAW_SESSION_WRITE_LOCK_STALE_MS`. - - `maxHoldMs`: milliseconds a held in-process lock may remain held before the watchdog releases it. Default: `300000`; env override `OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS`. - **`threadBindings`**: global defaults for thread-bound session features. - `enabled`: master default switch (providers can override; Discord uses `channels.discord.threadBindings.enabled`) - `idleHours`: default inactivity auto-unfocus in hours (`0` disables; providers can override) diff --git a/docs/gateway/config-channels.md b/docs/gateway/config-channels.md index 85687b85c9e3..296900c6e8c7 100644 --- a/docs/gateway/config-channels.md +++ b/docs/gateway/config-channels.md @@ -116,19 +116,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat { web: { enabled: true, - heartbeatSeconds: 60, - whatsapp: { - keepAliveIntervalMs: 25000, - connectTimeoutMs: 60000, - defaultQueryTimeoutMs: 60000, - }, - reconnect: { - initialMs: 2000, - maxMs: 30000, - factor: 1.8, - jitter: 0.25, - maxAttempts: 12, // 0 = retry forever - }, }, channels: { whatsapp: { @@ -148,8 +135,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat } ``` -- `web.whatsapp.keepAliveIntervalMs` (default `25000`), `connectTimeoutMs` (default `60000`), and `defaultQueryTimeoutMs` (default `60000`) tune the Baileys socket. -- `web.reconnect` defaults: `initialMs: 2000`, `maxMs: 30000`, `factor: 1.8`, `jitter: 0.25`, `maxAttempts: 12`. `maxAttempts: 0` retries forever instead of giving up. - Top-level `bindings[]` entries with `type: "acp"` configure persistent ACP bindings for WhatsApp DMs and groups. Use an E.164 direct number or WhatsApp group JID in `match.peer.id`. Field semantics are shared in [ACP Agents](/tools/acp-agents#persistent-channel-bindings). diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index caadbb244813..b235226070df 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -227,56 +227,11 @@ Tool-loop safety checks are **disabled by default**. Set `enabled: true` to acti tools: { loopDetection: { enabled: true, - historySize: 30, - warningThreshold: 10, - unknownToolThreshold: 10, - criticalThreshold: 20, - globalCircuitBreakerThreshold: 30, - detectors: { - genericRepeat: true, - knownPollNoProgress: true, - pingPong: true, - }, - postCompactionGuard: { - windowSize: 3, - }, }, }, } ``` - - Max tool-call history retained for loop analysis. - - - Repeating no-progress pattern threshold for warnings. - - - Blocks repeated calls to the same unavailable/unknown tool name after this many misses. - - - Higher repeating threshold for blocking critical loops. - - - Hard stop threshold for any no-progress run. - - - Warn on repeated same-tool/same-args calls. - - - Warn/block on known poll tools (`process.poll`, `command_status`, etc.). - - - Warn/block on alternating no-progress pair patterns. - - - Number of attempts after auto-compaction the guard stays armed for; aborts if the agent repeats the same (tool, args, result) inside that window. - - - -If `warningThreshold >= criticalThreshold` or `criticalThreshold >= globalCircuitBreakerThreshold`, validation fails. - - ### `tools.web` ```json5 diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 214f40b765f2..5b59cefe060a 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -173,7 +173,6 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number. maxDiskBytes: "500mb", // optional highWaterBytes: "400mb", // optional (defaults to 80% of maxDiskBytes) }, - typingIntervalSeconds: 5, sendPolicy: { default: "allow", rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }], @@ -384,7 +383,6 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number. cron: { enabled: true, store: "~/.openclaw/cron/jobs.json", - maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution sessionRetention: "24h", }, diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 49e42b866258..c0c63165cf04 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -85,8 +85,6 @@ target server during config edits. ```json5 { mcp: { - // Optional. Default: 600000 ms (10 minutes). Set 0 to disable idle eviction. - sessionIdleTtlMs: 600000, servers: { docs: { command: "npx", @@ -157,9 +155,8 @@ target server during config edits. block before passing native `mcp_servers` config to Codex. Omit the block to keep the server projected for every Codex app-server agent with Codex's default MCP approval behavior. -- `mcp.sessionIdleTtlMs`: idle TTL for session-scoped bundled MCP runtimes. - One-shot embedded runs request run-end cleanup; this TTL is the backstop for - long-lived sessions and future callers. +- Session-scoped bundled MCP runtimes use a built-in 10-minute idle TTL. + One-shot embedded runs request run-end cleanup; the TTL is the backstop for long-lived sessions and future callers. - Changes under `mcp.*` hot-apply by disposing cached session MCP runtimes. The next tool discovery/use recreates them from the new config, so removed `mcp.servers` entries are reaped immediately instead of waiting for idle TTL. @@ -451,9 +448,7 @@ See [Inferred commitments](/concepts/commitments). - `tabCleanup` controls best-effort periodic cleanup for tracked primary-agent tabs after idle time or when a session exceeds its cap. Tracking applies only to tabs created by browser tool `action: "open"`; tabs opened by the user or - with unknown ownership are never adopted. Set `idleMinutes: 0` or - `maxTabsPerSession: 0` to disable those individual cleanup modes. Disabling - `tabCleanup` does not disable explicit session lifecycle cleanup. + with unknown ownership are never adopted. Disabling `tabCleanup` does not disable explicit session lifecycle cleanup. - Host-local opens with a stable native CDP target and browser identity are stored in shared SQLite state and remain eligible across Gateway restarts for `/new` and session lifecycle cleanup. Native tool-facing CDP targets also @@ -475,10 +470,6 @@ See [Inferred commitments](/concepts/commitments). - `profiles.*.cdpUrl` accepts `http://`, `https://`, `ws://`, and `wss://`. Use HTTP(S) when you want OpenClaw to discover `/json/version`; use WS(S) when your provider gives you a direct DevTools WebSocket URL. -- `remoteCdpTimeoutMs` and `remoteCdpHandshakeTimeoutMs` apply to remote and - `attachOnly` CDP reachability plus tab-opening requests. Managed loopback - profiles keep local CDP defaults. Persistent remote Playwright tab - enumeration uses the larger value as its operation deadline. - If an externally managed CDP service is reachable through loopback, set that profile's `attachOnly: true`; otherwise OpenClaw treats the loopback port as a local managed browser profile and may report local port ownership errors. @@ -500,11 +491,6 @@ See [Inferred commitments](/concepts/commitments). - Local managed profiles can set `executablePath` to override the global `browser.executablePath` for that profile. Use this to run one profile in Chrome and another in Brave. -- Local managed profiles use `browser.localLaunchTimeoutMs` for Chrome CDP HTTP - discovery after process start and `browser.localCdpReadyTimeoutMs` for - post-launch CDP websocket readiness. Raise them on slower hosts where Chrome - starts successfully but readiness checks race startup. Both values must be - positive integers up to `120000` ms; invalid config values are rejected. - Auto-detect order: default browser if Chromium-based → Chrome → Brave → Edge → Chromium → Chrome Canary. - `browser.executablePath` and `browser.profiles..executablePath` both accept `~` and `~/...` for your OS home directory before Chromium launch. @@ -681,10 +667,7 @@ See [Inferred commitments](/concepts/commitments). - Relay-backed registrations are delegated to a specific gateway identity. The paired iOS app fetches `gateway.identity.get`, includes that identity in the relay registration, and forwards a registration-scoped send grant to the gateway. Another gateway cannot reuse that stored registration. - `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above. - `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true`: development-only escape hatch for loopback HTTP relay URLs. Production relay URLs should stay on HTTPS. -- `gateway.handshakeTimeoutMs`: pre-auth Gateway WebSocket handshake timeout in milliseconds. Default: `15000`. `OPENCLAW_HANDSHAKE_TIMEOUT_MS` takes precedence when set. Increase this on loaded or low-powered hosts where local clients can connect while startup warmup is still settling. -- `gateway.channelHealthCheckMinutes`: channel health-monitor interval in minutes. Set `0` to disable health-monitor restarts globally. Default: `5`. -- `gateway.channelStaleEventThresholdMinutes`: stale-socket threshold in minutes. Keep this greater than or equal to `gateway.channelHealthCheckMinutes`. Default: `30`. -- `gateway.channelMaxRestartsPerHour`: maximum health-monitor restarts per channel/account in a rolling hour. Default: `10`. +- `OPENCLAW_HANDSHAKE_TIMEOUT_MS`: optional environment override for the built-in pre-auth Gateway WebSocket handshake timeout. - `channels..healthMonitor.enabled`: per-channel opt-out for health-monitor restarts while keeping the global monitor enabled. - `channels..accounts..healthMonitor.enabled`: per-account override for multi-account channels. When set, it takes precedence over the channel-level override. - Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset. @@ -887,7 +870,6 @@ Lifetime values are data only in the first cloud-worker release; automatic enfor enabled: true, token: "shared-secret", path: "/hooks", - maxBodyBytes: 262144, defaultSessionKey: "hook:ingress", allowRequestSessionKey: true, allowedSessionKeyPrefixes: ["hook:", "hook:gmail:"], @@ -1193,42 +1175,6 @@ Notes: - See [OAuth](/concepts/oauth). - Secrets runtime behavior and `audit/configure/apply` tooling: [Secrets Management](/gateway/secrets). -### `auth.cooldowns` - -```json5 -{ - auth: { - cooldowns: { - billingBackoffHours: 5, - billingBackoffHoursByProvider: { anthropic: 3, openai: 8 }, - billingMaxHours: 24, - authPermanentBackoffMinutes: 10, - authPermanentMaxMinutes: 60, - failureWindowHours: 24, - overloadedProfileRotations: 1, - overloadedBackoffMs: 0, - rateLimitedProfileRotations: 1, - }, - }, -} -``` - -- `billingBackoffHours`: base backoff in hours when a profile fails due to true - billing/insufficient-credit errors (default: `5`). Explicit billing text can - still land here even on `401`/`403` responses, but provider-specific text - matchers stay scoped to the provider that owns them (for example OpenRouter - `Key limit exceeded`). Retryable HTTP `402` usage-window or - organization/workspace spend-limit messages stay in the `rate_limit` path - instead. -- `billingBackoffHoursByProvider`: optional per-provider overrides for billing backoff hours. -- `billingMaxHours`: cap in hours for billing backoff exponential growth (default: `24`). -- `authPermanentBackoffMinutes`: base backoff in minutes for high-confidence `auth_permanent` failures (default: `10`). -- `authPermanentMaxMinutes`: cap in minutes for `auth_permanent` backoff growth (default: `60`). -- `failureWindowHours`: rolling window in hours used for backoff counters (default: `24`). -- `overloadedProfileRotations`: maximum same-provider auth-profile rotations for overloaded errors before switching to model fallback (default: `1`). Provider-busy shapes such as `ModelNotReadyException` land here. -- `overloadedBackoffMs`: fixed delay before retrying an overloaded provider/profile rotation (default: `0`). -- `rateLimitedProfileRotations`: maximum same-provider auth-profile rotations for rate-limit errors before switching to model fallback (default: `1`). That rate-limit bucket includes provider-shaped text such as `Too many concurrent requests`, `ThrottlingException`, `concurrency limit reached`, `workers_ai ... quota limit exceeded`, and `resource exhausted`. - --- ## Audit @@ -1307,9 +1253,6 @@ writer is best-effort, not a lossless compliance archive. diagnostics: { enabled: true, flags: ["telegram.*"], - stuckSessionWarnMs: 30000, - stuckSessionAbortMs: 300000, - memoryPressureSnapshot: false, otel: { enabled: false, @@ -1350,9 +1293,6 @@ writer is best-effort, not a lossless compliance archive. - `enabled`: master toggle for instrumentation output (default: `true`). - `flags`: array of flag strings enabling targeted log output (supports wildcards like `"telegram.*"` or `"*"`). -- `stuckSessionWarnMs`: no-progress age threshold in ms for classifying long-running processing sessions as `session.long_running`, `session.stalled`, or `session.stuck` (default: `120000`). Reply, tool, status, block, and ACP progress reset the timer; repeated `session.stuck` diagnostics back off while unchanged. -- `stuckSessionAbortMs`: no-progress age threshold in ms before eligible stalled active work may be abort-drained for recovery. When unset, OpenClaw uses the safer extended embedded-run window of at least 5 minutes and 3x `stuckSessionWarnMs`. -- `memoryPressureSnapshot`: captures a redacted pre-OOM stability snapshot when memory pressure reaches `critical` (default: `false`). Set to `true` to add the stability bundle file scan/write while keeping normal memory pressure events. - `otel.enabled`: enables the OpenTelemetry export pipeline (default: `false`). For the full configuration, signal catalog, and privacy model, see [OpenTelemetry export](/gateway/opentelemetry). - `otel.endpoint`: collector URL for OTel export. - `otel.tracesEndpoint` / `otel.metricsEndpoint` / `otel.logsEndpoint`: optional signal-specific OTLP endpoints. When set, they override `otel.endpoint` for that signal only. @@ -1383,9 +1323,6 @@ writer is best-effort, not a lossless compliance archive. auto: { enabled: false, - stableDelayHours: 6, - stableJitterHours: 12, - betaCheckIntervalHours: 1, }, }, } @@ -1394,9 +1331,6 @@ writer is best-effort, not a lossless compliance archive. - `channel`: release channel - `"stable"`, `"extended-stable"`, `"beta"`, or `"dev"`. Extended-stable is package-only: foreground commands own installation, while the Gateway may emit read-only update hints. - `checkOnStart`: check for npm updates when the gateway starts (default: `true`). Stored extended-stable selections use the same read-only hint and 24-hour hint schedule. - `auto.enabled`: enable background auto-update for stable and beta package installs (default: `false`). Extended-stable never applies automatically. -- `auto.stableDelayHours`: minimum delay in hours before stable-channel auto-apply (default: `6`; max: `168`). -- `auto.stableJitterHours`: extra stable-channel rollout spread window in hours (default: `12`; max: `168`). -- `auto.betaCheckIntervalHours`: how often beta-channel checks run in hours (default: `1`; max: `24`). Stable delay/jitter and beta polling settings do not apply to extended-stable. --- @@ -1411,20 +1345,9 @@ writer is best-effort, not a lossless compliance archive. fallbacks: ["acpx-secondary"], defaultAgent: "main", allowedAgents: ["main", "ops"], - maxConcurrentSessions: 10, - stream: { - coalesceIdleMs: 50, - maxChunkChars: 1000, repeatSuppression: true, deliveryMode: "live", // live | final_only - hiddenBoundarySeparator: "paragraph", // none | space | newline | paragraph - maxOutputChars: 50000, - maxSessionUpdateChars: 500, - }, - - runtime: { - ttlMinutes: 30, }, }, } @@ -1437,16 +1360,9 @@ writer is best-effort, not a lossless compliance archive. - `fallbacks`: ordered list of fallback ACP backend ids tried when the primary backend fails early with a transient-looking error (unavailable, rate-limited, quota exhausted, or overloaded) before it produced any output. Each entry must match a registered ACP runtime plugin backend. - `defaultAgent`: fallback ACP target agent id when spawns do not specify an explicit target. - `allowedAgents`: allowlist of agent ids permitted for ACP runtime sessions; empty means no additional restriction. -- `maxConcurrentSessions`: maximum concurrently active ACP sessions. -- `stream.coalesceIdleMs`: idle flush window in ms for streamed text. -- `stream.maxChunkChars`: maximum chunk size before splitting streamed block projection. - `stream.repeatSuppression`: suppress repeated status/tool lines per turn (default: `true`). - `stream.deliveryMode`: `"live"` streams incrementally; `"final_only"` buffers until turn terminal events. -- `stream.hiddenBoundarySeparator`: separator before visible text after hidden tool events (default: `"paragraph"`). -- `stream.maxOutputChars`: maximum assistant output characters projected per ACP turn. -- `stream.maxSessionUpdateChars`: maximum characters for projected ACP status/update lines. - `stream.tagVisibility`: record of tag names to boolean visibility overrides for streamed events. -- `runtime.ttlMinutes`: idle TTL in minutes for ACP session workers before eligible cleanup. - `runtime.installCommand`: optional install command to run when bootstrapping an ACP runtime environment. --- @@ -1532,7 +1448,6 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway { cron: { enabled: true, - maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs webhookToken: "replace-with-dedicated-token", // optional bearer token for outbound webhook auth sessionRetention: "24h", // duration string or false @@ -1545,26 +1460,6 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway - `webhookToken`: bearer token used for cron webhook POST delivery (`delivery.mode = "webhook"`), if omitted no auth header is sent. - `webhook`: deprecated legacy fallback webhook URL (http/https) used by `openclaw doctor --fix` to migrate stored jobs that still have `notify: true`; runtime delivery uses per-job `delivery.mode="webhook"` plus `delivery.to`, or `delivery.completionDestination` when preserving announce delivery. -### `cron.retry` - -```json5 -{ - cron: { - retry: { - maxAttempts: 3, - backoffMs: [30000, 60000, 300000], - retryOn: ["rate_limit", "overloaded", "network", "timeout", "server_error"], - }, - }, -} -``` - -- `maxAttempts`: maximum retries for cron jobs on transient errors (default: `3`; range: `0`-`10`). -- `backoffMs`: array of backoff delays in ms for each retry attempt (default: `[30000, 60000, 300000]`; 1-10 entries). -- `retryOn`: error types that trigger retries - `"rate_limit"`, `"overloaded"`, `"network"`, `"timeout"`, `"server_error"`. Omit to retry all transient types. - -One-shot jobs stay enabled until retry attempts are exhausted, then disable while keeping the final error state. Recurring jobs use the same transient retry policy to run again after backoff before their next scheduled slot; permanent errors or exhausted transient retries fall back to the normal recurring schedule with error backoff. - ### `cron.failureAlert` ```json5 @@ -1615,30 +1510,6 @@ One-shot jobs stay enabled until retry attempts are exhausted, then disable whil See [Cron Jobs](/automation/cron-jobs). Isolated cron executions are tracked as [background tasks](/automation/tasks). ---- - -## Worktrees - -```json5 -{ - worktrees: { - cleanup: { - maxCount: 25, // max managed worktrees across all repositories; 0 or unset disables - maxTotalSizeGb: 50, // max total size in GB across all managed worktrees; 0 or unset disables - }, - }, -} -``` - -Retention limits for OpenClaw-managed worktrees, enforced by hourly cleanup, `openclaw worktrees gc`, and the Control UI **Clean up now** action. When a limit is exceeded, cleanup snapshots and removes the least recently active session- and Workboard-owned worktrees until the count and total size fit. Manual worktrees, worktrees with live locks or run leases, and worktrees owned by recently active sessions are never limit-evicted, so a limit can remain exceeded when only protected worktrees are left. Removed worktrees stay restorable from their snapshots for 30 days. - -- `cleanup.maxCount`: maximum number of managed worktrees to retain across all repositories. Default: unset (no limit). -- `cleanup.maxTotalSizeGb`: maximum total disk size in GB across all managed worktrees, measured during cleanup. Fractional values such as `0.5` are accepted. Default: unset (no limit). - -The Control UI **Worktrees** page under Settings exposes both limits as stepper controls. See [Managed worktrees](/concepts/managed-worktrees). - ---- - ## Media model template variables Template placeholders expanded in `tools.media.models[].args`: diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 37cc4ce0efbc..8dff9e264e96 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -238,16 +238,11 @@ candidate contains a redacted secret placeholder such as `***` or `[redacted]`. - - Control how aggressively the gateway restarts channels that look stale: + + Disable or enable automatic health restarts for a channel or account: ```json5 { - gateway: { - channelHealthCheckMinutes: 5, - channelStaleEventThresholdMinutes: 30, - channelMaxRestartsPerHour: 10, - }, channels: { telegram: { healthMonitor: { enabled: false }, @@ -261,31 +256,11 @@ candidate contains a redacted secret placeholder such as `***` or `[redacted]`. } ``` - - Values shown are the defaults. Set `gateway.channelHealthCheckMinutes: 0` to disable health-monitor restarts globally. - - `channelStaleEventThresholdMinutes` should be greater than or equal to the check interval. - - Use `channels..healthMonitor.enabled` or `channels..accounts..healthMonitor.enabled` to disable auto-restarts for one channel or account without disabling the global monitor. + - Use `channels..healthMonitor.enabled` or `channels..accounts..healthMonitor.enabled` to control auto-restarts for one channel or account. - See [Health Checks](/gateway/health) for operational debugging and the [full reference](/gateway/configuration-reference#gateway) for all fields. - - Give local clients more time to complete the pre-auth WebSocket handshake on - loaded or low-powered hosts: - - ```json5 - { - gateway: { - handshakeTimeoutMs: 30000, - }, - } - ``` - - - Default is `15000` milliseconds. - - `OPENCLAW_HANDSHAKE_TIMEOUT_MS` still takes precedence for one-off service or shell overrides. - - Prefer fixing startup/event-loop stalls first; this knob is for hosts that are healthy but slow during warmup. - - - Sessions control conversation continuity and isolation: @@ -420,7 +395,6 @@ candidate contains a redacted secret placeholder such as `***` or `[redacted]`. { cron: { enabled: true, - maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution sessionRetention: "24h", }, } diff --git a/docs/gateway/diagnostics.md b/docs/gateway/diagnostics.md index 0ace34b37401..d87f7ab70f8b 100644 --- a/docs/gateway/diagnostics.md +++ b/docs/gateway/diagnostics.md @@ -175,21 +175,9 @@ diagnostic event collection: Disabling diagnostics reduces bug-report detail; it does not affect normal Gateway logging. -Critical memory pressure snapshots are off by default. To capture the -pre-OOM stability snapshot in addition to normal diagnostics events: - -```json5 -{ - diagnostics: { - memoryPressureSnapshot: true, - }, -} -``` - -Use this only on hosts that can tolerate the extra file-system scan and -snapshot write during critical memory pressure. Normal memory pressure events -still record RSS, heap, threshold, and growth facts (`rss_threshold`, -`heap_threshold`, `rss_growth`) when the snapshot is off. +Memory pressure events record RSS, heap, threshold, and growth facts +(`rss_threshold`, `heap_threshold`, `rss_growth`) without performing a +file-system scan or writing a pre-OOM snapshot. ## Related diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 1ec5c6263eec..0d23a81d4e8f 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -302,7 +302,7 @@ That stages grounded durable candidates into the short-term dreaming store while | `plugins.openai-codex` policy ids | `plugins.openai` | | `tools.web.x_search.apiKey` | `plugins.entries.xai.config.webSearch.apiKey` | | `session.maintenance.rotateBytes`, `session.parentForkMaxTokens` | removed (deprecated) | - | `diagnostics.memoryPressureBundle` | `diagnostics.memoryPressureSnapshot` | + | Runtime and channel tuning knobs retired in 2026.7 | removed (built-in production defaults apply) | The `plugins.entries.voice-call.config.*` rows above are normalized by diff --git a/docs/gateway/health.md b/docs/gateway/health.md index 528574ab3e14..9c4fe84fb283 100644 --- a/docs/gateway/health.md +++ b/docs/gateway/health.md @@ -31,15 +31,12 @@ health commands above for live connectivity checks. - Creds on disk: `ls -l ~/.openclaw/credentials/whatsapp//creds.json` (mtime should be recent). - Session store: `ls -l ~/.openclaw/agents//agent/openclaw-agent.sqlite`. Count and recent recipients are surfaced via `status`. - Relink flow: `openclaw channels logout && openclaw channels login --verbose` when status codes 409-515 or `loggedOut` appear in logs. The QR login flow auto-restarts once for status 515 after pairing. -- Diagnostics are enabled by default (`diagnostics.enabled: false` disables them). Memory events record RSS/heap byte counts and threshold/growth pressure; critical memory pressure logs through the gateway logger and, when `diagnostics.memoryPressureSnapshot: true` is set, also writes a pre-OOM stability bundle (V8 heap stats, Linux cgroup counters when available, active resource counts, largest session/transcript files by redacted relative path). Liveness warnings record event-loop delay/utilization, CPU-core ratio, and active/waiting/queued session counts when the process is running but saturated. Oversized-payload events record what was rejected/truncated/chunked plus sizes and limits, never message text, attachment contents, webhook bodies, raw request/response bodies, tokens, cookies, or secret values. -- The same heartbeat drives the bounded stability recorder: `openclaw gateway stability` (or the `diagnostics.stability` Gateway RPC). Fatal Gateway exits, shutdown timeouts, restart startup failures, and (when `diagnostics.memoryPressureSnapshot: true`) critical memory pressure persist the latest snapshot under `~/.openclaw/logs/stability/`. Inspect the newest bundle with `openclaw gateway stability --bundle latest`. +- Diagnostics are enabled by default (`diagnostics.enabled: false` disables them). Memory events record RSS/heap byte counts and threshold/growth pressure. Liveness warnings record event-loop delay/utilization, CPU-core ratio, and active/waiting/queued session counts when the process is running but saturated. Oversized-payload events record what was rejected/truncated/chunked plus sizes and limits, never message text, attachment contents, webhook bodies, raw request/response bodies, tokens, cookies, or secret values. +- The same heartbeat drives the bounded stability recorder: `openclaw gateway stability` (or the `diagnostics.stability` Gateway RPC). Fatal Gateway exits, shutdown timeouts, and restart startup failures persist the latest snapshot under `~/.openclaw/logs/stability/`. Inspect the newest bundle with `openclaw gateway stability --bundle latest`. - For bug reports, run `openclaw gateway diagnostics export` and attach the generated zip: a Markdown summary, the newest stability bundle, sanitized log metadata, sanitized Gateway status/health snapshots, and config shape. Chat text, webhook bodies, tool outputs, credentials, cookies, account/message identifiers, and secret values are omitted or redacted. See [Diagnostics Export](/gateway/diagnostics). ## Health monitor config -- `gateway.channelHealthCheckMinutes`: how often the gateway checks channel health. Default: `5`. Set `0` to disable health-monitor restarts globally. -- `gateway.channelStaleEventThresholdMinutes`: how long a connected channel can stay idle before the health monitor treats it as stale and restarts it. Default: `30`. Keep this greater than or equal to `gateway.channelHealthCheckMinutes`. -- `gateway.channelMaxRestartsPerHour`: rolling one-hour cap for health-monitor restarts per channel/account. Default: `10`. - `channels..healthMonitor.enabled`: disable health-monitor restarts for a specific channel while leaving global monitoring enabled. - `channels..accounts..healthMonitor.enabled`: multi-account override that wins over the channel-level setting. - These per-channel overrides apply to the built-in channels that expose them today: Discord, Google Chat, iMessage, IRC, Microsoft Teams, Signal, Slack, Telegram, and WhatsApp. diff --git a/docs/gateway/openai-http-api.md b/docs/gateway/openai-http-api.md index 1c7adbd3e305..78fe97e800cb 100644 --- a/docs/gateway/openai-http-api.md +++ b/docs/gateway/openai-http-api.md @@ -104,9 +104,12 @@ By default the endpoint is **stateless per request** (a new session key is gener If the request includes an OpenAI `user` string, the Gateway derives a stable session key from it so repeated calls can share an agent session. For custom apps, reuse the same `user` value per conversation thread; avoid account-level identifiers unless you want multiple conversations/devices to share one OpenClaw session. Use `x-openclaw-session-key` only when you need explicit routing control across multiple clients/threads, with application-owned keys that avoid the reserved namespaces above. -## Request limits (config) +## Request limits -Defaults can be tuned under `gateway.http.endpoints.chatCompletions`: +The endpoint uses built-in limits of 20 MB per request body, 8 `image_url` +parts from the latest user message, and 20 MB of cumulative decoded image +data. Image source policy remains configurable under +`gateway.http.endpoints.chatCompletions.images`: ```json5 { @@ -115,9 +118,6 @@ Defaults can be tuned under `gateway.http.endpoints.chatCompletions`: endpoints: { chatCompletions: { enabled: true, - maxBodyBytes: 20000000, - maxImageParts: 8, - maxTotalImageBytes: 20000000, images: { allowUrl: false, urlAllowlist: ["cdn.example.com", "*.assets.example.com"], @@ -140,17 +140,14 @@ Defaults can be tuned under `gateway.http.endpoints.chatCompletions`: } ``` -Defaults when omitted: +Image settings default to: -| Key | Default | -| --------------------- | --------------------------------------------------------------------------- | -| `maxBodyBytes` | 20MB | -| `maxImageParts` | 8 (max `image_url` parts read from the latest user message) | -| `maxTotalImageBytes` | 20MB (cumulative decoded bytes across all `image_url` parts in one request) | -| `images.allowUrl` | `false` (URL-sourced `image_url` parts are rejected unless enabled) | -| `images.maxBytes` | 10MB per image | -| `images.maxRedirects` | 3 | -| `images.timeoutMs` | 10s | +| Key | Default | +| --------------------- | ------------------------------------------------------------------- | +| `images.allowUrl` | `false` (URL-sourced `image_url` parts are rejected unless enabled) | +| `images.maxBytes` | 10MB per image | +| `images.maxRedirects` | 3 | +| `images.timeoutMs` | 10s | HEIC/HEIF `image_url` sources are accepted and normalized to JPEG before provider delivery through the shared OpenClaw image processor (Rastermill), which falls back to a system converter (`sips`, ImageMagick, GraphicsMagick, or ffmpeg) for formats needing external codec support. diff --git a/docs/gateway/openresponses-http-api.md b/docs/gateway/openresponses-http-api.md index 91b4946046fb..6554d922eb9d 100644 --- a/docs/gateway/openresponses-http-api.md +++ b/docs/gateway/openresponses-http-api.md @@ -135,9 +135,10 @@ URL fetch defaults: - Optional hostname allowlists are supported per input type (`files.urlAllowlist`, `images.urlAllowlist`): exact host (`"cdn.example.com"`) or wildcard subdomains (`"*.assets.example.com"`, does not match the apex). Empty or omitted allowlists mean no hostname allowlist restriction. - To disable URL-based fetches entirely, set `files.allowUrl: false` and/or `images.allowUrl: false`. -## File + image limits (config) +## File + image limits -Defaults can be tuned under `gateway.http.endpoints.responses`: +The endpoint uses a built-in 20 MB request-body limit. File and image source +policy remains configurable under `gateway.http.endpoints.responses`: ```json5 { @@ -146,7 +147,6 @@ Defaults can be tuned under `gateway.http.endpoints.responses`: endpoints: { responses: { enabled: true, - maxBodyBytes: 20000000, maxUrlParts: 8, files: { allowUrl: true, @@ -195,7 +195,6 @@ Defaults when omitted: | Key | Default | | ------------------------ | --------- | -| `maxBodyBytes` | 20MB | | `maxUrlParts` | 8 | | `files.maxBytes` | 5MB | | `files.maxChars` | 60k | diff --git a/docs/gateway/opentelemetry.md b/docs/gateway/opentelemetry.md index 3dfd5f06ebb6..cbf5cece0b40 100644 --- a/docs/gateway/opentelemetry.md +++ b/docs/gateway/opentelemetry.md @@ -350,28 +350,18 @@ bounds; content remains off by default. ### Session liveness telemetry -`diagnostics.stuckSessionWarnMs` is the no-progress age threshold for session -liveness diagnostics. A `processing` session does not age toward this -threshold while OpenClaw observes reply, tool, status, block, or ACP runtime -progress. Typing keepalives do not count as progress, so a silent model or -harness can still be detected. +A `processing` session does not age toward the built-in liveness threshold while OpenClaw observes reply, tool, status, block, or ACP runtime progress. Typing keepalives do not count as progress, so a silent model or harness can still be detected. OpenClaw classifies sessions by the work it can still observe: - `session.long_running`: active embedded work, model calls, or tool calls - are still making progress. Owned model calls that stay silent past - `diagnostics.stuckSessionWarnMs` also report as long-running before - `diagnostics.stuckSessionAbortMs`, so slow or non-streaming model providers - do not look like stalled gateway sessions while abort-observable. + are still making progress. Owned silent model calls also report as long-running before the built-in abort threshold, so slow or non-streaming model providers do not look like stalled gateway sessions while abort-observable. - `session.stalled`: active work exists, but the active run has not reported recent progress. Owned model calls switch from `session.long_running` to - `session.stalled` at or after `diagnostics.stuckSessionAbortMs`; ownerless + `session.stalled` at or after the built-in abort threshold; ownerless stale model/tool activity is not treated as harmless long-running work. Stalled embedded runs stay observe-only at first, then abort-drain after - `diagnostics.stuckSessionAbortMs` with no progress so queued turns behind - the lane can resume. When unset, the abort threshold defaults to the safer - extended window of at least 5 minutes and 3x - `diagnostics.stuckSessionWarnMs`. + the abort threshold with no progress so queued turns behind the lane can resume. - `session.stuck`: stale session bookkeeping with no active work, or an idle queued session with stale ownerless model/tool activity. This releases the affected session lane immediately after recovery gates pass. diff --git a/docs/gateway/secrets.md b/docs/gateway/secrets.md index 12bd656dcdd6..4950da7b4987 100644 --- a/docs/gateway/secrets.md +++ b/docs/gateway/secrets.md @@ -190,11 +190,6 @@ Define providers under `secrets.providers`: file: "filemain", exec: "vault", }, - resolution: { - maxProviderConcurrency: 4, - maxRefsPerProvider: 512, - maxBatchBytes: 262144, - }, }, } ``` diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 56d115cb0bb4..74c5b0aefad8 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -601,11 +601,11 @@ Look for: Common signatures: - `critical memory pressure bundle written` appears shortly before restart → OpenClaw captured a pre-OOM stability bundle. Inspect it with `openclaw gateway stability --bundle latest`. -- `memory pressure: level=critical ... memoryPressureSnapshot=disabled` appears in gateway logs → OpenClaw detected critical memory pressure, but the pre-OOM stability snapshot is off. +- `memory pressure: level=critical` appears in gateway logs → OpenClaw detected critical memory pressure and recorded the available in-process memory facts. - `Largest session files:` points at a very large redacted transcript path → reduce retained session history, inspect session growth, or move old transcripts out of the active store before restarting. - `V8 heap:` used bytes are close to the heap limit → lower prompt/session pressure or reduce concurrent work first. For a managed service, inspect `Gateway heap:` in `openclaw gateway status`; if it says `not set`, regenerate old service metadata with `openclaw gateway install --force`. Ambient shell `NODE_OPTIONS` is intentionally ignored. Use an explicit supervisor-level heap override only after confirming the sustained workload and leaving enough native-memory headroom. - `Memory pressure: critical/rss_growth` → memory grew quickly inside one sampling window. Check the latest logs for a large import, runaway tool output, repeated retries, or a batch of queued agent work. -- Critical memory pressure appears in logs but no bundle exists → this is the default. Set `diagnostics.memoryPressureSnapshot: true` to capture the pre-OOM stability bundle on future critical memory pressure events. +- Critical memory pressure appears in logs but no bundle exists → capture `openclaw gateway diagnostics export` after the event for the available operational evidence. The stability bundle is payload-free. It includes operational memory evidence and redacted relative file paths, not message text, webhook bodies, credentials, tokens, cookies, or raw session ids. Attach the diagnostics export to bug reports instead of copying raw logs. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index ff1a8a584813..0b6426cefdf2 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -462,8 +462,7 @@ Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-a OpenClaw may skip a profile in a short **cooldown** (rate limits, timeouts, auth failures) or a longer **disabled** state (billing/insufficient credits). Inspect with `openclaw models status - --json` and check `auth.unusableProfiles`. Tune with - `auth.cooldowns.billingBackoffHours*`. Rate-limit cooldowns can be + --json` and check `auth.unusableProfiles`. Rate-limit cooldowns can be model-scoped — a profile cooling down for one model can still serve a sibling model on the same provider; billing/disabled windows block the whole profile. diff --git a/docs/install/updating.md b/docs/install/updating.md index 1d9ab5e73242..6198213a412f 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -229,20 +229,17 @@ Off by default. Enable it in `~/.openclaw/openclaw.json`: channel: "stable", auto: { enabled: true, - stableDelayHours: 6, - stableJitterHours: 12, - betaCheckIntervalHours: 1, }, }, } ``` -| Channel | Behavior | -| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `stable` | Waits `stableDelayHours` (default: 6), then applies with deterministic jitter across `stableJitterHours` (default: 12) for a spread rollout. | -| `extended-stable` | Checks for a read-only update hint on startup and every 24 hours when `checkOnStart` is enabled. Never applies automatically. | -| `beta` | Checks every `betaCheckIntervalHours` (default: 1) and applies immediately. | -| `dev` | No automatic apply. Use `openclaw update` manually. | +| Channel | Behavior | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `stable` | Applies after a built-in delay with deterministic jitter for a spread rollout. | +| `extended-stable` | Checks for a read-only update hint on startup and every 24 hours when `checkOnStart` is enabled. Never applies automatically. | +| `beta` | Checks on a built-in interval and applies immediately. | +| `dev` | No automatic apply. Use `openclaw update` manually. | The gateway also logs an update hint on startup (disable with `update.checkOnStart: false`). Stored extended-stable selections use this diff --git a/docs/plugins/cli-backend-plugins.md b/docs/plugins/cli-backend-plugins.md index 101fb805ae6d..9846416f1fb8 100644 --- a/docs/plugins/cli-backend-plugins.md +++ b/docs/plugins/cli-backend-plugins.md @@ -196,7 +196,6 @@ runtime behavior. Runtime behavior starts when the plugin entry calls | `imagePathScope` | Where staged image files live before handoff: `temp` or `workspace` | | `serialize` | Keep same-backend runs ordered | | `reseedFromRawTranscriptWhenUncompacted` | Opt in to bounded raw-transcript reseed before compaction for safe session resets | -| `reliability.outputLimits` | Max raw JSONL chars/lines retained for one live CLI turn (live-session backends) | | `reliability.watchdog` | No-output timeout tuning, separate for fresh vs resumed runs | Prefer the smallest static config that matches the CLI. Add plugin callbacks diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index b1168ba5d510..5bf0991e2121 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -2034,8 +2034,6 @@ payload. 6. Delete file-lock-shaped session mutation. - Done for runtime lock creation and runtime lock APIs. - The standalone legacy `.jsonl.lock` doctor cleanup lane is removed. - - `session.writeLock` is doctor-migrated legacy config, not a typed runtime - setting. - State integrity no longer has a separate orphan transcript-file pruning path; doctor migration imports/removes legacy JSONL sources in one place. - Gateway singleton coordination uses typed SQLite `state_leases` rows under diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 73db56acc5d4..a6bc25c3cb8c 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -1,11 +1,11 @@ --- -summary: "All configuration knobs for memory search, embedding providers, QMD, hybrid search, and multimodal indexing" +summary: "Memory search providers, retrieval modes, QMD, and multimodal indexing" title: "Memory configuration reference" sidebarTitle: "Memory config" read_when: - You want to configure memory search providers or embedding models - You want to set up the QMD backend - - You want to tune hybrid search, MMR, or temporal decay + - You want to enable hybrid search, MMR, or temporal decay - You want to enable multimodal memory indexing --- @@ -372,21 +372,8 @@ All under `memorySearch.sync` unless noted: | `onSessionStart` | `boolean` | `true` | Sync the memory index when a session starts | | `onSearch` | `boolean` | `true` | Sync lazily on search after detecting content changes | | `watch` | `boolean` | `true` | Watch memory files (chokidar) and schedule reindex on changes | -| `watchDebounceMs` | `number` | `1500` | Debounce window for coalescing rapid file-watch events | -| `intervalMinutes` | `number` | `0` | Periodic reindex interval in minutes (`0` disables) | | `sessions.postCompactionForce` | `boolean` | `true` | Force a session reindex after compaction-triggered transcript updates | - - Chunk size in tokens used when splitting memory sources before embedding (default: 400). - - - Token overlap between adjacent chunks to preserve context near split boundaries (default: 80). - - - -Changing `chunking.tokens` or `chunking.overlap` changes chunk boundaries and invalidates the existing index identity (see the Warning under Provider selection). - - --- ## Hybrid search config @@ -400,25 +387,20 @@ All under `memorySearch.query`: And under `memorySearch.query.hybrid`: -| Key | Type | Default | Description | -| --------------------- | --------- | ------- | ---------------------------------- | -| `enabled` | `boolean` | `true` | Enable hybrid BM25 + vector search | -| `vectorWeight` | `number` | `0.7` | Weight for vector scores (0-1) | -| `textWeight` | `number` | `0.3` | Weight for BM25 scores (0-1) | -| `candidateMultiplier` | `number` | `4` | Candidate pool size multiplier | +| Key | Type | Default | Description | +| --------- | --------- | ------- | ---------------------------------- | +| `enabled` | `boolean` | `true` | Enable hybrid BM25 + vector search | - | Key | Type | Default | Description | - | ------------- | --------- | ------- | ------------------------------------- | - | `mmr.enabled` | `boolean` | `false` | Enable MMR re-ranking | - | `mmr.lambda` | `number` | `0.7` | 0 = max diversity, 1 = max relevance | + | Key | Type | Default | Description | + | ------------- | --------- | ------- | --------------------- | + | `mmr.enabled` | `boolean` | `false` | Enable MMR re-ranking | - | Key | Type | Default | Description | - | ---------------------------- | --------- | ------- | -------------------------- | - | `temporalDecay.enabled` | `boolean` | `false` | Enable recency boost | - | `temporalDecay.halfLifeDays` | `number` | `30` | Score halves every N days | + | Key | Type | Default | Description | + | ----------------------- | --------- | ------- | -------------------- | + | `temporalDecay.enabled` | `boolean` | `false` | Enable recency boost | Evergreen files (`MEMORY.md`, non-dated files in `memory/`) are never decayed. @@ -436,10 +418,8 @@ And under `memorySearch.query.hybrid`: maxResults: 6, minScore: 0.35, hybrid: { - vectorWeight: 0.7, - textWeight: 0.3, - mmr: { enabled: true, lambda: 0.7 }, - temporalDecay: { enabled: true, halfLifeDays: 30 }, + mmr: { enabled: true }, + temporalDecay: { enabled: true }, }, }, }, @@ -494,12 +474,11 @@ Supported formats: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif` (i ## Embedding cache -| Key | Type | Default | Description | -| ------------------ | --------- | ------- | -------------------------------------------- | -| `cache.enabled` | `boolean` | `true` | Cache chunk embeddings in SQLite | -| `cache.maxEntries` | `number` | unset | Best-effort upper bound on cached embeddings | +| Key | Type | Default | Description | +| --------------- | --------- | ------- | -------------------------------- | +| `cache.enabled` | `boolean` | `true` | Cache chunk embeddings in SQLite | -Prevents re-embedding unchanged text during reindex or transcript updates. Leave `maxEntries` unset for an unbounded cache; set it when disk growth matters more than peak reindex speed. When set, the oldest entries (by last-updated time) are pruned first once the cache exceeds the limit. +Prevents re-embedding unchanged text during reindex or transcript updates. --- diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index ceb466d38d10..fdff113c09ab 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -78,13 +78,9 @@ OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups duri Transcript mutations use the session write queue for the SQLite transcript target: -| Setting | Default | Env override | -| ------------------------------------ | --------- | ------------------------------------------------ | -| `session.writeLock.acquireTimeoutMs` | `60000` | `OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS` | -| `session.writeLock.staleMs` | `1800000` | `OPENCLAW_SESSION_WRITE_LOCK_STALE_MS` | -| `session.writeLock.maxHoldMs` | `300000` | `OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS` | - -`acquireTimeoutMs` is how long a lock wait surfaces a busy-session error before giving up; raise it only when legitimate prep, cleanup, compaction, or transcript mirror work contends longer on slow machines. `staleMs` is when an existing lock can be reclaimed as stale. `maxHoldMs` is the in-process watchdog release threshold. +Session write locks use fixed production defaults. The corresponding +`OPENCLAW_SESSION_WRITE_LOCK_*` environment variables remain available for +process-level diagnostics and emergency overrides. ### Downgrading After The SQLite Flip @@ -217,7 +213,7 @@ When splitting a long transcript into compaction chunks, OpenClaw keeps assistan Two triggers in the embedded OpenClaw agent: 1. **Overflow recovery**: the model returns a context-overflow error (`request_too_large`, `context length exceeded`, `input exceeds the maximum number of tokens`, `input token count exceeds the maximum number of input tokens`, `input is too long for the model`, `ollama error: context length exceeded`, and other provider-shaped variants) - compact, then retry. When the provider reports the attempted token count, OpenClaw forwards that observed count into overflow-recovery compaction; if the provider confirms overflow but exposes no parseable count, OpenClaw passes a minimally over-budget synthetic count to compaction engines and diagnostics. If overflow recovery still fails, OpenClaw surfaces explicit guidance and preserves the current session mapping instead of silently rotating to a fresh session id - retry the message, run `/compact`, or run `/new`. -2. **Threshold maintenance**: after a successful turn, when `contextTokens > contextWindow - reserveTokens`, where `contextWindow` is the model's context window and `reserveTokens` is headroom reserved for prompts plus the next model output. +2. **Threshold maintenance**: after a successful turn, when the current context exceeds the model window minus OpenClaw's built-in headroom for prompts and the next model output. Two additional guards run outside these two triggers: @@ -232,7 +228,6 @@ Two additional guards run outside these two triggers: defaults: { compaction: { enabled: true, - reserveTokens: 16384, keepRecentTokens: 20000, }, }, @@ -240,7 +235,7 @@ Two additional guards run outside these two triggers: } ``` -OpenClaw also enforces a safety floor for embedded runs: if `compaction.reserveTokens` is below `reserveTokensFloor` (default `20000`), OpenClaw bumps it up. Set `agents.defaults.compaction.reserveTokensFloor: 0` to disable the floor. When the active model context window is known, both the floor and the final effective reserve are capped so the reserve cannot consume the whole prompt budget. This keeps small-context models (for example a 16K-token local model) from entering compaction from the first token; without a known context window, configured and current reserve budgets remain uncapped. Why a floor at all: leave enough headroom for multi-turn "housekeeping" (like the memory flush, below) before compaction becomes unavoidable. Implementation: `applyAgentCompactionSettingsFromConfig()` in `src/agents/agent-settings.ts`, called from embedded-runner turn and compaction setup paths. +OpenClaw enforces a built-in reserve for embedded runs and caps it against the active model context window so it cannot consume the whole prompt budget. This keeps small-context local models from entering compaction from the first token while leaving enough headroom for multi-turn housekeeping such as the memory flush. Manual `/compact` honors an explicit `agents.defaults.compaction.keepRecentTokens` and keeps the runtime's recent-tail cut point. Without an explicit keep budget, manual compaction is a hard checkpoint and rebuilt context starts from the new summary. @@ -305,7 +300,7 @@ OpenClaw exposes a `session_before_compact` hook in the extension API, but the f - **Session key wrong?** Start with [/concepts/session](/concepts/session) and confirm the `sessionKey` in `/status`. - **Store vs transcript mismatch?** Confirm the Gateway host and the store path from `openclaw status`. -- **Compaction spam?** Check the model's context window (too small forces frequent compaction), `reserveTokens` (too high for the model window causes earlier compaction), and tool-result bloat (tune session pruning). +- **Compaction spam?** Check the model's context window (too small forces frequent compaction) and tool-result bloat (tune session pruning). - **Every prompt seems to overflow on a small local model?** Confirm the provider reports the correct model context window. OpenClaw can cap the effective reserve only when that window is known. - **Silent turns leaking?** Confirm the reply starts with the exact silent token `NO_REPLY` (case-insensitive) and you are on a build that includes the streaming-suppression fix (`2026.1.10`+). diff --git a/docs/tools/acp-agents-setup.md b/docs/tools/acp-agents-setup.md index 25b8cd5ec892..8fdaa88675a0 100644 --- a/docs/tools/acp-agents-setup.md +++ b/docs/tools/acp-agents-setup.md @@ -89,14 +89,8 @@ Core ACP baseline: "opencode", "qwen", ], - maxConcurrentSessions: 8, stream: { - // Defaults are coalesceIdleMs: 350, maxChunkChars: 1800; shown explicitly here. - coalesceIdleMs: 350, - maxChunkChars: 1800, - }, - runtime: { - ttlMinutes: 120, + deliveryMode: "live", }, }, } diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 04837fd70a8b..77b7c89f00b6 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -158,7 +158,7 @@ Quick `/acp` flow from chat: - `cancel` aborts the active turn when the backend supports cancellation; it does not delete the binding or session metadata. - `close` ends the ACP session from OpenClaw's point of view and removes the binding. A harness may still keep its own upstream history if it supports resume. - The acpx plugin cleans up OpenClaw-owned wrapper and adapter process trees after `close`, and reaps stale OpenClaw-owned ACPX orphans during Gateway startup. - - Idle runtime workers are eligible for cleanup after `acp.runtime.ttlMinutes`; stored session metadata remains available for `/acp sessions`. + - Idle runtime workers are eligible for cleanup after the built-in idle period; stored session metadata remains available for `/acp sessions`. diff --git a/docs/tools/browser-linux-troubleshooting.md b/docs/tools/browser-linux-troubleshooting.md index ec29c6d567e6..fd99813e4187 100644 --- a/docs/tools/browser-linux-troubleshooting.md +++ b/docs/tools/browser-linux-troubleshooting.md @@ -115,24 +115,18 @@ curl -s http://127.0.0.1:18791/tabs ### Config reference -| Option | Description | Default | -| -------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ | -| `browser.enabled` | Enable browser control | `true` | -| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected (prefers the OS default browser when Chromium-based) | -| `browser.headless` | Run without GUI | `false` | -| `OPENCLAW_BROWSER_HEADLESS` | Per-process override for local managed browser headless mode | unset | -| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` | -| `browser.attachOnly` | Do not launch a browser; only attach to an existing one | `false` | -| `browser.cdpPortRangeStart` | Starting local CDP port for auto-assigned profiles | `18800` (derived from the gateway port) | -| `browser.localLaunchTimeoutMs` | Local managed Chrome discovery timeout, up to `120000` | `15000` | -| `browser.localCdpReadyTimeoutMs` | Local managed post-launch CDP readiness timeout, up to `120000` | `8000` | +| Option | Description | Default | +| --------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `browser.enabled` | Enable browser control | `true` | +| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected (prefers the OS default browser when Chromium-based) | +| `browser.headless` | Run without GUI | `false` | +| `OPENCLAW_BROWSER_HEADLESS` | Per-process override for local managed browser headless mode | unset | +| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` | +| `browser.attachOnly` | Do not launch a browser; only attach to an existing one | `false` | -Both timeout values must be positive integers up to `120000` ms; other values -are rejected at config load. On Raspberry Pi, older VPS hosts, or slow -storage, raise `browser.localLaunchTimeoutMs` when Chrome needs more time to -expose its CDP HTTP endpoint. Raise `browser.localCdpReadyTimeoutMs` when -launch succeeds but `openclaw browser start` still reports `not reachable -after start`. +On Raspberry Pi, older VPS hosts, or slow storage, use a manually launched +browser with `attachOnly` when Chrome needs more time to expose its CDP HTTP +endpoint or become ready than the managed-browser deadline permits. ### Problem: No Chrome tabs found for profile="user" diff --git a/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md b/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md index da7415c3bf78..f8158a7474e0 100644 --- a/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md +++ b/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md @@ -202,8 +202,8 @@ Good result: | empty CDP reply / `other side closed` through a portproxy | Windows listener mismatch or a self-loop; inspect both loopback families and `netsh interface portproxy show all` | | `Browser attachOnly is enabled and CDP websocket for profile "remote" is not reachable` | the HTTP endpoint answered, but the DevTools WebSocket could not be opened | | stale viewport / dark-mode / locale / offline overrides after a remote session | run `openclaw browser --browser-profile remote stop` to close the session and release the cached Playwright/CDP connection without restarting the Gateway or the external browser | -| timeout around `remoteCdpTimeoutMs` (default 1500ms) | usually still CDP reachability, or a slow/unreachable remote endpoint | -| `Playwright page enumeration timed out after 3000ms` | the remote CDP connected, but its persistent tab read stalled; the deadline is the larger of `remoteCdpTimeoutMs` and `remoteCdpHandshakeTimeoutMs` | +| timeout during CDP reachability | usually still CDP reachability, or a slow/unreachable remote endpoint | +| `Playwright page enumeration timed out after 3000ms` | the remote CDP connected, but its persistent tab read stalled | | `No Chrome tabs found for profile="user"` | local Chrome MCP profile selected where no host-local tabs are available | ## Fast triage checklist diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 6cefc6796ec2..72950ab62e7f 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -157,16 +157,8 @@ Browser settings live in `~/.openclaw/openclaw.json`. // allowedHostnames: ["localhost"], }, // cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override - remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms) - remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms) - localLaunchTimeoutMs: 15000, // local managed Chrome discovery timeout (ms) - localCdpReadyTimeoutMs: 8000, // local managed post-launch CDP readiness timeout (ms) - actionTimeoutMs: 60000, // default browser act timeout (ms) tabCleanup: { enabled: true, // default: true - idleMinutes: 120, // set 0 to disable idle cleanup - maxTabsPerSession: 8, // set 0 to disable the per-session cap - sweepMinutes: 5, }, // snapshotDefaults: { mode: "efficient" }, // default snapshot mode when the caller omits one defaultProfile: "openclaw", @@ -301,22 +293,13 @@ main model can read the screenshot directly. - Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl` from a range starting 9 ports above the control port (default `18800`-`18899`); set those only for remote CDP profiles or existing-session endpoint attach. `cdpUrl` defaults to the managed local CDP port when unset. -- `remoteCdpTimeoutMs` applies to remote and `attachOnly` CDP HTTP reachability - checks and tab-opening HTTP requests; `remoteCdpHandshakeTimeoutMs` applies to - their CDP WebSocket handshakes. Persistent remote Playwright tab enumeration - uses the larger of the two as its operation deadline. -- `localLaunchTimeoutMs` is the budget for a locally launched managed Chrome - process to expose its CDP HTTP endpoint. `localCdpReadyTimeoutMs` is the - follow-up budget for CDP websocket readiness after the process is discovered. - Raise these on Raspberry Pi, low-end VPS, or older hardware where Chromium - starts slowly. Values must be positive integers up to `120000` ms; invalid - config values are rejected. +- Remote and `attachOnly` CDP reachability, WebSocket handshakes, and local + managed-Chrome startup use built-in deadlines. - Repeated managed Chrome launch/readiness failures are circuit-broken per profile. After several consecutive failures, OpenClaw pauses new launch attempts briefly instead of spawning Chromium on every browser tool call. Fix the startup problem, disable the browser if it is not needed, or restart the Gateway after repair. -- `actionTimeoutMs` is the default budget for browser `act` requests when the caller does not pass `timeoutMs`. The client transport adds a small slack window so long waits can finish instead of timing out at the HTTP boundary. @@ -491,8 +474,6 @@ Example: browser: { enabled: true, defaultProfile: "browserless", - remoteCdpTimeoutMs: 2000, - remoteCdpHandshakeTimeoutMs: 4000, profiles: { browserless: { cdpUrl: "wss://production-sfo.browserless.io?token=", @@ -585,8 +566,6 @@ proxies. browser: { enabled: true, defaultProfile: "browserbase", - remoteCdpTimeoutMs: 3000, - remoteCdpHandshakeTimeoutMs: 5000, profiles: { browserbase: { cdpUrl: "wss://connect.browserbase.com?apiKey=", @@ -619,8 +598,6 @@ WebSocket gateway. browser: { enabled: true, defaultProfile: "notte", - remoteCdpTimeoutMs: 3000, - remoteCdpHandshakeTimeoutMs: 5000, profiles: { notte: { cdpUrl: "wss://us-prod.notte.cc/sessions/connect?token=", diff --git a/docs/tools/loop-detection.md b/docs/tools/loop-detection.md index 5052f4a0602e..734dd6e02ae8 100644 --- a/docs/tools/loop-detection.md +++ b/docs/tools/loop-detection.md @@ -1,9 +1,9 @@ --- -summary: "How to enable and tune guardrails that detect repetitive tool-call loops" +summary: "How to enable guardrails that detect repetitive tool-call loops" title: "Tool-loop detection" read_when: - A user reports agents getting stuck repeating tool calls - - You need to tune repetitive-call protection + - You need to control repetitive-call protection - You are editing agent tool/runtime policies - You hit `compaction_loop_persisted` aborts after a context-overflow retry --- @@ -13,7 +13,7 @@ both configured under `tools.loopDetection`: 1. **Loop detection** (`enabled`) - disabled by default. Watches the rolling tool-call history for repeated patterns and unknown-tool retries. -2. **Post-compaction guard** (`postCompactionGuard`) - enabled whenever +2. **Post-compaction guard** - enabled whenever `enabled` is not explicitly `false`. Arms after every compaction-retry and aborts the run if the agent repeats the same `(tool, args, result)` triple within the window. @@ -31,26 +31,13 @@ Set `tools.loopDetection.enabled: false` to silence both guardrails. ## Configuration block -Global defaults, with every documented field shown: +Global setting: ```json5 { tools: { loopDetection: { enabled: false, // master switch for the rolling-history detectors - historySize: 30, - warningThreshold: 10, - criticalThreshold: 20, - unknownToolThreshold: 10, - globalCircuitBreakerThreshold: 30, - detectors: { - genericRepeat: true, - knownPollNoProgress: true, - pingPong: true, - }, - postCompactionGuard: { - windowSize: 3, // armed after compaction-retry; runs unless enabled is explicitly false - }, }, }, } @@ -67,8 +54,6 @@ Per-agent override (optional, at `agents.list[].tools.loopDetection`): tools: { loopDetection: { enabled: true, - warningThreshold: 8, - criticalThreshold: 16, }, }, }, @@ -77,24 +62,13 @@ Per-agent override (optional, at `agents.list[].tools.loopDetection`): } ``` -Per-agent settings overlay the global block field by field (including nested -`detectors` and `postCompactionGuard`), so an agent only needs to set the -fields it wants to change. +The per-agent setting overrides the global setting. ### Field behavior -| Field | Default | Effect | -| -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `enabled` | `false` | Master switch for the rolling-history detectors. `false` also disables the post-compaction guard. | -| `historySize` | `30` | Number of recent tool calls kept for analysis. | -| `warningThreshold` | `10` | Repeat count before a pattern is classified as warning-only. | -| `criticalThreshold` | `20` | Repeat count for blocking a no-progress loop pattern. Runtime clamps this above `warningThreshold` if misconfigured. | -| `unknownToolThreshold` | `10` | Blocks repeated calls to the same unavailable tool after this many misses. Not gated by `detectors`. | -| `globalCircuitBreakerThreshold` | `30` | Global no-progress breaker across all detectors. Runtime clamps this above `criticalThreshold` if misconfigured. Not gated by `detectors`. | -| `detectors.genericRepeat` | `true` | Warns on repeated same-tool + same-args calls; blocks once those calls also return identical outcomes. | -| `detectors.knownPollNoProgress` | `true` | Detects known no-progress polling patterns (`process` with `action: "poll"`/`"log"`, `command_status`). | -| `detectors.pingPong` | `true` | Detects alternating no-progress ping-pong patterns between two calls. | -| `postCompactionGuard.windowSize` | `3` | Attempts the guard stays armed after compaction, and the count of identical triples that aborts the run. | +| Field | Default | Effect | +| --------- | ------- | ------------------------------------------------------------------------------------------------- | +| `enabled` | `false` | Master switch for the rolling-history detectors. `false` also disables the post-compaction guard. | For `exec`, no-progress hashing compares stable command outcomes (status, exit code, timed-out flag, output) and ignores volatile runtime metadata such @@ -107,19 +81,9 @@ from earlier runs. ## Recommended setup -- For smaller models, set `enabled: true` and leave thresholds at their - defaults. Flagship models rarely need rolling-history detection and can +- For smaller models, set `enabled: true`. Flagship models rarely need rolling-history detection and can leave the master switch `false` while still benefiting from the post-compaction guard. -- Keep thresholds ordered `warningThreshold < criticalThreshold < -globalCircuitBreakerThreshold`; the runtime nudges `criticalThreshold` and - `globalCircuitBreakerThreshold` upward if you set them at or below the - threshold they must exceed. -- If false positives occur: - - Raise `warningThreshold` and/or `criticalThreshold`. - - Optionally raise `globalCircuitBreakerThreshold`. - - Disable only the specific detector causing issues (`detectors.: false`). - - Reduce `historySize` for a shorter historical window. - To disable everything, including the post-compaction guard, set `tools.loopDetection.enabled: false` explicitly. @@ -127,8 +91,7 @@ globalCircuitBreakerThreshold`; the runtime nudges `criticalThreshold` and After a compaction-retry following a context-overflow, the runner arms a short-window guard on the next few tool calls. If the agent emits the same -`(toolName, argsHash, resultHash)` triple `postCompactionGuard.windowSize` -times within that window, the guard concludes compaction did not break the +`(toolName, argsHash, resultHash)` triple enough times within that window, the guard concludes compaction did not break the loop and aborts the run with a `compaction_loop_persisted` error. The guard is gated by the master `tools.loopDetection.enabled` flag with one @@ -143,16 +106,11 @@ so a no-config user still gets the protection. loopDetection: { // master switch; set false to disable the guard along with the rolling detectors enabled: true, - postCompactionGuard: { - windowSize: 3, // default - }, }, }, } ``` -- Lower `windowSize` is stricter (fewer attempts before abort). -- Higher `windowSize` gives the agent more recovery attempts. - The guard never aborts while results are changing; only byte-identical results across the window trigger it. - It only arms in the immediate aftermath of a compaction-retry, not at other diff --git a/extensions/browser/src/browser-tool.actions.ts b/extensions/browser/src/browser-tool.actions.ts index edff10e9f266..7d4609dbeb17 100644 --- a/extensions/browser/src/browser-tool.actions.ts +++ b/extensions/browser/src/browser-tool.actions.ts @@ -120,10 +120,7 @@ function withConfiguredActTimeout( return request; } - const cfg = browserToolActionDeps.getRuntimeConfig(); - const configuredTimeout = - normalizePositiveTimeoutMs(cfg.browser?.actionTimeoutMs) ?? DEFAULT_BROWSER_ACTION_TIMEOUT_MS; - return { ...typedRequest, timeoutMs: configuredTimeout } as BrowserActRequest; + return { ...typedRequest, timeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS } as BrowserActRequest; } function resolveActProxyTimeoutMs(request: BrowserActRequest): number | undefined { diff --git a/extensions/browser/src/browser-tool.test.ts b/extensions/browser/src/browser-tool.test.ts index 4f8b97b40364..92fdcfe3b7ec 100644 --- a/extensions/browser/src/browser-tool.test.ts +++ b/extensions/browser/src/browser-tool.test.ts @@ -2361,109 +2361,6 @@ describe("browser tool act compatibility", () => { }); }); - it("applies configured browser action timeout when act timeout is omitted", async () => { - configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } }); - - const tool = createBrowserTool(); - await tool.execute?.("call-1", { - action: "act", - request: { - kind: "wait", - timeMs: 20_000, - }, - }); - - const request = lastMockCallArg<{ kind?: string; timeMs?: number; timeoutMs?: number }>( - browserActionsMocks.browserAct, - 1, - ); - const opts = lastMockCallArg<{ profile?: string }>(browserActionsMocks.browserAct, 2); - expect(request).toEqual({ kind: "wait", timeMs: 20_000, timeoutMs: 45_000 }); - expect(opts.profile).toBeUndefined(); - }); - - it("does not inject unsupported action timeout for existing-session type actions", async () => { - setResolvedBrowserProfiles({ - user: { driver: "existing-session", attachOnly: true, color: "#00AA00" }, - }); - configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } }); - - const tool = createBrowserTool(); - await tool.execute?.("call-1", { - action: "act", - profile: "user", - target: "host", - request: { - kind: "type", - ref: "f1e3", - text: "Test Title", - }, - }); - - const request = lastMockCallArg<{ kind?: string; ref?: string; text?: string }>( - browserActionsMocks.browserAct, - 1, - ); - const opts = lastMockCallArg<{ profile?: string }>(browserActionsMocks.browserAct, 2); - expect(request).toEqual({ kind: "type", ref: "f1e3", text: "Test Title" }); - expect(opts.profile).toBe("user"); - }); - - it("injects configured action timeout for existing-session evaluate actions", async () => { - setResolvedBrowserProfiles({ - user: { driver: "existing-session", attachOnly: true, color: "#00AA00" }, - }); - configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } }); - - const tool = createBrowserTool(); - await tool.execute?.("call-1", { - action: "act", - profile: "user", - target: "host", - request: { - kind: "evaluate", - fn: "() => 1 + 1", - }, - }); - - const request = lastMockCallArg<{ kind?: string; fn?: string; timeoutMs?: number }>( - browserActionsMocks.browserAct, - 1, - ); - const opts = lastMockCallArg<{ profile?: string }>(browserActionsMocks.browserAct, 2); - expect(request).toEqual({ kind: "evaluate", fn: "() => 1 + 1", timeoutMs: 45_000 }); - expect(opts.profile).toBe("user"); - }); - - it("passes configured act timeout through node proxy with transport slack", async () => { - mockSingleBrowserProxyNode(); - configMocks.loadConfig.mockReturnValue({ - browser: { - actionTimeoutMs: 45_000, - }, - gateway: { nodes: { browser: { node: "node-1" } } }, - }); - - const tool = createBrowserTool(); - await tool.execute?.("call-1", { - action: "act", - target: "node", - request: { kind: "wait", timeMs: 20_000, text: "ready" }, - }); - - const { options, request } = lastNodeInvokeCall(); - expect(options.timeoutMs).toBe(75_000); - expect(request.timeoutMs).toBe(75_000); - expect(request.params?.path).toBe("/act"); - expect(request.params?.body).toEqual({ - kind: "wait", - timeMs: 20_000, - text: "ready", - timeoutMs: 45_000, - }); - expect(request.params?.timeoutMs).toBe(70_000); - }); - it("honors string act request timeouts when sizing node proxy calls", async () => { mockSingleBrowserProxyNode(); const tool = createBrowserTool(); diff --git a/extensions/browser/src/browser/config-mutations.ts b/extensions/browser/src/browser/config-mutations.ts index c56bc37b2835..61b27d6ffebe 100644 --- a/extensions/browser/src/browser/config-mutations.ts +++ b/extensions/browser/src/browser/config-mutations.ts @@ -94,9 +94,7 @@ export async function createBrowserProfileConfig(params: { ? rawDraftBrowser.cdpPortRangeEnd : undefined; const useRebasedPortRange = - draft.gateway?.port !== undefined || - draft.browser?.cdpPortRangeStart !== undefined || - draftCdpPortRangeEnd !== undefined; + draft.gateway?.port !== undefined || draftCdpPortRangeEnd !== undefined; const latestResolved = resolveBrowserConfig( { ...params.resolved, diff --git a/extensions/browser/src/browser/config.test.ts b/extensions/browser/src/browser/config.test.ts index 4d16954fb118..b0860aa1baa1 100644 --- a/extensions/browser/src/browser/config.test.ts +++ b/extensions/browser/src/browser/config.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { BrowserConfig } from "../config/config.js"; import { resolveUserPath } from "../utils.js"; @@ -179,22 +178,6 @@ describe("browser config", () => { }); }); - it("supports overriding the local CDP auto-allocation range start", () => { - const resolved = resolveBrowserConfig({ - cdpPortRangeStart: 19000, - }); - const openclaw = resolveProfile(resolved, "openclaw"); - expect(resolved.cdpPortRangeStart).toBe(19000); - expect(openclaw?.cdpPort).toBe(19000); - expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19000"); - }); - - it("rejects cdpPortRangeStart values that overflow the CDP range window", () => { - expect(() => resolveBrowserConfig({ cdpPortRangeStart: 65535 })).toThrow( - /cdpPortRangeStart .* too high/i, - ); - }); - it("normalizes hex colors", () => { const resolved = resolveBrowserConfig({ color: "ff4500", @@ -202,47 +185,6 @@ describe("browser config", () => { expect(resolved.color).toBe("#FF4500"); }); - it("supports custom remote CDP timeouts", () => { - const resolved = resolveBrowserConfig({ - remoteCdpTimeoutMs: 2200, - remoteCdpHandshakeTimeoutMs: 5000, - actionTimeoutMs: 45_000, - }); - expect(resolved.remoteCdpTimeoutMs).toBe(2200); - expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(5000); - expect(resolved.actionTimeoutMs).toBe(45_000); - }); - - it("supports custom browser tab cleanup policy", () => { - const resolved = resolveBrowserConfig({ - tabCleanup: { - enabled: false, - idleMinutes: 0, - maxTabsPerSession: 0, - sweepMinutes: 15, - }, - }); - expect(resolved.tabCleanup).toEqual({ - enabled: false, - idleMinutes: 0, - maxTabsPerSession: 0, - sweepMinutes: 15, - }); - }); - - it("caps browser tab cleanup timer minutes before converting to milliseconds", () => { - const maxTimerMinutes = Math.floor(MAX_TIMER_TIMEOUT_MS / 60_000); - const resolved = resolveBrowserConfig({ - tabCleanup: { - idleMinutes: Number.MAX_SAFE_INTEGER, - sweepMinutes: Number.MAX_SAFE_INTEGER, - }, - }); - - expect(resolved.tabCleanup.idleMinutes).toBe(maxTimerMinutes); - expect(resolved.tabCleanup.sweepMinutes).toBe(maxTimerMinutes); - }); - it("expands tilde-prefixed executablePath with the OS home directory", () => { const resolved = resolveBrowserConfig({ executablePath: " ~/.local/bin/chromium ", @@ -300,22 +242,6 @@ describe("browser config", () => { expect(resolved.executablePath).toBe("/opt/~chromium/chrome"); }); - it("normalizes invalid browser tab cleanup numbers to defaults", () => { - const resolved = resolveBrowserConfig({ - tabCleanup: { - idleMinutes: -1, - maxTabsPerSession: -2, - sweepMinutes: 0, - }, - }); - expect(resolved.tabCleanup).toEqual({ - enabled: true, - idleMinutes: 120, - maxTabsPerSession: 8, - sweepMinutes: 5, - }); - }); - it("falls back to default color for invalid hex", () => { const resolved = resolveBrowserConfig({ color: "#GGGGGG", @@ -558,26 +484,6 @@ describe("browser config", () => { expect(resolved.localLaunchTimeoutMs).toBe(15_000); expect(resolved.localCdpReadyTimeoutMs).toBe(8_000); }); - - it("accepts custom local startup timeout values", () => { - const resolved = resolveBrowserConfig({ - localLaunchTimeoutMs: 45_000, - localCdpReadyTimeoutMs: 30_000, - }); - - expect(resolved.localLaunchTimeoutMs).toBe(45_000); - expect(resolved.localCdpReadyTimeoutMs).toBe(30_000); - }); - - it("clamps oversized local startup timeout values", () => { - const resolved = resolveBrowserConfig({ - localLaunchTimeoutMs: 999_999, - localCdpReadyTimeoutMs: 999_999, - }); - - expect(resolved.localLaunchTimeoutMs).toBe(120_000); - expect(resolved.localCdpReadyTimeoutMs).toBe(120_000); - }); }); it("inherits executablePath from global browser config when profile override is not set", () => { @@ -1193,4 +1099,3 @@ describe("browser config", () => { }); }); }); -/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/browser/src/browser/config.ts b/extensions/browser/src/browser/config.ts index 059add49867d..f349095ab9da 100644 --- a/extensions/browser/src/browser/config.ts +++ b/extensions/browser/src/browser/config.ts @@ -6,7 +6,6 @@ */ import os from "node:os"; import path from "node:path"; -import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { normalizeOptionalString, normalizeOptionalTrimmedStringList, @@ -130,6 +129,8 @@ export function getOwnBrowserProfile( } const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800; +const DEFAULT_BROWSER_REMOTE_CDP_TIMEOUT_MS = 1_500; +const DEFAULT_BROWSER_REMOTE_CDP_HANDSHAKE_TIMEOUT_MS = 3_000; /** * Default extension relay port offset from the browser control port. Sits just * below the CDP allocation range (controlPort+9..) so profile port allocation @@ -138,7 +139,6 @@ const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800; const EXTENSION_RELAY_PORT_OFFSET = 8; /** Username half of the relay's Basic credential; the password is the derived token. */ const EXTENSION_RELAY_CDP_USER = "openclaw"; -const MAX_BROWSER_STARTUP_TIMEOUT_MS = 120_000; /** Environment variable that overrides managed Chrome headless mode. */ const OPENCLAW_BROWSER_HEADLESS_ENV = "OPENCLAW_BROWSER_HEADLESS"; @@ -180,39 +180,6 @@ function normalizeHexColor(raw: string | undefined): string { return normalized.toUpperCase(); } -function normalizeTimeoutMs(raw: number | undefined, fallback: number): number { - const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; - return value < 0 ? fallback : value; -} - -function normalizeStartupTimeoutMs(raw: number | undefined, fallback: number): number { - const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; - if (value <= 0) { - return fallback; - } - return Math.min(value, MAX_BROWSER_STARTUP_TIMEOUT_MS); -} - -function normalizeNonNegativeInteger(raw: number | undefined, fallback: number): number { - const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; - return value < 0 ? fallback : value; -} - -function normalizePositiveInteger(raw: number | undefined, fallback: number): number { - const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; - return value <= 0 ? fallback : value; -} - -const MAX_BROWSER_TIMER_MINUTES = Math.floor(MAX_TIMER_TIMEOUT_MS / 60_000); - -function normalizeNonNegativeTimerMinutes(raw: number | undefined, fallback: number): number { - return Math.min(normalizeNonNegativeInteger(raw, fallback), MAX_BROWSER_TIMER_MINUTES); -} - -function normalizePositiveTimerMinutes(raw: number | undefined, fallback: number): number { - return Math.min(normalizePositiveInteger(raw, fallback), MAX_BROWSER_TIMER_MINUTES); -} - function normalizeExecutablePath(raw: string | undefined): string | undefined { const value = normalizeOptionalString(raw); if (!value) { @@ -269,42 +236,12 @@ function resolveBrowserTabCleanupConfig( const raw = cfg?.tabCleanup; return { enabled: raw?.enabled ?? true, - idleMinutes: normalizeNonNegativeTimerMinutes( - raw?.idleMinutes, - DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES, - ), - maxTabsPerSession: normalizeNonNegativeInteger( - raw?.maxTabsPerSession, - DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION, - ), - sweepMinutes: normalizePositiveTimerMinutes( - raw?.sweepMinutes, - DEFAULT_BROWSER_TAB_CLEANUP_SWEEP_MINUTES, - ), + idleMinutes: DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES, + maxTabsPerSession: DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION, + sweepMinutes: DEFAULT_BROWSER_TAB_CLEANUP_SWEEP_MINUTES, }; } -function resolveCdpPortRangeStart( - rawStart: number | undefined, - fallbackStart: number, - rangeSpan: number, -): number { - const start = - typeof rawStart === "number" && Number.isFinite(rawStart) - ? Math.floor(rawStart) - : fallbackStart; - if (start < 1 || start > 65535) { - throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`); - } - const maxStart = 65535 - rangeSpan; - if (start > maxStart) { - throw new Error( - `browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`, - ); - } - return start; -} - const normalizeStringList = normalizeOptionalTrimmedStringList; function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined { @@ -445,32 +382,15 @@ export function resolveBrowserConfig( const gatewayPort = resolveGatewayPort(rootConfig); const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT); const defaultColor = normalizeHexColor(cfg?.color); - const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500); - const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs( - cfg?.remoteCdpHandshakeTimeoutMs, - Math.max(2000, remoteCdpTimeoutMs * 2), - ); - const localLaunchTimeoutMs = normalizeStartupTimeoutMs( - cfg?.localLaunchTimeoutMs, - DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS, - ); - const localCdpReadyTimeoutMs = normalizeStartupTimeoutMs( - cfg?.localCdpReadyTimeoutMs, - DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS, - ); - const actionTimeoutMs = normalizeTimeoutMs( - cfg?.actionTimeoutMs, - DEFAULT_BROWSER_ACTION_TIMEOUT_MS, - ); + const remoteCdpTimeoutMs = DEFAULT_BROWSER_REMOTE_CDP_TIMEOUT_MS; + const remoteCdpHandshakeTimeoutMs = DEFAULT_BROWSER_REMOTE_CDP_HANDSHAKE_TIMEOUT_MS; + const localLaunchTimeoutMs = DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS; + const localCdpReadyTimeoutMs = DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS; + const actionTimeoutMs = DEFAULT_BROWSER_ACTION_TIMEOUT_MS; const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort); - const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start; - const cdpPortRangeStart = resolveCdpPortRangeStart( - cfg?.cdpPortRangeStart, - derivedCdpRange.start, - cdpRangeSpan, - ); - const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan; + const cdpPortRangeStart = derivedCdpRange.start; + const cdpPortRangeEnd = derivedCdpRange.end; const rawCdpUrl = (cfg?.cdpUrl ?? "").trim(); let cdpInfo: diff --git a/extensions/browser/src/browser/profiles-service.test.ts b/extensions/browser/src/browser/profiles-service.test.ts index 32a44ae11d44..4c40c4dede1e 100644 --- a/extensions/browser/src/browser/profiles-service.test.ts +++ b/extensions/browser/src/browser/profiles-service.test.ts @@ -274,18 +274,6 @@ describe("BrowserProfilesService", () => { expect(writeConfigFile).toHaveBeenCalled(); }); - it("allocates from configured cdpPortRangeStart for new local profiles", async () => { - const { result, state } = await createWorkProfileWithConfig({ - resolved: resolveBrowserConfig({ cdpPortRangeStart: 19000 }), - browserConfig: { cdpPortRangeStart: 19000, profiles: {} }, - }); - - expect(result.cdpPort).toBe(19001); - expect(result.isRemote).toBe(false); - expect(state.resolved.profiles.work?.cdpPort).toBe(19001); - expect(writeConfigFile).toHaveBeenCalled(); - }); - it("allocates local ports from the rebased config snapshot", async () => { const resolved = resolveBrowserConfig({}); const { ctx, state } = createCtx(resolved); @@ -310,12 +298,11 @@ describe("BrowserProfilesService", () => { }); it("allocates local ports from the rebased CDP range end", async () => { - const resolved = resolveBrowserConfig({ cdpPortRangeStart: 19000 }); + const resolved = resolveBrowserConfig({}); const { ctx, state } = createCtx(resolved); vi.mocked(getRuntimeConfig) .mockReturnValueOnce({ browser: { - cdpPortRangeStart: 19000, profiles: {}, }, } as OpenClawConfig) diff --git a/extensions/clickclack/doctor-contract-api.ts b/extensions/clickclack/doctor-contract-api.ts new file mode 100644 index 000000000000..4c09ea657e37 --- /dev/null +++ b/extensions/clickclack/doctor-contract-api.ts @@ -0,0 +1 @@ +export { normalizeCompatibilityConfig } from "./src/doctor-contract.js"; diff --git a/extensions/clickclack/src/accounts.test.ts b/extensions/clickclack/src/accounts.test.ts index 4cd8247ec519..b4ecfbca70fc 100644 --- a/extensions/clickclack/src/accounts.test.ts +++ b/extensions/clickclack/src/accounts.test.ts @@ -121,7 +121,6 @@ describe("ClickClack account resolution", () => { replyMode: "agent", systemPrompt: undefined, token: "test-token-placeholder", - timeoutSeconds: undefined, toolsAllow: undefined, workspace: "wsp_1", }); @@ -222,7 +221,6 @@ describe("ClickClack account resolution", () => { replyMode: "model", systemPrompt: undefined, token: "token-oversized", - timeoutSeconds: undefined, toolsAllow: ["web_search"], workspace: "wsp_1", }); diff --git a/extensions/clickclack/src/accounts.ts b/extensions/clickclack/src/accounts.ts index 80882f1ce39f..cc094215420b 100644 --- a/extensions/clickclack/src/accounts.ts +++ b/extensions/clickclack/src/accounts.ts @@ -174,7 +174,6 @@ export function resolveClickClackAccount(params: { replyMode: merged.replyMode === "model" ? "model" : "agent", model: normalizeOptionalString(merged.model), systemPrompt: normalizeOptionalString(merged.systemPrompt), - timeoutSeconds: merged.timeoutSeconds, toolsAllow: merged.toolsAllow, defaultTo: merged.defaultTo?.trim() || "channel:general", allowFrom: merged.allowFrom ?? ["*"], diff --git a/extensions/clickclack/src/config-schema.ts b/extensions/clickclack/src/config-schema.ts index 9c08c7d183f3..f2c841cd257a 100644 --- a/extensions/clickclack/src/config-schema.ts +++ b/extensions/clickclack/src/config-schema.ts @@ -21,7 +21,6 @@ const ClickClackAccountConfigSchema = z replyMode: z.enum(["agent", "model"]).optional(), model: z.string().optional(), systemPrompt: z.string().optional(), - timeoutSeconds: z.number().int().min(1).max(3_600).optional(), toolsAllow: z.array(z.string()).optional(), defaultTo: z.string().optional(), allowFrom: z.array(z.string()).optional(), diff --git a/extensions/clickclack/src/doctor-contract.test.ts b/extensions/clickclack/src/doctor-contract.test.ts new file mode 100644 index 000000000000..ae5ccdefca61 --- /dev/null +++ b/extensions/clickclack/src/doctor-contract.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { normalizeCompatibilityConfig } from "./doctor-contract.js"; + +describe("ClickClack doctor contract", () => { + it("strips root and account timeout tuning", () => { + const result = normalizeCompatibilityConfig({ + cfg: { + channels: { + clickclack: { + timeoutSeconds: 1, + reconnectMs: 2, + accounts: { work: { timeoutSeconds: 3, reconnectMs: 4 } }, + }, + }, + } as never, + }); + + expect((result.config.channels as Record).clickclack).toEqual({ + reconnectMs: 2, + accounts: { work: { reconnectMs: 4 } }, + }); + expect(result.changes).toEqual(["Removed retired ClickClack timeout tuning knobs."]); + }); +}); diff --git a/extensions/clickclack/src/doctor-contract.ts b/extensions/clickclack/src/doctor-contract.ts new file mode 100644 index 000000000000..ab1e9a163546 --- /dev/null +++ b/extensions/clickclack/src/doctor-contract.ts @@ -0,0 +1,49 @@ +import type { ChannelDoctorConfigMutation } from "openclaw/plugin-sdk/channel-contract"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor"; + +function stripTimeoutSeconds(value: unknown): { value: unknown; changed: boolean } { + const record = asObjectRecord(value); + if (!record) { + return { value, changed: false }; + } + let changed = false; + const next: Record = {}; + for (const [key, child] of Object.entries(record)) { + if (key === "timeoutSeconds") { + changed = true; + continue; + } + const stripped = stripTimeoutSeconds(child); + changed = changed || stripped.changed; + next[key] = stripped.value; + } + return { value: changed ? next : value, changed }; +} + +export function normalizeCompatibilityConfig({ + cfg, +}: { + cfg: OpenClawConfig; +}): ChannelDoctorConfigMutation { + const rawEntry = asObjectRecord( + (cfg.channels as Record | undefined)?.clickclack, + ); + if (!rawEntry) { + return { config: cfg, changes: [] }; + } + const stripped = stripTimeoutSeconds(rawEntry); + if (!stripped.changed) { + return { config: cfg, changes: [] }; + } + return { + config: { + ...cfg, + channels: { + ...cfg.channels, + clickclack: stripped.value, + } as OpenClawConfig["channels"], + }, + changes: ["Removed retired ClickClack timeout tuning knobs."], + }; +} diff --git a/extensions/clickclack/src/types.ts b/extensions/clickclack/src/types.ts index fb36eda8b732..f936c46159b2 100644 --- a/extensions/clickclack/src/types.ts +++ b/extensions/clickclack/src/types.ts @@ -16,7 +16,6 @@ export type ClickClackAccountConfig = { replyMode?: "agent" | "model"; model?: string; systemPrompt?: string; - timeoutSeconds?: number; toolsAllow?: string[]; defaultTo?: string; allowFrom?: string[]; @@ -54,7 +53,6 @@ export type ResolvedClickClackAccount = { replyMode: "agent" | "model"; model?: string; systemPrompt?: string; - timeoutSeconds?: number; toolsAllow?: string[]; defaultTo: string; allowFrom: string[]; diff --git a/extensions/codex/src/app-server/context-engine-projection.test.ts b/extensions/codex/src/app-server/context-engine-projection.test.ts index d931522e8803..e694d6bec2e8 100644 --- a/extensions/codex/src/app-server/context-engine-projection.test.ts +++ b/extensions/codex/src/app-server/context-engine-projection.test.ts @@ -5,7 +5,6 @@ import { fitCodexProjectedContextForTurnStart, projectContextEngineAssemblyForCodex, resolveCodexContextEngineProjectionMaxChars, - resolveCodexContextEngineProjectionReserveTokens, } from "./context-engine-projection.js"; const CODEX_TURN_START_TEXT_INPUT_MAX_CHARS = 1 << 20; @@ -393,31 +392,6 @@ describe("projectContextEngineAssemblyForCodex", () => { ); }); - it("maps OpenClaw compaction reserve config onto Codex projection reserves", () => { - expect( - resolveCodexContextEngineProjectionReserveTokens({ - config: { agents: { defaults: { compaction: { reserveTokens: 12_000 } } } }, - }), - ).toBe(20_000); - expect( - resolveCodexContextEngineProjectionReserveTokens({ - config: { - agents: { defaults: { compaction: { reserveTokens: 12_000, reserveTokensFloor: 0 } } }, - }, - }), - ).toBe(12_000); - expect( - resolveCodexContextEngineProjectionReserveTokens({ - config: { agents: { defaults: { compaction: { reserveTokens: 48_000 } } } }, - }), - ).toBe(48_000); - expect( - resolveCodexContextEngineProjectionReserveTokens({ - config: { agents: { defaults: { compaction: { reserveTokensFloor: 0 } } } }, - }), - ).toBe(0); - }); - it("applies configured reserve tokens to the scaled projection cap", () => { expect( resolveCodexContextEngineProjectionMaxChars({ diff --git a/extensions/codex/src/app-server/context-engine-projection.ts b/extensions/codex/src/app-server/context-engine-projection.ts index 9b61a840c7b2..568cca284282 100644 --- a/extensions/codex/src/app-server/context-engine-projection.ts +++ b/extensions/codex/src/app-server/context-engine-projection.ts @@ -98,24 +98,9 @@ export function resolveCodexContextEngineProjectionMaxChars(params: { return normalizeRenderedContextMaxChars(scaledChars); } -/** Reads Codex projection reserve tokens from compaction config. */ -export function resolveCodexContextEngineProjectionReserveTokens(params: { - config?: unknown; -}): number | undefined { - const compaction = asRecord(asRecord(asRecord(params.config)?.agents)?.defaults)?.compaction; - const configuredReserveTokens = toNonNegativeInt(asRecord(compaction)?.reserveTokens); - const configuredReserveTokensFloor = toNonNegativeInt(asRecord(compaction)?.reserveTokensFloor); - - if (configuredReserveTokens !== undefined) { - return Math.max( - configuredReserveTokens, - configuredReserveTokensFloor ?? DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS, - ); - } - if (configuredReserveTokensFloor !== undefined) { - return configuredReserveTokensFloor; - } - return undefined; +/** Returns the fixed reserve used for Codex context-engine projections. */ +export function resolveCodexContextEngineProjectionReserveTokens(): number { + return DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS; } /** Fits projected context prompts under Codex app-server turn/start text limits. */ @@ -233,17 +218,6 @@ function resolveProjectionPromptBudgetTokens(params: { return Math.max(1, params.contextTokenBudget - effectiveReserveTokens); } -function asRecord(value: unknown): Record | undefined { - return value && typeof value === "object" ? (value as Record) : undefined; -} - -function toNonNegativeInt(value: unknown): number | undefined { - if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { - return undefined; - } - return Math.floor(value); -} - function dropDuplicateTrailingPrompt(messages: AgentMessage[], prompt: string): AgentMessage[] { if (!prompt) { return messages; diff --git a/extensions/codex/src/app-server/run-attempt-context.ts b/extensions/codex/src/app-server/run-attempt-context.ts index 6d014847f90a..9eedde549200 100644 --- a/extensions/codex/src/app-server/run-attempt-context.ts +++ b/extensions/codex/src/app-server/run-attempt-context.ts @@ -167,7 +167,7 @@ export async function prepareCodexAttemptContext( }; const codexContextProjectionMaxChars = resolveCodexContextEngineProjectionMaxChars({ contextTokenBudget: effectiveContextTokenBudget, - reserveTokens: resolveCodexContextEngineProjectionReserveTokens({ config: params.config }), + reserveTokens: resolveCodexContextEngineProjectionReserveTokens(), }); return { runtime, diff --git a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts index 690b864ac990..26c38509d9bd 100644 --- a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +++ b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts @@ -679,37 +679,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { await run; }); - it("uses configured compaction reserve when sizing Codex context-engine projections", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const longContext = `configured reserve context start ${"x".repeat(30_000)} CONFIG_END`; - const contextEngine = createContextEngine({ - assemble: vi.fn(async () => ({ - messages: [assistantMessage(longContext, 10)], - estimatedTokens: 10_000, - systemPromptAddition: "context-engine system", - })), - }); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.contextEngine = contextEngine; - params.contextTokenBudget = 80_000; - params.config = { - agents: { defaults: { compaction: { reserveTokens: 60_000, reserveTokensFloor: 0 } } }, - } as EmbeddedRunAttemptParams["config"]; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); - - const inputText = getRequestInputText(harness); - expect(inputText).toContain("configured reserve context start"); - expect(inputText).toContain("[truncated "); - expect(inputText).not.toContain("CONFIG_END"); - - await harness.completeTurn(); - await run; - }); - it("projects thread-bootstrap context only once for a matching context-engine epoch", async () => { const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined); const sessionFile = path.join(tempDir, "session.jsonl"); diff --git a/extensions/codex/src/app-server/startup-binding.ts b/extensions/codex/src/app-server/startup-binding.ts index f7c22ab56ead..e8c6ce33800c 100644 --- a/extensions/codex/src/app-server/startup-binding.ts +++ b/extensions/codex/src/app-server/startup-binding.ts @@ -260,13 +260,6 @@ function readCodexAppServerRolloutTokenSnapshotLine( } } -function toNonNegativeInt(value: unknown): number | undefined { - if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { - return undefined; - } - return Math.floor(value); -} - function readCompactionConfig(config: EmbeddedRunAttemptParams["config"] | undefined) { return isJsonObject(config?.agents?.defaults?.compaction) ? config.agents.defaults.compaction @@ -274,18 +267,9 @@ function readCompactionConfig(config: EmbeddedRunAttemptParams["config"] | undef } function resolveCodexAppServerNativeThreadReserveTokens( - config: EmbeddedRunAttemptParams["config"] | undefined, + _config: EmbeddedRunAttemptParams["config"] | undefined, ): number { - const compaction = readCompactionConfig(config); - const reserveTokens = toNonNegativeInt(compaction?.reserveTokens); - const reserveTokensFloor = toNonNegativeInt(compaction?.reserveTokensFloor); - if (reserveTokens !== undefined) { - return Math.max( - reserveTokens, - reserveTokensFloor ?? CODEX_APP_SERVER_NATIVE_THREAD_DEFAULT_RESERVE_TOKENS, - ); - } - return reserveTokensFloor ?? CODEX_APP_SERVER_NATIVE_THREAD_DEFAULT_RESERVE_TOKENS; + return CODEX_APP_SERVER_NATIVE_THREAD_DEFAULT_RESERVE_TOKENS; } function resolveCodexAppServerNativeThreadTokenFuse(params: { diff --git a/extensions/codex/src/app-server/thread-context-engine.ts b/extensions/codex/src/app-server/thread-context-engine.ts index d28044ebaa61..ca82287b4059 100644 --- a/extensions/codex/src/app-server/thread-context-engine.ts +++ b/extensions/codex/src/app-server/thread-context-engine.ts @@ -42,9 +42,7 @@ export function buildContextEngineBinding( contextTokenBudget: params.contextTokenBudget, projectionMaxChars: resolveCodexContextEngineProjectionMaxChars({ contextTokenBudget: params.contextTokenBudget, - reserveTokens: resolveCodexContextEngineProjectionReserveTokens({ - config: params.config, - }), + reserveTokens: resolveCodexContextEngineProjectionReserveTokens(), }), }), projection: projection ? buildContextEngineProjectionBinding(projection) : undefined, diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts index 4a2cda42a88c..2c16850b0131 100644 --- a/extensions/discord/src/channel.test.ts +++ b/extensions/discord/src/channel.test.ts @@ -343,29 +343,6 @@ describe("discordPlugin outbound", () => { expect(resolveReplyToMode({ cfg, accountId: "default" })).toBe("all"); }); - it("inherits Discord gateway READY timeout settings per account", () => { - const cfg = { - channels: { - discord: { - token: "discord-token", - gatewayReadyTimeoutMs: 90_000, - gatewayRuntimeReadyTimeoutMs: 120_000, - accounts: { - work: { - token: "discord-token-work", - gatewayReadyTimeoutMs: 60_000, - }, - }, - }, - }, - } as OpenClawConfig; - - expect(resolveAccount(cfg).config.gatewayReadyTimeoutMs).toBe(90_000); - expect(resolveAccount(cfg).config.gatewayRuntimeReadyTimeoutMs).toBe(120_000); - expect(resolveAccount(cfg, "work").config.gatewayReadyTimeoutMs).toBe(60_000); - expect(resolveAccount(cfg, "work").config.gatewayRuntimeReadyTimeoutMs).toBe(120_000); - }); - it("forwards full media send context to sendMessageDiscord", async () => { const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" })); const mediaReadFile = vi.fn(async () => Buffer.from("media")); diff --git a/extensions/discord/src/client.test.ts b/extensions/discord/src/client.test.ts index 8c0b000a8703..56fd28ef1889 100644 --- a/extensions/discord/src/client.test.ts +++ b/extensions/discord/src/client.test.ts @@ -19,7 +19,6 @@ describe("createDiscordClient", () => { channels: { discord: { token: "discord-token", - retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, }, }, }, @@ -59,38 +58,6 @@ describe("createDiscordRestClient", () => { expect(result.account.accountId).toBe("default"); }); - it("keeps account retry config when explicit token is provided", () => { - const cfg = { - channels: { - discord: { - accounts: { - ops: { - token: { - source: "exec", - provider: "vault", - id: "discord/ops-token", - }, - retry: { - attempts: 7, - }, - }, - }, - }, - }, - } as OpenClawConfig; - - const result = createDiscordRestClient({ - cfg, - accountId: "ops", - token: "Bot explicit-account-token", - rest: fakeRest, - }); - - expect(result.token).toBe("explicit-account-token"); - expect(result.account.accountId).toBe("ops"); - expect(result.account.config.retry).toEqual({ attempts: 7 }); - }); - it("applies a caller timeout to a dedicated REST client", () => { const cfg = { channels: { discord: { token: "discord-token" } } } as OpenClawConfig; diff --git a/extensions/discord/src/client.ts b/extensions/discord/src/client.ts index 39cc62e8e02e..ab4740d6e139 100644 --- a/extensions/discord/src/client.ts +++ b/extensions/discord/src/client.ts @@ -147,7 +147,6 @@ export function createDiscordClient(opts: DiscordClientOpts): { const { token, rest, account } = createDiscordRestClient(opts); const request = createDiscordRetryRunner({ retry: opts.retry, - configRetry: account.config.retry, verbose: opts.verbose, isGatewayDisconnected: () => { const gateway = getGateway(account.accountId); diff --git a/extensions/discord/src/config-ui-hints.ts b/extensions/discord/src/config-ui-hints.ts index a5e1a14d3ea8..8b551f24f8b5 100644 --- a/extensions/discord/src/config-ui-hints.ts +++ b/extensions/discord/src/config-ui-hints.ts @@ -65,7 +65,6 @@ export const discordChannelConfigUiHints = { ...createChannelConfigUiHints({ channelLabel: "Discord", progress: { includeCommentary: true }, - retry: true, }), maxLinesPerMessage: { label: "Discord Max Lines Per Message", @@ -79,18 +78,6 @@ export const discordChannelConfigUiHints = { label: "Discord Thread Parent Inheritance", help: "If true, Discord thread sessions inherit the parent channel transcript (default: false).", }, - "eventQueue.listenerTimeout": { - label: "Discord EventQueue Listener Timeout (ms)", - help: "Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts..eventQueue.listenerTimeout.", - }, - "eventQueue.maxQueueSize": { - label: "Discord EventQueue Max Queue Size", - help: "Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts..eventQueue.maxQueueSize.", - }, - "eventQueue.maxConcurrency": { - label: "Discord EventQueue Max Concurrency", - help: "Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts..eventQueue.maxConcurrency.", - }, "threadBindings.enabled": { label: "Discord Thread Binding Enabled", help: "Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set.", @@ -135,18 +122,6 @@ export const discordChannelConfigUiHints = { label: "Discord Voice States Intent", help: "Enable the Guild Voice States intent. Defaults to the effective Discord voice setting; set true only for Discord voice channel conversations.", }, - gatewayInfoTimeoutMs: { - label: "Discord Gateway Metadata Timeout (ms)", - help: "Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset.", - }, - gatewayReadyTimeoutMs: { - label: "Discord Gateway READY Timeout (ms)", - help: "Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset.", - }, - gatewayRuntimeReadyTimeoutMs: { - label: "Discord Gateway Runtime READY Timeout (ms)", - help: "Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset.", - }, "voice.enabled": { label: "Discord Voice Enabled", help: "Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent.", diff --git a/extensions/discord/src/doctor-contract.ts b/extensions/discord/src/doctor-contract.ts index 7c4c7e2ecc67..ccd086bd76d2 100644 --- a/extensions/discord/src/doctor-contract.ts +++ b/extensions/discord/src/doctor-contract.ts @@ -11,8 +11,57 @@ import { import { asObjectRecord, defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor"; const LEGACY_TTS_PROVIDER_KEYS = ["openai", "elevenlabs", "microsoft", "edge"] as const; +const RETIRED_TUNING_KEYS = new Set([ + "gatewayInfoTimeoutMs", + "gatewayReadyTimeoutMs", + "gatewayRuntimeReadyTimeoutMs", + "eventQueue", + "retry", +]); type AgentBindingConfig = NonNullable[number]; +function stripRetiredTuningKnobs(value: unknown): { value: unknown; changed: boolean } { + const record = asObjectRecord(value); + if (!record) { + return { value, changed: false }; + } + const next = { ...record }; + let changed = false; + for (const key of RETIRED_TUNING_KEYS) { + if (Object.hasOwn(next, key)) { + delete next[key]; + changed = true; + } + } + return { value: changed ? next : record, changed }; +} + +function stripRetiredDiscordTuningKnobs(value: unknown): { + value: Record; + changed: boolean; +} { + const root = asObjectRecord(value) ?? {}; + const rootResult = stripRetiredTuningKnobs(root); + const nextRoot = rootResult.value as Record; + const accounts = asObjectRecord(nextRoot.accounts); + if (!accounts) { + return { value: nextRoot, changed: rootResult.changed }; + } + let accountsChanged = false; + const nextAccounts = { ...accounts }; + for (const [accountId, account] of Object.entries(accounts)) { + const accountResult = stripRetiredTuningKnobs(account); + if (accountResult.changed) { + nextAccounts[accountId] = accountResult.value; + accountsChanged = true; + } + } + return { + value: accountsChanged ? { ...nextRoot, accounts: nextAccounts } : nextRoot, + changed: rootResult.changed || accountsChanged, + }; +} + const streamingAliasMigration = defineChannelAliasMigration({ channelId: "discord", streaming: { @@ -521,6 +570,12 @@ export function normalizeCompatibilityConfig({ } let updated = rawEntry; let changed = aliases.config !== cfg; + const tuningKnobs = stripRetiredDiscordTuningKnobs(updated); + if (tuningKnobs.changed) { + updated = tuningKnobs.value as Record; + changes.push("Removed retired Discord tuning knobs."); + changed = true; + } const guildAliases = normalizeDiscordGuildChannelAllowAliases({ entry: updated, diff --git a/extensions/discord/src/doctor.test.ts b/extensions/discord/src/doctor.test.ts index c396ae75da23..a46bbeacc994 100644 --- a/extensions/discord/src/doctor.test.ts +++ b/extensions/discord/src/doctor.test.ts @@ -21,6 +21,43 @@ function getDiscordCompatibilityNormalizer(): NonNullable< } describe("discord doctor", () => { + it("strips retired gateway, queue, and retry tuning at root and account scope", () => { + const normalize = getDiscordCompatibilityNormalizer(); + const result = normalize({ + cfg: { + channels: { + discord: { + gatewayInfoTimeoutMs: 1, + gatewayReadyTimeoutMs: 2, + gatewayRuntimeReadyTimeoutMs: 3, + eventQueue: { listenerTimeout: 4 }, + retry: { attempts: 5 }, + voice: { + realtime: { + providers: { + custom: { retry: { attempts: 9 }, eventQueue: { maxConcurrency: 2 } }, + }, + }, + }, + accounts: { + work: { eventQueue: { maxConcurrency: 6 }, retry: { attempts: 7 } }, + }, + }, + }, + } as never, + }); + + expect(result.config.channels?.discord).toEqual({ + voice: { + realtime: { + providers: { custom: { retry: { attempts: 9 }, eventQueue: { maxConcurrency: 2 } } }, + }, + }, + accounts: { work: {} }, + }); + expect(result.changes).toContain("Removed retired Discord tuning knobs."); + }); + it("normalizes legacy discord streaming aliases for runtime config", () => { const normalize = getDiscordCompatibilityNormalizer(); diff --git a/extensions/discord/src/durable-delivery.test.ts b/extensions/discord/src/durable-delivery.test.ts index 7d3ef10527b9..def4dc924c40 100644 --- a/extensions/discord/src/durable-delivery.test.ts +++ b/extensions/discord/src/durable-delivery.test.ts @@ -54,7 +54,6 @@ describe("durable Discord delivery", () => { channels: { discord: { token: "test-token", - retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, }, }, }, diff --git a/extensions/discord/src/monitor/gateway-metadata.test.ts b/extensions/discord/src/monitor/gateway-metadata.test.ts index 3c4c23f5ef65..b4017e59d65b 100644 --- a/extensions/discord/src/monitor/gateway-metadata.test.ts +++ b/extensions/discord/src/monitor/gateway-metadata.test.ts @@ -67,17 +67,14 @@ function createStalledLookup() { } describe("Discord gateway metadata", () => { - it("resolves gateway info timeouts from strict integer config and env values", () => { - expect(resolveDiscordGatewayInfoTimeoutMs({ configuredTimeoutMs: 45_000 })).toBe(45_000); + it("resolves gateway info timeouts from strict integer env values", () => { expect( resolveDiscordGatewayInfoTimeoutMs({ env: { OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS: "90000" }, }), ).toBe(90_000); - expect(resolveDiscordGatewayInfoTimeoutMs({ configuredTimeoutMs: 150_000 })).toBe(120_000); expect( resolveDiscordGatewayInfoTimeoutMs({ - configuredTimeoutMs: 1.5, env: { OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS: "0x1000" }, }), ).toBe(30_000); diff --git a/extensions/discord/src/monitor/gateway-metadata.ts b/extensions/discord/src/monitor/gateway-metadata.ts index 6fac931ac2b6..702a85f74809 100644 --- a/extensions/discord/src/monitor/gateway-metadata.ts +++ b/extensions/discord/src/monitor/gateway-metadata.ts @@ -82,12 +82,8 @@ function normalizeGatewayInfoTimeoutMs(value: unknown): number | undefined { return Math.min(numeric, MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS); } -export function resolveDiscordGatewayInfoTimeoutMs(params?: { - configuredTimeoutMs?: number; - env?: NodeJS.ProcessEnv; -}): number { +export function resolveDiscordGatewayInfoTimeoutMs(params?: { env?: NodeJS.ProcessEnv }): number { return ( - normalizeGatewayInfoTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayInfoTimeoutMs(params?.env?.[DISCORD_GATEWAY_INFO_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS ); diff --git a/extensions/discord/src/monitor/gateway-plugin.test.ts b/extensions/discord/src/monitor/gateway-plugin.test.ts index 7942393a605f..527521365f5e 100644 --- a/extensions/discord/src/monitor/gateway-plugin.test.ts +++ b/extensions/discord/src/monitor/gateway-plugin.test.ts @@ -139,8 +139,7 @@ describe("createDiscordGatewayPlugin", () => { expect(intents & GatewayIntents.GuildMembers).toBe(GatewayIntents.GuildMembers); }); - it("resolves gateway metadata timeout from config, env, then default", () => { - expect(resolveDiscordGatewayInfoTimeoutMs({ configuredTimeoutMs: 45_000 })).toBe(45_000); + it("resolves gateway metadata timeout from env, then default", () => { expect( resolveDiscordGatewayInfoTimeoutMs({ env: { OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS: "25000" }, @@ -249,22 +248,6 @@ describe("createDiscordGatewayPlugin", () => { }); }); - it("keeps OpenClaw metadata timeout out of gateway options", () => { - const plugin = createDiscordGatewayPlugin({ - discordConfig: { gatewayInfoTimeoutMs: 5_000 }, - runtime: { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }, - }); - - expect( - (plugin as unknown as { options?: { gatewayInfoTimeoutMs?: number } }).options - ?.gatewayInfoTimeoutMs, - ).toBeUndefined(); - }); - it("emits transport activity for current gateway socket messages", () => { const socket = new EventEmitter() as EventEmitter & { binaryType?: string }; const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000); diff --git a/extensions/discord/src/monitor/gateway-plugin.ts b/extensions/discord/src/monitor/gateway-plugin.ts index 5137c5ec673d..10677d83fc5e 100644 --- a/extensions/discord/src/monitor/gateway-plugin.ts +++ b/extensions/discord/src/monitor/gateway-plugin.ts @@ -389,7 +389,6 @@ export function createDiscordGatewayPlugin(params: { const proxy = resolveEffectiveDebugProxyUrl(params.discordConfig?.proxy); const debugProxySettings = resolveDebugProxySettings(); const gatewayInfoTimeoutMs = resolveDiscordGatewayInfoTimeoutMs({ - configuredTimeoutMs: params.discordConfig?.gatewayInfoTimeoutMs, env: process.env, }); let fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled); diff --git a/extensions/discord/src/monitor/message-handler.process-reactions.ts b/extensions/discord/src/monitor/message-handler.process-reactions.ts index 31090bb3e611..d2535dfacdb1 100644 --- a/extensions/discord/src/monitor/message-handler.process-reactions.ts +++ b/extensions/discord/src/monitor/message-handler.process-reactions.ts @@ -94,10 +94,7 @@ export function createDiscordMessageReactionRuntime(params: { messageId: message.id, reactionContext: ackReactionContext, }); - const statusReactionTiming = { - ...DEFAULT_TIMING, - ...cfg.messages?.statusReactions?.timing, - }; + const statusReactionTiming = DEFAULT_TIMING; let statusReactionTarget = `${messageChannelId}/${message.id}`; let statusReactionsActive = statusReactionsEnabled; let statusReactions: StatusReactionController = createStatusReactionController({ diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index d7d36232d0ae..b8b607144cf1 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -128,8 +128,7 @@ async function processDiscordMessageInner( }); const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only"; const configuredTypingMode = cfg.session?.typingMode ?? cfg.agents?.defaults?.typingMode; - const configuredTypingInterval = - cfg.agents?.defaults?.typingIntervalSeconds ?? cfg.session?.typingIntervalSeconds; + const configuredTypingInterval = cfg.agents?.defaults?.typingIntervalSeconds; const shouldDisableCoreTypingKeepalive = sourceRepliesAreToolOnly && configuredTypingMode === undefined && diff --git a/extensions/discord/src/monitor/provider.lifecycle.test.ts b/extensions/discord/src/monitor/provider.lifecycle.test.ts index 6d4bb705244b..b998aa91d867 100644 --- a/extensions/discord/src/monitor/provider.lifecycle.test.ts +++ b/extensions/discord/src/monitor/provider.lifecycle.test.ts @@ -272,35 +272,6 @@ describe("runDiscordGatewayLifecycle", () => { ); }); - it("does not treat a missing gateway handle as ready", async () => { - vi.useFakeTimers(); - try { - const { lifecycleParams, threadStop, statusSink, gatewaySupervisor } = createLifecycleHarness( - { - gateway: null, - }, - ); - lifecycleParams.gatewayReadyTimeoutMs = 5_000; - - const lifecyclePromise = runDiscordGatewayLifecycle(lifecycleParams); - lifecyclePromise.catch(() => {}); - await vi.advanceTimersByTimeAsync(0); - await vi.advanceTimersByTimeAsync(5_500); - - await expect(lifecyclePromise).rejects.toThrow( - "discord gateway did not reach READY within 5000ms", - ); - expect(statusPatches(statusSink).every((patch) => patch.connected !== true)).toBe(true); - expectLifecycleCleanup({ - threadStop, - waitCalls: 0, - gatewaySupervisor, - }); - } finally { - vi.useRealTimers(); - } - }); - it("records throttled gateway socket activity as transport liveness", async () => { const { emitter, gateway } = createGatewayHarness(); gateway.isConnected = true; @@ -700,45 +671,6 @@ describe("runDiscordGatewayLifecycle", () => { vi.useRealTimers(); } }); - - it("force-stops when a runtime reconnect opens but never becomes ready", async () => { - vi.useFakeTimers(); - try { - const { emitter, gateway } = createGatewayHarness(); - gateway.isConnected = true; - getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter); - waitForDiscordGatewayStopMock.mockImplementationOnce( - (params: WaitForDiscordGatewayStopParams) => - new Promise((_resolve, reject) => { - params.registerForceStop?.((err) => - reject(toLintErrorObject(err, "Non-Error rejection")), - ); - gateway.isConnected = false; - emitter.emit("debug", "Gateway websocket opened"); - }), - ); - - const { lifecycleParams, runtimeError, statusSink } = createLifecycleHarness({ gateway }); - lifecycleParams.gatewayRuntimeReadyTimeoutMs = 5_000; - const lifecyclePromise = runDiscordGatewayLifecycle(lifecycleParams); - lifecyclePromise.catch(() => {}); - - await vi.advanceTimersByTimeAsync(5_500); - await expect(lifecyclePromise).rejects.toThrow( - "discord gateway opened but did not reach READY within 5000ms", - ); - expectMockMessageContains(runtimeError, "did not reach READY within 5000ms"); - expectStatusPatch( - statusSink, - (patch) => - patch.connected === false && - patch.lastDisconnect !== null && - patch.lastDisconnect?.error === "runtime-not-ready", - ); - } finally { - vi.useRealTimers(); - } - }); }); describe("waitForGatewayReady", () => { @@ -786,17 +718,3 @@ describe("waitForGatewayReady", () => { } }); }); - -function toLintErrorObject(value: unknown, fallbackMessage: string): Error { - if (value instanceof Error) { - return value; - } - if (typeof value === "string") { - return new Error(value); - } - const error = new Error(fallbackMessage, { cause: value }); - if ((typeof value === "object" && value !== null) || typeof value === "function") { - Object.assign(error, value); - } - return error; -} diff --git a/extensions/discord/src/monitor/provider.lifecycle.ts b/extensions/discord/src/monitor/provider.lifecycle.ts index 6774914b688e..a50d51e25d35 100644 --- a/extensions/discord/src/monitor/provider.lifecycle.ts +++ b/extensions/discord/src/monitor/provider.lifecycle.ts @@ -43,23 +43,15 @@ function normalizeGatewayReadyTimeoutMs(value: unknown): number | undefined { return Math.min(numeric, MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS); } -function resolveDiscordGatewayReadyTimeoutMs(params?: { - configuredTimeoutMs?: number; - env?: NodeJS.ProcessEnv; -}): number { +function resolveDiscordGatewayReadyTimeoutMs(params?: { env?: NodeJS.ProcessEnv }): number { return ( - normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_READY_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS ); } -function resolveDiscordGatewayRuntimeReadyTimeoutMs(params?: { - configuredTimeoutMs?: number; - env?: NodeJS.ProcessEnv; -}): number { +function resolveDiscordGatewayRuntimeReadyTimeoutMs(params?: { env?: NodeJS.ProcessEnv }): number { return ( - normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS ); @@ -421,8 +413,6 @@ export async function runDiscordGatewayLifecycle(params: { threadBindings: { stop: () => void }; gatewaySupervisor: DiscordGatewaySupervisor; statusSink?: DiscordMonitorStatusSink; - gatewayReadyTimeoutMs?: number; - gatewayRuntimeReadyTimeoutMs?: number; }) { const gateway = params.gateway; const gatewayReadyAtLifecycleStart = gateway?.isConnected === true; @@ -440,11 +430,9 @@ export async function runDiscordGatewayLifecycle(params: { params.statusSink?.(patch); }; const gatewayReadyTimeoutMs = resolveDiscordGatewayReadyTimeoutMs({ - configuredTimeoutMs: params.gatewayReadyTimeoutMs, env: process.env, }); const gatewayRuntimeReadyTimeoutMs = resolveDiscordGatewayRuntimeReadyTimeoutMs({ - configuredTimeoutMs: params.gatewayRuntimeReadyTimeoutMs, env: process.env, }); const statusObserver = createGatewayStatusObserver({ diff --git a/extensions/discord/src/monitor/provider.proxy.test.ts b/extensions/discord/src/monitor/provider.proxy.test.ts index 295c9d44c945..63be23a8c16f 100644 --- a/extensions/discord/src/monitor/provider.proxy.test.ts +++ b/extensions/discord/src/monitor/provider.proxy.test.ts @@ -737,26 +737,6 @@ describe("createDiscordGatewayPlugin", () => { ); }); - it("uses configured gateway metadata timeout before falling back", async () => { - vi.useFakeTimers(); - const runtime = createRuntime(); - globalFetchMock.mockImplementation(() => new Promise(() => {})); - const plugin = createDiscordGatewayPlugin({ - discordConfig: { gatewayInfoTimeoutMs: 5_000 }, - runtime, - }); - - const registerPromise = registerGatewayClient(plugin); - await vi.advanceTimersByTimeAsync(4_999); - expect(baseRegisterClientSpy).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(1); - await registerPromise; - - expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe( - "wss://gateway.discord.gg/", - ); - }); - it("uses env gateway metadata timeout when config is unset", async () => { vi.useFakeTimers(); vi.stubEnv("OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS", "6000"); diff --git a/extensions/discord/src/monitor/provider.startup.ts b/extensions/discord/src/monitor/provider.startup.ts index f459abe6dada..e030f8a56a2e 100644 --- a/extensions/discord/src/monitor/provider.startup.ts +++ b/extensions/discord/src/monitor/provider.startup.ts @@ -96,12 +96,7 @@ export async function createDiscordMonitorClient(params: { components: BaseMessageInteractiveComponent[]; modals: Modal[]; voiceEnabled: boolean; - discordConfig: Parameters[0] & { - eventQueue?: Pick< - DiscordEventQueueOptions, - "listenerTimeout" | "maxQueueSize" | "maxConcurrency" - >; - }; + discordConfig: Parameters[0]; runtime: RuntimeEnv; commandDeployHashStore?: DiscordCommandDeployHashStore; createClient: CreateClientFn; @@ -128,7 +123,6 @@ export async function createDiscordMonitorClient(params: { const eventQueueOpts = { listenerTimeout: 120_000, slowListenerThreshold: 30_000, - ...params.discordConfig.eventQueue, } satisfies DiscordEventQueueOptions; const readyListener = createDiscordStatusReadyListener({ discordConfig: params.discordConfig, diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index a68bd695751c..db533902711b 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -210,20 +210,6 @@ describe("monitorDiscordProvider", () => { return reconcileParams.healthProbe; }; - const getMonitorLifecycleParams = (): { - gatewayReadyTimeoutMs?: number; - gatewayRuntimeReadyTimeoutMs?: number; - } => { - expect(monitorLifecycleMock).toHaveBeenCalledTimes(1); - const params = firstMockArg(monitorLifecycleMock, "Discord lifecycle monitor") as - | { gatewayReadyTimeoutMs?: number; gatewayRuntimeReadyTimeoutMs?: number } - | undefined; - if (!params) { - throw new Error("expected lifecycle monitor params"); - } - return params; - }; - beforeAll(async () => { vi.doMock("openclaw/plugin-sdk/plugin-runtime", () => ({ getPluginCommandSpecs: getPluginCommandSpecsMock, @@ -442,30 +428,6 @@ describe("monitorDiscordProvider", () => { expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1); }); - it("passes configured gateway READY timeouts to the lifecycle monitor", async () => { - resolveDiscordAccountMock.mockReturnValueOnce({ - accountId: "default", - token: "cfg-token", - config: { - commands: { native: true, nativeSkills: false }, - voice: { enabled: false }, - agentComponents: { enabled: false }, - execApprovals: { enabled: false }, - gatewayReadyTimeoutMs: 90_000, - gatewayRuntimeReadyTimeoutMs: 120_000, - }, - }); - - await monitorDiscordProvider({ - config: baseConfig(), - runtime: baseRuntime(), - }); - - const lifecycleParams = getMonitorLifecycleParams(); - expect(lifecycleParams.gatewayReadyTimeoutMs).toBe(90_000); - expect(lifecycleParams.gatewayRuntimeReadyTimeoutMs).toBe(120_000); - }); - it("does not load the Discord voice runtime when voice is disabled", async () => { await monitorDiscordProvider({ config: baseConfig(), diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index 0f856e102769..a18b2be3f05a 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -504,8 +504,6 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { voiceManagerRef, threadBindings, gatewaySupervisor, - gatewayReadyTimeoutMs: account.config.gatewayReadyTimeoutMs, - gatewayRuntimeReadyTimeoutMs: account.config.gatewayRuntimeReadyTimeoutMs, }); } finally { await cleanupDiscordProviderStartup({ diff --git a/extensions/discord/src/outbound-adapter.test.ts b/extensions/discord/src/outbound-adapter.test.ts index 39d620137f96..3d69e7bcb74a 100644 --- a/extensions/discord/src/outbound-adapter.test.ts +++ b/extensions/discord/src/outbound-adapter.test.ts @@ -181,7 +181,6 @@ describe("discordOutbound", () => { channels: { discord: { token: "test-token", - retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, }, }, }, diff --git a/extensions/discord/src/retry.ts b/extensions/discord/src/retry.ts index 39bf79632adc..6cf86f04fae6 100644 --- a/extensions/discord/src/retry.ts +++ b/extensions/discord/src/retry.ts @@ -130,14 +130,10 @@ function isRetryableDiscordGatewayTransportError(err: unknown): boolean { export function createDiscordRetryRunner(params: { retry?: RetryConfig; - configRetry?: RetryConfig; verbose?: boolean; isGatewayDisconnected?: () => boolean; }): DiscordRetryRunner { - const retryConfig = resolveRetryConfig(DISCORD_RETRY_DEFAULTS, { - ...params.configRetry, - ...params.retry, - }); + const retryConfig = resolveRetryConfig(DISCORD_RETRY_DEFAULTS, params.retry); // Extend only the per-request runner. A delivery may contain several REST // writes, so replaying its outer adapter can duplicate already-sent chunks. const attempts = diff --git a/extensions/memory-core/index.test.ts b/extensions/memory-core/index.test.ts index 391b6519ac05..d9b09b2c717e 100644 --- a/extensions/memory-core/index.test.ts +++ b/extensions/memory-core/index.test.ts @@ -264,7 +264,6 @@ describe("buildMemoryFlushPlan", () => { agents: { defaults: { compaction: { - reserveTokensFloor: Number.NaN, memoryFlush: { softThresholdTokens: -100, }, @@ -276,7 +275,6 @@ describe("buildMemoryFlushPlan", () => { expect(plan?.softThresholdTokens).toBe(4000); expect(plan?.forceFlushTranscriptBytes).toBe(2 * 1024 * 1024); - expect(plan?.reserveTokensFloor).toBe(20_000); }); it("parses forceFlushTranscriptBytes from byte-size strings", () => { diff --git a/extensions/memory-core/src/flush-plan.ts b/extensions/memory-core/src/flush-plan.ts index 637b794b1740..422d1cb57f4a 100644 --- a/extensions/memory-core/src/flush-plan.ts +++ b/extensions/memory-core/src/flush-plan.ts @@ -113,9 +113,7 @@ export function buildMemoryFlushPlan( const forceFlushTranscriptBytes = parseNonNegativeByteSize(defaults?.forceFlushTranscriptBytes) ?? DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES; - const reserveTokensFloor = - normalizeNonNegativeInt(cfg?.agents?.defaults?.compaction?.reserveTokensFloor) ?? - DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; + const reserveTokensFloor = DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; const { timeLine, userTimezone } = resolveCronStyleNow(cfg ?? {}, nowMs); const dateStamp = formatDateStampInTimezone(nowMs, userTimezone); diff --git a/extensions/memory-core/src/memory/index.test.ts b/extensions/memory-core/src/memory/index.test.ts index adbc8b506878..9ac78e240cc9 100644 --- a/extensions/memory-core/src/memory/index.test.ts +++ b/extensions/memory-core/src/memory/index.test.ts @@ -395,8 +395,6 @@ describe("memory index", () => { fallback: params.fallback, outputDimensionality: params.outputDimensionality, store: { vector: { enabled: params.vectorEnabled ?? false } }, - // Perf: keep test indexes to a single chunk to reduce sqlite work. - chunking: { tokens: 4000, overlap: 0 }, sync: { watch: false, onSessionStart: false, onSearch: params.onSearch ?? true }, remote: params.batchEnabled ? { diff --git a/extensions/memory-core/src/memory/manager-watch-ops.ts b/extensions/memory-core/src/memory/manager-watch-ops.ts index e2464b56a557..78f8252f61af 100644 --- a/extensions/memory-core/src/memory/manager-watch-ops.ts +++ b/extensions/memory-core/src/memory/manager-watch-ops.ts @@ -237,7 +237,7 @@ export abstract class MemoryManagerWatchOps extends MemoryManagerSyncBase { count, unit, "Large memory folders or extraPaths can make OpenClaw run out of file watchers or open files.", - "Remove large extraPaths, or set memorySearch.sync.watch to false and refresh memory manually or with sync.intervalMinutes.", + "Remove large extraPaths, or set memorySearch.sync.watch to false and refresh memory manually.", (message) => log.warn(message), ); } diff --git a/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts b/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts index 0e5aeb0299bf..5f31123b6aa6 100644 --- a/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts +++ b/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts @@ -70,7 +70,6 @@ describe("memory manager reindex recovery", () => { provider: params.provider ?? "openai", model: "mock-embed", store: { vector: { enabled: false } }, - chunking: { tokens: 4000, overlap: 0 }, sync: { watch: false, onSessionStart: false, onSearch: false }, remote: { nonBatchConcurrency: 1 }, cache: { enabled: false }, diff --git a/extensions/memory-core/src/memory/manager.watcher-config.test.ts b/extensions/memory-core/src/memory/manager.watcher-config.test.ts index f2ad8ed61510..e487635a59f7 100644 --- a/extensions/memory-core/src/memory/manager.watcher-config.test.ts +++ b/extensions/memory-core/src/memory/manager.watcher-config.test.ts @@ -231,7 +231,7 @@ describe("memory watcher config", () => { provider: "openai", model: "mock-embed", store: { vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, + sync: { watch: true, onSessionStart: false, onSearch: false }, query: { minScore: 0, hybrid: { enabled: false } }, extraPaths: [extraDir], ...overrides, diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 666d8c76089f..1ecaf547772d 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -1145,7 +1145,7 @@ describe("QmdMemoryManager", () => { provider: "openai", model: "mock-embed", store: { vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, + sync: { watch: true, onSessionStart: false, onSearch: false }, }, }, list: [{ id: agentId, default: true, workspace: workspaceDir }], @@ -1213,7 +1213,7 @@ describe("QmdMemoryManager", () => { provider: "openai", model: "mock-embed", store: { vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, + sync: { watch: true, onSessionStart: false, onSearch: false }, }, }, list: [{ id: agentId, default: true, workspace: workspaceDir }], @@ -1260,7 +1260,7 @@ describe("QmdMemoryManager", () => { provider: "openai", model: "mock-embed", store: { vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, + sync: { watch: true, onSessionStart: false, onSearch: false }, }, }, list: [{ id: agentId, default: true, workspace: workspaceDir }], @@ -1298,7 +1298,7 @@ describe("QmdMemoryManager", () => { provider: "openai", model: "mock-embed", store: { vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, + sync: { watch: true, onSessionStart: false, onSearch: false }, }, }, list: [{ id: agentId, default: true, workspace: workspaceDir }], diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 498c99412ee2..b9cfc06a9bbb 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -1308,7 +1308,7 @@ export class QmdMemoryManager implements MemorySearchManager { count, "paths", "Large QMD collections can make OpenClaw run out of file watchers or open files.", - "Remove large collections, or set memorySearch.sync.watch to false and refresh memory manually or with sync.intervalMinutes.", + "Remove large collections, or set memorySearch.sync.watch to false and refresh memory manually.", (message) => log.warn(message), ); } diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts index ff2369d26b87..e6fd461ce409 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts @@ -209,7 +209,6 @@ describe("discord live qa runtime", () => { expect(next.messages?.ackReactionScope).toBe("all"); expect(next.messages?.groupChat?.visibleReplies).toBe("message_tool"); expect(next.messages?.statusReactions?.enabled).toBe(true); - expect(next.messages?.statusReactions?.timing?.debounceMs).toBe(0); const discordAccount = next.channels?.discord?.accounts?.sut; expect(discordAccount?.allowBots).toBe(true); expect(discordAccount?.guilds?.["123456789012345678"]?.requireMention).toBe(false); diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts index 48a0912ce70c..31999c0c556a 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts @@ -412,10 +412,6 @@ function buildDiscordQaConfig( statusReactions: { ...baseCfg.messages?.statusReactions, enabled: true, - timing: { - ...baseCfg.messages?.statusReactions?.timing, - debounceMs: 0, - }, }, } : { diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.config.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.config.ts index e8bc1257f74b..3be1cd7f1c97 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.config.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.config.ts @@ -361,14 +361,6 @@ export function buildWhatsAppQaConfig( statusReactions: { ...baseCfg.messages?.statusReactions, enabled: true, - ...(statusReactionOverride?.timing - ? { - timing: { - ...baseCfg.messages?.statusReactions?.timing, - ...statusReactionOverride.timing, - }, - } - : {}), }, } : {}), diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts index 814379713a65..655402341e93 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.contracts.ts @@ -3,7 +3,6 @@ import type { WhatsAppQaDriverObservedMessage, WhatsAppQaDriverSession, } from "@openclaw/whatsapp/api.js"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { startQaGatewayChild } from "../../gateway-child.js"; @@ -265,7 +264,6 @@ export type WhatsAppQaConfigOverrides = { | boolean | { removeAckAfterReply?: boolean; - timing?: NonNullable["statusReactions"]>["timing"]; }; }; diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts index f9746b973aba..6d4fd10219b2 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts @@ -2177,10 +2177,6 @@ describe("WhatsApp QA live runtime", () => { replyToMode: "all", statusReactions: { removeAckAfterReply: true, - timing: { - debounceMs: 0, - stallSoftMs: 60_000, - }, }, }, }); @@ -2193,10 +2189,6 @@ describe("WhatsApp QA live runtime", () => { }); expect(cfg.messages?.removeAckAfterReply).toBe(true); expect(cfg.messages?.statusReactions?.enabled).toBe(true); - expect(cfg.messages?.statusReactions?.timing).toMatchObject({ - debounceMs: 0, - stallSoftMs: 60_000, - }); }); it("maps WhatsApp broadcast overrides without deleting existing agent defaults", () => { diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.scenarios.delivery.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.scenarios.delivery.ts index bd57e9b9703c..3f6462fb62eb 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.scenarios.delivery.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.scenarios.delivery.ts @@ -132,13 +132,7 @@ export const WHATSAPP_QA_DELIVERY_SCENARIOS: WhatsAppQaScenarioDefinition[] = [ title: "WhatsApp status reaction lifecycle updates the triggering message", timeoutMs: 90_000, configOverrides: { - statusReactions: { - timing: { - debounceMs: 0, - stallSoftMs: 60_000, - stallHardMs: 120_000, - }, - }, + statusReactions: true, }, buildRun: () => { const token = `WHATSAPP_QA_STATUS_LIFECYCLE_${randomUUID().slice(0, 8).toUpperCase()}`; diff --git a/extensions/qa-lab/src/qa-gateway-config.test.ts b/extensions/qa-lab/src/qa-gateway-config.test.ts index 02b03cbc8ddd..77ff475ad151 100644 --- a/extensions/qa-lab/src/qa-gateway-config.test.ts +++ b/extensions/qa-lab/src/qa-gateway-config.test.ts @@ -109,7 +109,6 @@ describe("buildQaGatewayConfig", () => { expect(cfg.plugins?.entries?.["memory-core"]).toEqual({ enabled: true }); expect(cfg.plugins?.entries?.["qa-channel"]).toEqual({ enabled: true }); expect(cfg.plugins?.entries?.openai).toBeUndefined(); - expect(cfg.gateway?.reload?.deferralTimeoutMs).toBe(1_000); expect(cfg.tools?.profile).toBe("coding"); expect(cfg.agents?.list?.[0]?.tools?.profile).toBe("coding"); expect(cfg.channels?.["qa-channel"]?.enabled).toBe(true); diff --git a/extensions/qa-lab/src/qa-gateway-config.ts b/extensions/qa-lab/src/qa-gateway-config.ts index e11f2b4411e9..c07b45b91c7d 100644 --- a/extensions/qa-lab/src/qa-gateway-config.ts +++ b/extensions/qa-lab/src/qa-gateway-config.ts @@ -228,7 +228,6 @@ export function buildQaGatewayConfig(params: { ...mockMemorySearch, sync: { watch: true, - watchDebounceMs: 25, onSessionStart: true, onSearch: true, }, @@ -290,11 +289,6 @@ export function buildQaGatewayConfig(params: { mode: "token", token: params.gatewayToken, }, - reload: { - // QA restart scenarios need deterministic reload timing instead of the - // much longer production deferral window. - deferralTimeoutMs: 1_000, - }, controlUi: { enabled: params.controlUiEnabled ?? true, ...((params.controlUiEnabled ?? true) && params.controlUiRoot diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 3d2e281b0ccb..f1b75d1a82fa 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -444,7 +444,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { }, initialEmoji: ackReaction, emojis: resolveSignalStatusReactionEmojis(statusReactionsConfig.emojis), - timing: statusReactionsConfig.timing, + timing: DEFAULT_TIMING, onError: (err) => { logAckFailure({ log: logVerbose, @@ -455,10 +455,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { }, }) : null; - const statusReactionTiming = { - ...DEFAULT_TIMING, - ...statusReactionsConfig?.timing, - }; + const statusReactionTiming = DEFAULT_TIMING; if (statusReactionController) { void statusReactionController.setQueued(); } diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index a9c976502690..159cc78e5d37 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -482,16 +482,13 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); }, }; - const statusReactionTiming = { - ...DEFAULT_TIMING, - ...cfg.messages?.statusReactions?.timing, - }; + const statusReactionTiming = DEFAULT_TIMING; const statusReactions = createStatusReactionController({ enabled: statusReactionsEnabled, adapter: slackStatusAdapter, initialEmoji: prepared.ackReactionValue || "eyes", emojis: cfg.messages?.statusReactions?.emojis, - timing: cfg.messages?.statusReactions?.timing, + timing: DEFAULT_TIMING, onError: (err) => { logAckFailure({ log: logVerbose, diff --git a/extensions/telegram/src/bot-core.ts b/extensions/telegram/src/bot-core.ts index 2e5af36b773a..c0cf86f3c5f1 100644 --- a/extensions/telegram/src/bot-core.ts +++ b/extensions/telegram/src/bot-core.ts @@ -121,16 +121,15 @@ export function createTelegramBotCore( }); const finalFetch = createTelegramClientFetch({ fetchImpl: asTelegramClientFetch(telegramTransport.fetch), - timeoutSeconds: telegramCfg?.timeoutSeconds, shutdownSignal: opts.fetchAbortSignal, transport: telegramTransport, }); const timeoutSeconds = resolveTelegramClientTimeoutSeconds({ - value: telegramCfg?.timeoutSeconds, + value: undefined, minimum: resolveTelegramClientTimeoutMinimumSeconds([ opts.minimumClientTimeoutSeconds, - resolveTelegramOutboundClientTimeoutFloorSeconds(telegramCfg?.timeoutSeconds), + resolveTelegramOutboundClientTimeoutFloorSeconds(undefined), ]), }); const apiRoot = normalizeOptionalString(telegramCfg.apiRoot); diff --git a/extensions/telegram/src/bot-handlers.inbound-media-group.runtime.ts b/extensions/telegram/src/bot-handlers.inbound-media-group.runtime.ts index 47173566bac2..81769918a0c4 100644 --- a/extensions/telegram/src/bot-handlers.inbound-media-group.runtime.ts +++ b/extensions/telegram/src/bot-handlers.inbound-media-group.runtime.ts @@ -66,7 +66,6 @@ export function createTelegramInboundMediaGroupRuntime( | "opts" | "runtime" | "mediaMaxBytes" - | "telegramCfg" | "logger" | "resolveGroupActivation" | "resolveGroupRequireMention" @@ -79,7 +78,6 @@ export function createTelegramInboundMediaGroupRuntime( opts, runtime, mediaMaxBytes, - telegramCfg, logger, resolveGroupActivation, resolveGroupRequireMention, @@ -102,10 +100,7 @@ export function createTelegramInboundMediaGroupRuntime( typeof opts.testTimings?.mediaGroupFlushMs === "number" && Number.isFinite(opts.testTimings.mediaGroupFlushMs) ? Math.max(10, Math.floor(opts.testTimings.mediaGroupFlushMs)) - : typeof telegramCfg.mediaGroupFlushMs === "number" && - Number.isFinite(telegramCfg.mediaGroupFlushMs) - ? Math.max(10, Math.floor(telegramCfg.mediaGroupFlushMs)) - : MEDIA_GROUP_TIMEOUT_MS; + : MEDIA_GROUP_TIMEOUT_MS; const buffer = new Map(); const queue = new KeyedAsyncQueue(); diff --git a/extensions/telegram/src/bot-handlers.inbound.runtime.ts b/extensions/telegram/src/bot-handlers.inbound.runtime.ts index 8ec68bc7fd21..640c0f7dd2cc 100644 --- a/extensions/telegram/src/bot-handlers.inbound.runtime.ts +++ b/extensions/telegram/src/bot-handlers.inbound.runtime.ts @@ -46,7 +46,6 @@ export function createTelegramHandlerInboundRuntime( opts, runtime, mediaMaxBytes, - telegramCfg, logger, resolveGroupActivation, resolveGroupRequireMention, @@ -74,7 +73,6 @@ export function createTelegramHandlerInboundRuntime( opts, runtime, mediaMaxBytes, - telegramCfg, logger, resolveGroupActivation, resolveGroupRequireMention, diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index a10cefd09247..e041db42ef84 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -610,7 +610,6 @@ export const buildTelegramMessageContext = async ({ }, initialEmoji: ackReaction, emojis: resolvedStatusReactionEmojis ?? undefined, - timing: statusReactionsConfig?.timing, onError: (err) => { logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`); }, diff --git a/extensions/telegram/src/bot-message-dispatch-status.ts b/extensions/telegram/src/bot-message-dispatch-status.ts index b14720f17dbf..3c3d87a377ab 100644 --- a/extensions/telegram/src/bot-message-dispatch-status.ts +++ b/extensions/telegram/src/bot-message-dispatch-status.ts @@ -15,7 +15,7 @@ export function createTelegramDispatchStatus(params: { const { context } = params; const controller = context.ctxPayload.InboundEventKind === "room_event" ? null : context.statusReactionController; - const timing = { ...DEFAULT_TIMING, ...params.cfg.messages?.statusReactions?.timing }; + const timing = DEFAULT_TIMING; const clear = async () => { if (!context.msg.message_id || !context.reactionApi) { diff --git a/extensions/telegram/src/bot-message-dispatch.pipeline-init.test.ts b/extensions/telegram/src/bot-message-dispatch.pipeline-init.test.ts index 1fc5b508a402..fa4262828be5 100644 --- a/extensions/telegram/src/bot-message-dispatch.pipeline-init.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.pipeline-init.test.ts @@ -37,13 +37,6 @@ describeTelegramDispatch("dispatchTelegramMessage pipeline-init", () => { reactionApi, removeAckAfterReply: true, }), - cfg: { - messages: { - statusReactions: { - timing: { errorHoldMs: 0 }, - }, - }, - }, runtime, suppressFailureFallback: true, }); diff --git a/extensions/telegram/src/bot-message-dispatch.status-reactions.test.ts b/extensions/telegram/src/bot-message-dispatch.status-reactions.test.ts index 3987969defeb..f3d6e9e96ac7 100644 --- a/extensions/telegram/src/bot-message-dispatch.status-reactions.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.status-reactions.test.ts @@ -104,49 +104,6 @@ describeTelegramDispatch("dispatchTelegramMessage status-reactions", () => { ); }); - it("uses configured doneHoldMs when clearing Telegram status reactions after reply", async () => { - vi.useFakeTimers(); - const reactionApi = vi.fn(async () => true); - const statusReactionController = createStatusReactionController(); - dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { - await dispatcherOptions.deliver({ text: "Done" }, { kind: "final" }); - return { queuedFinal: true }; - }); - deliverReplies.mockResolvedValue({ delivered: true }); - - try { - await dispatchWithContext({ - context: createContext({ - reactionApi: reactionApi as never, - removeAckAfterReply: true, - statusReactionController: statusReactionController as never, - }), - cfg: { - messages: { - statusReactions: { - timing: { - doneHoldMs: 250, - }, - }, - }, - }, - streamMode: "off", - }); - - expect(statusReactionController.setDone).toHaveBeenCalledTimes(1); - expect(statusReactionController.restoreInitial).not.toHaveBeenCalled(); - expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); - - await vi.advanceTimersByTimeAsync(249); - expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); - - await vi.advanceTimersByTimeAsync(1); - expect(reactionApi).toHaveBeenCalledWith(123, 456, []); - } finally { - vi.useRealTimers(); - } - }); - it("restores the initial Telegram status reaction after reply when removeAckAfterReply is disabled", async () => { const reactionApi = vi.fn(async () => true); const statusReactionController = createStatusReactionController(); @@ -173,47 +130,6 @@ describeTelegramDispatch("dispatchTelegramMessage status-reactions", () => { expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); }); - it("uses configured errorHoldMs to clear Telegram status reactions after an error fallback", async () => { - vi.useFakeTimers(); - const reactionApi = vi.fn(async () => true); - const statusReactionController = createStatusReactionController(); - dispatchReplyWithBufferedBlockDispatcher.mockRejectedValue(new Error("dispatcher exploded")); - deliverReplies.mockResolvedValue({ delivered: true }); - - try { - await dispatchWithContext({ - context: createContext({ - reactionApi: reactionApi as never, - removeAckAfterReply: true, - statusReactionController: statusReactionController as never, - }), - cfg: { - messages: { - statusReactions: { - timing: { - errorHoldMs: 320, - }, - }, - }, - }, - streamMode: "off", - }); - - expect(statusReactionController.setError).toHaveBeenCalledTimes(1); - expect(statusReactionController.setDone).not.toHaveBeenCalled(); - expect(statusReactionController.restoreInitial).not.toHaveBeenCalled(); - expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); - - await vi.advanceTimersByTimeAsync(319); - expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); - - await vi.advanceTimersByTimeAsync(1); - expect(reactionApi).toHaveBeenCalledWith(123, 456, []); - } finally { - vi.useRealTimers(); - } - }); - it("restores the initial Telegram status reaction after an error when no final reply is sent", async () => { vi.useFakeTimers(); const reactionApi = vi.fn(async () => true); @@ -228,15 +144,6 @@ describeTelegramDispatch("dispatchTelegramMessage status-reactions", () => { removeAckAfterReply: true, statusReactionController: statusReactionController as never, }), - cfg: { - messages: { - statusReactions: { - timing: { - errorHoldMs: 320, - }, - }, - }, - }, streamMode: "off", }); @@ -244,10 +151,7 @@ describeTelegramDispatch("dispatchTelegramMessage status-reactions", () => { expect(statusReactionController.restoreInitial).not.toHaveBeenCalled(); expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); - await vi.advanceTimersByTimeAsync(319); - expect(statusReactionController.restoreInitial).not.toHaveBeenCalled(); - - await vi.advanceTimersByTimeAsync(1); + await vi.runAllTimersAsync(); expect(statusReactionController.restoreInitial).toHaveBeenCalledTimes(1); expect(reactionApi).not.toHaveBeenCalledWith(123, 456, []); } finally { diff --git a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts index 99a6c117998a..320a5d5356e9 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts @@ -103,11 +103,6 @@ function getChannelPostHandler() { return getOnHandler("channel_post") as (ctx: Record) => Promise; } -function getChannelPostHandlerWithRuntimeTimings() { - createTelegramBot({ token: "tok" }); - return getOnHandler("channel_post") as (ctx: Record) => Promise; -} - function resolveFlushTimer(setTimeoutSpy: ReturnType) { return resolveFlushTimerForDelay(setTimeoutSpy, TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs); } @@ -292,45 +287,6 @@ describe("createTelegramBot channel_post media", () => { } }); - it("honors configured mediaGroupFlushMs for channel_post albums", async () => { - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - mediaGroupFlushMs: 75, - groups: { - "-100777111222": { - enabled: true, - requireMention: false, - }, - }, - }, - }, - }); - - const fetchSpy = createImageFetchSpy(); - const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); - try { - const handler = getChannelPostHandlerWithRuntimeTimings(); - await queueChannelPostAlbum(handler, { - caption: "configured album", - mediaGroupId: "channel-album-configured", - firstMessageId: 211, - secondMessageId: 212, - }); - expect(replySpy).not.toHaveBeenCalled(); - await flushChannelPostMediaGroupForDelay(setTimeoutSpy, 75); - await waitForMockCalls(replySpy, 1); - - await vi.waitFor(() => expect(replySpy).toHaveBeenCalledTimes(1)); - const payload = replyPayload() as { Body?: string }; - expect(payload.Body).toContain("configured album"); - } finally { - setTimeoutSpy.mockRestore(); - fetchSpy.mockRestore(); - } - }); - it("coalesces channel_post near-limit text fragments into one message", async () => { setOpenChannelPostConfig(); diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 3ebc6dace11c..630e2c798443 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -692,7 +692,6 @@ async function resolveTelegramTargets(params: { proxyUrl: account.config.proxy, apiRoot: account.config.apiRoot, network: account.config.network, - timeoutSeconds: account.config.timeoutSeconds, }); if (!id) { return { @@ -1067,18 +1066,14 @@ export const telegramPlugin = createChatChannelPlugin({ let botInfo: TelegramBotInfo | undefined; try { const probe = await withTelegramStartupProbeSlot(ctx.abortSignal, () => - resolveTelegramProbe()( - token, - resolveTelegramStartupProbeTimeoutMs(account.config.timeoutSeconds), - { - accountId: account.accountId, - proxyUrl: account.config.proxy, - network: account.config.network, - apiRoot: account.config.apiRoot, - includeWebhookInfo: false, - abortSignal: ctx.abortSignal, - }, - ), + resolveTelegramProbe()(token, resolveTelegramStartupProbeTimeoutMs(undefined), { + accountId: account.accountId, + proxyUrl: account.config.proxy, + network: account.config.network, + apiRoot: account.config.apiRoot, + includeWebhookInfo: false, + abortSignal: ctx.abortSignal, + }), ); const username = probe.ok ? probe.bot?.username?.trim() : null; if (username) { diff --git a/extensions/telegram/src/config-schema.test.ts b/extensions/telegram/src/config-schema.test.ts index a4d02fae7356..ef7a661f8c20 100644 --- a/extensions/telegram/src/config-schema.test.ts +++ b/extensions/telegram/src/config-schema.test.ts @@ -67,37 +67,6 @@ describe("telegram custom commands schema", () => { } }); - it("accepts pollingStallThresholdMs overrides per account", () => { - const res = TelegramConfigSchema.safeParse({ - pollingStallThresholdMs: 120_000, - accounts: { ops: { pollingStallThresholdMs: 180_000 } }, - }); - - expect(res.success).toBe(true); - if (res.success) { - expect(res.data.pollingStallThresholdMs).toBe(120_000); - expect(res.data.accounts?.ops?.pollingStallThresholdMs).toBe(180_000); - } - }); - - it("accepts mediaGroupFlushMs overrides per account", () => { - const res = TelegramConfigSchema.safeParse({ - mediaGroupFlushMs: 750, - accounts: { ops: { mediaGroupFlushMs: 1500 } }, - }); - - expect(res.success).toBe(true); - if (res.success) { - expect(res.data.mediaGroupFlushMs).toBe(750); - expect(res.data.accounts?.ops?.mediaGroupFlushMs).toBe(1500); - } - }); - - it("rejects mediaGroupFlushMs outside the supported flush bounds", () => { - expectTelegramConfigIssue({ mediaGroupFlushMs: 9 }, "mediaGroupFlushMs"); - expectTelegramConfigIssue({ mediaGroupFlushMs: 60_001 }, "mediaGroupFlushMs"); - }); - it("accepts Telegram progress commentary config", () => { expectTelegramConfigValid({ streaming: { @@ -132,11 +101,6 @@ describe("telegram custom commands schema", () => { ); }); - it("rejects pollingStallThresholdMs outside the watchdog bounds", () => { - expectTelegramConfigIssue({ pollingStallThresholdMs: 29_999 }, "pollingStallThresholdMs"); - expectTelegramConfigIssue({ pollingStallThresholdMs: 600_001 }, "pollingStallThresholdMs"); - }); - it("accepts textChunkLimit", () => { const res = TelegramConfigSchema.safeParse({ enabled: true, diff --git a/extensions/telegram/src/config-ui-hints.ts b/extensions/telegram/src/config-ui-hints.ts index 2b7b8a5dd39a..50f3b6ccc9d3 100644 --- a/extensions/telegram/src/config-ui-hints.ts +++ b/extensions/telegram/src/config-ui-hints.ts @@ -73,7 +73,6 @@ export const telegramChannelConfigUiHints = { ...createChannelConfigUiHints({ channelLabel: "Telegram", progress: { includeCommentary: true, commentaryOrder: "after-command" }, - retry: true, }), "network.autoSelectFamily": { label: "Telegram autoSelectFamily", @@ -83,18 +82,6 @@ export const telegramChannelConfigUiHints = { 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.", }, - timeoutSeconds: { - label: "Telegram API Timeout (seconds)", - help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).", - }, - mediaGroupFlushMs: { - label: "Telegram Media Group Flush (ms)", - help: "Milliseconds to buffer Telegram albums/media groups before dispatching them as one inbound message. Default: 500.", - }, - pollingStallThresholdMs: { - label: "Telegram Polling Stall Threshold (ms)", - help: "Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000.", - }, silentErrorReplies: { label: "Telegram Silent Error Replies", help: "When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.", diff --git a/extensions/telegram/src/doctor-contract.ts b/extensions/telegram/src/doctor-contract.ts index 7d75be81efb9..d82505ea5b2c 100644 --- a/extensions/telegram/src/doctor-contract.ts +++ b/extensions/telegram/src/doctor-contract.ts @@ -14,6 +14,42 @@ const streamingAliasMigration = defineChannelAliasMigration({ streaming: { defaultMode: "partial", includePreviewChunk: true }, }); +const RETIRED_TUNING_KEYS = new Set([ + "timeoutSeconds", + "mediaGroupFlushMs", + "pollingStallThresholdMs", + "retry", + "errorCooldownMs", +]); + +function stripRetiredTuningKnobs(value: unknown): { value: unknown; changed: boolean } { + if (Array.isArray(value)) { + let changed = false; + const next = value.map((item) => { + const stripped = stripRetiredTuningKnobs(item); + changed = changed || stripped.changed; + return stripped.value; + }); + return { value: changed ? next : value, changed }; + } + const record = asObjectRecord(value); + if (!record) { + return { value, changed: false }; + } + let changed = false; + const next: Record = {}; + for (const [key, child] of Object.entries(record)) { + if (RETIRED_TUNING_KEYS.has(key)) { + changed = true; + continue; + } + const stripped = stripRetiredTuningKnobs(child); + changed = changed || stripped.changed; + next[key] = stripped.value; + } + return { value: changed ? next : value, changed }; +} + function hasRetiredTelegramDmConfig(value: unknown): boolean { const entry = asObjectRecord(value); if (!entry) { @@ -257,6 +293,12 @@ export function normalizeCompatibilityConfig({ let updated = rawEntry; let changed = aliases.config !== cfg; + const tuningKnobs = stripRetiredTuningKnobs(updated); + if (tuningKnobs.changed) { + updated = tuningKnobs.value as Record; + changes.push("Removed retired Telegram tuning knobs."); + changed = true; + } const rootGroupHistoryContextMode = updated.includeGroupHistoryContext; const rootGroupHistoryLimitBeforeMigration = typeof updated.historyLimit === "number" diff --git a/extensions/telegram/src/doctor.test.ts b/extensions/telegram/src/doctor.test.ts index 5ac5c1242900..db9f5fa76406 100644 --- a/extensions/telegram/src/doctor.test.ts +++ b/extensions/telegram/src/doctor.test.ts @@ -85,6 +85,41 @@ describe("telegram doctor", () => { lookupTelegramChatIdMock.mockReset(); }); + it("strips retired tuning knobs at root, account, group, and topic scope", () => { + const normalize = telegramDoctor.normalizeCompatibilityConfig; + if (!normalize) { + throw new Error("expected telegram compatibility normalizer"); + } + const result = normalize({ + cfg: { + channels: { + telegram: { + timeoutSeconds: 1, + mediaGroupFlushMs: 2, + pollingStallThresholdMs: 3, + retry: { attempts: 4 }, + errorCooldownMs: 5, + accounts: { + work: { timeoutSeconds: 6, retry: { attempts: 7 } }, + }, + groups: { + "-100": { + errorCooldownMs: 8, + topics: { "1": { errorCooldownMs: 9, requireMention: true } }, + }, + }, + }, + }, + } as never, + }); + + expect(result.config.channels?.telegram).toEqual({ + accounts: { work: {} }, + groups: { "-100": { topics: { "1": { requireMention: true } } } }, + }); + expect(result.changes).toContain("Removed retired Telegram tuning knobs."); + }); + it("normalizes legacy telegram streaming aliases into the nested streaming shape", () => { const normalize = telegramDoctor.normalizeCompatibilityConfig; if (!normalize) { diff --git a/extensions/telegram/src/doctor.ts b/extensions/telegram/src/doctor.ts index cf95b7c4d4ab..d108807534c7 100644 --- a/extensions/telegram/src/doctor.ts +++ b/extensions/telegram/src/doctor.ts @@ -460,7 +460,6 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi chatId: username, network: account.config.network, signal: undefined, - timeoutSeconds: account.config.timeoutSeconds, }); if (id) { return id; diff --git a/extensions/telegram/src/error-policy.test.ts b/extensions/telegram/src/error-policy.test.ts index acf1588cf6c0..1db9fda258a8 100644 --- a/extensions/telegram/src/error-policy.test.ts +++ b/extensions/telegram/src/error-policy.test.ts @@ -21,16 +21,16 @@ describe("telegram error policy", () => { vi.useRealTimers(); }); - it("resolves policy and cooldown from the most specific config", () => { + it("resolves policy from the most specific config", () => { expect( resolveTelegramErrorPolicy({ - accountConfig: { errorPolicy: "once", errorCooldownMs: 1000 }, - groupConfig: { errorCooldownMs: 2000 }, + accountConfig: { errorPolicy: "once" }, + groupConfig: {}, topicConfig: { errorPolicy: "silent" }, }), ).toEqual({ policy: "silent", - cooldownMs: 2000, + cooldownMs: 14_400_000, }); }); diff --git a/extensions/telegram/src/error-policy.ts b/extensions/telegram/src/error-policy.ts index 48cf214003c1..e3ed066e48b4 100644 --- a/extensions/telegram/src/error-policy.ts +++ b/extensions/telegram/src/error-policy.ts @@ -44,18 +44,14 @@ export function resolveTelegramErrorPolicy(params: { params.topicConfig, ]; let policy: TelegramErrorPolicy = "always"; - let cooldownMs = DEFAULT_ERROR_COOLDOWN_MS; for (const config of configs) { if (config?.errorPolicy) { policy = config.errorPolicy; } - if (typeof config?.errorCooldownMs === "number") { - cooldownMs = config.errorCooldownMs; - } } - return { policy, cooldownMs }; + return { policy, cooldownMs: DEFAULT_ERROR_COOLDOWN_MS }; } export function buildTelegramErrorScopeKey(params: { diff --git a/extensions/telegram/src/monitor.test.ts b/extensions/telegram/src/monitor.test.ts index a17ac80ec636..27a39663b79c 100644 --- a/extensions/telegram/src/monitor.test.ts +++ b/extensions/telegram/src/monitor.test.ts @@ -982,33 +982,6 @@ describe("monitorTelegramProvider (grammY)", () => { vi.useRealTimers(); }); - it("uses configured Telegram polling stall threshold", async () => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - const abort = new AbortController(); - const firstCycle = mockRunOnceWithStalledPollingRunner(); - const secondCycle = mockRunOnceAndAbort(abort); - - const monitor = monitorTelegramProvider( - withLegacyPolling({ - token: "tok", - abortSignal: abort.signal, - config: { - agents: { defaults: { maxConcurrent: 2 } }, - channels: { telegram: { pollingStallThresholdMs: 30_000 } }, - }, - }), - ); - await firstCycle.waitForRunStart(); - - vi.advanceTimersByTime(60_000); - await secondCycle.waitForRunStart(); - await monitor; - - expect(firstCycle.stop.mock.calls.length).toBeGreaterThanOrEqual(1); - expectRecoverableRetryState(2); - vi.useRealTimers(); - }); - it("does not call getUpdates for offset confirmation (avoids 409 conflicts)", async () => { const { order } = await runMonitorAndCaptureStartupOrder({ persistedOffset: 549076203, diff --git a/extensions/telegram/src/monitor.ts b/extensions/telegram/src/monitor.ts index 1c279615d8a4..abc5bb173b10 100644 --- a/extensions/telegram/src/monitor.ts +++ b/extensions/telegram/src/monitor.ts @@ -285,12 +285,10 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { log, telegramTransport, createTelegramTransport: createTelegramTransportForPolling, - stallThresholdMs: account.config.pollingStallThresholdMs, setStatus: opts.setStatus, isolatedIngress: { enabled: opts.isolatedIngress?.enabled ?? true, apiRoot: account.config.apiRoot, - timeoutSeconds: account.config.timeoutSeconds, proxy: account.config.proxy, network: account.config.network, }, diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index fbbe0aa30acf..2d323a203561 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -451,11 +451,7 @@ function setCachedTelegramClientOptions( function resolveTelegramClientOptions( account: ResolvedTelegramAccount, ): ResolvedTelegramClientOptions { - const timeoutSeconds = - typeof account.config.timeoutSeconds === "number" && - Number.isFinite(account.config.timeoutSeconds) - ? Math.max(1, Math.floor(account.config.timeoutSeconds)) - : undefined; + const timeoutSeconds = undefined; const cacheEnabled = shouldUseTelegramClientOptionsCache(); const cacheKey = cacheEnabled @@ -726,7 +722,6 @@ function createTelegramRequestWithDiag(params: { }): TelegramRequestWithDiag { const request = createChannelApiRetryRunner({ retry: params.retry, - configRetry: params.account.config.retry, verbose: params.verbose, ...(params.retryAfterMaxDelayMs !== undefined ? { retryAfterMaxDelayMs: params.retryAfterMaxDelayMs } diff --git a/extensions/whatsapp/src/auto-reply/monitor.ts b/extensions/whatsapp/src/auto-reply/monitor.ts index 34e3f8c53ba4..1314104b56c0 100644 --- a/extensions/whatsapp/src/auto-reply/monitor.ts +++ b/extensions/whatsapp/src/auto-reply/monitor.ts @@ -181,7 +181,7 @@ export async function monitorWebChannel( const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account); const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds); const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect); - const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming); + const socketTiming = resolveWhatsAppSocketTiming(tuning.socketTiming); const baseMentionConfig = buildMentionConfig(cfg); const groupHistoryLimit = account.historyLimit ?? diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts index fe6c292629c1..a178ff5f9d52 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts @@ -535,10 +535,7 @@ export function createWhatsAppReplyPlan(params: { const conversationId = admission.conversation.id; const conversationKind = admission.conversation.kind; const statusReactionController = params.statusReactionController ?? null; - const statusReactionTiming = { - ...DEFAULT_TIMING, - ...params.cfg.messages?.statusReactions?.timing, - }; + const statusReactionTiming = DEFAULT_TIMING; const removeAckAfterReply = params.cfg.messages?.removeAckAfterReply ?? false; const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp"); const chunkMode = resolveChunkMode(params.cfg, "whatsapp", params.route.accountId); diff --git a/extensions/whatsapp/src/auto-reply/monitor/status-reaction.test.ts b/extensions/whatsapp/src/auto-reply/monitor/status-reaction.test.ts index 3ba228745479..2902a9b1f49d 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/status-reaction.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/status-reaction.test.ts @@ -50,13 +50,6 @@ describe("createWhatsAppStatusReactionController", () => { messages: { statusReactions: { enabled: true, - timing: { - debounceMs: 1_000_000, - stallSoftMs: 1_000_000, - stallHardMs: 1_000_000, - doneHoldMs: 0, - errorHoldMs: 0, - }, }, }, channels: { @@ -136,13 +129,6 @@ describe("createWhatsAppStatusReactionController", () => { messages: { statusReactions: { enabled: true, - timing: { - debounceMs: 1_000_000, - stallSoftMs: 1_000_000, - stallHardMs: 1_000_000, - doneHoldMs: 0, - errorHoldMs: 0, - }, }, }, channels: { @@ -186,13 +172,6 @@ describe("createWhatsAppStatusReactionController", () => { messages: { statusReactions: { enabled: true, - timing: { - debounceMs: 1_000_000, - stallSoftMs: 1_000_000, - stallHardMs: 1_000_000, - doneHoldMs: 0, - errorHoldMs: 0, - }, }, }, channels: { diff --git a/extensions/whatsapp/src/auto-reply/monitor/status-reaction.ts b/extensions/whatsapp/src/auto-reply/monitor/status-reaction.ts index c9eb80b2c3c6..120a0c4797e1 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/status-reaction.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/status-reaction.ts @@ -107,7 +107,6 @@ export async function createWhatsAppStatusReactionController( }, initialEmoji: ackEmoji, emojis: statusReactionsConfig.emojis, - timing: statusReactionsConfig.timing, onError: (err) => { logVerbose(`WhatsApp status-reaction error for chat ${chatId}/${msgId}: ${String(err)}`); }, diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index c90b4acaf326..bb444c95a0f9 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -225,7 +225,6 @@ vi.mock("./session.js", async () => { let monitorWebInbox: typeof import("./inbound.js").monitorWebInbox; let resetWebInboundDedupe: typeof import("./inbound.js").resetWebInboundDedupe; let createWaSocket: typeof import("./session.js").createWaSocket; -let waitForWaConnection: typeof import("./session.js").waitForWaConnection; async function waitForMessage(onMessage: ReturnType) { await vi.waitFor(() => expect(onMessage).toHaveBeenCalledTimes(1), { @@ -268,7 +267,7 @@ describe("web inbound media saves with extension", () => { beforeAll(async () => { await fs.rm(HOME, { recursive: true, force: true }); ({ monitorWebInbox, resetWebInboundDedupe } = await import("./inbound.js")); - ({ createWaSocket, waitForWaConnection } = await import("./session.js")); + ({ createWaSocket } = await import("./session.js")); }); afterAll(async () => { @@ -280,30 +279,6 @@ describe("web inbound media saves with extension", () => { } }); - it("closes the socket when connection wait fails before inbox attach", async () => { - const error = new Error("connection timeout"); - vi.mocked(waitForWaConnection).mockRejectedValueOnce(error); - - await expect( - monitorWebInbox({ - cfg: { - channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { responsePrefix: undefined }, - web: { whatsapp: { connectTimeoutMs: 12_345 } }, - } as never, - verbose: false, - onMessage: vi.fn(), - accountId: "default", - authDir: path.join(HOME, "wa-auth"), - }), - ).rejects.toThrow("connection timeout"); - - expect(vi.mocked(waitForWaConnection)).toHaveBeenCalledWith(currentMockSocket, { - timeoutMs: 12_345, - }); - expect(currentMockSocket?.ws.close).toHaveBeenCalledOnce(); - }); - it("stores image extension and keeps document filename", async () => { const onMessage = vi.fn(); const listener = await monitorWebInbox({ diff --git a/extensions/whatsapp/src/inbound/monitor.ts b/extensions/whatsapp/src/inbound/monitor.ts index 9407e7ce0bf5..08f4474c74c4 100644 --- a/extensions/whatsapp/src/inbound/monitor.ts +++ b/extensions/whatsapp/src/inbound/monitor.ts @@ -1933,7 +1933,7 @@ export async function attachWebInboxToSocket( } export async function monitorWebInbox(options: MonitorWebInboxOptions) { - const socketTiming = options.socketTiming ?? resolveWhatsAppSocketTiming(options.cfg); + const socketTiming = options.socketTiming ?? resolveWhatsAppSocketTiming(); const recentMessageKeys: WhatsAppBaileysMessageCache = options.recentMessageKeys ?? new Map(); const baileysGroupMetaCache: WhatsAppBaileysGroupMetadataCache = options.baileysGroupMetaCache ?? new Map(); diff --git a/extensions/whatsapp/src/login-qr.ts b/extensions/whatsapp/src/login-qr.ts index 79e50b328ea7..959da2e87126 100644 --- a/extensions/whatsapp/src/login-qr.ts +++ b/extensions/whatsapp/src/login-qr.ts @@ -322,7 +322,7 @@ export async function startWebLoginWithQr( const runtime = opts.runtime ?? defaultRuntime; const cfg = getRuntimeConfig(); const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId }); - const socketTiming = resolveWhatsAppSocketTiming(cfg); + const socketTiming = resolveWhatsAppSocketTiming(); const authState = await readWebAuthExistsForDecision(account.authDir); if (authState.outcome === "unstable") { return { diff --git a/extensions/whatsapp/src/login.ts b/extensions/whatsapp/src/login.ts index cfaa00465ceb..f256d2b37030 100644 --- a/extensions/whatsapp/src/login.ts +++ b/extensions/whatsapp/src/login.ts @@ -25,7 +25,7 @@ export async function loginWeb( ) { const cfg = getRuntimeConfig(); const account = resolveWhatsAppAccount({ cfg, accountId }); - const socketTiming = resolveWhatsAppSocketTiming(cfg); + const socketTiming = resolveWhatsAppSocketTiming(); const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir, { beforeCredentialPersistence: options?.beforeCredentialPersistence, }); diff --git a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts index 64053ee15a0f..11fc7d70d425 100644 --- a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts +++ b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test-support.ts @@ -19,7 +19,6 @@ import { type InboxMonitorOptions, buildNotifyMessageUpsert, DEFAULT_ACCOUNT_ID, - DEFAULT_WEB_INBOX_CONFIG, getAuthDir, getSock, installWebMonitorInboxUnitTestHooks, @@ -1223,41 +1222,6 @@ describe("web monitor inbox", () => { await listener.close(); }); - it("lets configured slow socket sends complete beyond thirty seconds", async () => { - const onMessage = vi.fn(async () => undefined); - const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage, { - cfg: { - ...DEFAULT_WEB_INBOX_CONFIG, - web: { whatsapp: { defaultQueryTimeoutMs: 45_000 } }, - }, - }); - vi.useFakeTimers(); - try { - sock.sendMessage.mockImplementationOnce( - async () => - await new Promise((resolve) => { - setTimeout(() => resolve({ key: { id: "slow-success" } }), 40_000); - }), - ); - - let settled = false; - const sendPromise = listener.sendMessage("+1555", "hello").finally(() => { - settled = true; - }); - - await vi.advanceTimersByTimeAsync(30_000); - expect(settled).toBe(false); - - await vi.advanceTimersByTimeAsync(10_000); - await expect(sendPromise).resolves.toMatchObject({ messageId: "slow-success" }); - expect(vi.getTimerCount()).toBe(0); - expect(sock.sendMessage).toHaveBeenCalledTimes(1); - } finally { - vi.useRealTimers(); - await listener.close(); - } - }); - it("rejects direct sends before Baileys sendMessage when reachout timelock is active", async () => { const onMessage = vi.fn(async () => undefined); const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage); diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 16a2765de8c1..d0d4c1186baa 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -16,7 +16,7 @@ type AnyMockFn = any; export const DEFAULT_ACCOUNT_ID = "default"; -export const DEFAULT_WEB_INBOX_CONFIG = { +const DEFAULT_WEB_INBOX_CONFIG = { channels: { whatsapp: { // Allow all in tests by default. diff --git a/extensions/whatsapp/src/reconnect.ts b/extensions/whatsapp/src/reconnect.ts index 7bfc129b5dd3..0c273458dc60 100644 --- a/extensions/whatsapp/src/reconnect.ts +++ b/extensions/whatsapp/src/reconnect.ts @@ -22,7 +22,8 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = { }; export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number { - const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; + void cfg; + const candidate = overrideSeconds; if (typeof candidate === "number" && candidate > 0) { return candidate; } @@ -33,11 +34,10 @@ export function resolveReconnectPolicy( cfg: OpenClawConfig, overrides?: Partial, ): ReconnectPolicy { - const reconnectOverrides = cfg.web?.reconnect ?? {}; + void cfg; const overrideConfig = overrides ?? {}; const merged = { ...DEFAULT_RECONNECT_POLICY, - ...reconnectOverrides, ...overrideConfig, } as ReconnectPolicy; diff --git a/extensions/whatsapp/src/socket-timing.test.ts b/extensions/whatsapp/src/socket-timing.test.ts index 387671377530..b0876802039b 100644 --- a/extensions/whatsapp/src/socket-timing.test.ts +++ b/extensions/whatsapp/src/socket-timing.test.ts @@ -13,68 +13,28 @@ import { describe("resolveWhatsAppSocketTiming", () => { it("uses OpenClaw's explicit WhatsApp Web socket defaults", () => { - expect(resolveWhatsAppSocketTiming({})).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING); + expect(resolveWhatsAppSocketTiming()).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING); }); - it("reads Baileys timing values from web.whatsapp config", () => { + it("lets call-site overrides take precedence over defaults", () => { expect( resolveWhatsAppSocketTiming({ - web: { - whatsapp: { - keepAliveIntervalMs: 10_000, - connectTimeoutMs: 90_000, - defaultQueryTimeoutMs: 120_000, - }, - }, + keepAliveIntervalMs: 20_000, }), - ).toEqual({ - keepAliveIntervalMs: 10_000, - connectTimeoutMs: 90_000, - defaultQueryTimeoutMs: 120_000, - }); - }); - - it("lets call-site overrides take precedence over config", () => { - expect( - resolveWhatsAppSocketTiming( - { - web: { - whatsapp: { - keepAliveIntervalMs: 10_000, - connectTimeoutMs: 90_000, - defaultQueryTimeoutMs: 120_000, - }, - }, - }, - { - keepAliveIntervalMs: 20_000, - }, - ), ).toEqual({ keepAliveIntervalMs: 20_000, - connectTimeoutMs: 90_000, - defaultQueryTimeoutMs: 120_000, + connectTimeoutMs: DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs, + defaultQueryTimeoutMs: DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs, }); }); it("rejects invalid numeric timing values", () => { expect( - resolveWhatsAppSocketTiming( - { - web: { - whatsapp: { - keepAliveIntervalMs: 0, - connectTimeoutMs: Number.NaN, - defaultQueryTimeoutMs: 1.5, - }, - }, - }, - { - keepAliveIntervalMs: -1, - connectTimeoutMs: Number.POSITIVE_INFINITY, - defaultQueryTimeoutMs: Number.MAX_SAFE_INTEGER + 1, - }, - ), + resolveWhatsAppSocketTiming({ + keepAliveIntervalMs: -1, + connectTimeoutMs: Number.POSITIVE_INFINITY, + defaultQueryTimeoutMs: Number.MAX_SAFE_INTEGER + 1, + }), ).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING); }); diff --git a/extensions/whatsapp/src/socket-timing.ts b/extensions/whatsapp/src/socket-timing.ts index a06ad47494bb..e8d6dae6c186 100644 --- a/extensions/whatsapp/src/socket-timing.ts +++ b/extensions/whatsapp/src/socket-timing.ts @@ -5,7 +5,6 @@ import type { WAMessage, WAPresence, } from "baileys"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { parseStrictPositiveInteger, resolveTimerTimeoutMs, @@ -51,22 +50,17 @@ class WhatsAppSocketOperationTimeoutError extends Error { } export function resolveWhatsAppSocketTiming( - cfg: OpenClawConfig, overrides?: WhatsAppSocketTimingOptions, ): Required { - const configured = cfg.web?.whatsapp; return { keepAliveIntervalMs: parseStrictPositiveInteger(overrides?.keepAliveIntervalMs) ?? - parseStrictPositiveInteger(configured?.keepAliveIntervalMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs, connectTimeoutMs: parseStrictPositiveInteger(overrides?.connectTimeoutMs) ?? - parseStrictPositiveInteger(configured?.connectTimeoutMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs, defaultQueryTimeoutMs: parseStrictPositiveInteger(overrides?.defaultQueryTimeoutMs) ?? - parseStrictPositiveInteger(configured?.defaultQueryTimeoutMs) ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs, }; } diff --git a/scripts/e2e/lib/config-reload/mutate-metadata.mjs b/scripts/e2e/lib/config-reload/mutate-metadata.mjs index 4f39855554a1..409d1dfa6161 100644 --- a/scripts/e2e/lib/config-reload/mutate-metadata.mjs +++ b/scripts/e2e/lib/config-reload/mutate-metadata.mjs @@ -1,7 +1,7 @@ -// Mutates plugin metadata fixtures for config reload E2E scenarios. +// Mutates display metadata for config reload E2E scenarios. import fs from "node:fs"; const configPath = process.env.OPENCLAW_CONFIG_PATH; const config = JSON.parse(fs.readFileSync(configPath, "utf8")); -config.gateway.channelHealthCheckMinutes = 2; +config.ui.seamColor = "#ff6600"; fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); diff --git a/scripts/e2e/lib/fixtures/config.mjs b/scripts/e2e/lib/fixtures/config.mjs index 5f28779bd0bf..a7ed0194bf9f 100644 --- a/scripts/e2e/lib/fixtures/config.mjs +++ b/scripts/e2e/lib/fixtures/config.mjs @@ -15,10 +15,10 @@ function writeConfig(kind) { mode: "token", token: { source: "env", provider: "default", id: "GATEWAY_AUTH_TOKEN_REF" }, }, - channelHealthCheckMinutes: 1, controlUi: { enabled: false }, - reload: { mode: "hybrid", debounceMs: 0 }, + reload: { mode: "hybrid" }, }, + ui: { seamColor: "#ff4500" }, } : kind === "browser-cdp" ? { diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 1b6f3dfa9ab6..0af22370cfad 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -290,7 +290,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // channel-outbound and its two compatibility barrels. // Net +1: public session catalog locator types after the protocol cleanup harvest. // +2: lifecycle-owned prepared model catalog sync and async readers. - 8192, + // Harvest: retired tuning-knob config types -10. + 8182, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/acp/control-plane/manager.core.ts b/src/acp/control-plane/manager.core.ts index 64dff4ee98d4..a235262bead0 100644 --- a/src/acp/control-plane/manager.core.ts +++ b/src/acp/control-plane/manager.core.ts @@ -61,6 +61,8 @@ import { } from "./runtime-options.js"; import { SessionActorQueue } from "./session-actor-queue.js"; +const DEFAULT_ACP_MAX_CONCURRENT_SESSIONS = Number.POSITIVE_INFINITY; + /** Coordinates ACP session metadata, runtime handles, per-session queues, and turn execution. */ export class AcpSessionManager { private readonly actorQueue = new SessionActorQueue(); @@ -114,12 +116,12 @@ export class AcpSessionManager { }; } - getObservabilitySnapshot(cfg: OpenClawConfig): AcpManagerObservabilitySnapshot { + getObservabilitySnapshot(): AcpManagerObservabilitySnapshot { const completedTurns = this.turnLatencyStats.completed + this.turnLatencyStats.failed; const averageLatencyMs = completedTurns > 0 ? Math.round(this.turnLatencyStats.totalMs / completedTurns) : 0; return { - runtimeCache: this.runtimeHandles.getObservabilitySnapshot(cfg), + runtimeCache: this.runtimeHandles.getObservabilitySnapshot(), turns: { active: this.activeTurnBySession.size, queueDepth: this.actorQueue.getTotalPendingCount(), @@ -159,7 +161,7 @@ export class AcpSessionManager { if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(input.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor(sessionKey, async () => { return await runManagerInitializeSession({ input, @@ -182,7 +184,7 @@ export class AcpSessionManager { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } this.throwIfAborted(params.signal); - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor( sessionKey, async () => @@ -211,7 +213,7 @@ export class AcpSessionManager { } const runtimeMode = validateRuntimeModeInput(params.runtimeMode); - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor(sessionKey, async () => { return await runSetManagerSessionRuntimeMode({ cfg: params.cfg, @@ -236,7 +238,7 @@ export class AcpSessionManager { const key = normalizedOption.key; const value = normalizedOption.value; - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor(sessionKey, async () => { return await runSetManagerSessionConfigOption({ cfg: params.cfg, @@ -259,7 +261,7 @@ export class AcpSessionManager { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor(sessionKey, async () => { return await runUpdateManagerSessionRuntimeOptions({ cfg: params.cfg, @@ -278,7 +280,7 @@ export class AcpSessionManager { if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor(sessionKey, async () => { return await runResetManagerSessionRuntimeOptions({ cfg: params.cfg, @@ -296,7 +298,7 @@ export class AcpSessionManager { if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(input.cfg); + await this.evictIdleRuntimeHandles(); await this.withSessionActor( sessionKey, async () => @@ -327,7 +329,7 @@ export class AcpSessionManager { if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(params.cfg); + await this.evictIdleRuntimeHandles(); await runManagerCancelSession({ cfg: params.cfg, sessionKey, @@ -348,7 +350,7 @@ export class AcpSessionManager { if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } - await this.evictIdleRuntimeHandles(input.cfg); + await this.evictIdleRuntimeHandles(); return await this.withSessionActor( sessionKey, async () => @@ -390,11 +392,7 @@ export class AcpSessionManager { } private enforceConcurrentSessionLimit(params: { cfg: OpenClawConfig; sessionKey: string }): void { - const configuredLimit = params.cfg.acp?.maxConcurrentSessions; - if (typeof configuredLimit !== "number" || !Number.isFinite(configuredLimit)) { - return; - } - const limit = Math.max(1, Math.floor(configuredLimit)); + const limit = DEFAULT_ACP_MAX_CONCURRENT_SESSIONS; if (this.runtimeHandles.has(params.sessionKey)) { return; } @@ -432,9 +430,8 @@ export class AcpSessionManager { return await resolveManagerRuntimeCapabilities(params); } - private async evictIdleRuntimeHandles(cfg: OpenClawConfig): Promise { + private async evictIdleRuntimeHandles(): Promise { await this.runtimeHandles.evictIdle({ - cfg, actorQueue: this.actorQueue, activeTurnBySession: this.activeTurnBySession, }); diff --git a/src/acp/control-plane/manager.initialize-session.test.ts b/src/acp/control-plane/manager.initialize-session.test.ts index ffa0a417f880..a10aff25ce64 100644 --- a/src/acp/control-plane/manager.initialize-session.test.ts +++ b/src/acp/control-plane/manager.initialize-session.test.ts @@ -5,59 +5,16 @@ import { baseCfg, createRuntime, expectRecordFields, - expectRejectedRecord, extractRuntimeOptionsFromUpserts, hoisted, installAcpSessionManagerTestLifecycle, mockCallArg, readySessionMeta, - type OpenClawConfig, } from "./manager.test-helpers.js"; describe("AcpSessionManager initializeSession", () => { installAcpSessionManagerTestLifecycle(); - it("enforces acp.maxConcurrentSessions during initializeSession", async () => { - const runtimeState = createRuntime(); - hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ - id: "acpx", - runtime: runtimeState.runtime, - }); - hoisted.upsertAcpSessionMetaMock.mockResolvedValue({ - sessionKey: "agent:codex:acp:session-a", - storeSessionKey: "agent:codex:acp:session-a", - acp: readySessionMeta(), - }); - const limitedCfg = { - acp: { - ...baseCfg.acp, - maxConcurrentSessions: 1, - }, - } as OpenClawConfig; - - const manager = new AcpSessionManager(); - await manager.initializeSession({ - cfg: limitedCfg, - sessionKey: "agent:codex:acp:session-a", - agent: "codex", - mode: "persistent", - }); - - await expectRejectedRecord( - manager.initializeSession({ - cfg: limitedCfg, - sessionKey: "agent:codex:acp:session-b", - agent: "codex", - mode: "persistent", - }), - { - code: "ACP_SESSION_INIT_FAILED", - message: "ACP max concurrent sessions reached (1/1).", - }, - ); - expect(runtimeState.ensureSession).toHaveBeenCalledTimes(1); - }); - it("persists runtime options provided during initializeSession", async () => { const runtimeState = createRuntime(); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ diff --git a/src/acp/control-plane/manager.runtime-handle-cache.ts b/src/acp/control-plane/manager.runtime-handle-cache.ts index dc7a6c69ce3e..455295ade72e 100644 --- a/src/acp/control-plane/manager.runtime-handle-cache.ts +++ b/src/acp/control-plane/manager.runtime-handle-cache.ts @@ -9,10 +9,9 @@ import type { AcpRuntimeStatus, } from "@openclaw/acp-core/runtime/types"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; import type { ActiveTurnState, SessionAcpMeta } from "./manager.types.js"; -import { normalizeActorKey, resolveRuntimeIdleTtlMs } from "./manager.utils.js"; +import { DEFAULT_ACP_RUNTIME_IDLE_TTL_MS, normalizeActorKey } from "./manager.utils.js"; import { RuntimeCache, type CachedRuntimeState } from "./runtime-cache.js"; import { normalizeText } from "./runtime-options.js"; import type { SessionActorQueue } from "./session-actor-queue.js"; @@ -44,10 +43,10 @@ export class ManagerRuntimeHandleCache { } /** Returns cache counters used by ACP manager observability snapshots. */ - getObservabilitySnapshot(cfg: OpenClawConfig) { + getObservabilitySnapshot() { return { activeSessions: this.runtimeCache.size(), - idleTtlMs: resolveRuntimeIdleTtlMs(cfg), + idleTtlMs: DEFAULT_ACP_RUNTIME_IDLE_TTL_MS, evictedTotal: this.evictedRuntimeCount, ...(this.lastEvictedAt ? { lastEvictedAt: this.lastEvictedAt } : {}), }; @@ -84,11 +83,10 @@ export class ManagerRuntimeHandleCache { /** Closes handles that exceeded the configured idle TTL without racing active turns. */ async evictIdle(params: { - cfg: OpenClawConfig; actorQueue: SessionActorQueue; activeTurnBySession: Map; }): Promise { - const idleTtlMs = resolveRuntimeIdleTtlMs(params.cfg); + const idleTtlMs = DEFAULT_ACP_RUNTIME_IDLE_TTL_MS; if (idleTtlMs <= 0 || this.runtimeCache.size() === 0) { return; } diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index 08d4295e6222..edca144c924d 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -678,7 +678,7 @@ describe("AcpSessionManager", () => { await first; await vi.waitFor( () => { - expect(manager.getObservabilitySnapshot(baseCfg).turns.queueDepth).toBe(0); + expect(manager.getObservabilitySnapshot().turns.queueDepth).toBe(0); }, { interval: 1 }, ); @@ -768,7 +768,7 @@ describe("AcpSessionManager", () => { reason: "turn-timeout", }); expect(runtimeState.close).not.toHaveBeenCalled(); - const snapshot = manager.getObservabilitySnapshot(cfg); + const snapshot = manager.getObservabilitySnapshot(); expect(snapshot.runtimeCache.activeSessions).toBe(1); expectRecordFields(snapshot.turns, { active: 0, @@ -818,94 +818,6 @@ describe("AcpSessionManager", () => { } }); - it("keeps timed-out runtime handles counted until timeout cleanup finishes", async () => { - vi.useFakeTimers(); - try { - const runtimeState = createRuntime(); - runtimeState.cancel.mockImplementation(() => new Promise(() => {})); - hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ - id: "acpx", - runtime: runtimeState.runtime, - }); - hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { - const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? ""; - return { - sessionKey, - storeSessionKey: sessionKey, - acp: { - ...readySessionMeta(), - runtimeSessionName: `runtime:${sessionKey}`, - }, - }; - }); - - let firstTurnStarted = false; - runtimeState.runTurn.mockImplementation(async function* (input: { requestId: string }) { - if (input.requestId === "r1") { - firstTurnStarted = true; - await new Promise(() => {}); - } - yield { type: "done" as const }; - }); - - const manager = new AcpSessionManager(); - const cfg = { - ...baseCfg, - acp: { - ...baseCfg.acp, - maxConcurrentSessions: 1, - }, - agents: { - defaults: { - timeoutSeconds: 1, - }, - }, - } as OpenClawConfig; - - const first = manager.runTurn({ - provenance: "system", - cfg, - sessionKey: "agent:codex:acp:session-a", - text: "first", - mode: "prompt", - requestId: "r1", - }); - void first.catch(() => undefined); - await vi.waitFor( - () => { - expect(firstTurnStarted).toBe(true); - }, - { interval: 1 }, - ); - - await vi.advanceTimersByTimeAsync(4_500); - - await expectRejectedRecord(first, { - code: "ACP_TURN_FAILED", - message: "ACP turn timed out after 1s.", - }); - expect(manager.getObservabilitySnapshot(cfg).runtimeCache.activeSessions).toBe(1); - - await expectRejectedRecord( - manager.runTurn({ - provenance: "system", - cfg, - sessionKey: "agent:codex:acp:session-b", - text: "second", - mode: "prompt", - requestId: "r2", - }), - { - code: "ACP_SESSION_INIT_FAILED", - message: "ACP max concurrent sessions reached (1/1).", - }, - ); - expect(runtimeState.ensureSession).toHaveBeenCalledTimes(1); - } finally { - vi.useRealTimers(); - } - }); - it("runs turns for different ACP sessions in parallel", async () => { const runtimeState = createRuntime(); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ @@ -969,57 +881,6 @@ describe("AcpSessionManager", () => { expect(maxInFlight).toBe(2); }); - it("enforces acp.maxConcurrentSessions when opening new runtime handles", async () => { - const runtimeState = createRuntime(); - hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ - id: "acpx", - runtime: runtimeState.runtime, - }); - hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { - const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? ""; - return { - sessionKey, - storeSessionKey: sessionKey, - acp: { - ...readySessionMeta(), - runtimeSessionName: `runtime:${sessionKey}`, - }, - }; - }); - const limitedCfg = { - acp: { - ...baseCfg.acp, - maxConcurrentSessions: 1, - }, - } as OpenClawConfig; - - const manager = new AcpSessionManager(); - await manager.runTurn({ - provenance: "system", - cfg: limitedCfg, - sessionKey: "agent:codex:acp:session-a", - text: "first", - mode: "prompt", - requestId: "r1", - }); - - await expectRejectedRecord( - manager.runTurn({ - provenance: "system", - cfg: limitedCfg, - sessionKey: "agent:codex:acp:session-b", - text: "second", - mode: "prompt", - requestId: "r2", - }), - { - code: "ACP_SESSION_INIT_FAILED", - message: "ACP max concurrent sessions reached (1/1).", - }, - ); - expect(runtimeState.ensureSession).toHaveBeenCalledTimes(1); - }); - it("uses metadata backend when global acp.backend is unset", async () => { const runtimeState = createRuntime(); runtimeState.ensureSession.mockImplementation(async (input) => ({ @@ -1101,17 +962,10 @@ describe("AcpSessionManager", () => { }, }; }); - const limitedCfg = { - acp: { - ...baseCfg.acp, - maxConcurrentSessions: 1, - }, - } as OpenClawConfig; - const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", - cfg: limitedCfg, + cfg: baseCfg, sessionKey: "agent:codex:acp:session-a", text: "first", mode: "prompt", @@ -1119,7 +973,7 @@ describe("AcpSessionManager", () => { }); const closeResult = await manager.closeSession({ - cfg: limitedCfg, + cfg: baseCfg, sessionKey: "agent:codex:acp:session-a", reason: "manual-close", allowBackendUnavailable: true, @@ -1130,7 +984,7 @@ describe("AcpSessionManager", () => { await expect( manager.runTurn({ provenance: "system", - cfg: limitedCfg, + cfg: baseCfg, sessionKey: "agent:codex:acp:session-b", text: "second", mode: "prompt", @@ -1417,69 +1271,6 @@ describe("AcpSessionManager", () => { expect(entry.acp?.identity).not.toHaveProperty("agentSessionId"); }); - it("evicts idle cached runtimes before enforcing max concurrent limits", async () => { - vi.useFakeTimers(); - try { - vi.setSystemTime(new Date("2026-02-23T00:00:00.000Z")); - const runtimeState = createRuntime(); - hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ - id: "acpx", - runtime: runtimeState.runtime, - }); - hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { - const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? ""; - return { - sessionKey, - storeSessionKey: sessionKey, - acp: { - ...readySessionMeta(), - runtimeSessionName: `runtime:${sessionKey}`, - }, - }; - }); - const cfg = { - acp: { - ...baseCfg.acp, - maxConcurrentSessions: 1, - runtime: { - ttlMinutes: 0.01, - }, - }, - } as OpenClawConfig; - - const manager = new AcpSessionManager(); - await manager.runTurn({ - provenance: "system", - cfg, - sessionKey: "agent:codex:acp:session-a", - text: "first", - mode: "prompt", - requestId: "r1", - }); - - vi.advanceTimersByTime(2_000); - await manager.runTurn({ - provenance: "system", - cfg, - sessionKey: "agent:codex:acp:session-b", - text: "second", - mode: "prompt", - requestId: "r2", - }); - - expect(runtimeState.ensureSession).toHaveBeenCalledTimes(2); - const closeInput = mockCallArg(runtimeState.close); - expectRecordFields(closeInput, { - reason: "idle-evicted", - }); - expectRecordFields(closeInput.handle, { - sessionKey: "agent:codex:acp:session-a", - }); - } finally { - vi.useRealTimers(); - } - }); - it("tracks ACP turn latency and error-code observability", async () => { const runtimeState = createRuntime(); runtimeState.runTurn.mockImplementation(async function* (input: { requestId: string }) { @@ -1525,7 +1316,7 @@ describe("AcpSessionManager", () => { { code: "ACP_TURN_FAILED" }, ); - const snapshot = manager.getObservabilitySnapshot(baseCfg); + const snapshot = manager.getObservabilitySnapshot(); expect(snapshot.turns.completed).toBe(1); expect(snapshot.turns.failed).toBe(1); expect(snapshot.turns.active).toBe(0); diff --git a/src/acp/control-plane/manager.utils.ts b/src/acp/control-plane/manager.utils.ts index 426f9af39273..657e4b84dda6 100644 --- a/src/acp/control-plane/manager.utils.ts +++ b/src/acp/control-plane/manager.utils.ts @@ -114,13 +114,7 @@ export function createUnsupportedControlError(params: { ); } -export function resolveRuntimeIdleTtlMs(cfg: OpenClawConfig): number { - const ttlMinutes = cfg.acp?.runtime?.ttlMinutes; - if (typeof ttlMinutes !== "number" || !Number.isFinite(ttlMinutes) || ttlMinutes <= 0) { - return 0; - } - return Math.round(ttlMinutes * 60 * 1000); -} +export const DEFAULT_ACP_RUNTIME_IDLE_TTL_MS = 0; export function hasLegacyAcpIdentityProjection(meta: SessionAcpMeta): boolean { const raw = meta as Record; diff --git a/src/agents/agent-bundle-mcp-manager.ts b/src/agents/agent-bundle-mcp-manager.ts index cc746101be26..e3a76155302a 100644 --- a/src/agents/agent-bundle-mcp-manager.ts +++ b/src/agents/agent-bundle-mcp-manager.ts @@ -56,7 +56,7 @@ export function createSessionMcpRuntimeManager( const manager: SessionMcpRuntimeManager = { async getOrCreate(params) { - const idleTtlMs = resolveSessionMcpRuntimeIdleTtlMs(params.cfg); + const idleTtlMs = resolveSessionMcpRuntimeIdleTtlMs(); await lifecycle.sweepIdleRuntimes(); if (idleTtlMs > 0) { lifecycle.ensureIdleSweepTimer(); @@ -209,7 +209,7 @@ export function createSessionMcpRuntimeManager( async getOrCreateRequesterScoped(params) { // Scoped-only path for shared-thread harnesses: never open static transports // (those stay harness-native) so we do not double-connect. - const idleTtlMs = resolveSessionMcpRuntimeIdleTtlMs(params.cfg); + const idleTtlMs = resolveSessionMcpRuntimeIdleTtlMs(); await lifecycle.sweepIdleRuntimes(); if (idleTtlMs > 0) { lifecycle.ensureIdleSweepTimer(); diff --git a/src/agents/agent-bundle-mcp-runtime-shared.ts b/src/agents/agent-bundle-mcp-runtime-shared.ts index c87081bb5334..3f117403c851 100644 --- a/src/agents/agent-bundle-mcp-runtime-shared.ts +++ b/src/agents/agent-bundle-mcp-runtime-shared.ts @@ -27,10 +27,6 @@ export type CreateSessionMcpRuntime = (params: { configFingerprint?: string; }) => SessionMcpRuntime; -export function resolveSessionMcpRuntimeIdleTtlMs(cfg?: OpenClawConfig): number { - const raw = cfg?.mcp?.sessionIdleTtlMs; - if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) { - return Math.floor(raw); - } +export function resolveSessionMcpRuntimeIdleTtlMs(): number { return DEFAULT_SESSION_MCP_RUNTIME_IDLE_TTL_MS; } diff --git a/src/agents/agent-bundle-mcp-runtime.test.ts b/src/agents/agent-bundle-mcp-runtime.test.ts index 5ecb952196e2..28fb0f20fb6b 100644 --- a/src/agents/agent-bundle-mcp-runtime.test.ts +++ b/src/agents/agent-bundle-mcp-runtime.test.ts @@ -2032,7 +2032,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-view-lease", sessionKey: "agent:test:session-view-lease", workspaceDir: "/workspace", - cfg: { mcp: { sessionIdleTtlMs: 0 } }, + cfg: { mcp: {} }, }); const release = runtime.acquireLease?.(); updateMcpAppModelContext( @@ -2072,7 +2072,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-view-reset", sessionKey: "agent:test:session-view-reset", workspaceDir: "/workspace", - cfg: { mcp: { sessionIdleTtlMs: 0 } }, + cfg: { mcp: {} }, }); const release = runtime.acquireLease?.(); updateMcpAppModelContext( @@ -2106,7 +2106,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-view-reset", sessionKey: "agent:test:session-view-reset", workspaceDir: "/workspace", - cfg: { mcp: { sessionIdleTtlMs: 0 } }, + cfg: { mcp: {} }, }); expect(reused).toBe(runtime); expect(reused.mcpAppModelContextRevoked).toBe(true); @@ -2121,7 +2121,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-run-lease", sessionKey: "agent:test:session-run-lease", workspaceDir: "/workspace", - cfg: { mcp: { sessionIdleTtlMs: 0 } }, + cfg: { mcp: {} }, }); const materialized = await materializeBundleMcpToolsForRun({ runtime }); @@ -2155,7 +2155,6 @@ process.on("SIGINT", shutdown);`, workspaceDir: "/workspace", cfg: { mcp: { - sessionIdleTtlMs: 0, servers: { child: { command: process.execPath, args: [serverPath] }, }, @@ -2210,7 +2209,7 @@ process.on("SIGINT", shutdown);`, sessionId, sessionKey, workspaceDir: "/workspace", - cfg: { mcp: { sessionIdleTtlMs: 0 } }, + cfg: { mcp: {} }, }); const materialized = await materializeBundleMcpToolsForRun({ runtime }); expect(runtime.activeLeases).toBe(1); @@ -2241,7 +2240,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-reused-after-view", sessionKey: "agent:test:session-reused-after-view", workspaceDir: "/workspace", - cfg: { mcp: { servers: {}, sessionIdleTtlMs: 0 } }, + cfg: { mcp: { servers: {} } }, }; const runtime = await manager.getOrCreate(params); const release = runtime.acquireLease?.(); @@ -2263,7 +2262,7 @@ process.on("SIGINT", shutdown);`, sessionId: "session-required-retirement", sessionKey: "agent:test:session-required-retirement", workspaceDir: "/workspace", - cfg: { mcp: { servers: {}, sessionIdleTtlMs: 0 } }, + cfg: { mcp: { servers: {} } }, }; expect(manager.deferRetirement(params.sessionId, { retainAcrossReuse: true })).toBe(true); @@ -2316,154 +2315,6 @@ process.on("SIGINT", shutdown);`, ).resolves.toBe(false); }); - it("evicts idle runtimes after the configured TTL but skips active leases", async () => { - let now = 1_000; - const disposed: string[] = []; - const createRuntime: RuntimeFactory = (params) => { - let lastUsedAt = now; - let activeLeases = 0; - return { - ...makeRuntime([{ toolName: "bundle_probe", description: "Bundle MCP probe" }]), - sessionId: params.sessionId, - sessionKey: params.sessionKey, - workspaceDir: params.workspaceDir, - configFingerprint: params.configFingerprint ?? "fingerprint", - get lastUsedAt() { - return lastUsedAt; - }, - get activeLeases() { - return activeLeases; - }, - markUsed: () => { - lastUsedAt = now; - }, - acquireLease: () => { - activeLeases += 1; - return () => { - activeLeases -= 1; - }; - }, - dispose: async () => { - disposed.push(params.sessionId); - }, - }; - }; - const manager = testing.createSessionMcpRuntimeManager({ - createRuntime, - now: () => now, - enableIdleSweepTimer: false, - }); - - const runtime = await manager.getOrCreate({ - sessionId: "session-idle", - sessionKey: "agent:test:session-idle", - workspaceDir: "/workspace", - cfg: { mcp: { servers: {}, sessionIdleTtlMs: 50 } }, - }); - const releaseLease = runtime.acquireLease?.(); - - now += 60; - await expect(manager.sweepIdleRuntimes()).resolves.toBe(0); - expect(manager.listSessionIds()).toEqual(["session-idle"]); - - releaseLease?.(); - now += 60; - await expect(manager.sweepIdleRuntimes()).resolves.toBe(1); - - expect(disposed).toEqual(["session-idle"]); - expect(manager.listSessionIds()).toStrictEqual([]); - expect(manager.resolveSessionId("agent:test:session-idle")).toBeUndefined(); - }); - - it("evicts immediately after release when TTL already elapsed during active lease", async () => { - let now = 1_000; - const disposed: string[] = []; - const createRuntime: RuntimeFactory = (params) => { - let lastUsedAt = now; - let activeLeases = 0; - return { - ...makeRuntime([{ toolName: "bundle_probe", description: "Bundle MCP probe" }]), - sessionId: params.sessionId, - sessionKey: params.sessionKey, - workspaceDir: params.workspaceDir, - configFingerprint: params.configFingerprint ?? "fingerprint", - get lastUsedAt() { - return lastUsedAt; - }, - get activeLeases() { - return activeLeases; - }, - markUsed: () => { - lastUsedAt = now; - }, - acquireLease: () => { - activeLeases += 1; - return () => { - activeLeases -= 1; - }; - }, - dispose: async () => { - disposed.push(params.sessionId); - }, - }; - }; - const manager = testing.createSessionMcpRuntimeManager({ - createRuntime, - now: () => now, - enableIdleSweepTimer: false, - }); - - const runtime = await manager.getOrCreate({ - sessionId: "session-idle-post-release", - sessionKey: "agent:test:session-idle-post-release", - workspaceDir: "/workspace", - cfg: { mcp: { servers: {}, sessionIdleTtlMs: 50 } }, - }); - const releaseLease = runtime.acquireLease?.(); - - // TTL elapses while the lease is still held, so sweep skips active runtimes. - now += 60; - await expect(manager.sweepIdleRuntimes()).resolves.toBe(0); - - // Release must not reset lastUsedAt; the runtime is evictable on the very - // next sweep without waiting another full TTL. - releaseLease?.(); - await expect(manager.sweepIdleRuntimes()).resolves.toBe(1); - - expect(disposed).toEqual(["session-idle-post-release"]); - expect(manager.listSessionIds()).toStrictEqual([]); - }); - - it("keeps idle runtime eviction disabled when the TTL is zero", async () => { - let now = 1_000; - const disposed: string[] = []; - const manager = testing.createSessionMcpRuntimeManager({ - createRuntime: (params) => ({ - ...makeRuntime([{ toolName: "bundle_probe", description: "Bundle MCP probe" }]), - sessionId: params.sessionId, - sessionKey: params.sessionKey, - workspaceDir: params.workspaceDir, - configFingerprint: params.configFingerprint ?? "fingerprint", - dispose: async () => { - disposed.push(params.sessionId); - }, - }), - now: () => now, - enableIdleSweepTimer: false, - }); - - await manager.getOrCreate({ - sessionId: "session-no-ttl", - workspaceDir: "/workspace", - cfg: { mcp: { servers: {}, sessionIdleTtlMs: 0 } }, - }); - - now += 60_000_000; - await expect(manager.sweepIdleRuntimes()).resolves.toBe(0); - expect(manager.listSessionIds()).toEqual(["session-no-ttl"]); - expect(disposed).toStrictEqual([]); - }); - it("production createSessionMcpRuntime acquireLease release does not refresh lastUsedAt", () => { const runtime = createSessionMcpRuntime({ sessionId: "session-lease-timestamp-check", @@ -2871,62 +2722,6 @@ describe("requester-scoped MCP connection resolution", () => { await manager.disposeAll(); }); - it("idle-sweeps requester-scoped runtimes and disposes them with the session", async () => { - const { testing: resolverTesting } = await import("./mcp-connection-resolver.js"); - resolverTesting.setMcpServerConnectionResolversForTest([ - { - serverName: "user-mail", - resolve: async () => ({ url: "https://mcp.example.test/user" }), - }, - ]); - - let nowMs = 1_000_000; - const disposed: string[] = []; - const createRuntime: RuntimeFactory = (params) => { - const runtime = makeRuntime([{ toolName: "probe", description: "probe" }]); - return { - ...runtime, - sessionId: params.sessionId, - workspaceDir: params.workspaceDir, - configFingerprint: params.configFingerprint ?? "fingerprint", - requesterScope: params.requesterScope, - lastUsedAt: nowMs, - dispose: async () => { - disposed.push(params.requesterScope?.requesterSenderId ?? "static"); - }, - }; - }; - const manager = testing.createSessionMcpRuntimeManager({ - createRuntime, - now: () => nowMs, - enableIdleSweepTimer: false, - }); - const cfg = { - mcp: { - sessionIdleTtlMs: 1_000, - servers: { - shared: { command: "true" }, - "user-mail": { transport: "streamable-http" }, - }, - }, - }; - - await manager.getOrCreate({ - sessionId: "session-idle", - workspaceDir: "/workspace", - cfg: cfg as never, - requesterSenderId: "sender-a", - messageChannel: "telegram", - }); - expect(manager.listRuntimeKeys()).toHaveLength(2); - - nowMs += 2_000; - const swept = await manager.sweepIdleRuntimes(); - expect(swept).toBe(2); - expect(disposed.toSorted()).toEqual(["sender-a", "static"]); - expect(manager.listSessionIds()).toEqual([]); - }); - it("omits a stalled resolver without hanging static materialization", async () => { const { testing: resolverTesting } = await import("./mcp-connection-resolver.js"); resolverTesting.setMcpConnectionResolverTimeoutMsForTest(25); @@ -3832,7 +3627,6 @@ describe("requester-scoped MCP connection resolution", () => { workspaceDir: "/workspace", cfg: { mcp: { - sessionIdleTtlMs: 0, servers: { "user-mail": { transport: "streamable-http" }, }, diff --git a/src/agents/agent-hooks/context-pruning/settings.ts b/src/agents/agent-hooks/context-pruning/settings.ts index ca670fd3918a..1b99de9797ba 100644 --- a/src/agents/agent-hooks/context-pruning/settings.ts +++ b/src/agents/agent-hooks/context-pruning/settings.ts @@ -13,16 +13,7 @@ type ContextPruningConfig = { mode?: ContextPruningMode; /** TTL to consider cache expired (duration string, default unit: minutes). */ ttl?: string; - keepLastAssistants?: number; - softTrimRatio?: number; - hardClearRatio?: number; - minPrunableToolChars?: number; tools?: ContextPruningToolMatch; - softTrim?: { - maxChars?: number; - headChars?: number; - tailChars?: number; - }; hardClear?: { enabled?: boolean; placeholder?: string; @@ -89,32 +80,9 @@ export function computeEffectiveSettings(raw: unknown): EffectiveContextPruningS } } - if (typeof cfg.keepLastAssistants === "number" && Number.isFinite(cfg.keepLastAssistants)) { - s.keepLastAssistants = Math.max(0, Math.floor(cfg.keepLastAssistants)); - } - if (typeof cfg.softTrimRatio === "number" && Number.isFinite(cfg.softTrimRatio)) { - s.softTrimRatio = Math.min(1, Math.max(0, cfg.softTrimRatio)); - } - if (typeof cfg.hardClearRatio === "number" && Number.isFinite(cfg.hardClearRatio)) { - s.hardClearRatio = Math.min(1, Math.max(0, cfg.hardClearRatio)); - } - if (typeof cfg.minPrunableToolChars === "number" && Number.isFinite(cfg.minPrunableToolChars)) { - s.minPrunableToolChars = Math.max(0, Math.floor(cfg.minPrunableToolChars)); - } if (cfg.tools) { s.tools = cfg.tools; } - if (cfg.softTrim) { - if (typeof cfg.softTrim.maxChars === "number" && Number.isFinite(cfg.softTrim.maxChars)) { - s.softTrim.maxChars = Math.max(0, Math.floor(cfg.softTrim.maxChars)); - } - if (typeof cfg.softTrim.headChars === "number" && Number.isFinite(cfg.softTrim.headChars)) { - s.softTrim.headChars = Math.max(0, Math.floor(cfg.softTrim.headChars)); - } - if (typeof cfg.softTrim.tailChars === "number" && Number.isFinite(cfg.softTrim.tailChars)) { - s.softTrim.tailChars = Math.max(0, Math.floor(cfg.softTrim.tailChars)); - } - } if (cfg.hardClear) { if (typeof cfg.hardClear.enabled === "boolean") { s.hardClear.enabled = cfg.hardClear.enabled; diff --git a/src/agents/agent-project-settings.test.ts b/src/agents/agent-project-settings.test.ts index cc9605e72991..4c6c04fb77ea 100644 --- a/src/agents/agent-project-settings.test.ts +++ b/src/agents/agent-project-settings.test.ts @@ -183,29 +183,4 @@ describe("createPreparedEmbeddedAgentSettingsManager", () => { await fs.rm(baseDir, { recursive: true, force: true }); } }); - - it("keeps compaction reserve overrides after disabling runtime retry", () => { - const settingsManager = createPreparedEmbeddedAgentSettingsManager({ - cwd: "/tmp/workspace", - agentDir: "/tmp/agent", - cfg: { - agents: { - defaults: { - compaction: { - reserveTokensFloor: 50_000, - keepRecentTokens: 16_000, - }, - }, - }, - }, - contextTokenBudget: 200_000, - }); - - expect(settingsManager.getRetryEnabled()).toBe(false); - expect(settingsManager.getCompactionSettings()).toEqual({ - enabled: true, - reserveTokens: 50_000, - keepRecentTokens: 16_000, - }); - }); }); diff --git a/src/agents/agent-scope-config.ts b/src/agents/agent-scope-config.ts index fcb9670b9051..0822e3e5b8e0 100644 --- a/src/agents/agent-scope-config.ts +++ b/src/agents/agent-scope-config.ts @@ -42,7 +42,6 @@ export type ResolvedAgentConfig = { identity?: AgentEntry["identity"]; groupChat?: AgentEntry["groupChat"]; subagents?: AgentEntry["subagents"]; - runRetries?: AgentEntry["runRetries"]; embeddedAgent?: AgentEntry["embeddedAgent"]; sandbox?: AgentEntry["sandbox"]; tools?: AgentEntry["tools"]; @@ -159,10 +158,6 @@ export function resolveAgentConfig( identity: entry.identity, groupChat: entry.groupChat, subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined, - runRetries: - typeof entry.runRetries === "object" && entry.runRetries - ? { ...agentDefaults?.runRetries, ...entry.runRetries } - : agentDefaults?.runRetries, embeddedAgent: typeof entry.embeddedAgent === "object" && entry.embeddedAgent ? entry.embeddedAgent diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.test.ts index 13e0f852e76c..4919a8d4df5f 100644 --- a/src/agents/agent-scope.test.ts +++ b/src/agents/agent-scope.test.ts @@ -150,36 +150,6 @@ describe("resolveAgentConfig", () => { }); }); - it("merges runRetries from defaults with per-agent overrides", () => { - const cfg: OpenClawConfig = { - agents: { - defaults: { - runRetries: { - base: 24, - perProfile: 8, - min: 32, - max: 160, - }, - }, - list: [ - { - id: "main", - runRetries: { - max: 50, - }, - }, - ], - }, - }; - - expect(resolveAgentConfig(cfg, "main")?.runRetries).toEqual({ - base: 24, - perProfile: 8, - min: 32, - max: 50, - }); - }); - it("resolves explicit and effective model primary separately", () => { const cfgWithStringDefault = { agents: { diff --git a/src/agents/agent-settings.test.ts b/src/agents/agent-settings.test.ts index a08b0ad01a2f..f64726c9489d 100644 --- a/src/agents/agent-settings.test.ts +++ b/src/agents/agent-settings.test.ts @@ -25,13 +25,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { }); it("can restore reserveTokens after a simulated resource loader reload drops them below floor", () => { - const cfg = { - agents: { - defaults: { - compaction: { reserveTokensFloor: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR }, - }, - }, - } as const; let reserve = 16_384; const keep = 20_000; const settingsManager = { @@ -46,7 +39,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { const first = applyAgentCompactionSettingsFromConfig({ settingsManager, - cfg, contextTokenBudget: 100_000, }); expect(first.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); @@ -54,7 +46,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { reserve = 16_384; const second = applyAgentCompactionSettingsFromConfig({ settingsManager, - cfg, contextTokenBudget: 100_000, }); expect(second.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); @@ -78,30 +69,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { expect(settingsManager.applyOverrides).not.toHaveBeenCalled(); }); - it("applies explicit reserveTokens but still enforces floor", () => { - const settingsManager = { - getCompactionReserveTokens: () => 10_000, - getCompactionKeepRecentTokens: () => 20_000, - applyOverrides: vi.fn(), - }; - - const result = applyAgentCompactionSettingsFromConfig({ - settingsManager, - cfg: { - agents: { - defaults: { - compaction: { reserveTokens: 12_000, reserveTokensFloor: 20_000 }, - }, - }, - }, - }); - - expect(result.compaction.reserveTokens).toBe(20_000); - expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: 20_000 }, - }); - }); - it("applies keepRecentTokens when explicitly configured", () => { const settingsManager = { getCompactionReserveTokens: () => 20_000, @@ -184,35 +151,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { expect(shouldCompact(1, 16_384, { enabled: true, ...result.compaction })).toBe(false); }); - it("applies capped floor over user-configured reserveTokens when default floor exceeds context window", () => { - const settingsManager = { - getCompactionReserveTokens: () => 16_384, - getCompactionKeepRecentTokens: () => 20_000, - applyOverrides: vi.fn(), - }; - - // User sets reserveTokens=2048 but NOT reserveTokensFloor (default 20_000 applies). - // Pre-fix: target = max(2048, 20_000) = 20_000 → exceeds 16_384 context → infinite loop. - // Post-fix: floor capped to 8_384 → target = max(2048, 8_384) = 8_384 → works. - const result = applyAgentCompactionSettingsFromConfig({ - settingsManager, - cfg: { - agents: { - defaults: { - compaction: { reserveTokens: 2_048 }, - }, - }, - }, - contextTokenBudget: 16_384, - }); - - expect(result.didOverride).toBe(true); - expect(result.compaction.reserveTokens).toBe(8_384); // capped floor wins over user's 2_048 - expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: 8_384 }, - }); - }); - it("applies capped floor when current reserve is below it on small-context models", () => { // Simulate an embedded runner default of 4 096 with a 16 384 context window. // minPromptBudget = min(8_000, floor(16_384 * 0.5)) = 8_000. @@ -235,64 +173,6 @@ describe("applyAgentCompactionSettingsFromConfig", () => { }); }); - it("respects user-configured reserveTokens below capped floor for small models", () => { - const settingsManager = { - getCompactionReserveTokens: () => 16_384, - getCompactionKeepRecentTokens: () => 20_000, - applyOverrides: vi.fn(), - }; - - // User explicitly sets reserveTokens=2048 and reserveTokensFloor=0. - // With contextTokenBudget=16384, the capped floor = min(0, 8192) = 0. - // targetReserveTokens = max(2048, 0) = 2048. - const result = applyAgentCompactionSettingsFromConfig({ - settingsManager, - cfg: { - agents: { - defaults: { - compaction: { reserveTokens: 2_048, reserveTokensFloor: 0 }, - }, - }, - }, - contextTokenBudget: 16_384, - }); - - expect(result.compaction.reserveTokens).toBe(2_048); - expect(result.compaction.keepRecentTokens).toBe(20_000); - expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: 2_048 }, - }); - }); - - it("caps an explicit reserve even when the configured floor is disabled", () => { - const settingsManager = { - getCompactionReserveTokens: () => 16_384, - getCompactionKeepRecentTokens: () => 20_000, - applyOverrides: vi.fn(), - }; - - const result = applyAgentCompactionSettingsFromConfig({ - settingsManager, - cfg: { - agents: { - defaults: { - compaction: { - reserveTokens: 20_000, - reserveTokensFloor: 0, - keepRecentTokens: 15_000, - }, - }, - }, - }, - contextTokenBudget: 16_000, - }); - - expect(result.compaction).toEqual({ reserveTokens: 8_000, keepRecentTokens: 15_000 }); - expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: 8_000, keepRecentTokens: 15_000 }, - }); - }); - it("does not cap the reserve floor for a mid-size context", () => { const settingsManager = { getCompactionReserveTokens: () => 16_384, @@ -538,14 +418,9 @@ describe("applyAgentAutoCompactionGuard", () => { it("preserves configured reserve tokens when disabling embedded auto-compaction", () => { const settingsManager = SettingsManager.inMemory({ - compaction: { enabled: true, reserveTokens: 16_384 }, + compaction: { enabled: true, reserveTokens: 50_000 }, }); - applyAgentCompactionSettingsFromConfig({ - settingsManager, - cfg: { agents: { defaults: { compaction: { reserveTokensFloor: 50_000 } } } }, - contextTokenBudget: 200_000, - }); const result = applyAgentAutoCompactionGuard({ settingsManager, compactionMode: "safeguard", diff --git a/src/agents/agent-settings.ts b/src/agents/agent-settings.ts index e001405e632d..5937e404fc73 100644 --- a/src/agents/agent-settings.ts +++ b/src/agents/agent-settings.ts @@ -20,22 +20,6 @@ type AgentSettingsManagerLike = { setCompactionEnabled?: (enabled: boolean) => void; }; -/** Resolves the configured reserve-token floor for agent compaction. */ -function resolveCompactionReserveTokensFloor(cfg?: OpenClawConfig): number { - const raw = cfg?.agents?.defaults?.compaction?.reserveTokensFloor; - if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) { - return Math.floor(raw); - } - return DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; -} - -function toNonNegativeInt(value: unknown): number | undefined { - if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { - return undefined; - } - return Math.floor(value); -} - function toPositiveInt(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { return undefined; @@ -57,9 +41,8 @@ export function applyAgentCompactionSettingsFromConfig(params: { const currentKeepRecentTokens = params.settingsManager.getCompactionKeepRecentTokens(); const compactionCfg = params.cfg?.agents?.defaults?.compaction; - const configuredReserveTokens = toNonNegativeInt(compactionCfg?.reserveTokens); const configuredKeepRecentTokens = toPositiveInt(compactionCfg?.keepRecentTokens); - let reserveTokensFloor = resolveCompactionReserveTokensFloor(params.cfg); + let reserveTokensFloor = DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; let maxReserveTokens: number | undefined; // Cap the floor to a safe fraction of the context window so that @@ -77,10 +60,7 @@ export function applyAgentCompactionSettingsFromConfig(params: { reserveTokensFloor = Math.min(reserveTokensFloor, maxReserveTokens); } - let targetReserveTokens = Math.max( - configuredReserveTokens ?? currentReserveTokens, - reserveTokensFloor, - ); + let targetReserveTokens = Math.max(currentReserveTokens, reserveTokensFloor); if (maxReserveTokens !== undefined) { // Cap the effective value too: the harness default or explicit config can otherwise // undo the floor cap and make shouldCompact() true from the first token. diff --git a/src/agents/auth-profiles.markauthprofilefailure.test.ts b/src/agents/auth-profiles.markauthprofilefailure.test.ts index 6b016e8419a4..b6d81e2baa09 100644 --- a/src/agents/auth-profiles.markauthprofilefailure.test.ts +++ b/src/agents/auth-profiles.markauthprofilefailure.test.ts @@ -168,30 +168,6 @@ describe("markAuthProfileFailure", () => { expectCooldownInRange(remainingMs, 4.5 * 60 * 60 * 1000, 5.5 * 60 * 60 * 1000); }); }); - it("honors per-provider billing backoff overrides", async () => { - await withAuthProfileStore(async ({ agentDir, store }) => { - const startedAt = Date.now(); - await markAuthProfileFailure({ - store, - profileId: "anthropic:default", - reason: "billing", - agentDir, - cfg: { - auth: { - cooldowns: { - billingBackoffHoursByProvider: { Anthropic: 1 }, - billingMaxHours: 2, - }, - }, - } as never, - }); - - const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil; - expect(typeof disabledUntil).toBe("number"); - const remainingMs = (disabledUntil as number) - startedAt; - expectCooldownInRange(remainingMs, 0.8 * 60 * 60 * 1000, 1.2 * 60 * 60 * 1000); - }); - }); it("keeps persisted cooldownUntil unchanged across mid-window retries", async () => { await withAuthProfileStore(async ({ agentDir, store }) => { await markAuthProfileFailure({ @@ -275,30 +251,6 @@ describe("markAuthProfileFailure", () => { }); }); - it("honors auth_permanent backoff overrides", async () => { - await withAuthProfileStore(async ({ agentDir, store }) => { - const startedAt = Date.now(); - await markAuthProfileFailure({ - store, - profileId: "anthropic:default", - reason: "auth_permanent", - agentDir, - cfg: { - auth: { - cooldowns: { - authPermanentBackoffMinutes: 15, - authPermanentMaxMinutes: 45, - }, - }, - } as never, - }); - - const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil; - expect(typeof disabledUntil).toBe("number"); - const remainingMs = (disabledUntil as number) - startedAt; - expectCooldownInRange(remainingMs, 14 * 60 * 1000, 16 * 60 * 1000); - }); - }); it("resets backoff counters outside the failure window", async () => { const agentDir = makeAgentDir("reset-window"); const now = Date.now(); @@ -330,9 +282,6 @@ describe("markAuthProfileFailure", () => { profileId: "anthropic:default", reason: "billing", agentDir, - cfg: { - auth: { cooldowns: { failureWindowHours: 24 } }, - } as never, }); expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1); diff --git a/src/agents/auth-profiles/usage.test.ts b/src/agents/auth-profiles/usage.test.ts index c08267de937a..642fdea3d859 100644 --- a/src/agents/auth-profiles/usage.test.ts +++ b/src/agents/auth-profiles/usage.test.ts @@ -937,26 +937,6 @@ describe("markAuthProfileFailure — active windows do not extend on retry", () const stats = store.usageStats?.["anthropic:default"]; expect(testCase.readUntil(stats)).toBe(MAX_DATE_TIMESTAMP_MS); }); - - it("preserves fractional disabled cooldown config durations", async () => { - const now = 1_000_000; - const store = makeStore({}); - - await markFailureAt({ - store, - now, - reason: "billing", - cfg: { - auth: { - cooldowns: { - billingBackoffHours: 0.0166667, - }, - }, - } as OpenClawConfig, - }); - - expect(store.usageStats?.["anthropic:default"]?.disabledUntil).toBe(now + 60_000); - }); }); describe("markAuthProfileBlockedUntil", () => { diff --git a/src/agents/auth-profiles/usage.ts b/src/agents/auth-profiles/usage.ts index 4a9860a2c1c3..dbb9f06bcaf9 100644 --- a/src/agents/auth-profiles/usage.ts +++ b/src/agents/auth-profiles/usage.ts @@ -704,62 +704,19 @@ const DISABLED_FAILURE_BACKOFF_POLICIES = { }, } as const satisfies Record; -function resolveAuthCooldownConfig(params: { - cfg?: OpenClawConfig; - providerId: string; -}): ResolvedAuthCooldownConfig { - const defaults = { - billingBackoffHours: 5, - billingMaxHours: 24, - authPermanentBackoffMinutes: 10, - authPermanentMaxMinutes: 60, - failureWindowHours: 24, - } as const; - - const resolvePositiveNumber = (value: unknown, fallback: number) => - typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback; - - const cooldowns = params.cfg?.auth?.cooldowns; - const billingOverride = (() => { - const map = cooldowns?.billingBackoffHoursByProvider; - if (!map) { - return undefined; - } - for (const [key, value] of Object.entries(map)) { - if (normalizeProviderId(key) === params.providerId) { - return value; - } - } - return undefined; - })(); - - const billingBackoffHours = resolvePositiveNumber( - billingOverride ?? cooldowns?.billingBackoffHours, - defaults.billingBackoffHours, - ); - const billingMaxHours = resolvePositiveNumber( - cooldowns?.billingMaxHours, - defaults.billingMaxHours, - ); - const authPermanentBackoffMinutes = resolvePositiveNumber( - cooldowns?.authPermanentBackoffMinutes, - defaults.authPermanentBackoffMinutes, - ); - const authPermanentMaxMinutes = resolvePositiveNumber( - cooldowns?.authPermanentMaxMinutes, - defaults.authPermanentMaxMinutes, - ); - const failureWindowHours = resolvePositiveNumber( - cooldowns?.failureWindowHours, - defaults.failureWindowHours, - ); +const DEFAULT_BILLING_BACKOFF_HOURS = 5; +const DEFAULT_BILLING_MAX_HOURS = 24; +const DEFAULT_AUTH_PERMANENT_BACKOFF_MINUTES = 10; +const DEFAULT_AUTH_PERMANENT_MAX_MINUTES = 60; +const DEFAULT_FAILURE_WINDOW_HOURS = 24; +function resolveAuthCooldownConfig(): ResolvedAuthCooldownConfig { return { - billingBackoffMs: billingBackoffHours * 60 * 60 * 1000, - billingMaxMs: billingMaxHours * 60 * 60 * 1000, - authPermanentBackoffMs: authPermanentBackoffMinutes * 60 * 1000, - authPermanentMaxMs: authPermanentMaxMinutes * 60 * 1000, - failureWindowMs: failureWindowHours * 60 * 60 * 1000, + billingBackoffMs: DEFAULT_BILLING_BACKOFF_HOURS * 60 * 60 * 1000, + billingMaxMs: DEFAULT_BILLING_MAX_HOURS * 60 * 60 * 1000, + authPermanentBackoffMs: DEFAULT_AUTH_PERMANENT_BACKOFF_MINUTES * 60 * 1000, + authPermanentMaxMs: DEFAULT_AUTH_PERMANENT_MAX_MINUTES * 60 * 1000, + failureWindowMs: DEFAULT_FAILURE_WINDOW_HOURS * 60 * 60 * 1000, }; } @@ -967,7 +924,7 @@ export async function markAuthProfileFailure(params: { runId?: string; modelId?: string; }): Promise { - const { store, profileId, reason, agentDir, cfg, runId, modelId } = params; + const { store, profileId, reason, agentDir, runId, modelId } = params; const profile = store.profiles[profileId]; if (!profile || isAuthCooldownBypassedForProvider(profile.provider)) { return; @@ -1004,11 +961,7 @@ export async function markAuthProfileFailure(params: { return false; } const now = Date.now(); - const providerKey = normalizeProviderId(profileValue.provider); - const cfgResolved = resolveAuthCooldownConfig({ - cfg, - providerId: providerKey, - }); + const cfgResolved = resolveAuthCooldownConfig(); previousStats = freshStore.usageStats?.[profileId]; updateTime = now; diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index 205013b31ccc..9e1533e7a957 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -494,77 +494,6 @@ describe("resolveCliBackendConfig reliability merge", () => { "--skip-git-repo-check", ]); }); - - it("deep-merges reliability watchdog overrides for codex", () => { - const cfg = { - agents: { - defaults: { - cliBackends: { - "codex-cli": { - command: "codex", - reliability: { - watchdog: { - resume: { - noOutputTimeoutMs: 42_000, - }, - }, - }, - }, - }, - }, - }, - } satisfies OpenClawConfig; - - const resolved = requireCliBackendConfig("codex-cli", cfg); - - expect(resolved.config.reliability?.watchdog?.resume?.noOutputTimeoutMs).toBe(42_000); - // Ensure defaults are retained when only one field is overridden. - expect(resolved.config.reliability?.watchdog?.resume?.noOutputTimeoutRatio).toBe(0.3); - expect(resolved.config.reliability?.watchdog?.resume?.minMs).toBe(60_000); - expect(resolved.config.reliability?.watchdog?.resume?.maxMs).toBe(180_000); - expect(resolved.config.reliability?.watchdog?.fresh?.noOutputTimeoutRatio).toBe(0.8); - }); - - it("deep-merges reliability output-limit overrides", () => { - runtimeBackendEntries.unshift( - createRuntimeBackendEntry({ - pluginId: "test", - id: "test-cli", - config: { - command: "test-cli", - reliability: { - outputLimits: { - maxTurnRawChars: 8192, - maxTurnLines: 20_000, - }, - }, - }, - }), - ); - const cfg = { - agents: { - defaults: { - cliBackends: { - "test-cli": { - command: "test-cli", - reliability: { - outputLimits: { - maxTurnRawChars: 16_384, - }, - }, - }, - }, - }, - }, - } satisfies OpenClawConfig; - - const resolved = requireCliBackendConfig("test-cli", cfg); - - expect(resolved?.config.reliability?.outputLimits).toEqual({ - maxTurnRawChars: 16_384, - maxTurnLines: 20_000, - }); - }); }); describe("resolveCliBackendLiveTest", () => { diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index e00d5cac9706..d5bc43de4189 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -337,10 +337,8 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig) } const baseFresh = base.reliability?.watchdog?.fresh ?? {}; const baseResume = base.reliability?.watchdog?.resume ?? {}; - const baseOutputLimits = base.reliability?.outputLimits ?? {}; const overrideFresh = override.reliability?.watchdog?.fresh ?? {}; const overrideResume = override.reliability?.watchdog?.resume ?? {}; - const overrideOutputLimits = override.reliability?.outputLimits ?? {}; return { ...base, ...override, @@ -354,10 +352,6 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig) reliability: { ...base.reliability, ...override.reliability, - outputLimits: { - ...baseOutputLimits, - ...overrideOutputLimits, - }, watchdog: { ...base.reliability?.watchdog, ...override.reliability?.watchdog, diff --git a/src/agents/cli-output.test.ts b/src/agents/cli-output.test.ts index 13bb9254e70d..7b31d177d4d4 100644 --- a/src/agents/cli-output.test.ts +++ b/src/agents/cli-output.test.ts @@ -1231,33 +1231,6 @@ describe("createCliJsonlStreamingParser", () => { }); }); - it("reports an output-limit error and ignores later chunks", () => { - const parser = createCliJsonlStreamingParser({ - backend: { - command: "local-cli", - output: "jsonl", - jsonlDialect: "claude-stream-json", - reliability: { outputLimits: { maxTurnRawChars: 1024 } }, - }, - providerId: "local-cli", - onAssistantDelta: () => {}, - }); - - parser.push("x".repeat(1025)); - parser.push(`${JSON.stringify({ type: "result", result: "late" })}\n`); - parser.finish(); - - expect(parser.getErrorText()).toBe( - "CLI JSONL output exceeded 1024 characters; refusing to parse output.", - ); - expect(parser.getOutput()).toEqual({ - text: "", - sessionId: undefined, - usage: undefined, - errorText: "CLI JSONL output exceeded 1024 characters; refusing to parse output.", - }); - }); - it("streams thinking deltas, skips signature deltas, and dedupes the snapshot", () => { const thinking: Array<{ text: string; delta: string; isReasoningSnapshot?: boolean }> = []; const parser = createCliJsonlStreamingParser({ diff --git a/src/agents/cli-output.ts b/src/agents/cli-output.ts index ac0dc9ba9b43..24990b1735c7 100644 --- a/src/agents/cli-output.ts +++ b/src/agents/cli-output.ts @@ -95,11 +95,7 @@ export function formatCliOutputError( } export const CLI_STREAM_JSON_DEFAULT_MAX_TURN_RAW_CHARS = 8 * 1024 * 1024; -const CLI_STREAM_JSON_MIN_TURN_RAW_CHARS = 1_024; -const CLI_STREAM_JSON_MAX_CONFIGURABLE_TURN_RAW_CHARS = 64 * 1024 * 1024; const CLI_STREAM_JSON_DEFAULT_MAX_TURN_LINES = 20_000; -const CLI_STREAM_JSON_MIN_TURN_LINES = 100; -const CLI_STREAM_JSON_MAX_CONFIGURABLE_TURN_LINES = 100_000; const CLI_STREAM_JSON_MISSING_RESULT_ERROR = "CLI stream-json output ended without a result event."; /** Incremental assistant text emitted while parsing a streaming CLI response. */ @@ -513,37 +509,13 @@ function shouldUnwrapNestedCliResultText(params: { return !Object.hasOwn(params.parsed, "type") || params.parsed.type === "result"; } -function normalizePositiveInt( - value: number | undefined, - fallback: number, - min: number, - max: number, -): number { - if (typeof value !== "number" || !Number.isInteger(value)) { - return fallback; - } - return Math.min(Math.max(value, min), max); -} - export function resolveCliStreamJsonOutputLimits( - backend: CliBackendConfig, + _backend: CliBackendConfig, ): CliStreamJsonOutputLimits { - const configured = backend.reliability?.outputLimits; - const maxTurnRawChars = normalizePositiveInt( - configured?.maxTurnRawChars, - CLI_STREAM_JSON_DEFAULT_MAX_TURN_RAW_CHARS, - CLI_STREAM_JSON_MIN_TURN_RAW_CHARS, - CLI_STREAM_JSON_MAX_CONFIGURABLE_TURN_RAW_CHARS, - ); return { - maxTurnRawChars, - maxPendingLineChars: maxTurnRawChars, - maxTurnLines: normalizePositiveInt( - configured?.maxTurnLines, - CLI_STREAM_JSON_DEFAULT_MAX_TURN_LINES, - CLI_STREAM_JSON_MIN_TURN_LINES, - CLI_STREAM_JSON_MAX_CONFIGURABLE_TURN_LINES, - ), + maxTurnRawChars: CLI_STREAM_JSON_DEFAULT_MAX_TURN_RAW_CHARS, + maxPendingLineChars: CLI_STREAM_JSON_DEFAULT_MAX_TURN_RAW_CHARS, + maxTurnLines: CLI_STREAM_JSON_DEFAULT_MAX_TURN_LINES, }; } diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index 359da28f3839..bfa92c13abcf 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -4381,24 +4381,6 @@ describe("runCliAgent reliability", () => { }); describe("resolveCliNoOutputTimeoutMs", () => { - it("uses backend-configured resume watchdog override", () => { - const timeoutMs = resolveCliNoOutputTimeoutMs({ - backend: { - command: "codex", - reliability: { - watchdog: { - resume: { - noOutputTimeoutMs: 42_000, - }, - }, - }, - }, - timeoutMs: 120_000, - useResume: true, - }); - expect(timeoutMs).toBe(42_000); - }); - it("lets explicit cron timeouts lift the default resume no-output ceiling", () => { const timeoutMs = resolveCliNoOutputTimeoutMs({ backend: { command: "codex" }, diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index 4ae0d55889ec..8549e8304f3a 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -2941,107 +2941,6 @@ describe("runCliAgent spawn path", () => { expect(result.text).toBe(largeText); }); - it("honors configured Claude live stream-json raw turn limits", async () => { - const largeText = "x".repeat(1500); - let stdoutListener: ((chunk: string) => void) | undefined; - const stdin = { - write: vi.fn((dataValue: string, cb?: (err?: Error | null) => void) => { - stdoutListener?.( - JSON.stringify({ - type: "result", - session_id: "live-session-tight-output-limit", - result: largeText, - }) + "\n", - ); - cb?.(); - }), - end: vi.fn(), - }; - supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => { - const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void }; - stdoutListener = input.onStdout; - return { - runId: "live-run-tight-output-limit", - pid: 2345, - startedAtMs: Date.now(), - stdin, - wait: vi.fn(() => new Promise(() => {})), - cancel: vi.fn(), - }; - }); - - await expectRejectsWithFields( - executePreparedCliRun( - buildPreparedCliRunContext({ - provider: "claude-cli", - model: "sonnet", - runId: "run-live-tight-output-limit", - backend: { - liveSession: "claude-stdio", - reliability: { - outputLimits: { - maxTurnRawChars: 1024, - }, - }, - }, - }), - ), - { - name: "FailoverError", - message: "Claude CLI JSONL line exceeded output limit.", - }, - ); - }); - - it("accepts operator-raised Claude live stream-json raw turn limits", async () => { - const largeText = "x".repeat(1500); - let stdoutListener: ((chunk: string) => void) | undefined; - const stdin = { - write: vi.fn((dataValue: string, cb?: (err?: Error | null) => void) => { - stdoutListener?.( - JSON.stringify({ - type: "result", - session_id: "live-session-raised-output-limit", - result: largeText, - }) + "\n", - ); - cb?.(); - }), - end: vi.fn(), - }; - supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => { - const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void }; - stdoutListener = input.onStdout; - return { - runId: "live-run-raised-output-limit", - pid: 2345, - startedAtMs: Date.now(), - stdin, - wait: vi.fn(() => new Promise(() => {})), - cancel: vi.fn(), - }; - }); - - const result = await executePreparedCliRun( - buildPreparedCliRunContext({ - provider: "claude-cli", - model: "sonnet", - runId: "run-live-raised-output-limit", - backend: { - liveSession: "claude-stdio", - reliability: { - outputLimits: { - maxTurnRawChars: 4096, - }, - }, - }, - }), - ); - - expect(result.text).toHaveLength(largeText.length); - expect(result.text).toBe(largeText); - }); - it("reports Claude live session reply backends as streaming until the turn finishes", async () => { let stdoutListener: ((chunk: string) => void) | undefined; let markWriteReady: (() => void) | undefined; diff --git a/src/agents/cli-runner/reliability.ts b/src/agents/cli-runner/reliability.ts index d18a846b712a..b502305296f8 100644 --- a/src/agents/cli-runner/reliability.ts +++ b/src/agents/cli-runner/reliability.ts @@ -57,11 +57,7 @@ function pickWatchdogProfile( })(); return { - noOutputTimeoutMs: - typeof configured?.noOutputTimeoutMs === "number" && - Number.isFinite(configured.noOutputTimeoutMs) - ? Math.max(CLI_WATCHDOG_MIN_TIMEOUT_MS, Math.floor(configured.noOutputTimeoutMs)) - : undefined, + noOutputTimeoutMs: undefined, noOutputTimeoutRatio: ratio, minMs: Math.min(minMs, maxMs), maxMs: Math.max(minMs, maxMs), diff --git a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts index 6983042074c5..ae9714375bae 100644 --- a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts @@ -148,26 +148,8 @@ afterEach(() => { resetLoggerFn(); }); -const makeConfig = (opts?: { - fallbacks?: string[]; - apiKey?: string; - overloadedBackoffMs?: number; - overloadedProfileRotations?: number; -}): OpenClawConfig => +const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): OpenClawConfig => ({ - auth: - opts?.overloadedBackoffMs != null || opts?.overloadedProfileRotations != null - ? { - cooldowns: { - ...(opts?.overloadedBackoffMs != null - ? { overloadedBackoffMs: opts.overloadedBackoffMs } - : {}), - ...(opts?.overloadedProfileRotations != null - ? { overloadedProfileRotations: opts.overloadedProfileRotations } - : {}), - }, - } - : undefined, agents: { defaults: { model: { @@ -993,19 +975,6 @@ describe("runEmbeddedAgent auth profile rotation", () => { } }); - it("uses configured overload backoff before rotating profiles", async () => { - const { usageStats } = await runAutoPinnedRotationCase({ - errorMessage: '{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}', - sessionKey: "agent:test:overloaded-configured-backoff", - runId: "run:overloaded-configured-backoff", - config: makeConfig({ overloadedBackoffMs: 321 }), - }); - expect(typeof usageStats["openai:p2"]?.lastUsed).toBe("number"); - expect(computeBackoffMock).not.toHaveBeenCalled(); - expect(sleepWithAbortMock).toHaveBeenCalledTimes(1); - expect(sleepWithAbortMock).toHaveBeenCalledWith(321, undefined); - }); - it("rotates on timeout without cooling down the timed-out profile", async () => { const { usageStats } = await runAutoPinnedRotationCase({ errorMessage: "request ended without sending any chunks", diff --git a/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts b/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts index 0020ca5af06a..6e1870009ca8 100644 --- a/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts +++ b/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts @@ -281,7 +281,7 @@ describe("runContextEngineMaintenance", () => { reason: "turn", executionMode: "background", sessionManager, - config: { session: { writeLock: { acquireTimeoutMs: 75_000 } } }, + config: {}, }); expect(rewriteTranscriptEntriesInSessionManagerMock).not.toHaveBeenCalled(); @@ -303,7 +303,7 @@ describe("runContextEngineMaintenance", () => { }, ], }, - config: { session: { writeLock: { acquireTimeoutMs: 75_000 } } }, + config: {}, }); }); @@ -436,7 +436,7 @@ describe("runContextEngineMaintenance", () => { tokenBudget: 2048, currentTokenCount: 1536, }, - config: { session: { writeLock: { acquireTimeoutMs: 91_000 } } }, + config: {}, }); expect(result).toBeUndefined(); @@ -460,7 +460,7 @@ describe("runContextEngineMaintenance", () => { }, ], }, - config: { session: { writeLock: { acquireTimeoutMs: 91_000 } } }, + config: {}, }), ); diff --git a/src/agents/embedded-agent-runner/extensions.ts b/src/agents/embedded-agent-runner/extensions.ts index 757a70c1bc8d..0686e2efd518 100644 --- a/src/agents/embedded-agent-runner/extensions.ts +++ b/src/agents/embedded-agent-runner/extensions.ts @@ -193,7 +193,6 @@ export function buildEmbeddedExtensionFactories(params: { defaultTokens: DEFAULT_CONTEXT_TOKENS, }); setCompactionSafeguardRuntime(params.sessionManager, { - maxHistoryShare: compactionCfg?.maxHistoryShare, contextWindowTokens: contextWindowInfo.tokens, identifierPolicy: compactionCfg?.identifierPolicy, identifierInstructions: compactionCfg?.identifierInstructions, diff --git a/src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts b/src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts index 1e6cc6ca0c11..b7b42dd46e6d 100644 --- a/src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts +++ b/src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts @@ -19,15 +19,15 @@ describe("createPostCompactionLoopGuard", () => { expect(verdict.armed).toBe(false); }); - it("arms for the configured window after compaction", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + it("arms for the built-in window after compaction", () => { + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); expect(guard.snapshot().armed).toBe(true); expect(guard.snapshot().remainingAttempts).toBe(3); }); it("decrements remainingAttempts on each observation", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/x" }, "r1")); expect(guard.snapshot().remainingAttempts).toBe(2); @@ -38,10 +38,10 @@ describe("createPostCompactionLoopGuard", () => { expect(guard.snapshot().armed).toBe(false); }); - it("aborts on the windowSize-th identical (tool,args,result) call within the window", () => { + it("aborts on the third identical (tool,args,result) call within the window", () => { // Repeating the same tool, args, and result right after compaction means the // model likely lost progress and is stuck replaying the same recovery step. - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); expect( guard.observe(callOutcome("gateway", { action: "lookup", path: "x" }, "r1")).shouldAbort, @@ -59,7 +59,7 @@ describe("createPostCompactionLoopGuard", () => { }); it("does NOT abort when the result hash changes (progress was made)", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/x" }, "r1")); guard.observe(callOutcome("read", { path: "/x" }, "r2")); @@ -68,7 +68,7 @@ describe("createPostCompactionLoopGuard", () => { }); it("does NOT abort when the args hash changes", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/a" }, "r1")); guard.observe(callOutcome("read", { path: "/b" }, "r1")); @@ -77,28 +77,31 @@ describe("createPostCompactionLoopGuard", () => { }); it("does NOT abort outside the window", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 2 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/x" }, "r1")); guard.observe(callOutcome("read", { path: "/x" }, "r1")); + expect(guard.snapshot().armed).toBe(true); + guard.observe(callOutcome("read", { path: "/y" }, "r2")); expect(guard.snapshot().armed).toBe(false); const after = guard.observe(callOutcome("read", { path: "/x" }, "r1")); expect(after.shouldAbort).toBe(false); }); it("re-arms when armPostCompaction is called again (multiple compactions per run)", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 2 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/x" }, "r1")); + guard.observe(callOutcome("read", { path: "/y" }, "r2")); guard.observe(callOutcome("read", { path: "/x" }, "r1")); expect(guard.snapshot().armed).toBe(false); guard.armPostCompaction(); expect(guard.snapshot().armed).toBe(true); - expect(guard.snapshot().remainingAttempts).toBe(2); + expect(guard.snapshot().remainingAttempts).toBe(3); }); it("respects the parent loop detection disabled state", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }, { enabled: false }); + const guard = createPostCompactionLoopGuard({ enabled: false }); guard.armPostCompaction(); guard.observe(callOutcome("gateway", { x: 1 }, "r1")); guard.observe(callOutcome("gateway", { x: 1 }, "r1")); @@ -106,8 +109,8 @@ describe("createPostCompactionLoopGuard", () => { expect(third.shouldAbort).toBe(false); }); - it("disarms after observing windowSize calls regardless of verdict", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 3 }); + it("disarms after observing the built-in window regardless of verdict", () => { + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/a" }, "r1")); guard.observe(callOutcome("write", { path: "/b" }, "r2")); @@ -134,9 +137,10 @@ describe("PostCompactionLoopPersistedError", () => { }); it("can be built from a guard verdict via fromVerdict", () => { - const guard = createPostCompactionLoopGuard({ windowSize: 2 }); + const guard = createPostCompactionLoopGuard(); guard.armPostCompaction(); guard.observe(callOutcome("read", { path: "/x" }, "r1")); + guard.observe(callOutcome("read", { path: "/x" }, "r1")); const verdict = guard.observe(callOutcome("read", { path: "/x" }, "r1")); expect(verdict.shouldAbort).toBe(true); if (!verdict.shouldAbort) { diff --git a/src/agents/embedded-agent-runner/post-compaction-loop-guard.ts b/src/agents/embedded-agent-runner/post-compaction-loop-guard.ts index c9e32a205715..2375db8a2a34 100644 --- a/src/agents/embedded-agent-runner/post-compaction-loop-guard.ts +++ b/src/agents/embedded-agent-runner/post-compaction-loop-guard.ts @@ -1,7 +1,6 @@ /** * Guards against repeated tool-loop compactions that never make progress. */ -import type { ToolLoopPostCompactionGuardConfig } from "../../config/types.tools.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; /** @@ -45,21 +44,13 @@ type GuardState = { history: PostCompactionGuardObservation[]; }; -function asPositiveInt(value: number | undefined, fallback: number): number { - if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) { - return fallback; - } - return value; -} - /** Creates a stateful post-compaction loop detector for one embedded run. */ -export function createPostCompactionLoopGuard( - config?: ToolLoopPostCompactionGuardConfig, - options?: { enabled?: boolean }, -): PostCompactionLoopGuard { +export function createPostCompactionLoopGuard(options?: { + enabled?: boolean; +}): PostCompactionLoopGuard { const state: GuardState = { enabled: options?.enabled ?? true, - windowSize: asPositiveInt(config?.windowSize, DEFAULT_WINDOW_SIZE), + windowSize: DEFAULT_WINDOW_SIZE, remainingAttempts: 0, history: [], }; diff --git a/src/agents/embedded-agent-runner/run-loop.ts b/src/agents/embedded-agent-runner/run-loop.ts index 9dcbb06d1fa1..6191144639ba 100644 --- a/src/agents/embedded-agent-runner/run-loop.ts +++ b/src/agents/embedded-agent-runner/run-loop.ts @@ -166,11 +166,7 @@ export async function runPreparedEmbeddedLoop( const maxReasoningOnlyRetryAttempts = DEFAULT_REASONING_ONLY_RETRY_LIMIT; const maxEmptyResponseRetryAttempts = DEFAULT_EMPTY_RESPONSE_RETRY_LIMIT; - const MAX_RUN_LOOP_ITERATIONS = resolveMaxRunRetryIterations( - profileCandidates.length, - params.config, - sessionAgentId, - ); + const MAX_RUN_LOOP_ITERATIONS = resolveMaxRunRetryIterations(profileCandidates.length); const contextRecoveryState = createEmbeddedRunContextRecoveryState(); let bootstrapPromptWarningSignaturesSeen = params.bootstrapPromptWarningSignaturesSeen ?? @@ -195,10 +191,9 @@ export async function runPreparedEmbeddedLoop( cfg: params.config, agentId: sessionAgentId, }); - const postCompactionGuard = createPostCompactionLoopGuard( - resolvedLoopDetectionConfig?.postCompactionGuard, - { enabled: resolvedLoopDetectionConfig?.enabled !== false }, - ); + const postCompactionGuard = createPostCompactionLoopGuard({ + enabled: resolvedLoopDetectionConfig?.enabled !== false, + }); let postCompactionAbortController: AbortController | undefined; let postCompactionAbortError: PostCompactionLoopPersistedError | undefined; const attemptTerminalToolPresentation = { diff --git a/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts b/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts index 532de416be97..be9c361bb3b1 100644 --- a/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts +++ b/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts @@ -429,15 +429,14 @@ describe("post-compaction loop guard wired into runEmbeddedAgent", () => { expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); }); - it("disarms after windowSize observations regardless of match, so later identical calls do not abort", async () => { - // Use windowSize: 2 so the guard disarms after 2 observations. + it("disarms after the built-in observation window, so later identical calls do not abort", async () => { const overflowError = makeOverflowError(); // Attempt 1: overflow → triggers compaction. mockedRunEmbeddedAttempt.mockImplementationOnce(async () => makeAttemptResult({ promptError: overflowError }), ); - // Attempt 2 (post-compaction): two distinct records → window full, + // Attempt 2 (post-compaction): three distinct records → window full, // guard disarms with no abort. We then append more identical records // afterwards in this test to confirm they are not observed by the guard. mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => { @@ -445,9 +444,10 @@ describe("post-compaction loop guard wired into runEmbeddedAgent", () => { .onToolOutcome; await executeWrappedToolOutcome("read", { path: "/a" }, "ra", onToolOutcome); await executeWrappedToolOutcome("write", { path: "/b" }, "rb", onToolOutcome); + await executeWrappedToolOutcome("read", { path: "/c" }, "rc", onToolOutcome); return makeAttemptResult({ promptError: null, - toolMetas: [{ toolName: "read" }, { toolName: "write" }], + toolMetas: [{ toolName: "read" }, { toolName: "write" }, { toolName: "read" }], }); }); @@ -459,76 +459,7 @@ describe("post-compaction loop guard wired into runEmbeddedAgent", () => { }), ); - const result = await runEmbeddedAgent({ - ...baseParams, - config: { - tools: { - loopDetection: { - postCompactionGuard: { windowSize: 2 }, - }, - }, - } as never, - }); - - expect(result.meta.error).toBeUndefined(); - expect(mockedCompactDirect).toHaveBeenCalledTimes(1); - expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); - }); - - it("uses the active agent post-compaction guard window over the global default", async () => { - const overflowError = makeOverflowError(); - - mockedRunEmbeddedAttempt.mockImplementationOnce(async () => - makeAttemptResult({ promptError: overflowError }), - ); - mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => { - const onToolOutcome = (attemptParams as { onToolOutcome?: ToolOutcomeObserver }) - .onToolOutcome; - for (let i = 0; i < 3; i += 1) { - await executeWrappedToolOutcome( - "gateway", - { action: "lookup", path: "x" }, - "identical-result", - onToolOutcome, - ); - } - return makeAttemptResult({ - promptError: null, - toolMetas: [{ toolName: "gateway" }, { toolName: "gateway" }, { toolName: "gateway" }], - }); - }); - - mockedCompactDirect.mockResolvedValueOnce( - makeCompactionSuccess({ - summary: "Compacted session", - firstKeptEntryId: "entry-5", - tokensBefore: 150000, - }), - ); - - const result = await runEmbeddedAgent({ - ...baseParams, - agentId: "agent-a", - config: { - tools: { - loopDetection: { - postCompactionGuard: { windowSize: 2 }, - }, - }, - agents: { - list: [ - { - id: "agent-a", - tools: { - loopDetection: { - postCompactionGuard: { windowSize: 4 }, - }, - }, - }, - ], - }, - } as never, - }); + const result = await runEmbeddedAgent(baseParams); expect(result.meta.error).toBeUndefined(); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); @@ -572,7 +503,6 @@ describe("post-compaction loop guard wired into runEmbeddedAgent", () => { tools: { loopDetection: { enabled: false, - postCompactionGuard: { windowSize: 2 }, }, }, } as never, diff --git a/src/agents/embedded-agent-runner/run/attempt.run-decisions.test.ts b/src/agents/embedded-agent-runner/run/attempt.run-decisions.test.ts index debc97eed7f4..8ddea44021ed 100644 --- a/src/agents/embedded-agent-runner/run/attempt.run-decisions.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.run-decisions.test.ts @@ -115,17 +115,5 @@ describe("resolveUnknownToolGuardThreshold", () => { // Unknown-tool guard is a model-safety circuit, separate from configurable // repeated-tool loop detection. expect(resolveUnknownToolGuardThreshold({ enabled: false })).toBe(10); - expect(resolveUnknownToolGuardThreshold({ enabled: false, unknownToolThreshold: 3 })).toBe(3); - }); - - it("uses positive integer thresholds and floors fractions", () => { - expect(resolveUnknownToolGuardThreshold({ enabled: true, unknownToolThreshold: 4 })).toBe(4); - expect(resolveUnknownToolGuardThreshold({ unknownToolThreshold: 3.7 })).toBe(3); - }); - - it("falls back to the default threshold when the override is non-positive", () => { - expect(resolveUnknownToolGuardThreshold({ unknownToolThreshold: 0 })).toBe(10); - expect(resolveUnknownToolGuardThreshold({ unknownToolThreshold: -5 })).toBe(10); - expect(resolveUnknownToolGuardThreshold({ unknownToolThreshold: Number.NaN })).toBe(10); }); }); diff --git a/src/agents/embedded-agent-runner/run/attempt.run-decisions.ts b/src/agents/embedded-agent-runner/run/attempt.run-decisions.ts index 97a416045a66..a5a3d5f67c7c 100644 --- a/src/agents/embedded-agent-runner/run/attempt.run-decisions.ts +++ b/src/agents/embedded-agent-runner/run/attempt.run-decisions.ts @@ -46,10 +46,7 @@ export function resolveAttemptStreamAuthProfileId( * guard. The guard remains active even when generic loop detection is disabled * because an unregistered tool call is an objective dead end for this run. */ -export function resolveUnknownToolGuardThreshold(loopDetection?: { - enabled?: boolean; - unknownToolThreshold?: number; -}): number { +export function resolveUnknownToolGuardThreshold(loopDetection?: { enabled?: boolean }): number { // The unknown-tool guard is a safety net against the model hallucinating a // tool name or calling a tool that has since been removed from the allowlist // (for example after a `skills.allowBundled` config change). After `threshold` @@ -59,10 +56,7 @@ export function resolveUnknownToolGuardThreshold(loopDetection?: { // pingPong / pollNoProgress detectors this guard has no false-positive // surface because the tool is objectively not registered in this run, so it // stays on regardless of `tools.loopDetection.enabled`. - const raw = loopDetection?.unknownToolThreshold; - if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) { - return Math.floor(raw); - } + void loopDetection; return UNKNOWN_TOOL_THRESHOLD; } diff --git a/src/agents/embedded-agent-runner/run/failover-retry-controller.ts b/src/agents/embedded-agent-runner/run/failover-retry-controller.ts index 4f7fb23a61c5..1f54c277b268 100644 --- a/src/agents/embedded-agent-runner/run/failover-retry-controller.ts +++ b/src/agents/embedded-agent-runner/run/failover-retry-controller.ts @@ -39,9 +39,9 @@ export function createEmbeddedRunFailoverRetryController(input: { fallbackConfigured, profileFailureStore, } = input; - const overloadFailoverBackoffMs = resolveOverloadFailoverBackoffMs(params.config); - const overloadProfileRotationLimit = resolveOverloadProfileRotationLimit(params.config); - const rateLimitProfileRotationLimit = resolveRateLimitProfileRotationLimit(params.config); + const overloadFailoverBackoffMs = resolveOverloadFailoverBackoffMs(); + const overloadProfileRotationLimit = resolveOverloadProfileRotationLimit(); + const rateLimitProfileRotationLimit = resolveRateLimitProfileRotationLimit(); let rateLimitProfileRotations = 0; let consecutiveSameModelRateLimitRetries = 0; diff --git a/src/agents/embedded-agent-runner/run/helpers.ts b/src/agents/embedded-agent-runner/run/helpers.ts index 3b4e88ff5710..2f07db2492df 100644 --- a/src/agents/embedded-agent-runner/run/helpers.ts +++ b/src/agents/embedded-agent-runner/run/helpers.ts @@ -1,11 +1,9 @@ /** * Shared run helpers for retry limits, model reporting, and final text. */ -import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { generateSecureToken } from "../../../infra/secure-random.js"; import type { AssistantMessage } from "../../../llm/types.js"; import { extractAssistantTextForPhase } from "../../../shared/chat-message-content.js"; -import { resolveAgentConfig } from "../../agent-scope-config.js"; import { extractAssistantVisibleText } from "../../embedded-agent-utils.js"; import { deriveContextPromptTokens, @@ -53,18 +51,16 @@ export const MAX_SAME_MODEL_RATE_LIMIT_RETRIES = 3; const SAME_MODEL_RATE_LIMIT_BACKOFF_STEP_MS = 10_000; const SAME_MODEL_RATE_LIMIT_MAX_BACKOFF_MS = 60_000; -export function resolveOverloadFailoverBackoffMs(cfg?: OpenClawConfig): number { - return cfg?.auth?.cooldowns?.overloadedBackoffMs ?? DEFAULT_OVERLOAD_FAILOVER_BACKOFF_MS; +export function resolveOverloadFailoverBackoffMs(): number { + return DEFAULT_OVERLOAD_FAILOVER_BACKOFF_MS; } -export function resolveOverloadProfileRotationLimit(cfg?: OpenClawConfig): number { - return cfg?.auth?.cooldowns?.overloadedProfileRotations ?? DEFAULT_MAX_OVERLOAD_PROFILE_ROTATIONS; +export function resolveOverloadProfileRotationLimit(): number { + return DEFAULT_MAX_OVERLOAD_PROFILE_ROTATIONS; } -export function resolveRateLimitProfileRotationLimit(cfg?: OpenClawConfig): number { - return ( - cfg?.auth?.cooldowns?.rateLimitedProfileRotations ?? DEFAULT_MAX_RATE_LIMIT_PROFILE_ROTATIONS - ); +export function resolveRateLimitProfileRotationLimit(): number { + return DEFAULT_MAX_RATE_LIMIT_PROFILE_ROTATIONS; } /** @@ -128,22 +124,11 @@ const MIN_RUN_RETRY_ITERATIONS = 32; const MAX_RUN_RETRY_ITERATIONS = 160; // Defensive guard for the outer run loop across all retry branches. -export function resolveMaxRunRetryIterations( - profileCandidateCount: number, - cfg?: OpenClawConfig, - agentId?: string, -): number { - const configRetries = - (cfg && agentId ? resolveAgentConfig(cfg, agentId)?.runRetries : undefined) ?? - cfg?.agents?.defaults?.runRetries; - - const base = Math.max(1, configRetries?.base ?? BASE_RUN_RETRY_ITERATIONS); - const perProfile = Math.max(0, configRetries?.perProfile ?? RUN_RETRY_ITERATIONS_PER_PROFILE); - const minLimit = Math.max(1, configRetries?.min ?? MIN_RUN_RETRY_ITERATIONS); - const maxLimit = Math.max(minLimit, configRetries?.max ?? MAX_RUN_RETRY_ITERATIONS); - - const scaled = base + Math.max(1, profileCandidateCount) * perProfile; - return Math.min(maxLimit, Math.max(minLimit, scaled)); +export function resolveMaxRunRetryIterations(profileCandidateCount: number): number { + const scaled = + BASE_RUN_RETRY_ITERATIONS + + Math.max(1, profileCandidateCount) * RUN_RETRY_ITERATIONS_PER_PROFILE; + return Math.min(MAX_RUN_RETRY_ITERATIONS, Math.max(MIN_RUN_RETRY_ITERATIONS, scaled)); } export function resolveActiveErrorContext(params: { diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index 5b0150c59ac3..67d4e59f9b17 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -485,8 +485,6 @@ describe("memory search config", () => { onSessionStart: false, onSearch: true, watch: false, - watchDebounceMs: 25, - intervalMinutes: 3, sessions: { deltaBytes: 321, deltaMessages: 7, @@ -502,8 +500,8 @@ describe("memory search config", () => { onSessionStart: false, onSearch: true, watch: false, - watchDebounceMs: 25, - intervalMinutes: 3, + watchDebounceMs: 1500, + intervalMinutes: 0, embeddingBatchTimeoutSeconds: undefined, sessions: { deltaBytes: 321, @@ -543,7 +541,6 @@ describe("memory search config", () => { extensionPath: "/opt/sqlite-vec.dylib", }, }, - chunking: { tokens: 500, overlap: 100 }, query: { maxResults: 4, minScore: 0.2 }, }, }, @@ -552,7 +549,6 @@ describe("memory search config", () => { id: "main", default: true, memorySearch: { - chunking: { tokens: 320 }, query: { maxResults: 8 }, store: { vector: { @@ -567,8 +563,6 @@ describe("memory search config", () => { const resolved = resolveMemorySearchConfig(cfg, "main"); expect(resolved?.provider).toBe("openai"); expect(resolved?.model).toBe("text-embedding-3-small"); - expect(resolved?.chunking.tokens).toBe(320); - expect(resolved?.chunking.overlap).toBe(100); expect(resolved?.query.maxResults).toBe(8); expect(resolved?.query.minScore).toBe(0.2); expect(resolved?.store.vector.enabled).toBe(true); diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index d2a7821fd9eb..97ce903b18bb 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -136,6 +136,7 @@ const DEFAULT_MMR_LAMBDA = 0.7; const DEFAULT_TEMPORAL_DECAY_ENABLED = false; const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30; const DEFAULT_CACHE_ENABLED = true; +const DEFAULT_CACHE_MAX_ENTRIES = undefined; const DEFAULT_SOURCES: Array<"memory" | "sessions"> = ["memory"]; const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; const DEFAULT_REMOTE_BATCH_POLL_INTERVAL_MS = 2_000; @@ -330,8 +331,8 @@ function mergeConfig( vector, }; const chunking = { - tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS, - overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP, + tokens: DEFAULT_CHUNK_TOKENS, + overlap: DEFAULT_CHUNK_OVERLAP, }; const sync = resolveSyncConfig(defaults, overrides); const query = { @@ -343,42 +344,27 @@ function mergeConfig( overrides?.query?.hybrid?.enabled ?? defaults?.query?.hybrid?.enabled ?? DEFAULT_HYBRID_ENABLED, - vectorWeight: - overrides?.query?.hybrid?.vectorWeight ?? - defaults?.query?.hybrid?.vectorWeight ?? - DEFAULT_HYBRID_VECTOR_WEIGHT, - textWeight: - overrides?.query?.hybrid?.textWeight ?? - defaults?.query?.hybrid?.textWeight ?? - DEFAULT_HYBRID_TEXT_WEIGHT, - candidateMultiplier: - overrides?.query?.hybrid?.candidateMultiplier ?? - defaults?.query?.hybrid?.candidateMultiplier ?? - DEFAULT_HYBRID_CANDIDATE_MULTIPLIER, + vectorWeight: DEFAULT_HYBRID_VECTOR_WEIGHT, + textWeight: DEFAULT_HYBRID_TEXT_WEIGHT, + candidateMultiplier: DEFAULT_HYBRID_CANDIDATE_MULTIPLIER, mmr: { enabled: overrides?.query?.hybrid?.mmr?.enabled ?? defaults?.query?.hybrid?.mmr?.enabled ?? DEFAULT_MMR_ENABLED, - lambda: - overrides?.query?.hybrid?.mmr?.lambda ?? - defaults?.query?.hybrid?.mmr?.lambda ?? - DEFAULT_MMR_LAMBDA, + lambda: DEFAULT_MMR_LAMBDA, }, temporalDecay: { enabled: overrides?.query?.hybrid?.temporalDecay?.enabled ?? defaults?.query?.hybrid?.temporalDecay?.enabled ?? DEFAULT_TEMPORAL_DECAY_ENABLED, - halfLifeDays: - overrides?.query?.hybrid?.temporalDecay?.halfLifeDays ?? - defaults?.query?.hybrid?.temporalDecay?.halfLifeDays ?? - DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS, + halfLifeDays: DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS, }, }; const cache = { enabled: overrides?.cache?.enabled ?? defaults?.cache?.enabled ?? DEFAULT_CACHE_ENABLED, - maxEntries: overrides?.cache?.maxEntries ?? defaults?.cache?.maxEntries, + maxEntries: DEFAULT_CACHE_MAX_ENTRIES, }; const overlap = clampNumber(chunking.overlap, 0, Math.max(0, chunking.tokens - 1)); @@ -467,11 +453,8 @@ function resolveSyncConfig( onSessionStart: overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true, onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true, watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true, - watchDebounceMs: - overrides?.sync?.watchDebounceMs ?? - defaults?.sync?.watchDebounceMs ?? - DEFAULT_WATCH_DEBOUNCE_MS, - intervalMinutes: overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0, + watchDebounceMs: DEFAULT_WATCH_DEBOUNCE_MS, + intervalMinutes: 0, embeddingBatchTimeoutSeconds: overrides?.sync?.embeddingBatchTimeoutSeconds ?? defaults?.sync?.embeddingBatchTimeoutSeconds, sessions: { diff --git a/src/agents/model-fallback.run-embedded.e2e.test.ts b/src/agents/model-fallback.run-embedded.e2e.test.ts index 4451f1fe1f55..ff255160a1e3 100644 --- a/src/agents/model-fallback.run-embedded.e2e.test.ts +++ b/src/agents/model-fallback.run-embedded.e2e.test.ts @@ -631,7 +631,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { agentDir, config: { ...makeConfig(), - auth: { cooldowns: { rateLimitedProfileRotations: 0 } }, }, prompt: "hello", provider: "openai", @@ -665,7 +664,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { runId: "run:outer-fallback-suspension", config: { ...makeConfig(), - auth: { cooldowns: { rateLimitedProfileRotations: 0 } }, }, }); @@ -811,45 +809,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { }); }); - it("rethrows AbortError during overload backoff instead of falling through fallback", async () => { - await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - await writeAuthStore(agentDir); - const controller = new AbortController(); - mockPrimaryOverloadedThenFallbackSuccess(); - sleepWithAbortMock.mockImplementationOnce(async () => { - controller.abort(); - throw new Error("aborted"); - }); - - let thrown: unknown; - try { - await runEmbeddedFallback({ - agentDir, - workspaceDir, - sessionKey: "agent:test:overloaded-backoff-abort", - runId: "run:overloaded-backoff-abort", - abortSignal: controller.signal, - config: { - ...makeConfig(), - auth: { cooldowns: { overloadedBackoffMs: 321 } }, - }, - }); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(Error); - expect((thrown as Error).name).toBe("AbortError"); - expect((thrown as Error).message).toBe("Operation aborted"); - - expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(1); - const firstCall = runEmbeddedAttemptMock.mock.calls[0]?.[0] as - | { provider?: string } - | undefined; - expect(firstCall?.provider).toBe("openai"); - }); - }); - it("caps overloaded profile rotations and escalates to cross-provider fallback (#58348)", async () => { // When a provider has multiple auth profiles and all return overloaded_error, // the runner should not exhaust all profiles before falling back. It should @@ -880,27 +839,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { }); }); - it("respects overloadedProfileRotations=0 and falls back immediately", async () => { - await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - await writeMultiProfileAuthStore(agentDir); - mockPrimaryOverloadedThenFallbackSuccess(); - - const result = await runEmbeddedFallback({ - agentDir, - workspaceDir, - sessionKey: "agent:test:overloaded-no-rotation", - runId: "run:overloaded-no-rotation", - config: { - ...makeConfig(), - auth: { cooldowns: { overloadedProfileRotations: 0 } }, - }, - }); - - expect(result.provider).toBe("groq"); - expectProviderAttemptCounts({ openai: 1, groq: 1 }); - }); - }); - it("caps rate-limit profile rotations and escalates to cross-provider fallback (#58572)", async () => { await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { await writeMultiProfileAuthStore(agentDir); @@ -935,7 +873,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { runId: "run:rate-limit-retry-limit-fallback", config: { ...makeConfig(), - auth: { cooldowns: { rateLimitedProfileRotations: 999 } }, }, }); @@ -947,28 +884,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { }); }); - it("respects rateLimitedProfileRotations=0 and falls back immediately", async () => { - await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - await writeMultiProfileAuthStore(agentDir); - - mockPrimaryErrorThenFallbackSuccess(RATE_LIMIT_ERROR_MESSAGE); - - const result = await runEmbeddedFallback({ - agentDir, - workspaceDir, - sessionKey: "agent:test:rate-limit-no-rotation", - runId: "run:rate-limit-no-rotation", - config: { - ...makeConfig(), - auth: { cooldowns: { rateLimitedProfileRotations: 0 } }, - }, - }); - - expect(result.provider).toBe("groq"); - expectProviderAttemptCounts({ openai: 1, groq: 1 }); - }); - }); - it("caps prompt-side rate-limit profile rotations before cross-provider fallback", async () => { await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { await writeMultiProfileAuthStore(agentDir); @@ -1023,26 +938,4 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { expect(primaryCalls.map((params) => params.modelId)).toStrictEqual(["mock-1", "mock-1"]); }); }); - - it("respects prompt-side rateLimitedProfileRotations=0 and falls back immediately", async () => { - await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - await writeMultiProfileAuthStore(agentDir); - - mockPrimaryPromptErrorThenFallbackSuccess(RATE_LIMIT_ERROR_MESSAGE); - - const result = await runEmbeddedFallback({ - agentDir, - workspaceDir, - sessionKey: "agent:test:prompt-rate-limit-no-rotation", - runId: "run:prompt-rate-limit-no-rotation", - config: { - ...makeConfig(), - auth: { cooldowns: { rateLimitedProfileRotations: 0 } }, - }, - }); - - expect(result.provider).toBe("groq"); - expectProviderAttemptCounts({ openai: 1, groq: 1 }); - }); - }); }); diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 6b7019196c89..9877fc425841 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -26,7 +26,6 @@ vi.mock("../config/config.js", () => ({ session: { mainKey: "main", scope: "per-sender", - agentToAgent: { maxPingPongTurns: 2 }, }, tools: { // Keep sessions tools permissive in this suite; dedicated visibility tests cover defaults. @@ -54,7 +53,6 @@ const TEST_CONFIG = { session: { mainKey: "main", scope: "per-sender", - agentToAgent: { maxPingPongTurns: 2 }, }, tools: { sessions: { visibility: "all" }, @@ -1370,13 +1368,13 @@ describe("sessions tools", () => { expect(waitedDetails.reply).toBe("initial"); await vi.waitFor( () => { - expect(countMatching(calls, (call) => call.method === "agent")).toBe(3); + expect(countMatching(calls, (call) => call.method === "agent")).toBe(6); }, { timeout: 2_000, interval: 5 }, ); const agentCalls = calls.filter((call) => call.method === "agent"); - expect(agentCalls).toHaveLength(3); + expect(agentCalls).toHaveLength(6); for (const call of agentCalls) { const params = agentParams(call); expect(params.lane).toMatch(/^nested(?::|$)/); @@ -1392,7 +1390,7 @@ describe("sessions tools", () => { "Agent-to-agent reply step", ), ); - expect(replySteps).toHaveLength(2); + expect(replySteps).toHaveLength(5); expect(sendParams.to).toBe("group:target"); expect(sendParams.channel).toBe("discord"); expect(sendParams.message).toBe("announce now"); @@ -1463,7 +1461,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 1 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); @@ -1552,7 +1549,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 0 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); @@ -1616,7 +1612,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 0 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); @@ -1672,7 +1667,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 0 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); @@ -1749,7 +1743,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 0 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); @@ -1827,7 +1820,6 @@ describe("sessions tools", () => { ...TEST_CONFIG, session: { ...TEST_CONFIG.session, - agentToAgent: { maxPingPongTurns: 0 }, }, }, }).find((candidate) => candidate.name === "sessions_send"); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index ac6cb553dc70..d5eb330d7080 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -106,7 +106,7 @@ async function executeBoundAccountSpawn(params: { let spawnAccountId: string | undefined; setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, - messages: { queue: { debounceMs: 0 } }, + messages: { queue: {} }, agents: { defaults: { subagents: { allowAgents: ["bot-alpha"] } }, list: [{ id: "main" }, { id: "bot-alpha" }], @@ -173,9 +173,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { scope: "per-sender", }, messages: { - queue: { - debounceMs: 0, - }, + queue: {}, }, agents: { defaults: { @@ -298,7 +296,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { }); setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, - messages: { queue: { debounceMs: 0 } }, + messages: { queue: {} }, agents: { defaults: { subagents: { diff --git a/src/agents/session-suspension.test.ts b/src/agents/session-suspension.test.ts index 2833577e34d3..e4d730e883ef 100644 --- a/src/agents/session-suspension.test.ts +++ b/src/agents/session-suspension.test.ts @@ -88,26 +88,6 @@ describe("session suspension", () => { ); }); - it("auto-resumes cron lanes to configured and clamped cron concurrency", async () => { - vi.useFakeTimers(); - - await suspendLane(100, { cron: { maxConcurrentRuns: 3 } } as OpenClawConfig, CommandLane.Cron); - await vi.advanceTimersByTimeAsync(100); - - expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenLastCalledWith( - CommandLane.Cron, - 3, - ); - - await suspendLane(100, { cron: { maxConcurrentRuns: 0 } } as OpenClawConfig, CommandLane.Cron); - await vi.advanceTimersByTimeAsync(100); - - expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenLastCalledWith( - CommandLane.Cron, - 1, - ); - }); - it("clamps oversized suspension TTLs for timers and persisted resume time", async () => { // Persisted expectedResumeBy must match the clamped timer, not MAX_SAFE_INTEGER. vi.useFakeTimers(); diff --git a/src/agents/session-suspension.ts b/src/agents/session-suspension.ts index 964a6d528198..53fa5eda96e5 100644 --- a/src/agents/session-suspension.ts +++ b/src/agents/session-suspension.ts @@ -129,7 +129,7 @@ function resolveLaneResumeConcurrency(cfg: OpenClawConfig | undefined, laneId: s return resolveSubagentMaxConcurrent(cfg); case "cron": case "cron-nested": - return resolveCronMaxConcurrentRuns(cfg?.cron); + return resolveCronMaxConcurrentRuns(); default: return DEFAULT_CUSTOM_LANE_RESUME_CONCURRENCY; } diff --git a/src/agents/session-write-lock.test.ts b/src/agents/session-write-lock.test.ts index 8925b52596e8..a7e66a7b454b 100644 --- a/src/agents/session-write-lock.test.ts +++ b/src/agents/session-write-lock.test.ts @@ -699,56 +699,17 @@ describe("acquireSessionWriteLock", () => { it("resolves the session write-lock acquire timeout", () => { expect(resolveSessionWriteLockAcquireTimeoutMs()).toBe(60_000); - expect( - resolveSessionWriteLockAcquireTimeoutMs({ - session: { writeLock: { acquireTimeoutMs: 90_000 } }, - }), - ).toBe(90_000); - expect( - resolveSessionWriteLockAcquireTimeoutMs({ - session: { writeLock: { acquireTimeoutMs: 0 } }, - }), - ).toBe(60_000); }); - it("resolves session write-lock stale and max-hold policy", () => { + it("lets session write-lock env override built-in defaults for emergency tuning", () => { expect( - resolveSessionWriteLockOptions({ - session: { - writeLock: { - acquireTimeoutMs: 90_000, - staleMs: 45_000, - maxHoldMs: 30_000, - }, + resolveSessionWriteLockOptions(undefined, { + env: { + OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS: "120000", + OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "60000", + OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS: "50000", }, }), - ).toEqual({ - timeoutMs: 90_000, - staleMs: 45_000, - maxHoldMs: 30_000, - }); - }); - - it("lets session write-lock env override config for emergency tuning", () => { - expect( - resolveSessionWriteLockOptions( - { - session: { - writeLock: { - acquireTimeoutMs: 90_000, - staleMs: 45_000, - maxHoldMs: 30_000, - }, - }, - }, - { - env: { - OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS: "120000", - OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "60000", - OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS: "50000", - }, - }, - ), ).toEqual({ timeoutMs: 120_000, staleMs: 60_000, @@ -758,28 +719,17 @@ describe("acquireSessionWriteLock", () => { it("ignores non-decimal and unsafe session write-lock env values", () => { expect( - resolveSessionWriteLockOptions( - { - session: { - writeLock: { - acquireTimeoutMs: 90_000, - staleMs: 45_000, - maxHoldMs: 30_000, - }, - }, + resolveSessionWriteLockOptions(undefined, { + env: { + OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS: "1e3", + OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "0x1000", + OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS: "9007199254740993", }, - { - env: { - OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS: "1e3", - OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "0x1000", - OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS: "9007199254740993", - }, - }, - ), + }), ).toEqual({ - timeoutMs: 90_000, - staleMs: 45_000, - maxHoldMs: 30_000, + timeoutMs: 60_000, + staleMs: 30 * 60_000, + maxHoldMs: 5 * 60_000, }); }); @@ -822,18 +772,8 @@ describe("acquireSessionWriteLock", () => { "utf8", ); - const configOnly = await cleanStaleLockFiles({ - sessionsDir, - config: { session: { writeLock: { staleMs: 30_000 } } }, - nowMs, - removeStale: false, - readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"], - }); - expect(configOnly.locks[0]?.stale).toBe(true); - const envOverride = await cleanStaleLockFiles({ sessionsDir, - config: { session: { writeLock: { staleMs: 30_000 } } }, env: { OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "60000" }, nowMs, removeStale: false, diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index 92f7989627ba..0204c4e2d3bf 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -9,6 +9,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { parseSqliteSessionFileMarker } from "../config/sessions/sqlite-marker.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createFileLockManager } from "../infra/file-lock-manager.js"; import { readGatewayProcessArgsSync as readProcessArgsSync } from "../infra/gateway-processes.js"; import { getProcessStartTime, isPidAlive } from "../shared/pid-alive.js"; @@ -96,15 +97,7 @@ function isFileLockError(error: unknown, code: string): boolean { return (error as { code?: unknown } | null)?.code === code; } -export type SessionWriteLockAcquireTimeoutConfig = { - session?: { - writeLock?: { - acquireTimeoutMs?: number; - staleMs?: number; - maxHoldMs?: number; - }; - }; -}; +export type SessionWriteLockAcquireTimeoutConfig = OpenClawConfig; type SessionWriteLockMsKey = "acquireTimeoutMs" | "staleMs" | "maxHoldMs"; @@ -153,7 +146,6 @@ function parsePositiveMs( } function resolveSessionWriteLockMs(params: { - config?: SessionWriteLockAcquireTimeoutConfig; env?: NodeJS.ProcessEnv; key: SessionWriteLockMsKey; fallback: number; @@ -162,17 +154,15 @@ function resolveSessionWriteLockMs(params: { const opts = { allowInfinity: params.allowInfinity }; return ( readPositiveMsEnv(params.env ?? process.env, SESSION_WRITE_LOCK_ENV[params.key], opts) ?? - parsePositiveMs(params.config?.session?.writeLock?.[params.key], opts) ?? params.fallback ); } export function resolveSessionWriteLockAcquireTimeoutMs( - config?: SessionWriteLockAcquireTimeoutConfig, + _config?: SessionWriteLockAcquireTimeoutConfig, env?: NodeJS.ProcessEnv, ): number { return resolveSessionWriteLockMs({ - config, env, key: "acquireTimeoutMs", fallback: DEFAULT_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS, @@ -181,11 +171,10 @@ export function resolveSessionWriteLockAcquireTimeoutMs( } export function resolveSessionWriteLockStaleMs( - config?: SessionWriteLockAcquireTimeoutConfig, + _config?: SessionWriteLockAcquireTimeoutConfig, env?: NodeJS.ProcessEnv, ): number { return resolveSessionWriteLockMs({ - config, env, key: "staleMs", fallback: DEFAULT_SESSION_WRITE_LOCK_STALE_MS, @@ -193,11 +182,10 @@ export function resolveSessionWriteLockStaleMs( } function resolveSessionWriteLockMaxHoldMs( - config?: SessionWriteLockAcquireTimeoutConfig, + _config?: SessionWriteLockAcquireTimeoutConfig, params: { env?: NodeJS.ProcessEnv; fallback?: number } = {}, ): number { return resolveSessionWriteLockMs({ - config, env: params.env, key: "maxHoldMs", fallback: params.fallback ?? DEFAULT_SESSION_WRITE_LOCK_MAX_HOLD_MS, diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 68dd2dcb757b..a6396f2754f4 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -781,7 +781,6 @@ describe("deliverSubagentAnnouncement active requester steering", () => { messages: { queue: { mode: params.mode ?? "followup", - debounceMs: 0, }, }, }) as never, @@ -898,7 +897,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { "child done", { steeringMode: "all", - debounceMs: 0, + debounceMs: 500, waitForTranscriptCommit: true, deliveryTimeoutMs: 120_000, }, @@ -909,7 +908,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { "child done", { steeringMode: "all", - debounceMs: 0, + debounceMs: 500, deliveryTimeoutMs: 120_000, }, ); @@ -940,7 +939,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { const retryOptions = mockCallArg(queueEmbeddedAgentMessageWithOutcome, 1, 2); expectRecordFields(retryOptions, { steeringMode: "all", - debounceMs: 0, + debounceMs: 500, waitForTranscriptCommit: true, }); expect(retryOptions.deliveryTimeoutMs).toBeGreaterThan(0); @@ -1024,7 +1023,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { const retryOptions = mockCallArg(queueEmbeddedAgentMessageWithOutcome, 1, 2); expectRecordFields(retryOptions, { steeringMode: "all", - debounceMs: 0, + debounceMs: 500, waitForTranscriptCommit: true, }); expect(retryOptions.deliveryTimeoutMs).toBeGreaterThan(0); @@ -1055,7 +1054,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { queueEmbeddedAgentMessageWithOutcome, getRuntimeConfig: () => ({ - messages: { queue: { mode: "steer", debounceMs: 0 } }, + messages: { queue: { mode: "steer" } }, }) as never, }); @@ -1095,7 +1094,6 @@ describe("deliverSubagentAnnouncement active requester steering", () => { messages: { queue: { mode: "steer", - debounceMs: 0, }, }, }) as never, @@ -1145,7 +1143,6 @@ describe("deliverSubagentAnnouncement active requester steering", () => { messages: { queue: { mode: "steer", - debounceMs: 0, }, }, }) as never, @@ -1199,7 +1196,6 @@ describe("deliverSubagentAnnouncement active requester steering", () => { messages: { queue: { mode: "steer", - debounceMs: 0, }, }, }) as never, diff --git a/src/agents/subagent-announce.live.test.ts b/src/agents/subagent-announce.live.test.ts index 24eaadec2fb7..40dc566b9984 100644 --- a/src/agents/subagent-announce.live.test.ts +++ b/src/agents/subagent-announce.live.test.ts @@ -304,7 +304,7 @@ describeLive("subagent announce live", () => { }); await state.writeConfig( liveSubagentConfig(modelKey, state.workspaceDir, port, token, { - queue: { mode: "collect", debounceMs: 2_500 }, + queue: { mode: "collect" }, toolAllow: ["sessions_spawn", "bash"], }), ); diff --git a/src/agents/tool-loop-detection-config.ts b/src/agents/tool-loop-detection-config.ts index 1ff175bb5836..a838a46d00f6 100644 --- a/src/agents/tool-loop-detection-config.ts +++ b/src/agents/tool-loop-detection-config.ts @@ -1,7 +1,7 @@ /** * Tool loop-detection config resolver. * Overlays per-agent loop detection settings on global tool defaults while - * preserving nested detector and post-compaction guard fields. + * preserving the per-agent enabled override. */ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ToolLoopDetectionConfig } from "../config/types.tools.js"; @@ -25,16 +25,5 @@ export function resolveToolLoopDetectionConfig(params: { return agent; } - return { - ...global, - ...agent, - detectors: { - ...global.detectors, - ...agent.detectors, - }, - postCompactionGuard: { - ...global.postCompactionGuard, - ...agent.postCompactionGuard, - }, - }; + return { enabled: agent.enabled ?? global.enabled }; } diff --git a/src/agents/tool-loop-detection.test.ts b/src/agents/tool-loop-detection.test.ts index b9fc977bac94..1b041c7dbe4e 100644 --- a/src/agents/tool-loop-detection.test.ts +++ b/src/agents/tool-loop-detection.test.ts @@ -45,11 +45,6 @@ function recordArgsHash(toolName: string, params: unknown): string { const enabledLoopDetectionConfig: ToolLoopDetectionConfig = { enabled: true }; -const shortHistoryLoopConfig: ToolLoopDetectionConfig = { - enabled: true, - historySize: 4, -}; - function recordSuccessfulCall( state: SessionState, toolName: string, @@ -310,17 +305,6 @@ describe("tool-loop-detection", () => { expect(state.toolCallHistory?.[0]?.runId).toBe("run-1"); }); - - it("respects configured historySize", () => { - const state = createState(); - - for (let i = 0; i < 10; i += 1) { - recordToolCall(state, "tool", { iteration: i }, `call-${i}`, shortHistoryLoopConfig); - } - - expect(state.toolCallHistory).toHaveLength(4); - expect(state.toolCallHistory?.[0]?.argsHash).toBe(recordArgsHash("tool", { iteration: 6 })); - }); }); describe("detectToolCallLoop", () => { @@ -443,73 +427,6 @@ describe("tool-loop-detection", () => { } }); - it("applies custom thresholds when detection is enabled", () => { - const state = createState(); - const { params, result } = createNoProgressPollFixture("sess-custom"); - const config: ToolLoopDetectionConfig = { - enabled: true, - warningThreshold: 2, - criticalThreshold: 4, - detectors: { - genericRepeat: false, - knownPollNoProgress: true, - pingPong: false, - }, - }; - - recordRepeatedSuccessfulCalls({ - state, - toolName: "process", - toolParams: params, - result, - count: 2, - }); - const warningResult = detectToolCallLoop(state, "process", params, config); - expect(warningResult.stuck).toBe(true); - if (warningResult.stuck) { - expect(warningResult.level).toBe("warning"); - } - - recordRepeatedSuccessfulCalls({ - state, - toolName: "process", - toolParams: params, - result, - count: 2, - startIndex: 2, - }); - const criticalResult = detectToolCallLoop(state, "process", params, config); - expect(criticalResult.stuck).toBe(true); - if (criticalResult.stuck) { - expect(criticalResult.level).toBe("critical"); - expect(criticalResult.detector).toBe("known_poll_no_progress"); - } - }); - - it("can disable specific detectors", () => { - const state = createState(); - const { params, result } = createNoProgressPollFixture("sess-no-detectors"); - const config: ToolLoopDetectionConfig = { - enabled: true, - detectors: { - genericRepeat: false, - knownPollNoProgress: false, - pingPong: false, - }, - }; - - recordRepeatedSuccessfulCalls({ - state, - toolName: "process", - toolParams: params, - result, - count: CRITICAL_THRESHOLD, - }); - - const loopResult = detectToolCallLoop(state, "process", params, config); - expect(loopResult.stuck).toBe(false); - }); - it("warns for known polling no-progress loops", () => { const { params, result } = createNoProgressPollFixture("sess-1"); const loopResult = detectLoopAfterRepeatedCalls({ @@ -565,10 +482,7 @@ describe("tool-loop-detection", () => { toolParams: fixture.params, result: fixture.result, count: GLOBAL_CIRCUIT_BREAKER_THRESHOLD, - config: { - enabled: true, - detectors: { genericRepeat: false, knownPollNoProgress: true, pingPong: true }, - }, + config: enabledLoopDetectionConfig, }); expect(loopResult.stuck).toBe(true); if (loopResult.stuck) { @@ -866,7 +780,7 @@ describe("tool-loop-detection", () => { it("returns the recorded call when a pre-recorded tool call receives its result", () => { const state = createState(); - const params = { action: "lookup", path: "cron.maxConcurrentRuns" }; + const params = { action: "lookup", path: "cron.enabled" }; recordToolCall(state, "gateway", params, "call-1"); diff --git a/src/agents/tool-loop-detection.ts b/src/agents/tool-loop-detection.ts index 83faba9c9f9e..f8ddd9aeefac 100644 --- a/src/agents/tool-loop-detection.ts +++ b/src/agents/tool-loop-detection.ts @@ -81,52 +81,15 @@ function selectHistoryForScope( return history.filter((record) => normalizeRunId(record.runId) === runId); } -function asPositiveInt(value: number | undefined, fallback: number): number { - if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) { - return fallback; - } - return value; -} - function resolveLoopDetectionConfig(config?: ToolLoopDetectionConfig): ResolvedLoopDetectionConfig { - const warningThreshold = asPositiveInt( - config?.warningThreshold, - DEFAULT_LOOP_DETECTION_CONFIG.warningThreshold, - ); - let criticalThreshold = asPositiveInt( - config?.criticalThreshold, - DEFAULT_LOOP_DETECTION_CONFIG.criticalThreshold, - ); - let globalCircuitBreakerThreshold = asPositiveInt( - config?.globalCircuitBreakerThreshold, - DEFAULT_LOOP_DETECTION_CONFIG.globalCircuitBreakerThreshold, - ); - - if (criticalThreshold <= warningThreshold) { - criticalThreshold = warningThreshold + 1; - } - if (globalCircuitBreakerThreshold <= criticalThreshold) { - globalCircuitBreakerThreshold = criticalThreshold + 1; - } - return { enabled: config?.enabled ?? DEFAULT_LOOP_DETECTION_CONFIG.enabled, - historySize: asPositiveInt(config?.historySize, DEFAULT_LOOP_DETECTION_CONFIG.historySize), - warningThreshold, - unknownToolThreshold: asPositiveInt( - config?.unknownToolThreshold, - DEFAULT_LOOP_DETECTION_CONFIG.unknownToolThreshold, - ), - criticalThreshold, - globalCircuitBreakerThreshold, - detectors: { - genericRepeat: - config?.detectors?.genericRepeat ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.genericRepeat, - knownPollNoProgress: - config?.detectors?.knownPollNoProgress ?? - DEFAULT_LOOP_DETECTION_CONFIG.detectors.knownPollNoProgress, - pingPong: config?.detectors?.pingPong ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.pingPong, - }, + historySize: DEFAULT_LOOP_DETECTION_CONFIG.historySize, + warningThreshold: DEFAULT_LOOP_DETECTION_CONFIG.warningThreshold, + unknownToolThreshold: DEFAULT_LOOP_DETECTION_CONFIG.unknownToolThreshold, + criticalThreshold: DEFAULT_LOOP_DETECTION_CONFIG.criticalThreshold, + globalCircuitBreakerThreshold: DEFAULT_LOOP_DETECTION_CONFIG.globalCircuitBreakerThreshold, + detectors: DEFAULT_LOOP_DETECTION_CONFIG.detectors, }; } @@ -818,4 +781,3 @@ export function recordToolCallOutcome( } return recordedOutcome; } -/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/agents/tools/sessions-send-helpers.test.ts b/src/agents/tools/sessions-send-helpers.test.ts index 46b8e58f7651..16420664a8b9 100644 --- a/src/agents/tools/sessions-send-helpers.test.ts +++ b/src/agents/tools/sessions-send-helpers.test.ts @@ -74,27 +74,8 @@ describe("resolveAnnounceTargetFromKey", () => { }); describe("resolvePingPongTurns", () => { - it("defaults to 5 when unset", () => { - expect(resolvePingPongTurns(undefined)).toBe(5); - expect(resolvePingPongTurns({ session: {} } as never)).toBe(5); - }); - - it("uses configured values through the 20-turn ceiling", () => { - expect( - resolvePingPongTurns({ session: { agentToAgent: { maxPingPongTurns: 10 } } } as never), - ).toBe(10); - expect( - resolvePingPongTurns({ session: { agentToAgent: { maxPingPongTurns: 20 } } } as never), - ).toBe(20); - }); - - it("keeps defensive floor and ceiling clamps", () => { - expect( - resolvePingPongTurns({ session: { agentToAgent: { maxPingPongTurns: -1 } } } as never), - ).toBe(0); - expect( - resolvePingPongTurns({ session: { agentToAgent: { maxPingPongTurns: 50 } } } as never), - ).toBe(20); + it("uses the fixed five-turn limit", () => { + expect(resolvePingPongTurns()).toBe(5); }); }); diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 13d1adcda8eb..d83c5715df94 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -9,7 +9,6 @@ import { } from "../../channels/plugins/index.js"; import { resolveSessionConversationRef } from "../../channels/plugins/session-conversation.js"; import { normalizeChannelId as normalizeChatChannelId } from "../../channels/registry.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { ANNOUNCE_SKIP_TOKEN, REPLY_SKIP_TOKEN } from "./sessions-send-tokens.js"; export { isAnnounceSkip, @@ -130,13 +129,7 @@ export function buildAgentToAgentAnnounceContext(params: { return lines.join("\n"); } -/** Resolves the configured A2A ping-pong turn limit with a hard runtime cap. */ -export function resolvePingPongTurns(cfg?: OpenClawConfig) { - const raw = cfg?.session?.agentToAgent?.maxPingPongTurns; - const fallback = DEFAULT_AGENTNG_PONG_TURNS; - if (typeof raw !== "number" || !Number.isFinite(raw)) { - return fallback; - } - const rounded = Math.floor(raw); - return Math.max(0, Math.min(MAX_PING_PONG_TURNS, rounded)); +/** Resolves the fixed A2A ping-pong turn limit with a hard runtime cap. */ +export function resolvePingPongTurns() { + return Math.min(MAX_PING_PONG_TURNS, DEFAULT_AGENTNG_PONG_TURNS); } diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index f322b09ec680..fc9702ad5225 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -700,7 +700,7 @@ export function createSessionsSendTool(opts?: { extraSystemPrompt: agentMessageContext, inputProvenance, }; - const maxPingPongTurns = resolvePingPongTurns(cfg); + const maxPingPongTurns = resolvePingPongTurns(); // Skip the A2A ping-pong + announce flow when the current caller is the // parent of a parent-owned child session it spawned itself and another diff --git a/src/agents/tools/sessions.test.ts b/src/agents/tools/sessions.test.ts index 2d012594c888..7cf4f09562b9 100644 --- a/src/agents/tools/sessions.test.ts +++ b/src/agents/tools/sessions.test.ts @@ -241,7 +241,7 @@ async function executeFireAndForgetA2AFrom(requesterSessionKey: string) { vi.mocked(runSessionsSendA2AFlow).mockClear(); const targetSessionKey = "agent:other:discord:group:ops"; loadConfigMock.mockReturnValue({ - session: { scope: "per-sender", mainKey: "main", agentToAgent: { maxPingPongTurns: 5 } }, + session: { scope: "per-sender", mainKey: "main" }, tools: { agentToAgent: { enabled: true }, sessions: { visibility: "all" }, diff --git a/src/agents/tools/transcripts-tool.test.ts b/src/agents/tools/transcripts-tool.test.ts index 1622261e714f..0eca605607f0 100644 --- a/src/agents/tools/transcripts-tool.test.ts +++ b/src/agents/tools/transcripts-tool.test.ts @@ -449,10 +449,14 @@ describe("transcripts tool", () => { }); it("bounds summary input while retaining the full transcript", async () => { - // Summary generation uses a bounded utterance window, but the durable JSONL - // transcript must retain every utterance. + // Exercise the fixed 2,000-utterance summary window while proving the + // durable transcript still retains the complete import. const stateDir = await makeStateDir(); - const { tool } = await createHarness(stateDir, { maxUtterances: 1 }); + const { tool } = await createHarness(stateDir); + const transcript = Array.from( + { length: 2_001 }, + (_, index) => `Alex: transcript line ${index}`, + ).join("\n"); await tool.execute( "call-1", @@ -461,8 +465,7 @@ describe("transcripts tool", () => { providerId: "manual-transcript", sessionId: "long-meeting", title: "Long meeting", - transcript: - "Alex: Action item: write the first draft.\nSam: Decision: ship the final plan.", + transcript, }, undefined, vi.fn(), @@ -472,16 +475,14 @@ describe("transcripts tool", () => { path.join(stateDir, "transcripts", currentDateDir(), "long-meeting", "summary.md"), "utf8", ); - expect(summary).toContain("Decision: ship the final plan."); - expect(summary).not.toContain("Action item: write the first draft."); - expect(summary).toContain("## Transcript"); - expect(summary).toContain("Sam: Decision: ship the final plan."); - const transcript = await fs.readFile( + expect(summary).not.toContain("transcript line 0\n"); + expect(summary).toContain("transcript line 2000"); + const storedTranscript = await fs.readFile( path.join(stateDir, "transcripts", currentDateDir(), "long-meeting", "transcript.jsonl"), "utf8", ); - expect(transcript).toContain("Action item: write the first draft."); - expect(transcript).toContain("Decision: ship the final plan."); + expect(storedTranscript).toContain("transcript line 0"); + expect(storedTranscript).toContain("transcript line 2000"); }); it("requires date-qualified selectors for repeated stored session ids", async () => { diff --git a/src/agents/worktrees/service.test.ts b/src/agents/worktrees/service.test.ts index b67f3794c2dc..3127e2b58404 100644 --- a/src/agents/worktrees/service.test.ts +++ b/src/agents/worktrees/service.test.ts @@ -1112,17 +1112,8 @@ describe("ManagedWorktreeService", () => { expect(getRegistryWorktree(env, created.id)?.removedAt).toBeUndefined(); }); - it("maps cleanup config onto limits with 0 and unset disabling each bound", () => { - expect(resolveWorktreeCleanupLimits(undefined)).toEqual({}); - expect(resolveWorktreeCleanupLimits({ cleanup: { maxCount: 0, maxTotalSizeGb: 0 } })).toEqual( - {}, - ); - expect(resolveWorktreeCleanupLimits({ cleanup: { maxCount: 25, maxTotalSizeGb: 50 } })).toEqual( - { maxCount: 25, maxTotalSizeBytes: 50 * 1024 ** 3 }, - ); - expect(resolveWorktreeCleanupLimits({ cleanup: { maxTotalSizeGb: 0.5 } })).toEqual({ - maxTotalSizeBytes: 512 * 1024 ** 2, - }); + it("uses the fixed no-limit cleanup policy", () => { + expect(resolveWorktreeCleanupLimits()).toEqual({}); }); it("prunes expired snapshot refs and registry rows", async () => { diff --git a/src/agents/worktrees/service.ts b/src/agents/worktrees/service.ts index b5d60b410dce..9de96e1cbb78 100644 --- a/src/agents/worktrees/service.ts +++ b/src/agents/worktrees/service.ts @@ -87,21 +87,9 @@ type ManagedWorktreeGcParams = { limits?: WorktreeCleanupLimits; }; -/** - * Maps `worktrees.cleanup` config into enforceable byte/count limits. - * 0 and unset both mean "no limit", so gc callers can pass the result verbatim. - */ -export function resolveWorktreeCleanupLimits(config?: { - cleanup?: { maxCount?: number; maxTotalSizeGb?: number }; -}): WorktreeCleanupLimits { - const maxCount = config?.cleanup?.maxCount; - const maxTotalSizeGb = config?.cleanup?.maxTotalSizeGb; - return { - ...(typeof maxCount === "number" && maxCount > 0 ? { maxCount: Math.floor(maxCount) } : {}), - ...(typeof maxTotalSizeGb === "number" && maxTotalSizeGb > 0 - ? { maxTotalSizeBytes: Math.round(maxTotalSizeGb * 1024 ** 3) } - : {}), - }; +/** Returns the default no-limit policy for age-based managed-worktree cleanup. */ +export function resolveWorktreeCleanupLimits(): WorktreeCleanupLimits { + return {}; } function resultMessage(result: GitResult): string { @@ -994,7 +982,7 @@ export class ManagedWorktreeService { } /** - * Enforces configured count/size retention across all live managed worktrees. + * Enforces optional count/size retention across all live managed worktrees. * Manual worktrees count toward the totals but are never limit-evicted, so a * limit can stay exceeded when only protected worktrees remain. */ diff --git a/src/auto-reply/reply/acp-projector.test.ts b/src/auto-reply/reply/acp-projector.test.ts index 63319524ebd0..3a6552046b0e 100644 --- a/src/auto-reply/reply/acp-projector.test.ts +++ b/src/auto-reply/reply/acp-projector.test.ts @@ -51,8 +51,6 @@ function createHiddenBoundaryCfg( streamOverrides: Record = {}, ): Parameters[0] { return createLiveCfgOverrides({ - coalesceIdleMs: 0, - maxChunkChars: 256, ...streamOverrides, }); } @@ -77,8 +75,6 @@ function createFinalOnlyStatusToolHarness() { acp: { enabled: true, stream: { - coalesceIdleMs: 0, - maxChunkChars: 512, deliveryMode: "final_only", tagVisibility: { available_commands_update: true, @@ -89,12 +85,7 @@ function createFinalOnlyStatusToolHarness() { }); } -function createLiveToolLifecycleHarness(params?: { - coalesceIdleMs?: number; - maxChunkChars?: number; - maxSessionUpdateChars?: number; - repeatSuppression?: boolean; -}) { +function createLiveToolLifecycleHarness(params?: { repeatSuppression?: boolean }) { return createProjectorHarness({ acp: { enabled: true, @@ -110,11 +101,7 @@ function createLiveToolLifecycleHarness(params?: { }); } -function createLiveStatusAndToolLifecycleHarness(params?: { - coalesceIdleMs?: number; - maxChunkChars?: number; - repeatSuppression?: boolean; -}) { +function createLiveStatusAndToolLifecycleHarness(params?: { repeatSuppression?: boolean }) { return createProjectorHarness({ acp: { enabled: true, @@ -321,7 +308,6 @@ describe("createAcpReplyProjector", () => { enabled: true, stream: { deliveryMode: "final_only", - hiddenBoundarySeparator: "space", tagVisibility: { tool_call: true, }, @@ -358,16 +344,11 @@ describe("createAcpReplyProjector", () => { await projector.onEvent({ type: "done" }); - expect(deliveries).toEqual([{ kind: "final", text: "fallback. I don't" }]); + expect(deliveries).toEqual([{ kind: "final", text: "fallback.\n\nI don't" }]); }); it("does not suppress identical short text across terminal turn boundaries", async () => { - const { deliveries, projector } = createProjectorHarness( - createLiveCfgOverrides({ - coalesceIdleMs: 0, - maxChunkChars: 64, - }), - ); + const { deliveries, projector } = createProjectorHarness(createLiveCfgOverrides({})); await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" }); await projector.onEvent({ type: "done", stopReason: "end_turn" }); @@ -383,12 +364,7 @@ describe("createAcpReplyProjector", () => { it("flushes staggered live text deltas after idle gaps", async () => { vi.useFakeTimers(); try { - const { deliveries, projector } = createProjectorHarness( - createLiveCfgOverrides({ - coalesceIdleMs: 50, - maxChunkChars: 64, - }), - ); + const { deliveries, projector } = createProjectorHarness(createLiveCfgOverrides({})); await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" }); await vi.advanceTimersByTimeAsync(760); @@ -412,38 +388,10 @@ describe("createAcpReplyProjector", () => { } }); - it("splits oversized live text by maxChunkChars", async () => { - const { deliveries, projector } = createProjectorHarness({ - acp: { - enabled: true, - stream: { - deliveryMode: "live", - coalesceIdleMs: 0, - maxChunkChars: 50, - }, - }, - }); - - const text = `${"a".repeat(50)}${"b".repeat(50)}${"c".repeat(20)}`; - await projector.onEvent({ type: "text_delta", text, tag: "agent_message_chunk" }); - await projector.flush(true); - - expect(blockDeliveries(deliveries)).toEqual([ - { kind: "block", text: "a".repeat(50) }, - { kind: "block", text: "b".repeat(50) }, - { kind: "block", text: "c".repeat(20) }, - ]); - }); - it("does not flush short live fragments mid-phrase on idle", async () => { vi.useFakeTimers(); try { - const { deliveries, projector } = createProjectorHarness( - createLiveCfgOverrides({ - coalesceIdleMs: 100, - maxChunkChars: 256, - }), - ); + const { deliveries, projector } = createProjectorHarness(createLiveCfgOverrides({})); await projector.onEvent({ type: "text_delta", @@ -550,8 +498,6 @@ describe("createAcpReplyProjector", () => { const { deliveries: shown, projector: shownProjector } = createProjectorHarness( createLiveCfgOverrides({ - coalesceIdleMs: 0, - maxChunkChars: 64, tagVisibility: { usage_update: true, }, @@ -635,9 +581,7 @@ describe("createAcpReplyProjector", () => { }); it("keeps terminal tool updates even when rendered summaries are truncated", async () => { - const { deliveries, projector } = createLiveToolLifecycleHarness({ - maxSessionUpdateChars: 48, - }); + const { deliveries, projector } = createLiveToolLifecycleHarness({}); const longTitle = "Run an intentionally long command title that truncates before lifecycle status is visible"; @@ -678,8 +622,6 @@ describe("createAcpReplyProjector", () => { it("allows repeated status/tool summaries when repeatSuppression is disabled", async () => { const { deliveries, projector } = createLiveStatusAndToolLifecycleHarness({ - coalesceIdleMs: 0, - maxChunkChars: 256, repeatSuppression: false, }); @@ -731,8 +673,6 @@ describe("createAcpReplyProjector", () => { it("suppresses exact duplicate status updates when repeatSuppression is enabled", async () => { const { deliveries, projector } = createProjectorHarness( createLiveCfgOverrides({ - coalesceIdleMs: 0, - maxChunkChars: 256, tagVisibility: { available_commands_update: true, }, @@ -761,73 +701,11 @@ describe("createAcpReplyProjector", () => { ]); }); - it("truncates status updates without splitting surrogate pairs", async () => { - const { deliveries, projector } = createProjectorHarness( - createLiveCfgOverrides({ - coalesceIdleMs: 0, - maxChunkChars: 256, - maxSessionUpdateChars: 64, - tagVisibility: { - memory_summary: true, - }, - }), - ); - - await projector.onEvent({ - type: "status", - tag: "memory_summary", - text: `${"a".repeat(62)}🎉${"b".repeat(10)}`, - }); - - expect(deliveries).toEqual([ - { - kind: "tool", - text: prefixSystemMessage(`${"a".repeat(62)}…`), - }, - ]); - }); - - it("truncates oversized turns at code-point boundaries and emits one notice", async () => { - const { deliveries, projector } = createProjectorHarness({ - acp: { - enabled: true, - stream: { - coalesceIdleMs: 0, - maxChunkChars: 256, - deliveryMode: "live", - maxOutputChars: 5, - }, - }, - }); - - await projector.onEvent({ - type: "text_delta", - text: "abcd😀 tail", - tag: "agent_message_chunk", - }); - await projector.onEvent({ - type: "text_delta", - text: "ignored tail", - tag: "agent_message_chunk", - }); - await projector.flush(true); - - expect(deliveries).toEqual([ - { kind: "block", text: "abcd" }, - { - kind: "tool", - text: prefixSystemMessage("output truncated"), - }, - ]); - }); - it("supports tagVisibility overrides for tool updates", async () => { const { deliveries, projector } = createProjectorHarness({ acp: { enabled: true, stream: { - coalesceIdleMs: 0, - maxChunkChars: 256, deliveryMode: "live", tagVisibility: { tool_call: true, @@ -922,26 +800,14 @@ describe("createAcpReplyProjector", () => { }); }); - it("supports hiddenBoundarySeparator=space", async () => { + it("uses the built-in space separator for hidden live boundaries", async () => { await runHiddenBoundaryCase({ - cfgOverrides: createHiddenBoundaryCfg({ - hiddenBoundarySeparator: "space", - }), + cfgOverrides: createHiddenBoundaryCfg({}), toolCallId: "call_hidden_2", expectedText: "fallback. I don't", }); }); - it("supports hiddenBoundarySeparator=none", async () => { - await runHiddenBoundaryCase({ - cfgOverrides: createHiddenBoundaryCfg({ - hiddenBoundarySeparator: "none", - }), - toolCallId: "call_hidden_3", - expectedText: "fallback.I don't", - }); - }); - it("does not duplicate newlines when previous visible text already ends with newline", async () => { await runHiddenBoundaryCase({ cfgOverrides: createHiddenBoundaryCfg(), @@ -956,8 +822,6 @@ describe("createAcpReplyProjector", () => { acp: { enabled: true, stream: { - coalesceIdleMs: 0, - maxChunkChars: 256, deliveryMode: "live", }, }, diff --git a/src/auto-reply/reply/acp-stream-settings.test.ts b/src/auto-reply/reply/acp-stream-settings.test.ts index af89198f7dbf..efbb15cfa032 100644 --- a/src/auto-reply/reply/acp-stream-settings.test.ts +++ b/src/auto-reply/reply/acp-stream-settings.test.ts @@ -17,17 +17,14 @@ describe("acp stream settings", () => { expect(settings.maxSessionUpdateChars).toBe(320); }); - it("applies explicit stream overrides", () => { + it("applies retained stream overrides while preserving built-in tuning", () => { const settings = resolveAcpProjectionSettings( createAcpTestConfig({ acp: { enabled: true, stream: { deliveryMode: "final_only", - hiddenBoundarySeparator: "space", repeatSuppression: false, - maxOutputChars: 500, - maxSessionUpdateChars: 123, tagVisibility: { usage_update: true, }, @@ -36,10 +33,10 @@ describe("acp stream settings", () => { }), ); expect(settings.deliveryMode).toBe("final_only"); - expect(settings.hiddenBoundarySeparator).toBe("space"); + expect(settings.hiddenBoundarySeparator).toBe("paragraph"); expect(settings.repeatSuppression).toBe(false); - expect(settings.maxOutputChars).toBe(500); - expect(settings.maxSessionUpdateChars).toBe(123); + expect(settings.maxOutputChars).toBe(24_000); + expect(settings.maxSessionUpdateChars).toBe(320); expect(settings.tagVisibility.usage_update).toBe(true); }); @@ -83,24 +80,22 @@ describe("acp stream settings", () => { expect(isAcpTagVisible(settings, "tool_call")).toBe(false); }); - it("resolves chunking/coalescing from ACP stream controls", () => { + it("resolves built-in ACP chunking and coalescing", () => { const streaming = resolveAcpStreamingConfig({ cfg: createAcpTestConfig(), provider: "quietchat", }); - expect(streaming.chunking.maxChars).toBe(64); - expect(streaming.coalescing.idleMs).toBe(0); + expect(streaming.chunking.maxChars).toBe(1800); + expect(streaming.coalescing.idleMs).toBe(350); }); - it("applies live-mode streaming overrides for incremental delivery", () => { + it("applies live-mode delivery with built-in streaming tuning", () => { const streaming = resolveAcpStreamingConfig({ cfg: createAcpTestConfig({ acp: { enabled: true, stream: { deliveryMode: "live", - coalesceIdleMs: 350, - maxChunkChars: 256, }, }, }), @@ -108,9 +103,9 @@ describe("acp stream settings", () => { deliveryMode: "live", }); expect(streaming.chunking.minChars).toBe(1); - expect(streaming.chunking.maxChars).toBe(256); + expect(streaming.chunking.maxChars).toBe(1800); expect(streaming.coalescing.minChars).toBe(1); - expect(streaming.coalescing.maxChars).toBe(256); + expect(streaming.coalescing.maxChars).toBe(1800); expect(streaming.coalescing.joiner).toBe(""); expect(streaming.coalescing.idleMs).toBe(350); }); diff --git a/src/auto-reply/reply/acp-stream-settings.ts b/src/auto-reply/reply/acp-stream-settings.ts index 6aed564daf0e..de4aa7f41807 100644 --- a/src/auto-reply/reply/acp-stream-settings.ts +++ b/src/auto-reply/reply/acp-stream-settings.ts @@ -1,7 +1,7 @@ /** ACP streaming and projection settings derived from config. */ import type { AcpSessionUpdateTag } from "@openclaw/acp-core/runtime/types"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { clampPositiveInteger, resolveEffectiveBlockStreamingConfig } from "./block-streaming.js"; +import { resolveEffectiveBlockStreamingConfig } from "./block-streaming.js"; const DEFAULT_ACP_STREAM_COALESCE_IDLE_MS = 350; const DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS = 1800; @@ -54,32 +54,12 @@ function resolveAcpDeliveryMode(value: unknown): AcpDeliveryMode { return DEFAULT_ACP_DELIVERY_MODE; } -function resolveAcpHiddenBoundarySeparator( - value: unknown, - fallback: AcpHiddenBoundarySeparator, -): AcpHiddenBoundarySeparator { - if (value === "none" || value === "space" || value === "newline" || value === "paragraph") { - return value; - } - return fallback; +function resolveAcpStreamCoalesceIdleMs(): number { + return DEFAULT_ACP_STREAM_COALESCE_IDLE_MS; } -function resolveAcpStreamCoalesceIdleMs(cfg: OpenClawConfig): number { - return clampPositiveInteger( - cfg.acp?.stream?.coalesceIdleMs, - DEFAULT_ACP_STREAM_COALESCE_IDLE_MS, - { - min: 0, - max: 5_000, - }, - ); -} - -function resolveAcpStreamMaxChunkChars(cfg: OpenClawConfig): number { - return clampPositiveInteger(cfg.acp?.stream?.maxChunkChars, DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS, { - min: 50, - max: 4_000, - }); +function resolveAcpStreamMaxChunkChars(): number { + return DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS; } /** Resolves ACP projection settings with bounded defaults. */ @@ -92,23 +72,10 @@ export function resolveAcpProjectionSettings(cfg: OpenClawConfig): AcpProjection : DEFAULT_ACP_HIDDEN_BOUNDARY_SEPARATOR; return { deliveryMode, - hiddenBoundarySeparator: resolveAcpHiddenBoundarySeparator( - stream?.hiddenBoundarySeparator, - hiddenBoundaryFallback, - ), + hiddenBoundarySeparator: hiddenBoundaryFallback, repeatSuppression: clampBoolean(stream?.repeatSuppression, DEFAULT_ACP_REPEAT_SUPPRESSION), - maxOutputChars: clampPositiveInteger(stream?.maxOutputChars, DEFAULT_ACP_MAX_OUTPUT_CHARS, { - min: 1, - max: 500_000, - }), - maxSessionUpdateChars: clampPositiveInteger( - stream?.maxSessionUpdateChars, - DEFAULT_ACP_MAX_SESSION_UPDATE_CHARS, - { - min: 64, - max: 8_000, - }, - ), + maxOutputChars: DEFAULT_ACP_MAX_OUTPUT_CHARS, + maxSessionUpdateChars: DEFAULT_ACP_MAX_SESSION_UPDATE_CHARS, tagVisibility: stream?.tagVisibility ?? {}, }; } @@ -124,8 +91,8 @@ export function resolveAcpStreamingConfig(params: { cfg: params.cfg, provider: params.provider, accountId: params.accountId, - maxChunkChars: resolveAcpStreamMaxChunkChars(params.cfg), - coalesceIdleMs: resolveAcpStreamCoalesceIdleMs(params.cfg), + maxChunkChars: resolveAcpStreamMaxChunkChars(), + coalesceIdleMs: resolveAcpStreamCoalesceIdleMs(), }); // In live mode, ACP text deltas should flush promptly and never be held diff --git a/src/auto-reply/reply/agent-runner-context-recovery.test.ts b/src/auto-reply/reply/agent-runner-context-recovery.test.ts index 1cd9f0caefc1..56256b9880c1 100644 --- a/src/auto-reply/reply/agent-runner-context-recovery.test.ts +++ b/src/auto-reply/reply/agent-runner-context-recovery.test.ts @@ -16,176 +16,19 @@ function makeTestModel(id: string, contextTokens: number): ModelDefinitionConfig } describe("buildContextOverflowRecoveryText", () => { - it.each([ - [99_999, "20000"], - [100_000, "35000"], - [199_999, "35000"], - [200_000, "50000"], - [999_999, "50000"], - [1_000_000, "100000"], - ])("selects the reserve floor for a %i-token model", (contextTokens, expectedFloor) => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - test: { - baseUrl: "https://provider.test", - models: [makeTestModel("model", contextTokens)], - }, - }, - }, - }, - primaryProvider: "test", - primaryModel: "model", - }); - - expect(text).toContain(`reserveTokensFloor\` to ${expectedFloor}`); - }); - - it("keeps the generic compaction-buffer hint without heartbeat model evidence", () => { + it("uses the built-in recovery hint without heartbeat model evidence", () => { const text = buildContextOverflowRecoveryText({ cfg: {}, primaryProvider: "openrouter", primaryModel: "qwen3.6-plus", }); - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); + expect(text).toContain("fresh session or using a model with a larger context window"); + expect(text).not.toContain("reserveTokensFloor"); expect(text).not.toContain("heartbeat model bleed"); }); - it("suggests 100000 reserveTokensFloor for 1M context models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("suggests 50000 reserveTokensFloor for 200k context models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("gpt-5.5-200k", 200_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "gpt-5.5-200k", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("50000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("suggests 35000 reserveTokensFloor for 100k context models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("gpt-5.5", 100_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "gpt-5.5", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("35000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("suggests 20000 reserveTokensFloor for small context windows", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - ollama: { - baseUrl: "http://ollama.test", - models: [makeTestModel("qwen3.5-9b-32k:latest", 32_768)], - }, - }, - }, - }, - primaryProvider: "ollama", - primaryModel: "qwen3.5-9b-32k:latest", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("uses session contextTokens as fallback when model metadata is unavailable", () => { - const text = buildContextOverflowRecoveryText({ - cfg: {}, - primaryProvider: "openrouter", - primaryModel: "unknown-model", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "openrouter", - model: "unknown-model", - contextTokens: 200_000, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("50000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("prefers model metadata over session contextTokens", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "openrouter", - model: "qwen3.6-plus", - contextTokens: 32_768, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("keeps the preserved-session copy with the existing overflow hint", () => { + it("keeps the preserved-session copy with the built-in recovery hint", () => { const text = buildContextOverflowRecoveryText({ preserveSessionMapping: true, cfg: {}, @@ -194,285 +37,11 @@ describe("buildContextOverflowRecoveryText", () => { }); expect(text).toContain("kept this conversation mapped to the current session"); - expect(text).toContain("reserveTokensFloor"); + expect(text).toContain("fresh session or using a model with a larger context window"); expect(text).not.toContain("reset our conversation"); }); - it("falls back to session entry model when runtimeProvider is not provided", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - ollama: { - baseUrl: "http://ollama.test", - models: [makeTestModel("qwen3.5-9b-32k:latest", 32_768)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "unknown-model", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "ollama", - model: "qwen3.5-9b-32k:latest", - contextTokens: 200_000, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("prefers session entry model context over session contextTokens numeric value", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - ollama: { - baseUrl: "http://ollama.test", - models: [makeTestModel("qwen3.5-9b-32k:latest", 32_768)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "unknown-model", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "ollama", - model: "qwen3.5-9b-32k:latest", - contextTokens: 1_000_000, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("uses session contextTokens before primary metadata for uncataloged runtime models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "custom", - model: "uncataloged-32k", - contextTokens: 32_768, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("does not use primary metadata for explicit uncataloged runtime models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - runtimeProvider: "custom", - runtimeModel: "uncataloged-32k", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("does not use stale session contextTokens for explicit uncataloged runtime models", () => { - const text = buildContextOverflowRecoveryText({ - cfg: {}, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - runtimeProvider: "custom", - runtimeModel: "uncataloged-32k", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "openrouter", - model: "qwen3.6-plus", - contextTokens: 1_000_000, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("caps reserveTokensFloor hint by agent.defaults.contextTokens", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - agents: { - defaults: { - contextTokens: 100_000, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("35000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("caps reserveTokensFloor hint by per-agent contextTokens over defaults", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - agents: { - defaults: { - contextTokens: 200_000, - }, - list: [ - { - id: "capped-agent", - contextTokens: 32_768, - }, - ], - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - agentId: "capped-agent", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("50000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("caps the session contextTokens fallback by agent contextTokens", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - agents: { - defaults: { - contextTokens: 200_000, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "unknown-model", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "openrouter", - model: "unknown-model", - contextTokens: 32_768, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("50000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("uses runtime model over primary model when both are available", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - ollama: { - baseUrl: "http://ollama.test", - models: [makeTestModel("qwen3.5-9b-32k:latest", 32_768)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - runtimeProvider: "ollama", - runtimeModel: "qwen3.5-9b-32k:latest", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("uses runtime model with 200k context when primary is 1M", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - openai: { - baseUrl: "https://openai.test", - models: [makeTestModel("gpt-5.5-200k", 200_000)], - }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - runtimeProvider: "openai", - runtimeModel: "gpt-5.5-200k", - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("50000"); - expect(text).not.toContain("100000"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("does not use stale heartbeat bleed hints for different explicit runtime refs", () => { + it("does not use stale heartbeat hints for a different explicit runtime model", () => { const text = buildContextOverflowRecoveryText({ cfg: { agents: { @@ -494,12 +63,11 @@ describe("buildContextOverflowRecoveryText", () => { }, }); - expect(text).toContain("reserveTokensFloor"); - expect(text).toContain("20000"); + expect(text).toContain("fresh session or using a model with a larger context window"); expect(text).not.toContain("heartbeat model bleed"); }); - it("points to heartbeat model bleed when the last runtime model matches configured heartbeat.model", () => { + it("points to heartbeat model bleed when the session model matches heartbeat.model", () => { const text = buildContextOverflowRecoveryText({ cfg: { models: { @@ -536,10 +104,9 @@ describe("buildContextOverflowRecoveryText", () => { expect(text).toContain("openrouter/qwen3.6-plus"); expect(text).toContain("heartbeat model bleed"); expect(text).toContain("heartbeat.isolatedSession"); - expect(text).not.toContain("reserveTokensFloor"); }); - it("uses the stored session context window as the uncataloged runtime model fallback", () => { + it("uses the stored context window for an uncataloged heartbeat model", () => { const text = buildContextOverflowRecoveryText({ cfg: { models: { @@ -574,41 +141,7 @@ describe("buildContextOverflowRecoveryText", () => { expect(text).toContain("heartbeat model bleed"); }); - it("does not blame heartbeat when the stored session fallback matches the capped primary window", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - models: { - providers: { - openrouter: { - baseUrl: "https://openrouter.test", - models: [makeTestModel("qwen3.6-plus", 1_000_000)], - }, - }, - }, - agents: { - defaults: { - contextTokens: 100_000, - heartbeat: { model: "ollama/custom-large" }, - }, - }, - }, - agentId: "agent", - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "ollama", - model: "custom-large", - contextTokens: 200_000, - }, - }); - - expect(text).toContain("reserveTokensFloor"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("does not blame heartbeat when the same agent cap constrains both cataloged models", () => { + it("does not blame heartbeat when the configured cap makes both windows equal", () => { const text = buildContextOverflowRecoveryText({ cfg: { models: { @@ -642,31 +175,7 @@ describe("buildContextOverflowRecoveryText", () => { }, }); - expect(text).toContain("reserveTokensFloor"); - expect(text).not.toContain("heartbeat model bleed"); - }); - - it("does not blame heartbeat when the smaller runtime model is not the configured heartbeat model", () => { - const text = buildContextOverflowRecoveryText({ - cfg: { - agents: { - defaults: { - heartbeat: { model: "ollama/qwen3.5-9b-32k:latest" }, - }, - }, - }, - primaryProvider: "openrouter", - primaryModel: "qwen3.6-plus", - activeSessionEntry: { - sessionId: "session", - updatedAt: 1, - modelProvider: "anthropic", - model: "claude-haiku-4-5", - contextTokens: 32_768, - }, - }); - - expect(text).toContain("reserveTokensFloor"); + expect(text).toContain("fresh session or using a model with a larger context window"); expect(text).not.toContain("heartbeat model bleed"); }); }); diff --git a/src/auto-reply/reply/agent-runner-context-recovery.ts b/src/auto-reply/reply/agent-runner-context-recovery.ts index c77832cae895..05ccba1cbba0 100644 --- a/src/auto-reply/reply/agent-runner-context-recovery.ts +++ b/src/auto-reply/reply/agent-runner-context-recovery.ts @@ -7,91 +7,8 @@ import { resolveModelRefFromString } from "../../agents/model-selection.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { FollowupRun } from "./queue.js"; -const DEFAULT_RESERVE_TOKENS_FLOOR = 20_000; - -/** Computes a reserve-token floor scaled to the selected context window. */ -function computeContextAwareReserveTokensFloor(contextWindow: number | undefined): number { - if (typeof contextWindow !== "number" || contextWindow <= 0) { - return DEFAULT_RESERVE_TOKENS_FLOOR; - } - if (contextWindow >= 1_000_000) { - return 100_000; - } - if (contextWindow >= 200_000) { - return 50_000; - } - if (contextWindow >= 100_000) { - return 35_000; - } - return DEFAULT_RESERVE_TOKENS_FLOOR; -} - -function resolveContextWindowForCompactionHint(params: { - cfg: FollowupRun["run"]["config"]; - primaryProvider?: string; - primaryModel?: string; - runtimeProvider?: string; - runtimeModel?: string; - agentId?: string; - activeSessionEntry?: SessionEntry; -}): number | undefined { - let modelWindow: number | undefined; - const entryProvider = params.activeSessionEntry?.modelProvider; - const entryModel = params.activeSessionEntry?.model; - const runtimeProvider = params.runtimeProvider ?? entryProvider; - const runtimeModel = params.runtimeModel ?? entryModel; - const hasExplicitRuntimeRef = Boolean(params.runtimeProvider && params.runtimeModel); - if (runtimeProvider && runtimeModel) { - const resolved = resolveContextTokensForModel({ - cfg: params.cfg, - provider: runtimeProvider, - model: runtimeModel, - allowAsyncLoad: false, - }); - if (typeof resolved === "number" && resolved > 0) { - modelWindow = resolved; - } - } - const sessionWindow = normalizePositiveContextTokens(params.activeSessionEntry?.contextTokens); - const sessionMatchesRuntimeRef = runtimeProvider === entryProvider && runtimeModel === entryModel; - const trustedSessionWindow = - !hasExplicitRuntimeRef || sessionMatchesRuntimeRef ? sessionWindow : undefined; - if (modelWindow === undefined && sessionMatchesRuntimeRef && sessionWindow !== undefined) { - modelWindow = sessionWindow; - } - if ( - modelWindow === undefined && - !hasExplicitRuntimeRef && - params.primaryProvider && - params.primaryModel - ) { - const resolved = resolveContextTokensForModel({ - cfg: params.cfg, - provider: params.primaryProvider, - model: params.primaryModel, - allowAsyncLoad: false, - }); - if (typeof resolved === "number" && resolved > 0) { - modelWindow = resolved; - } - } - const contextWindow = modelWindow ?? trustedSessionWindow; - const agentCap = resolveAgentContextTokensForHint({ - cfg: params.cfg, - agentId: params.agentId, - }); - if (agentCap !== undefined && contextWindow !== undefined) { - return Math.min(agentCap, contextWindow); - } - return agentCap ?? contextWindow; -} - -function buildContextOverflowResetHint(contextWindowTokens: number | undefined): string { - const reserveFloor = computeContextAwareReserveTokensFloor(contextWindowTokens); - return ( - "\n\nTo prevent this, increase your compaction buffer by setting " + - `\`agents.defaults.compaction.reserveTokensFloor\` to ${reserveFloor} or higher in your config.` - ); +function buildContextOverflowResetHint(): string { + return "\n\nTry starting a fresh session or using a model with a larger context window."; } type ModelRefLike = { @@ -279,15 +196,6 @@ export function buildContextOverflowRecoveryText(params: { : params.duringCompaction ? "⚠️ Context limit exceeded during compaction. I've reset our conversation to start fresh - please try again." : "⚠️ Context limit exceeded. I've reset our conversation to start fresh - please try again."; - const primaryContextWindow = resolveContextWindowForCompactionHint({ - cfg: params.cfg, - primaryProvider: params.primaryProvider, - primaryModel: params.primaryModel, - runtimeProvider: params.runtimeProvider, - runtimeModel: params.runtimeModel, - agentId: params.agentId, - activeSessionEntry: params.activeSessionEntry, - }); const explicitRuntimeMatchesSession = !params.runtimeProvider || !params.runtimeModel || @@ -302,5 +210,5 @@ export function buildContextOverflowRecoveryText(params: { activeSessionEntry: params.activeSessionEntry, }) : undefined; - return prefix + (heartbeatBleedHint ?? buildContextOverflowResetHint(primaryContextWindow)); + return prefix + (heartbeatBleedHint ?? buildContextOverflowResetHint()); } diff --git a/src/auto-reply/reply/agent-runner-execution-context-failures.test.ts b/src/auto-reply/reply/agent-runner-execution-context-failures.test.ts index afba9bae4d25..b81079688e32 100644 --- a/src/auto-reply/reply/agent-runner-execution-context-failures.test.ts +++ b/src/auto-reply/reply/agent-runner-execution-context-failures.test.ts @@ -51,7 +51,7 @@ describe("runAgentTurnWithFallback: context failures", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { expect(result.payload.text).toContain("kept this conversation mapped to the current session"); - expect(result.payload.text).toContain("reserveTokensFloor"); + expect(result.payload.text).toContain("fresh session or using a model"); expectRecordFields(requireRecord(getReplyPayloadMetadata(result.payload), "reply metadata"), { deliverDespiteSourceReplySuppression: true, }); @@ -94,7 +94,7 @@ describe("runAgentTurnWithFallback: context failures", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { expect(result.payload.text).toContain("kept this conversation mapped to the current session"); - expect(result.payload.text).toContain("reserveTokensFloor"); + expect(result.payload.text).toContain("fresh session or using a model"); expectRecordFields(requireRecord(getReplyPayloadMetadata(result.payload), "reply metadata"), { deliverDespiteSourceReplySuppression: true, }); @@ -179,7 +179,7 @@ describe("runAgentTurnWithFallback: context failures", () => { } }); - it("uses the throwing fallback candidate model for compaction failure hints", async () => { + it("uses the built-in compaction failure hint when the fallback candidate throws", async () => { state.isCompactionFailureErrorMock.mockReturnValue(true); state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => { await params.run("custom", "uncataloged-32k"); @@ -208,9 +208,7 @@ describe("runAgentTurnWithFallback: context failures", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { - expect(result.payload.text).toContain("reserveTokensFloor"); - expect(result.payload.text).toContain("20000"); - expect(result.payload.text).not.toContain("100000"); + expect(result.payload.text).toContain("fresh session or using a model"); } }); }); diff --git a/src/auto-reply/reply/agent-runner-execution-terminal-failures.test.ts b/src/auto-reply/reply/agent-runner-execution-terminal-failures.test.ts index 6bc429d72ac3..b3da62e68399 100644 --- a/src/auto-reply/reply/agent-runner-execution-terminal-failures.test.ts +++ b/src/auto-reply/reply/agent-runner-execution-terminal-failures.test.ts @@ -588,9 +588,8 @@ describe("runAgentTurnWithFallback: terminal failures", () => { expect(result.payload.text).toContain("CLI subprocess"); expect(result.payload.text).toContain("no-output watchdog"); expect(result.payload.text).toContain("separate from the overall agent timeout"); - expect(result.payload.text).toContain( - "agents.defaults.cliBackends..reliability.watchdog.{fresh,resume}.noOutputTimeoutMs", - ); + expect(result.payload.text).toContain("produced no output before its watchdog expired"); + expect(result.payload.text).not.toContain("noOutputTimeoutMs"); expect(result.payload.text).not.toContain("agents.defaults.timeoutSeconds"); } expect(result.payload.text).not.toContain("/new"); diff --git a/src/auto-reply/reply/agent-runner-failure-reply.ts b/src/auto-reply/reply/agent-runner-failure-reply.ts index 1c160ad30398..ee251e3b87a9 100644 --- a/src/auto-reply/reply/agent-runner-failure-reply.ts +++ b/src/auto-reply/reply/agent-runner-failure-reply.ts @@ -302,7 +302,7 @@ function buildCliBackendTimeoutFailureText(input: { `⚠️ CLI subprocess${routingSuffix}: no output for ${seconds}s, so the no-output watchdog stopped it. ` + `This is separate from the overall agent timeout; the gateway is unaffected.${workStatus} ` + "Check for an interactive prompt. " + - `For an intentionally quiet CLI, raise \`agents.defaults.cliBackends.${backendId}.reliability.watchdog.{fresh,resume}.noOutputTimeoutMs\`.` + `The CLI backend ${backendId} produced no output before its watchdog expired.` ); } return ( diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 8fe40e3d0622..a8334e2b82a8 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -811,10 +811,7 @@ export async function runPreflightCompactionIfNeeded(params: { agentCfgContextTokens: params.agentCfgContextTokens, }); const memoryFlushPlan = resolveMemoryFlushPlan({ cfg: params.cfg }); - const reserveTokensFloor = - memoryFlushPlan?.reserveTokensFloor ?? - params.cfg.agents?.defaults?.compaction?.reserveTokensFloor ?? - 20_000; + const reserveTokensFloor = memoryFlushPlan?.reserveTokensFloor ?? 20_000; const softThresholdTokens = memoryFlushPlan?.softThresholdTokens ?? 4_000; const freshPersistedTokens = resolveFreshSessionTotalTokens(entry); const persistedTotalTokens = entry.totalTokens; diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index f5415db219d9..70912e81b047 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -4520,7 +4520,7 @@ describe("runReplyAgent typing (heartbeat)", () => { throw new Error("expected payload"); } expect(payload.text).toContain("Auto-compaction could not recover this turn"); - expect(payload.text).toContain("reserveTokensFloor"); + expect(payload.text).toContain("fresh session or using a model with a larger context window"); expect(payload.text).toContain("/new"); }); @@ -4543,7 +4543,7 @@ describe("runReplyAgent typing (heartbeat)", () => { throw new Error("expected payload"); } expect(payload.text).toContain("Auto-compaction could not recover this turn"); - expect(payload.text).toContain("reserveTokensFloor"); + expect(payload.text).toContain("fresh session or using a model with a larger context window"); expect(payload.text).toContain("/new"); }); diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index 3c5e5c6708b5..44019ba2b9c3 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -87,7 +87,7 @@ type BlockStreamingChunking = { flushOnParagraph?: boolean; }; -export function clampPositiveInteger( +function clampPositiveInteger( value: unknown, fallback: number, bounds: { min: number; max: number }, diff --git a/src/auto-reply/reply/commands-acp/diagnostics.ts b/src/auto-reply/reply/commands-acp/diagnostics.ts index 6587619c6141..2ae528d78970 100644 --- a/src/auto-reply/reply/commands-acp/diagnostics.ts +++ b/src/auto-reply/reply/commands-acp/diagnostics.ts @@ -51,7 +51,7 @@ export async function handleAcpDoctorAction( const backendId = normalizeOptionalString(params.cfg.acp?.backend) ?? "acpx"; const installHint = resolveAcpInstallCommandHint(params.cfg); const registeredBackend = getAcpRuntimeBackend(backendId); - const managerSnapshot = getAcpSessionManager().getObservabilitySnapshot(params.cfg); + const managerSnapshot = getAcpSessionManager().getObservabilitySnapshot(); const lines = ["ACP doctor:", "-----", `configuredBackend: ${backendId}`]; lines.push(`activeRuntimeSessions: ${managerSnapshot.runtimeCache.activeSessions}`); lines.push(`runtimeIdleTtlMs: ${managerSnapshot.runtimeCache.idleTtlMs}`); diff --git a/src/auto-reply/reply/directive-handling.queue-validation.test.ts b/src/auto-reply/reply/directive-handling.queue-validation.test.ts index 43ee6b600e37..b419e340ccc1 100644 --- a/src/auto-reply/reply/directive-handling.queue-validation.test.ts +++ b/src/auto-reply/reply/directive-handling.queue-validation.test.ts @@ -30,7 +30,6 @@ describe("maybeHandleQueueDirective", () => { messages: { queue: { mode: "collect", - debounceMs: 1500, cap: 9, drop: "summarize", }, @@ -39,7 +38,7 @@ describe("maybeHandleQueueDirective", () => { channel: "quietchat", }); expect(current?.text).toContain( - "Current queue settings: mode=collect, debounce=1500ms, cap=9, drop=summarize.", + "Current queue settings: mode=collect, debounce=500ms, cap=9, drop=summarize.", ); expect(current?.text).toContain( "Options: modes steer, followup, collect, interrupt; debounce:, cap:, drop:old|new|summarize.", diff --git a/src/auto-reply/reply/dispatch-acp.test.ts b/src/auto-reply/reply/dispatch-acp.test.ts index ba6f5f1aba53..594b0c69ac95 100644 --- a/src/auto-reply/reply/dispatch-acp.test.ts +++ b/src/auto-reply/reply/dispatch-acp.test.ts @@ -959,7 +959,7 @@ describe("tryDispatchAcpReply", () => { cfg: createAcpTestConfig({ acp: { enabled: true, - stream: { deliveryMode: "live", coalesceIdleMs: 0, maxChunkChars: 64 }, + stream: { deliveryMode: "live" }, }, }), dispatcher, @@ -989,7 +989,7 @@ describe("tryDispatchAcpReply", () => { mockArg(transcriptMocks.persistAcpDispatchTranscript, 0, 0, "transcript call"), "transcript call", ); - expect(transcript.finalText).toBe(deliveredText); + expect(transcript.finalText).toBe(deliveredText.trimEnd()); }); it("keeps caller abort authoritative until completed output settles", async () => { @@ -2309,8 +2309,6 @@ describe("tryDispatchAcpReply", () => { enabled: true, stream: { deliveryMode: "live", - coalesceIdleMs: 0, - maxChunkChars: 64, }, }, }); @@ -2347,8 +2345,6 @@ describe("tryDispatchAcpReply", () => { enabled: true, stream: { deliveryMode: "live", - coalesceIdleMs: 0, - maxChunkChars: 64, }, }, }); diff --git a/src/auto-reply/reply/dispatch-acp.ts b/src/auto-reply/reply/dispatch-acp.ts index 41295b3ab60b..92ccb191f178 100644 --- a/src/auto-reply/reply/dispatch-acp.ts +++ b/src/auto-reply/reply/dispatch-acp.ts @@ -531,7 +531,7 @@ export async function tryDispatchAcpReply(params: { ...options, dispatcher: params.dispatcher, delivery, - getStats: () => acpManager.getObservabilitySnapshot(params.cfg), + getStats: () => acpManager.getObservabilitySnapshot(), sessionKey, startedAt: acpDispatchStartedAt, recordProcessed: params.recordProcessed, diff --git a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts index dad208d671a6..067b7e31b5dd 100644 --- a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts @@ -456,16 +456,9 @@ vi.mock("../../logging/diagnostic.js", () => ({ isStuckSessionRecoveryEnabled: (config?: { diagnostics?: { enabled?: boolean } }) => config?.diagnostics?.enabled !== false, requestStuckDiagnosticSessionRecovery: diagnosticMocks.requestStuckDiagnosticSessionRecovery, - resolveStuckSessionWarnMs: (config?: { diagnostics?: { stuckSessionWarnMs?: number } }) => - config?.diagnostics?.stuckSessionWarnMs ?? 120_000, - resolveStuckSessionAbortMs: ( - config: { diagnostics?: { stuckSessionAbortMs?: number } } | undefined, - stuckSessionWarnMs: number, - ) => - Math.max( - stuckSessionWarnMs, - config?.diagnostics?.stuckSessionAbortMs ?? Math.max(300_000, stuckSessionWarnMs * 3), - ), + resolveStuckSessionWarnMs: () => 120_000, + resolveStuckSessionAbortMs: (stuckSessionWarnMs: number) => + Math.max(300_000, stuckSessionWarnMs * 3), })); vi.mock("../../audit/message-audit-events.js", () => ({ emitTrustedMessageAuditEvent: messageAuditMocks.emitTrustedMessageAuditEvent, diff --git a/src/auto-reply/reply/dispatch-from-config.stale-recovery.test.ts b/src/auto-reply/reply/dispatch-from-config.stale-recovery.test.ts index f34aa98c8a7a..5c41d01e3abe 100644 --- a/src/auto-reply/reply/dispatch-from-config.stale-recovery.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.stale-recovery.test.ts @@ -35,12 +35,7 @@ function createVisibleDispatchParams(replyResolver: () => Promise) MessageThreadId: "501.000", BodyForAgent: "second telegram direct turn", }), - cfg: { - diagnostics: { - stuckSessionWarnMs: 1_000, - stuckSessionAbortMs: 1_000, - }, - } as OpenClawConfig, + cfg: {} as OpenClawConfig, dispatcher: createDispatcher(), replyResolver, }; @@ -95,7 +90,7 @@ describe("dispatchReplyFromConfig stale visible admission recovery", () => { return result; }); - await vi.advanceTimersByTimeAsync(1_000); + await vi.advanceTimersByTimeAsync(120_000); expect(settled).toBe(false); expect(waitChanges).toEqual([true]); diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index 171dceed0543..84bb3efc1b98 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -351,8 +351,7 @@ export async function getReplyFromConfig( }; }); const typing = resolverTiming.measureSync("reply.create_typing_controller", () => { - const configuredTypingSeconds = - agentCfg?.typingIntervalSeconds ?? sessionCfg?.typingIntervalSeconds; + const configuredTypingSeconds = agentCfg?.typingIntervalSeconds; const typingIntervalSeconds = typeof configuredTypingSeconds === "number" ? configuredTypingSeconds : 6; const controller = createTypingController({ diff --git a/src/auto-reply/reply/queue/settings.test.ts b/src/auto-reply/reply/queue/settings.test.ts index f569a1b82e00..c63e8f3c90be 100644 --- a/src/auto-reply/reply/queue/settings.test.ts +++ b/src/auto-reply/reply/queue/settings.test.ts @@ -39,7 +39,6 @@ describe("resolveQueueSettings", () => { messages: { queue: { mode: "followup", - debounceMs: 750, byChannel: { discord: "collect", }, @@ -50,7 +49,7 @@ describe("resolveQueueSettings", () => { }), ).toEqual({ mode: "collect", - debounceMs: 750, + debounceMs: 500, cap: 20, dropPolicy: "summarize", }); diff --git a/src/auto-reply/reply/queue/settings.ts b/src/auto-reply/reply/queue/settings.ts index ddb754117a77..22b2eaab3825 100644 --- a/src/auto-reply/reply/queue/settings.ts +++ b/src/auto-reply/reply/queue/settings.ts @@ -39,7 +39,6 @@ export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueS params.sessionEntry?.queueDebounceMs ?? resolveChannelDebounce(queueCfg?.debounceMsByChannel, channelKey) ?? params.pluginDebounceMs ?? - queueCfg?.debounceMs ?? DEFAULT_QUEUE_DEBOUNCE_MS; const capRaw = params.inlineOptions?.cap ?? diff --git a/src/cli/daemon-cli/lifecycle-safe-restart.ts b/src/cli/daemon-cli/lifecycle-safe-restart.ts index bb12cf68337e..037d779f9706 100644 --- a/src/cli/daemon-cli/lifecycle-safe-restart.ts +++ b/src/cli/daemon-cli/lifecycle-safe-restart.ts @@ -52,7 +52,7 @@ export async function requestSafeGatewayRestart(opts: DaemonLifecycleOptions): P ? "safe restart request joined an existing pending gateway restart" : result.status === "deferred" ? "safe restart requested; gateway will restart after active work drains " + - "(bounded by gateway.reload.deferralTimeoutMs; may force after timeout expires)" + "(bounded wait; may force after the timeout expires)" : skipDeferral ? "safe restart requested; gateway bypassing active-work deferral" : "safe restart requested; gateway will restart momentarily"; diff --git a/src/cli/daemon-cli/lifecycle.external-supervision.test.ts b/src/cli/daemon-cli/lifecycle.external-supervision.test.ts index 1f072fbf4e2e..2d4cf40c593e 100644 --- a/src/cli/daemon-cli/lifecycle.external-supervision.test.ts +++ b/src/cli/daemon-cli/lifecycle.external-supervision.test.ts @@ -280,20 +280,6 @@ describe("external gateway supervision lifecycle", () => { }); }); - it("waits indefinitely for owner release when drain timeout is zero", async () => { - loadConfig.mockReturnValue({ gateway: { reload: { deferralTimeoutMs: 0 } } }); - - await runDaemonRestart({ json: true }); - - expect(waitForGatewayHealthyListener).toHaveBeenCalledWith({ - port: 18_789, - attempts: 120, - delayMs: 500, - previousLockIdentity: gatewayLockIdentity, - waitIndefinitelyForPreviousOwner: true, - }); - }); - it("refuses a restart when the lock owner does not match the listener", async () => { readActiveGatewayLockIdentity.mockResolvedValue({ ...gatewayLockIdentity, diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index def0fdfd9b60..b4295845372e 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -243,10 +243,7 @@ async function resolveRestartListenerHealthWait( } else if (typeof restartIntent?.waitMs === "number" && Number.isFinite(restartIntent.waitMs)) { drainTimeoutMs = restartIntent.waitMs > 0 ? Math.floor(restartIntent.waitMs) : undefined; } else { - const config = await readBestEffortConfig().catch(() => undefined); - drainTimeoutMs = resolveGatewayRestartDeferralTimeoutMs( - config?.gateway?.reload?.deferralTimeoutMs, - ); + drainTimeoutMs = resolveGatewayRestartDeferralTimeoutMs(); } const replacementHealthAttempts = postRestartHealthAttempts(); diff --git a/src/cli/daemon-cli/probe.test.ts b/src/cli/daemon-cli/probe.test.ts index 67c960daa521..23d460bccb5c 100644 --- a/src/cli/daemon-cli/probe.test.ts +++ b/src/cli/daemon-cli/probe.test.ts @@ -168,7 +168,7 @@ describe("probeGatewayStatus", () => { }); }); - it("forwards configured handshake timeout to the connect probe and status RPC", async () => { + it("forwards the resolved handshake timeout to the connect probe and status RPC", async () => { callGatewayMock.mockReset(); probeGatewayMock.mockReset(); callGatewayMock.mockResolvedValueOnce({ status: "ok" }); @@ -180,7 +180,7 @@ describe("probeGatewayStatus", () => { capability: "admin_capable", }, }); - const config = { gateway: { handshakeTimeoutMs: 30_000 } }; + const config = {}; await probeGatewayStatus({ url: "ws://127.0.0.1:19191", diff --git a/src/cli/daemon-cli/register-service-commands.ts b/src/cli/daemon-cli/register-service-commands.ts index 1916ce3373fb..a890cc6f63a1 100644 --- a/src/cli/daemon-cli/register-service-commands.ts +++ b/src/cli/daemon-cli/register-service-commands.ts @@ -142,8 +142,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .option( "--safe", "Request an OpenClaw-aware restart after active work drains " + - "(bounded wait; may force after gateway.reload.deferralTimeoutMs expires; " + - "set deferralTimeoutMs=0 for indefinite wait)", + "(bounded wait; may force after the timeout expires)", false, ) .option("--skip-deferral", "Bypass the safe-restart deferral gate; requires --safe", false) diff --git a/src/cli/daemon-cli/status.gather.test.ts b/src/cli/daemon-cli/status.gather.test.ts index 1d6156a61fca..ce2193c154cc 100644 --- a/src/cli/daemon-cli/status.gather.test.ts +++ b/src/cli/daemon-cli/status.gather.test.ts @@ -461,32 +461,6 @@ describe("gatherDaemonStatus", () => { expect(probeInput.configPath).toBe("/tmp/openclaw-daemon/openclaw.json"); }); - it("uses configured handshake timeout as the default daemon probe budget", async () => { - daemonLoadedConfig = { - gateway: { - bind: "lan", - tls: { enabled: true }, - handshakeTimeoutMs: 30_000, - auth: { token: "daemon-token" }, - }, - }; - - await gatherDaemonStatus({ - rpc: {}, - probe: true, - deep: false, - }); - - const probeInput = callArg(callGatewayStatusProbe) as { - config?: unknown; - preauthHandshakeTimeoutMs?: number; - timeoutMs?: number; - }; - expect(probeInput.config).toBe(daemonLoadedConfig); - expect(probeInput.preauthHandshakeTimeoutMs).toBe(30_000); - expect(probeInput.timeoutMs).toBe(30_000); - }); - it("reuses the shared CLI config snapshot when the daemon uses the same config path", async () => { serviceReadCommand.mockResolvedValueOnce({ programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"], diff --git a/src/cli/daemon-cli/status.gather.ts b/src/cli/daemon-cli/status.gather.ts index d6eac420417b..064f9b7a54e2 100644 --- a/src/cli/daemon-cli/status.gather.ts +++ b/src/cli/daemon-cli/status.gather.ts @@ -644,9 +644,7 @@ export async function gatherDaemonStatus( .catch(() => []) : []; - const timeoutMs = - parseStrictPositiveInteger(opts.rpc.timeout ?? undefined) ?? - Math.max(10_000, daemonCfg.gateway?.handshakeTimeoutMs ?? 0); + const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? undefined) ?? 10_000; const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true; const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled; @@ -704,7 +702,6 @@ export async function gatherDaemonStatus( shouldUseLocalTlsRuntime && tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined, - preauthHandshakeTimeoutMs: daemonCfg.gateway?.handshakeTimeoutMs, timeoutMs, json: opts.rpc.json, requireRpc: opts.requireRpc, diff --git a/src/cli/gateway-cli/run-loop.test.ts b/src/cli/gateway-cli/run-loop.test.ts index 95d05e661a98..ddb5106ff17f 100644 --- a/src/cli/gateway-cli/run-loop.test.ts +++ b/src/cli/gateway-cli/run-loop.test.ts @@ -124,13 +124,7 @@ const DRAIN_TIMEOUT_LOG = "drain timeout reached; proceeding with restart"; const ACTIVE_RUN_DRAIN_TIMEOUT_LOG = "active embedded run drain timeout reached; aborting active run(s) before restart"; const DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS = 300_000; -const loadConfig = vi.fn<() => { gateway: { reload: { deferralTimeoutMs?: number } } }>(() => ({ - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, -})); +const loadConfig = vi.fn(() => ({})); const gatewayLog = { debug: vi.fn(), info: vi.fn(), @@ -535,8 +529,8 @@ describe("runGatewayLoop", () => { expect(markGatewayDraining.mock.invocationCallOrder[0]).toBeLessThan( loadConfig.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, ); - expect(waitForActiveTasks).toHaveBeenCalledWith(90_000); - expectRestartCloseCall(closeFirst, 90_000); + expect(waitForActiveTasks).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); await startedSecond; expect(start).toHaveBeenCalledTimes(2); await new Promise((resolve) => { @@ -641,48 +635,6 @@ describe("runGatewayLoop", () => { }); }); - it("waits indefinitely for active embedded runs when reload config disables deferral timeout", async () => { - vi.clearAllMocks(); - loadConfig.mockReturnValueOnce({ - gateway: { - reload: { - deferralTimeoutMs: 0, - }, - }, - }); - getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0); - waitForActiveEmbeddedRuns.mockResolvedValueOnce({ drained: true }); - - await withIsolatedSignals(async ({ captureSignal }) => { - const { close, start, exited } = await createSignaledLoopHarness(); - const sigusr1 = captureSignal("SIGUSR1"); - const sigint = captureSignal("SIGINT"); - - sigusr1(); - await new Promise((resolve) => { - setImmediate(resolve); - }); - await new Promise((resolve) => { - setImmediate(resolve); - }); - - expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(undefined); - expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { - mode: "compacting", - reason: "restart", - }); - expect(abortEmbeddedAgentRun).not.toHaveBeenCalledWith(undefined, { - mode: "all", - reason: "restart", - }); - expectRestartCloseCall(close, 15_000); - expect(start).toHaveBeenCalledTimes(2); - - sigint(); - await expect(exited).resolves.toBe(0); - }); - }); - it("uses the restart drain timeout for active embedded runs before aborting", async () => { vi.clearAllMocks(); consumeGatewayRestartIntentPayloadSync.mockReturnValueOnce({}); @@ -707,10 +659,12 @@ describe("runGatewayLoop", () => { setImmediate(resolve); }); - expect(waitForActiveTasks).toHaveBeenCalledWith(90_000); - expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(90_000); + expect(waitForActiveTasks).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); + expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(waitForActiveGatewayRootWork).toHaveBeenCalledOnce(); - expect(waitForActiveGatewayRootWork.mock.calls[0]?.[0]).toBeLessThanOrEqual(90_000); + expect(waitForActiveGatewayRootWork.mock.calls[0]?.[0]).toBeLessThanOrEqual( + DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS, + ); expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting", reason: "restart", @@ -722,13 +676,7 @@ describe("runGatewayLoop", () => { expect(gatewayLog.warn).toHaveBeenCalledWith(ACTIVE_RUN_DRAIN_TIMEOUT_LOG); expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ - cfg: { - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }, + cfg: {}, sessionIds: new Set(["session-embedded-timeout"]), sessionKeys: new Set(["agent:main:embedded-timeout"]), reason: "gateway restart drain timeout", @@ -736,7 +684,7 @@ describe("runGatewayLoop", () => { expect(gatewayLog.warn).toHaveBeenCalledWith( "failed to mark interrupted main sessions for restart recovery: Error: store read-only", ); - expectRestartCloseCall(close, 90_000); + expectRestartCloseCall(close, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(start).toHaveBeenCalledTimes(2); sigint(); @@ -781,13 +729,7 @@ describe("runGatewayLoop", () => { reason: "restart", }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ - cfg: { - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }, + cfg: {}, sessionIds: new Set(["session-deferral-timeout"]), sessionKeys: new Set(["agent:main:deferral-timeout"]), reason: "gateway restart drain", @@ -845,13 +787,7 @@ describe("runGatewayLoop", () => { reason: "restart", }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ - cfg: { - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }, + cfg: {}, sessionIds: new Set(["session-forced-task"]), sessionKeys: new Set(["agent:main:forced-task"]), reason: "gateway restart drain", @@ -871,13 +807,6 @@ describe("runGatewayLoop", () => { it("restarts after SIGUSR1 even when drain times out, and resets runtime state for the new iteration", async () => { vi.clearAllMocks(); - loadConfig.mockReturnValue({ - gateway: { - reload: { - deferralTimeoutMs: 1_234, - }, - }, - }); peekGatewaySigusr1RestartReason.mockReturnValue(undefined); respawnGatewayProcessForUpdate.mockReturnValue({ mode: "disabled", @@ -959,27 +888,21 @@ describe("runGatewayLoop", () => { mode: "compacting", reason: "restart", }); - expect(waitForActiveTasks).toHaveBeenCalledWith(1_234); - expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(1_234); + expect(waitForActiveTasks).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); + expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "all", reason: "restart", }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ - cfg: { - gateway: { - reload: { - deferralTimeoutMs: 1_234, - }, - }, - }, + cfg: {}, sessionIds: new Set(["session-issue-82433"]), sessionKeys: new Set(["agent:main:issue-82433"]), reason: "gateway restart drain", }); expect(markGatewayDraining).toHaveBeenCalledTimes(1); expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG); - expectRestartCloseCall(closeFirst, 1_234); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1); expect(abortActiveCronTaskRuns).toHaveBeenCalledWith("Gateway restarting."); expect(waitForActiveCronTaskRuns).toHaveBeenCalledWith(1_000); @@ -1021,7 +944,7 @@ describe("runGatewayLoop", () => { await new Promise((resolve) => { setImmediate(resolve); }); - expectRestartCloseCall(closeSecond, 1_234); + expectRestartCloseCall(closeSecond, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2); expect(markGatewayDraining).toHaveBeenCalledTimes(2); expect(abortActiveCronTaskRuns).toHaveBeenCalledTimes(2); @@ -1142,7 +1065,7 @@ describe("runGatewayLoop", () => { "expected queued SIGUSR1 to trigger the second gateway start", ); await startedSecond; - expectRestartCloseCall(closeFirst, 90_000); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1); expect(markGatewayDraining).toHaveBeenCalledTimes(1); expect(resetAllLanes).toHaveBeenCalledTimes(1); @@ -1332,7 +1255,7 @@ describe("runGatewayLoop", () => { "expected queued SIGUSR1 to advance past failed restart startup", ); await startedThird; - expectRestartCloseCall(closeFirst, 90_000); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2); expect(markGatewayDraining).toHaveBeenCalledTimes(2); expect(resetAllLanes).toHaveBeenCalledTimes(2); @@ -1405,7 +1328,7 @@ describe("runGatewayLoop", () => { "expected post-failure SIGUSR1 to retry gateway startup", ); await startedThird; - expectRestartCloseCall(closeFirst, 90_000); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2); expect(markGatewayDraining).toHaveBeenCalledTimes(2); expect(resetAllLanes).toHaveBeenCalledTimes(2); @@ -1477,7 +1400,7 @@ describe("runGatewayLoop", () => { "expected failed task-flow registry restore to be logged", ); - expectRestartCloseCall(closeFirst, 90_000); + expectRestartCloseCall(closeFirst, DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS); expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(1); expect(start).toHaveBeenCalledTimes(1); expect(runtime.exit).not.toHaveBeenCalled(); @@ -1516,9 +1439,8 @@ describe("runGatewayLoop", () => { } }); - it("uses the default restart drain timeout when config omits deferralTimeoutMs", async () => { + it("uses the built-in restart drain timeout", async () => { vi.clearAllMocks(); - loadConfig.mockReturnValue({ gateway: { reload: {} } }); peekGatewaySigusr1RestartReason.mockReturnValue(undefined); respawnGatewayProcessForUpdate.mockReturnValue({ mode: "disabled", @@ -1611,13 +1533,6 @@ describe("runGatewayLoop", () => { force: true, reason: "file-intent restart", }); - loadConfig.mockReturnValueOnce({ - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }); getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0); listActiveEmbeddedRunSessionIds.mockReturnValueOnce(["session-file-intent"]); listActiveEmbeddedRunSessionKeys.mockReturnValueOnce(["agent:main:file-intent"]); @@ -1641,13 +1556,7 @@ describe("runGatewayLoop", () => { markGatewaySigusr1RestartHandled.mock.invocationCallOrder[0] ?? 0, ); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ - cfg: { - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }, + cfg: {}, sessionIds: new Set(["session-file-intent"]), sessionKeys: new Set(["agent:main:file-intent"]), reason: "gateway restart drain", @@ -1666,13 +1575,6 @@ describe("runGatewayLoop", () => { reason: "file-intent restart", }); consumeGatewaySigusr1RestartAuthorization.mockReturnValueOnce(false); - loadConfig.mockReturnValueOnce({ - gateway: { - reload: { - deferralTimeoutMs: 90_000, - }, - }, - }); getActiveEmbeddedRunCount.mockReturnValueOnce(1).mockReturnValue(0); listActiveEmbeddedRunSessionIds.mockReturnValueOnce(["session-file-intent"]); listActiveEmbeddedRunSessionKeys.mockReturnValueOnce(["agent:main:file-intent"]); diff --git a/src/cli/gateway-cli/run-loop.ts b/src/cli/gateway-cli/run-loop.ts index e20d2bfc77a3..26f3ef2e4f01 100644 --- a/src/cli/gateway-cli/run-loop.ts +++ b/src/cli/gateway-cli/run-loop.ts @@ -431,10 +431,8 @@ export async function runGatewayLoop(params: { return restartIntent.waitMs > 0 ? Math.floor(restartIntent.waitMs) : undefined; } try { - const { getRuntimeConfig, resolveGatewayRestartDeferralTimeoutMs } = - await loadGatewayLifecycleRuntimeModule(); - const timeoutMs = getRuntimeConfig().gateway?.reload?.deferralTimeoutMs; - return resolveGatewayRestartDeferralTimeoutMs(timeoutMs); + const { resolveGatewayRestartDeferralTimeoutMs } = await loadGatewayLifecycleRuntimeModule(); + return resolveGatewayRestartDeferralTimeoutMs(); } catch { return DEFAULT_RESTART_DRAIN_TIMEOUT_MS; } diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index 90dbfb8d3e1d..dc93d22d14c5 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -2970,7 +2970,7 @@ describe("runCli exit behavior", () => { }); }); - it("carries canonical local TLS fingerprint and handshake timeout through bare root", async () => { + it("carries the canonical local TLS fingerprint through bare root", async () => { readConfigFileSnapshotMock.mockResolvedValueOnce({ exists: true, valid: true, @@ -2978,7 +2978,6 @@ describe("runCli exit behavior", () => { gateway: { mode: "local", tls: { enabled: true }, - handshakeTimeoutMs: 30_000, auth: { mode: "token", token: "configured-token" }, }, }, @@ -2997,7 +2996,6 @@ describe("runCli exit behavior", () => { url: "wss://127.0.0.1:18789", token: "configured-token", tlsFingerprint: "sha256:local-self-signed-fingerprint", - preauthHandshakeTimeoutMs: 30_000, }); expect(launchTuiCliMock).toHaveBeenCalledWith( { deliver: false, tlsFingerprint: "sha256:local-self-signed-fingerprint" }, diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 2da43de40dd0..c5ff9e4f518b 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -512,7 +512,6 @@ async function resolveGatewayProbeTargets(config: OpenClawConfig): Promise { }); describe("worktrees cli", () => { - it("passes session owner activity and configured limits to gc", async () => { - const cfg: OpenClawConfig = { - worktrees: { cleanup: { maxCount: 25, maxTotalSizeGb: 50 } }, - }; - setRuntimeConfigSnapshot(cfg, cfg); + it("passes session owner activity and built-in limits to gc", async () => { + setRuntimeConfigSnapshot({}, {}); const gc = vi.spyOn(managedWorktrees, "gc").mockResolvedValue({ removed: [], orphansDeleted: 0, @@ -29,7 +25,7 @@ describe("worktrees cli", () => { await program.parseAsync(["worktrees", "gc"], { from: "user" }); expect(gc).toHaveBeenCalledWith({ - limits: { maxCount: 25, maxTotalSizeBytes: 50 * 1024 ** 3 }, + limits: {}, shouldProtectOwner: expect.any(Function), }); }); diff --git a/src/cli/worktrees-cli.ts b/src/cli/worktrees-cli.ts index fe321ad94d7d..51462917c400 100644 --- a/src/cli/worktrees-cli.ts +++ b/src/cli/worktrees-cli.ts @@ -118,7 +118,7 @@ export function registerWorktreesCli(program: Command): void { .option("--json", "Output JSON", false) .action(async (opts: JsonOption) => { const cfg = getRuntimeConfig(); - const limits = resolveWorktreeCleanupLimits(cfg.worktrees); + const limits = resolveWorktreeCleanupLimits(); const result = await managedWorktrees.gc({ limits, shouldProtectOwner: createManagedWorktreeOwnerProtection(cfg), diff --git a/src/commands/doctor-session-locks.test.ts b/src/commands/doctor-session-locks.test.ts index 5edbdd3826c8..37d6f7789b6b 100644 --- a/src/commands/doctor-session-locks.test.ts +++ b/src/commands/doctor-session-locks.test.ts @@ -258,7 +258,7 @@ describe("noteSessionLockHealth", () => { await expect(fs.access(reportOnlyLock)).resolves.toBeUndefined(); }); - it("uses configured stale threshold without removing live OpenClaw lock files", async () => { + it("uses the emergency stale-threshold environment override without removing live OpenClaw lock files", async () => { const sessionsDir = state.sessionsDir(); await fs.mkdir(sessionsDir, { recursive: true }); @@ -271,7 +271,7 @@ describe("noteSessionLockHealth", () => { await noteSessionLockHealth({ shouldRepair: true, - config: { session: { writeLock: { staleMs: 30_000 } } }, + env: { OPENCLAW_SESSION_WRITE_LOCK_STALE_MS: "30000" }, readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"], }); diff --git a/src/commands/doctor/cron/index.ts b/src/commands/doctor/cron/index.ts index 0f7c48661838..322158792979 100644 --- a/src/commands/doctor/cron/index.ts +++ b/src/commands/doctor/cron/index.ts @@ -51,9 +51,8 @@ function countInFlightCronJobs(jobs: Array>): number { } // Fixed advisory threshold: three failures in a row is a clear chronic signal on -// its own. It coincides with the scheduler's default transient-retry budget, but -// `cron.retry.maxAttempts` is per-job configurable and doctor deliberately does -// not mirror retry config or exhaustion semantics (`consecutiveErrors > maxAttempts`). +// its own. It coincides with the scheduler's built-in transient-retry budget, but +// doctor deliberately does not mirror retry exhaustion semantics. const CHRONIC_FAILURE_MIN_CONSECUTIVE_ERRORS = 3; // Count enabled jobs stuck in repeated run failures. `state.consecutiveErrors` diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts index 880c1d7e3fa2..03649ea92c69 100644 --- a/src/commands/doctor/shared/deprecation-compat.ts +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -62,6 +62,22 @@ function deprecatedCompatRecord( // doctor fixes, and replacement notes should be revalidated against the current // architecture because ownership and config footprint can shift during rollout. const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ + deprecatedCompatRecord({ + code: "doctor-runtime-tuning-knobs-purge", + deprecated: "2026-07-19", + warningStarts: "2026-07-19", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-19", + source: "retired runtime and bundled-channel numeric tuning knobs", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts", + replacement: "built-in runtime and channel defaults", + docsPath: "/gateway/doctor", + tests: [ + "src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts", + "src/config/dead-config-keys.test.ts", + ], + }), deprecatedCompatRecord({ code: "doctor-phase2-channel-dm-aliases", deprecated: "2026-07-18", diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index 8283d015298c..bfbc45e8381c 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -1486,75 +1486,6 @@ describe("legacy session parent fork migrate", () => { }); }); -describe("legacy diagnostics memory pressure snapshot migrate", () => { - it("renames the boolean toggle", () => { - const res = migrateLegacyConfigForTest({ - diagnostics: { - enabled: true, - memoryPressureBundle: false, - }, - }); - - expect(res.config?.diagnostics).toEqual({ - enabled: true, - memoryPressureSnapshot: false, - }); - expect(res.changes).toStrictEqual([ - "Moved diagnostics.memoryPressureBundle → memoryPressureSnapshot.", - ]); - }); - - it("preserves the renamed toggle when both keys are present", () => { - const res = migrateLegacyConfigForTest({ - diagnostics: { - memoryPressureBundle: false, - memoryPressureSnapshot: true, - }, - }); - - expect(res.config?.diagnostics).toEqual({ - memoryPressureSnapshot: true, - }); - expect(res.changes).toStrictEqual([ - "Removed diagnostics.memoryPressureBundle (memoryPressureSnapshot already set).", - ]); - }); - - it("moves nested enabled to the renamed boolean", () => { - const res = migrateLegacyConfigForTest({ - diagnostics: { - enabled: true, - memoryPressureBundle: { - enabled: false, - }, - }, - }); - - expect(res.config?.diagnostics).toEqual({ - enabled: true, - memoryPressureSnapshot: false, - }); - expect(res.changes).toStrictEqual([ - "Moved diagnostics.memoryPressureBundle → memoryPressureSnapshot.", - ]); - }); - - it("moves empty object form to the renamed default boolean", () => { - const res = migrateLegacyConfigForTest({ - diagnostics: { - memoryPressureBundle: {}, - }, - }); - - expect(res.config?.diagnostics).toEqual({ - memoryPressureSnapshot: true, - }); - expect(res.changes).toStrictEqual([ - "Moved diagnostics.memoryPressureBundle → memoryPressureSnapshot.", - ]); - }); -}); - describe("legacy WebChat channel config migrate", () => { it("removes retired WebChat channel config", () => { const raw = { @@ -4131,14 +4062,16 @@ describe("legacy flat memory search field migrate", () => { expect(res.config?.agents?.defaults?.memorySearch).toEqual({ enabled: true, - chunking: { tokens: 800, overlap: 100 }, query: { maxResults: 5 }, }); - expect(res.changes).toStrictEqual([ - "Moved agents.defaults.memorySearch.chunkSize → agents.defaults.memorySearch.chunking.tokens.", - "Moved agents.defaults.memorySearch.chunkOverlap → agents.defaults.memorySearch.chunking.overlap.", - "Moved agents.defaults.memorySearch.maxResults → agents.defaults.memorySearch.query.maxResults.", - ]); + expect(res.changes).toEqual( + expect.arrayContaining([ + "Moved agents.defaults.memorySearch.chunkSize → agents.defaults.memorySearch.chunking.tokens.", + "Moved agents.defaults.memorySearch.chunkOverlap → agents.defaults.memorySearch.chunking.overlap.", + "Moved agents.defaults.memorySearch.maxResults → agents.defaults.memorySearch.query.maxResults.", + "Removed retired runtime tuning knobs; built-in defaults now apply.", + ]), + ); }); it("moves a top-level legacy object and keeps explicit canonical values", () => { @@ -4162,15 +4095,17 @@ describe("legacy flat memory search field migrate", () => { expect(res.config).not.toHaveProperty("memorySearch"); expect(res.config?.agents?.defaults?.memorySearch).toEqual({ enabled: true, - chunking: { tokens: 1200, overlap: 100 }, query: { maxResults: 9 }, }); - expect(res.changes).toEqual([ - "Merged memorySearch → agents.defaults.memorySearch (filled missing fields from legacy; kept explicit agents.defaults values).", - "Removed agents.defaults.memorySearch.chunkSize (agents.defaults.memorySearch.chunking.tokens already set).", - "Moved agents.defaults.memorySearch.chunkOverlap → agents.defaults.memorySearch.chunking.overlap.", - "Removed agents.defaults.memorySearch.maxResults (agents.defaults.memorySearch.query.maxResults already set).", - ]); + expect(res.changes).toEqual( + expect.arrayContaining([ + "Merged memorySearch → agents.defaults.memorySearch (filled missing fields from legacy; kept explicit agents.defaults values).", + "Removed agents.defaults.memorySearch.chunkSize (agents.defaults.memorySearch.chunking.tokens already set).", + "Moved agents.defaults.memorySearch.chunkOverlap → agents.defaults.memorySearch.chunking.overlap.", + "Removed agents.defaults.memorySearch.maxResults (agents.defaults.memorySearch.query.maxResults already set).", + "Removed retired runtime tuning knobs; built-in defaults now apply.", + ]), + ); }); it("moves per-agent flat fields independently", () => { @@ -4183,16 +4118,16 @@ describe("legacy flat memory search field migrate", () => { }, }); - expect(res.config?.agents?.list?.[0]?.memorySearch).toEqual({ - chunking: { tokens: 500 }, - }); + expect(res.config?.agents?.list?.[0]?.memorySearch).toBeUndefined(); expect(res.config?.agents?.list?.[1]?.memorySearch).toEqual({ - chunking: { overlap: 50 }, query: { maxResults: 10 }, }); + expect(res.changes).toContain( + "Removed retired runtime tuning knobs; built-in defaults now apply.", + ); }); - it("is a no-op for canonical memory search fields", () => { + it("removes retired canonical memory search chunking fields", () => { const raw = { agents: { defaults: { @@ -4204,8 +4139,14 @@ describe("legacy flat memory search field migrate", () => { }, }; - expect(findLegacyConfigIssues(raw)).toEqual([]); - expect(migrateLegacyConfigForTest(raw)).toEqual({ config: null, changes: [] }); + expect(findLegacyConfigIssues(raw)).toEqual([expect.objectContaining({ path: "" })]); + const res = migrateLegacyConfigForTest(raw); + expect(res.config?.agents?.defaults?.memorySearch).toEqual({ + query: { maxResults: 5 }, + }); + expect(res.changes).toContain( + "Removed retired runtime tuning knobs; built-in defaults now apply.", + ); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.diagnostics.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.diagnostics.ts index fe42815d47a0..70fd2f4ec272 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.diagnostics.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.diagnostics.ts @@ -1,48 +1,5 @@ -// Legacy diagnostics config migrations for renamed runtime diagnostic options. -import { - defineLegacyConfigMigration, - getRecord, - type LegacyConfigMigrationSpec, - type LegacyConfigRule, -} from "../../../config/legacy.shared.js"; - -function isLegacyMemoryPressureBundleConfig(value: unknown): boolean { - return typeof value === "boolean" || getRecord(value) !== null; -} - -const MEMORY_PRESSURE_BUNDLE_RULE: LegacyConfigRule = { - path: ["diagnostics", "memoryPressureBundle"], - message: - 'diagnostics.memoryPressureBundle was renamed; use diagnostics.memoryPressureSnapshot instead. Run "openclaw doctor --fix".', - match: isLegacyMemoryPressureBundleConfig, - requireSourceLiteral: true, -}; +// Legacy diagnostics migrations are currently folded into the tuning-knob purge. +import type { LegacyConfigMigrationSpec } from "../../../config/legacy.shared.js"; /** Legacy config migration specs for diagnostics runtime config. */ -export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_DIAGNOSTICS: LegacyConfigMigrationSpec[] = [ - defineLegacyConfigMigration({ - id: "diagnostics.memoryPressureBundle->memoryPressureSnapshot", - describe: "Move diagnostics.memoryPressureBundle to diagnostics.memoryPressureSnapshot", - legacyRules: [MEMORY_PRESSURE_BUNDLE_RULE], - apply: (raw, changes) => { - const diagnostics = getRecord(raw.diagnostics); - if (!diagnostics || !isLegacyMemoryPressureBundleConfig(diagnostics.memoryPressureBundle)) { - return; - } - if (Object.hasOwn(diagnostics, "memoryPressureSnapshot")) { - delete diagnostics.memoryPressureBundle; - changes.push( - "Removed diagnostics.memoryPressureBundle (memoryPressureSnapshot already set).", - ); - return; - } - const legacy = getRecord(diagnostics.memoryPressureBundle); - diagnostics.memoryPressureSnapshot = - typeof diagnostics.memoryPressureBundle === "boolean" - ? diagnostics.memoryPressureBundle - : legacy?.enabled !== false; - delete diagnostics.memoryPressureBundle; - changes.push("Moved diagnostics.memoryPressureBundle → memoryPressureSnapshot."); - }, - }), -]; +export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_DIAGNOSTICS: LegacyConfigMigrationSpec[] = []; diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts index da8cf8deb4a9..fc9ae857f397 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts @@ -21,7 +21,92 @@ function applyAll(raw: Record) { return { raw, changes }; } +function configWithPath(path: string): Record { + return path + .split(".") + .reduceRight( + (value, segment) => (segment === "0" ? [value] : { [segment]: value }), + 1, + ) as Record; +} + +function getPath(value: unknown, path: string): unknown { + return path.split(".").reduce((current, segment) => { + if (Array.isArray(current)) { + return current[Number(segment)]; + } + return current && typeof current === "object" + ? (current as Record)[segment] + : undefined; + }, value); +} + describe("retired runtime config migrations", () => { + it.each([ + "auth.cooldowns", + "secrets.resolution", + "browser.remoteCdpTimeoutMs", + "browser.tabCleanup.idleMinutes", + "tools.loopDetection.warningThreshold", + "tools.loopDetection.detectors", + "agents.defaults.compaction.reserveTokens", + "agents.defaults.compaction.reserveTokensFloor", + "agents.defaults.compaction.maxHistoryShare", + "agents.defaults.contextPruning.softTrim", + "agents.defaults.memorySearch.chunking", + "agents.defaults.memorySearch.sync.watchDebounceMs", + "agents.defaults.memorySearch.sync.intervalMinutes", + "agents.defaults.memorySearch.query.hybrid.vectorWeight", + "agents.defaults.memorySearch.query.hybrid.mmr.lambda", + "agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays", + "agents.defaults.memorySearch.cache.maxEntries", + "agents.defaults.cliBackends.codex.reliability.outputLimits", + "agents.defaults.cliBackends.codex.reliability.watchdog.fresh.noOutputTimeoutMs", + "agents.defaults.runRetries", + "agents.list.0.compaction.reserveTokens", + "agents.list.0.contextPruning.softTrimRatio", + "agents.list.0.memorySearch.chunking", + "agents.list.0.cliBackends.codex.reliability.outputLimits", + "agents.list.0.runRetries", + "agents.list.0.tools.loopDetection.warningThreshold", + "agents.list.0.tools.loopDetection.detectors", + "gateway.handshakeTimeoutMs", + "gateway.channelHealthCheckMinutes", + "gateway.reload.debounceMs", + "gateway.reload.deferralTimeoutMs", + "gateway.http.endpoints.chatCompletions.maxBodyBytes", + "gateway.http.endpoints.responses.maxBodyBytes", + "session.typingIntervalSeconds", + "session.writeLock", + "session.agentToAgent.maxPingPongTurns", + "cron.maxConcurrentRuns", + "cron.triggers.minIntervalMs", + "cron.retry", + "diagnostics.stuckSessionWarnMs", + "diagnostics.memoryPressureSnapshot", + "diagnostics.memoryPressureBundle", + "web.heartbeatSeconds", + "web.reconnect", + "web.whatsapp", + "messages.queue.debounceMs", + "messages.statusReactions.timing", + "acp.stream.coalesceIdleMs", + "acp.stream.hiddenBoundarySeparator", + "acp.maxConcurrentSessions", + "acp.runtime.ttlMinutes", + "mcp.sessionIdleTtlMs", + "worktrees", + "transcripts.maxUtterances", + "hooks.maxBodyBytes", + "update.auto.stableDelayHours", + ] as const)("strips retired tuning knob %s", (path) => { + const result = applyAll(configWithPath(path)); + expect(getPath(result.raw, path)).toBeUndefined(); + expect(result.changes).toContain( + "Removed retired runtime tuning knobs; built-in defaults now apply.", + ); + }); + it("moves aliases and strips dead keys", () => { const result = applyAll({ tui: { footer: { showRemoteHost: true } }, diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts index 76235a22a436..242e346a41a4 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts @@ -146,7 +146,177 @@ function hasMediaDeepgram(value: unknown): boolean { }); } +const RETIRED_TUNING_PATHS = [ + ["auth", "cooldowns"], + ["secrets", "resolution"], + ["browser", "remoteCdpTimeoutMs"], + ["browser", "remoteCdpHandshakeTimeoutMs"], + ["browser", "localLaunchTimeoutMs"], + ["browser", "localCdpReadyTimeoutMs"], + ["browser", "actionTimeoutMs"], + ["browser", "cdpPortRangeStart"], + ["browser", "tabCleanup", "idleMinutes"], + ["browser", "tabCleanup", "maxTabsPerSession"], + ["browser", "tabCleanup", "sweepMinutes"], + ["tools", "loopDetection", "genericRepeat"], + ["tools", "loopDetection", "knownPollNoProgress"], + ["tools", "loopDetection", "pingPong"], + ["tools", "loopDetection", "windowSize"], + ["tools", "loopDetection", "historySize"], + ["tools", "loopDetection", "warningThreshold"], + ["tools", "loopDetection", "unknownToolThreshold"], + ["tools", "loopDetection", "criticalThreshold"], + ["tools", "loopDetection", "globalCircuitBreakerThreshold"], + ["tools", "loopDetection", "detectors"], + ["tools", "loopDetection", "postCompactionGuard"], + ["gateway", "handshakeTimeoutMs"], + ["gateway", "channelHealthCheckMinutes"], + ["gateway", "channelStaleEventThresholdMinutes"], + ["gateway", "channelMaxRestartsPerHour"], + ["gateway", "reload", "debounceMs"], + ["gateway", "reload", "deferralTimeoutMs"], + ["gateway", "http", "endpoints", "chatCompletions", "maxBodyBytes"], + ["gateway", "http", "endpoints", "chatCompletions", "maxImageParts"], + ["gateway", "http", "endpoints", "chatCompletions", "maxTotalImageBytes"], + ["gateway", "http", "endpoints", "responses", "maxBodyBytes"], + ["session", "typingIntervalSeconds"], + ["session", "writeLock"], + ["session", "agentToAgent", "maxPingPongTurns"], + ["cron", "maxConcurrentRuns"], + ["cron", "triggers", "minIntervalMs"], + ["cron", "retry"], + ["diagnostics", "stuckSessionWarnMs"], + ["diagnostics", "stuckSessionAbortMs"], + ["diagnostics", "memoryPressureSnapshot"], + ["diagnostics", "memoryPressureBundle"], + ["web", "heartbeatSeconds"], + ["web", "reconnect"], + ["web", "whatsapp"], + ["messages", "queue", "debounceMs"], + ["messages", "statusReactions", "timing"], + ["acp", "stream", "coalesceIdleMs"], + ["acp", "stream", "maxChunkChars"], + ["acp", "stream", "maxOutputChars"], + ["acp", "stream", "maxSessionUpdateChars"], + ["acp", "stream", "hiddenBoundarySeparator"], + ["acp", "maxConcurrentSessions"], + ["acp", "runtime", "ttlMinutes"], + ["mcp", "sessionIdleTtlMs"], + ["worktrees"], + ["transcripts", "maxUtterances"], + ["hooks", "maxBodyBytes"], + ["update", "auto", "stableDelayHours"], + ["update", "auto", "stableJitterHours"], + ["update", "auto", "betaCheckIntervalHours"], +] as const; + +const RETIRED_AGENT_TUNING_PATHS = [ + ["compaction", "reserveTokens"], + ["compaction", "reserveTokensFloor"], + ["compaction", "maxHistoryShare"], + ["contextPruning", "keepLastAssistants"], + ["contextPruning", "softTrimRatio"], + ["contextPruning", "hardClearRatio"], + ["contextPruning", "minPrunableToolChars"], + ["contextPruning", "softTrim"], + ["memorySearch", "chunking"], + ["memorySearch", "sync", "watchDebounceMs"], + ["memorySearch", "sync", "intervalMinutes"], + ["memorySearch", "query", "hybrid", "vectorWeight"], + ["memorySearch", "query", "hybrid", "textWeight"], + ["memorySearch", "query", "hybrid", "candidateMultiplier"], + ["memorySearch", "query", "hybrid", "mmr", "lambda"], + ["memorySearch", "query", "hybrid", "temporalDecay", "halfLifeDays"], + ["memorySearch", "cache", "maxEntries"], + ["cliBackends", "*", "reliability", "outputLimits"], + ["cliBackends", "*", "reliability", "watchdog", "fresh", "noOutputTimeoutMs"], + ["cliBackends", "*", "reliability", "watchdog", "resume", "noOutputTimeoutMs"], + ["runRetries"], + ["tools", "loopDetection", "genericRepeat"], + ["tools", "loopDetection", "knownPollNoProgress"], + ["tools", "loopDetection", "pingPong"], + ["tools", "loopDetection", "windowSize"], + ["tools", "loopDetection", "historySize"], + ["tools", "loopDetection", "warningThreshold"], + ["tools", "loopDetection", "unknownToolThreshold"], + ["tools", "loopDetection", "criticalThreshold"], + ["tools", "loopDetection", "globalCircuitBreakerThreshold"], + ["tools", "loopDetection", "detectors"], + ["tools", "loopDetection", "postCompactionGuard"], +] as const; + +function deleteRetiredPath(owner: unknown, path: readonly string[], index = 0): boolean { + const record = getRecord(owner); + if (!record) { + return false; + } + const key = path[index]; + if (!key) { + return false; + } + if (key === "*") { + let changed = false; + for (const value of Object.values(record)) { + changed = deleteRetiredPath(value, path, index + 1) || changed; + } + return changed; + } + if (index === path.length - 1) { + if (!Object.hasOwn(record, key)) { + return false; + } + delete record[key]; + return true; + } + const child = getRecord(record[key]); + if (!child || !deleteRetiredPath(child, path, index + 1)) { + return false; + } + if (Object.keys(child).length === 0) { + delete record[key]; + } + return true; +} + +function stripRetiredTuningKnobs(raw: Record): boolean { + let changed = false; + for (const path of RETIRED_TUNING_PATHS) { + changed = deleteRetiredPath(raw, path) || changed; + } + const agents = getRecord(raw.agents); + const defaults = getRecord(agents?.defaults); + if (defaults) { + for (const path of RETIRED_AGENT_TUNING_PATHS) { + changed = deleteRetiredPath(defaults, path) || changed; + } + } + if (Array.isArray(agents?.list)) { + for (const agent of agents.list) { + for (const path of RETIRED_AGENT_TUNING_PATHS) { + changed = deleteRetiredPath(agent, path) || changed; + } + } + } + return changed; +} + export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "runtime.tuning-knobs-purge", + describe: "Remove retired runtime tuning knobs", + legacyRules: [ + rule( + [], + "Numeric runtime tuning knobs were retired and now use built-in defaults.", + (_value, root) => stripRetiredTuningKnobs(structuredClone(root)), + ), + ], + apply: (raw, changes) => { + if (stripRetiredTuningKnobs(raw)) { + changes.push("Removed retired runtime tuning knobs; built-in defaults now apply."); + } + }, + }), defineLegacyConfigMigration({ id: "runtime.retired-config-keys", describe: "Migrate retired root and tool config keys", diff --git a/src/commands/gateway-status.test.ts b/src/commands/gateway-status.test.ts index 4c1bf75a2218..93414f2c7ac0 100644 --- a/src/commands/gateway-status.test.ts +++ b/src/commands/gateway-status.test.ts @@ -1074,24 +1074,6 @@ describe("gateway-status command", () => { expect(requireProbeCall("wss://remote.example:18789").timeoutMs).toBe(15_000); }); - it("uses configured handshake timeout as the default local probe budget", async () => { - const { runtime } = createRuntimeCapture(); - probeGateway.mockClear(); - readBestEffortConfig.mockResolvedValueOnce({ - gateway: { - mode: "local", - handshakeTimeoutMs: 30_000, - auth: { mode: "token", token: "ltok" }, - }, - } as never); - - await gatewayStatusCommand({ json: true }, asRuntimeEnv(runtime)); - - const localProbeCall = requireProbeCall("ws://127.0.0.1:18789"); - expect(localProbeCall.preauthHandshakeTimeoutMs).toBe(30_000); - expect(localProbeCall.timeoutMs).toBe(30_000); - }); - it("keeps inactive local loopback probes on the short timeout in remote mode", async () => { const { runtime } = createRuntimeCapture(); probeGateway.mockClear(); diff --git a/src/commands/gateway-status.ts b/src/commands/gateway-status.ts index f3362870fb5d..d35e3ac589dc 100644 --- a/src/commands/gateway-status.ts +++ b/src/commands/gateway-status.ts @@ -55,7 +55,7 @@ export async function gatewayStatusCommand( const startedAt = Date.now(); const cfg = await readBestEffortConfig(); const rich = isRich() && opts.json !== true; - const defaultTimeoutMs = Math.max(3000, cfg.gateway?.handshakeTimeoutMs ?? 0); + const defaultTimeoutMs = 3000; const overallTimeoutMs = parseTimeoutMs(opts.timeout, defaultTimeoutMs); const portOverride = parseGatewayPortOption(opts.port); const wideAreaDomain = resolveWideAreaDiscoveryDomain({ diff --git a/src/commands/gateway-status/probe-run.ts b/src/commands/gateway-status/probe-run.ts index 5f4f1f3fbc12..66b6454814e1 100644 --- a/src/commands/gateway-status/probe-run.ts +++ b/src/commands/gateway-status/probe-run.ts @@ -139,7 +139,6 @@ export async function runGatewayStatusProbePass(params: { target.kind === "localLoopback" && target.url.startsWith("wss://") ? params.localTlsFingerprint : undefined, - preauthHandshakeTimeoutMs: params.cfg.gateway?.handshakeTimeoutMs, timeoutMs: resolveProbeBudgetMs(params.overallTimeoutMs, target), }); return { diff --git a/src/commands/status.scan.fast-json.test.ts b/src/commands/status.scan.fast-json.test.ts index dccee3f05218..526977006122 100644 --- a/src/commands/status.scan.fast-json.test.ts +++ b/src/commands/status.scan.fast-json.test.ts @@ -181,25 +181,6 @@ describe("scanStatusJsonFast", () => { expect(mocks.probeGateway).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 5000 })); }); - it("keeps configured gateway handshake timeouts on the lean JSON path", async () => { - mocks.hasConfiguredChannels.mockReturnValue(true); - applyStatusScanDefaults(mocks, { - resolvedConfig: { - ...createStatusMemorySearchConfig(), - gateway: { handshakeTimeoutMs: 30_000 }, - } as never, - }); - - await scanStatusJsonFast({}, {} as never); - - expect(mocks.probeGateway).toHaveBeenCalledWith( - expect.objectContaining({ - preauthHandshakeTimeoutMs: 30_000, - timeoutMs: 30_000, - }), - ); - }); - it("restores the local status RPC fallback when --all is requested", async () => { mocks.hasConfiguredChannels.mockReturnValue(true); mocks.callGateway.mockResolvedValue({ sessions: 1 }); diff --git a/src/commands/status.scan.fast-json.ts b/src/commands/status.scan.fast-json.ts index c5ba6069a9ee..3449ac34665e 100644 --- a/src/commands/status.scan.fast-json.ts +++ b/src/commands/status.scan.fast-json.ts @@ -128,10 +128,7 @@ export async function scanStatusJsonFast( fetchGitUpdate: opts.all === true, includeRegistryUpdate: opts.all === true, includeLocalStatusRpcFallback: opts.all === true, - gatewayProbeTimeoutMs: - opts.all === true - ? undefined - : (cfg) => opts.timeoutMs ?? Math.max(1000, cfg.gateway?.handshakeTimeoutMs ?? 0), + gatewayProbeTimeoutMs: opts.all === true ? undefined : () => opts.timeoutMs ?? 1000, resolveHasConfiguredChannels: (cfg) => hasPotentialConfiguredChannelsForStatusJson(cfg), resolveMemory: async ({ cfg, agentStatus, memoryPlugin }) => opts.all diff --git a/src/commands/status.scan.shared.test.ts b/src/commands/status.scan.shared.test.ts index 5b3a32e53569..38b00f668c70 100644 --- a/src/commands/status.scan.shared.test.ts +++ b/src/commands/status.scan.shared.test.ts @@ -288,7 +288,7 @@ describe("resolveGatewayProbeSnapshot", () => { expect(result.gatewayProbeAuthWarning).toBe("warn"); }); - it("keeps the local status RPC fallback timeout aligned with configured handshake timeout", async () => { + it("uses built-in probe defaults for the local status RPC fallback", async () => { mocks.resolveGatewayProbeTarget.mockReturnValue({ mode: "local", gatewayMode: "local", @@ -313,16 +313,16 @@ describe("resolveGatewayProbeSnapshot", () => { mocks.callGateway.mockResolvedValue({ sessions: 1 }); await resolveGatewayProbeSnapshot({ - cfg: { gateway: { handshakeTimeoutMs: 30_000 } }, + cfg: {}, opts: {}, }); const probeCall = readProbeCall(); - expect(probeCall.preauthHandshakeTimeoutMs).toBe(30_000); - expect(probeCall.timeoutMs).toBe(30_000); + expect(probeCall).not.toHaveProperty("preauthHandshakeTimeoutMs"); + expect(probeCall.timeoutMs).toBe(2500); const gatewayCall = readGatewayCall(); - expect(gatewayCall.config).toEqual({ gateway: { handshakeTimeoutMs: 30_000 } }); - expect(gatewayCall.timeoutMs).toBe(30_000); + expect(gatewayCall.config).toEqual({}); + expect(gatewayCall.timeoutMs).toBe(2000); }); it("does not raise an explicit local status RPC fallback timeout", async () => { @@ -350,12 +350,12 @@ describe("resolveGatewayProbeSnapshot", () => { mocks.callGateway.mockResolvedValue({ sessions: 1 }); await resolveGatewayProbeSnapshot({ - cfg: { gateway: { handshakeTimeoutMs: 30_000 } }, + cfg: {}, opts: { timeoutMs: 1000 }, }); const probeCall = readProbeCall(); - expect(probeCall.preauthHandshakeTimeoutMs).toBe(30_000); + expect(probeCall).not.toHaveProperty("preauthHandshakeTimeoutMs"); expect(probeCall.timeoutMs).toBe(1000); expect(readGatewayCall().timeoutMs).toBe(1000); }); diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index ba371d833476..1b5ea8a520ca 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -216,9 +216,7 @@ async function applyLocalStatusRpcFallback(params: { method: "status", token: params.gatewayProbeAuth.token, password: params.gatewayProbeAuth.password, - timeoutMs: params.timeoutMsExplicit - ? boundedFallbackTimeoutMs - : Math.max(params.cfg.gateway?.handshakeTimeoutMs ?? 0, boundedFallbackTimeoutMs), + timeoutMs: boundedFallbackTimeoutMs, mode: GATEWAY_CLIENT_MODES.BACKEND, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, }), @@ -295,10 +293,7 @@ export async function resolveGatewayProbeSnapshot(params: { ) : { auth: {}, warning: undefined }; let gatewayProbeAuthWarning = gatewayProbeAuthResolution.warning; - const defaultProbeTimeoutMs = Math.max( - params.opts.all ? 5000 : 2500, - params.cfg.gateway?.handshakeTimeoutMs ?? 0, - ); + const defaultProbeTimeoutMs = params.opts.all ? 5000 : 2500; const timeoutMsExplicit = params.opts.timeoutMs !== undefined; const probeTimeoutMs = params.opts.timeoutMs ?? defaultProbeTimeoutMs; const initialGatewayProbe = shouldProbe @@ -307,7 +302,6 @@ export async function resolveGatewayProbeSnapshot(params: { probeGateway({ url: gatewayConnection.url, auth: gatewayProbeAuthResolution.auth, - preauthHandshakeTimeoutMs: params.cfg.gateway?.handshakeTimeoutMs, timeoutMs: probeTimeoutMs, detailLevel: params.opts.detailLevel ?? "presence", }), diff --git a/src/commands/status.scan.test.ts b/src/commands/status.scan.test.ts index 2ba744c5a83f..f44bddd53328 100644 --- a/src/commands/status.scan.test.ts +++ b/src/commands/status.scan.test.ts @@ -344,7 +344,6 @@ describe("scanStatus", () => { expect(firstCallArg(mocks.probeGateway, "probeGateway args")).toStrictEqual({ url: "ws://127.0.0.1:18789", auth: {}, - preauthHandshakeTimeoutMs: undefined, timeoutMs: 2500, detailLevel: "presence", }); diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index b8028130552d..abc9caa00afa 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -15,24 +15,24 @@ type BundledChannelConfigMetadata = { }; const RAW_BUNDLED_CHANNEL_CONFIG_METADATA = [ - '[{"pluginId":"clickclack","channelId":"clickclack","order":85,"channelEnvVars":["CLICKCLACK_BOT_TOKEN"],"label":"ClickClack","description":"self-hosted chat via first-class ClickClack bot tokens.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"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":"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"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"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":"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"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"discord","channelId":"discord","channelEnvVars":["DISCORD_BOT_TOKEN"],"label":"Discord","description":"very well supported right now.","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":{"type":"string"}},"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"}},"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},"heartbeat":{"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"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"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":"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}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"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"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"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}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"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"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"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},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"', - 'type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"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":"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}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"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":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"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"}},"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},"heartbeat":{"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"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"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":"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}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"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"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"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}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"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"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"incl', - 'udeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"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},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"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":"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}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Discord","help":"Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed."},"dmPolicy":{"label":"Discord DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.discord.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Discord Config Writes","help":"Allow Discord to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Discord Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Discord channel IDs. Native Discord @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Discord Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Discord Mention Pattern Allowlist","help":"Discord channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Discord Mention Pattern Denylist","help":"Discord channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"proxy":{"label":"Discord Proxy URL","help":"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts..proxy."},"commands.native":{"label":"Discord Native Commands","help":"Override native commands for Discord (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Discord Native Skill Commands","help":"Override native skill commands for Discord (bool or \\"auto\\")."},"streaming":{"label":"Discord Streaming Mode","help":"Unified Discord stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Discord Streaming Mode","help":"Canonical Discord preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Discord Chunk Mode","help":"Chunking mode for outbound Discord text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Discord Block Streaming Enabled","help":"Enable chunked block-style Discord preview delivery when channels.discord.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Discord Block Streaming Coalesce","help":"Merge streamed Discord block replies before final delivery."},"streaming.preview.chunk.minChars":{"label":"Discord Draft Chunk Min Chars","help":"Minimum chars before emitting a Discord stream preview update when channels.discord.streaming.mode=\\"block\\" (default: 200)."},"streaming.preview.chunk.maxChars":{"label":"Discord Draft Chunk Max Chars","help":"Target max size for a Discord stream preview chunk when channels.discord.streaming.mode=\\"block\\" (default: 800; clamped to channels.discord.textChunkLimit)."},"streaming.preview.chunk.breakPreference":{"label":"Discord Draft Chunk Break Preference","help":"Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph."},"streaming.preview.toolProgress":{"label":"Discord 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":"Discord Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Discord 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":"Discord Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Discord Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Discord 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":"Discord 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.commentary":{"label":"Discord Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"streaming.progress.commandText":{"label":"Discord Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"retry.attempts":{"label":"Discord Retry Attempts","help":"Max retry attempts for outbound Discord API calls (default: 3)."},"retry.minDelayMs":{"label":"Discord Retry Min Delay (ms)","help":"Minimum retry delay in ms for Discord outbound calls."},"retry.maxDelayMs":{"label":"Discord Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Discord outbound calls."},"retry.jitter":{"label":"Discord Retry Jitter","help":"Jitter factor (0-1) applied to Discord retry delays."},"maxLinesPerMessage":{"label":"Discord Max Lines Per Message","help":"Soft max line count per Discord message (default: 17)."},"suppressEmbeds":{"label":"Discord Suppress Link Embeds","help":"Suppress Discord-generated link embeds on outbound messages by default. Explicit embeds still send normally. Default: true."},"thread.inheritParent":{"label":"Discord Thread Parent Inheritance","help":"If true, Discord thread sessions inherit the parent channel transcript (default: false)."},"', - 'eventQueue.listenerTimeout":{"label":"Discord EventQueue Listener Timeout (ms)","help":"Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts..eventQueue.listenerTimeout."},"eventQueue.maxQueueSize":{"label":"Discord EventQueue Max Queue Size","help":"Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts..eventQueue.maxQueueSize."},"eventQueue.maxConcurrency":{"label":"Discord EventQueue Max Concurrency","help":"Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts..eventQueue.maxConcurrency."},"threadBindings.enabled":{"label":"Discord Thread Binding Enabled","help":"Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Discord Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Discord Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Discord Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel."},"threadBindings.defaultSpawnContext":{"label":"Discord Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."},"subagentProgress":{"label":"Discord Subagent Progress","help":"Show active subagent count reactions and typing on the source message. Default: false."},"ui.components.accentColor":{"label":"Discord Component Accent Color","help":"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts..ui.components.accentColor."},"agentComponents.ttlMs":{"label":"Discord Component TTL (ms)","help":"How long sent Discord component callbacks remain registered. Default is 1800000 (30 minutes); maximum is 86400000 (24 hours)."},"intents.presence":{"label":"Discord Presence Intent","help":"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false."},"intents.guildMembers":{"label":"Discord Guild Members Intent","help":"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false."},"intents.voiceStates":{"label":"Discord Voice States Intent","help":"Enable the Guild Voice States intent. Defaults to the effective Discord voice setting; set true only for Discord voice channel conversations."},"gatewayInfoTimeoutMs":{"label":"Discord Gateway Metadata Timeout (ms)","help":"Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset."},"gatewayReadyTimeoutMs":{"label":"Discord Gateway READY Timeout (ms)","help":"Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset."},"gatewayRuntimeReadyTimeoutMs":{"label":"Discord Gateway Runtime READY Timeout (ms)","help":"Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset."},"voice.enabled":{"label":"Discord Voice Enabled","help":"Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent."},"voice.model":{"label":"Discord Voice Model","help":"Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai/gpt-5.6-sol). Leave unset to inherit the routed agent model."},"voice.mode":{"label":"Discord Voice Mode","help":"Conversation mode: agent-proxy (default) uses realtime voice as the microphone/speaker for the routed OpenClaw agent, stt-tts uses batch speech-to-text plus TTS, and bidi lets the realtime provider converse directly with the OpenClaw consult tool."},"voice.agentSession":{"label":"Discord Voice Agent Session","help":"Controls which OpenClaw conversation receives voice turns. Leave unset for the voice channel session, or set mode=\\"target\\" with a Discord target such as channel:123 to make voice an extension of an existing text channel session."},"voice.agentSession.target":{"label":"Discord Voice Agent Session Target","help":"Discord target used when voice.agentSession.mode=\\"target\\", for example channel:123."},"voice.followUsersEnabled":{"label":"Discord Voice Follow Users Enabled","help":"Toggle Discord voice follow-users behavior without removing the saved voice.followUsers list. Defaults to true when followUsers is configured."},"voice.followUsers":{"label":"Discord Voice Follow Users","help":"Discord user IDs to follow into voice channels. The bot joins when a followed user joins or moves, and leaves when that user disconnects."},"voice.realtime.provider":{"label":"Discord Realtime Provider","help":"Realtime voice provider for agent-proxy or bidi Discord voice modes, such as openai."},"voice.realtime.model":{"label":"Discord Realtime Model","help":"Provider realtime session model, such as gpt-realtime-2.1. This is separate from voice.model, which remains the OpenClaw agent brain model."},"voice.realtime.speakerVoice":{"label":"Discord Realtime Speaker Voice","help":"Provider realtime output voice name, such as cedar."},"voice.realtime.speakerVoiceId":{"label":"Discord Realtime Speaker Voice ID","help":"Provider realtime output voice id."},"voice.realtime.toolPolicy":{"label":"Discord Realtime Tool Policy","help":"Tool policy for the OpenClaw agent consult tool in realtime voice modes: safe-read-only, owner, or none. Default is owner for agent-proxy and safe-read-only for bidi."},"voice.realtime.consultPolicy":{"label":"Discord Realtime Consult Policy","help":"Use always to strongly prefer the OpenClaw agent brain for substantive realtime turns. agent-proxy defaults to always."},"voice.realtime.requireWakeName":{"label":"Discord Realtime Require Wake Name","help":"Control OpenAI agent-proxy wake-name gating. Unset listens naturally with one human and requires a wake name with two or more; true always requires one and false never does."},"voice.realtime.wakeNames":{"label":"Discord Realtime Wake Names","help":"One- or two-word activation names used whenever OpenAI agent-proxy Discord realtime voice has an active wake-name gate."},"voice.realtime.bootstrapContextFiles":{"label":"Discord Realtime Bootstrap Context Files","help":"Agent profile bootstrap files included in realtime provider instructions for direct voice identity/persona grounding. Defaults to IDENTITY.md, USER.md, and SOUL.md; set [] to disable."},"voice.realtime.bargeIn":{"label":"Discord Realtime Barge-In","help":"Allow Discord speaker-start events to interrupt active realtime playback. Set true to keep manual interruption when provider input-audio interruption is disabled for echo control."},"voice.realtime.minBargeInAudioEndMs":{"label":"Discord Realtime Minimum Barge-In Audio (ms)","help":"Minimum assistant playback duration before a Discord barge-in truncates realtime audio. Default: 250; set 0 for immediate interruption in low-echo rooms."},"voice.realtime.providers":{"label":"Discord Realtime Provider Settings","help":"Provider-specific realtime voice settings keyed by provider id.","advanced":true},"voice.autoJoin":{"label":"Discord Voice Auto-Join","help":"Voice channels to auto-join on startup (list of guildId/channelId entries)."},"voice.allowedChannels":{"label":"Discord Voice Allowed Channels","help":"Optional voice channel residency allowlist. When set, /vc join, auto-join, and bot voice-state moves are restricted to these guildId/channelId entries. Leave unset to allow any voice channel."},"voice.daveEncryption":{"label":"Discord Voice DAVE Encryption","help":"Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this)."},"voice.decryptionFailureTolerance":{"label":"Discord Voice Decrypt Failure Tolerance","help":"Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24)."},"voice.connectTimeoutMs":{"label":"Discord Voice Connect Timeout (ms)","help":"Initial @discordjs/voice Ready wait before a join is treated as failed. Default: 30000."},"voice.reconnectGraceMs":{"label":"Discord Voice Reconnect Grace (ms)","help":"Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000."},"voice.captureSilenceGraceMs":{"label":"Discord Voice Capture Silence Grace (ms)","help":"Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2000."},"voice.tts":{"label":"Discord Voice Text-to-Speech","help":"Optional TTS overrides for Discord voice playback (merged with messages.tts)."},"pluralkit.enabled":{"label":"Discord PluralKit Enabled","help":"Resolve PluralKit proxied messages and treat system members as distinct senders."},"pluralkit.token":{"label":"Discord PluralKit Token","help":"Optional PluralKit token for resolving private systems or members."},"activity":{"label":"Discord Presence Activity","help":"Discord presence activity text (defaults to custom status)."},"status":{"label":"Discord Presence Status","help":"Discord presence status (online, dnd, idle, invisible)."},"autoPresence.enabled":{"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd."},"autoPresence.intervalMs":{"label":"Discord Auto Presence Check Interval (ms)","help":"How often to evaluate Discord auto-presence state in milliseconds (default: 30000)."},"autoPresence.minUpdateIntervalMs":{"label":"Discord Auto Presence Min Update Interval (ms)","help":"Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes."},"autoPresence.healthyText":{"label":"Discord Auto Presence Healthy Text","help":"Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set."},"autoPresence.degradedText":{"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle)."},"autoPresence.exhaustedText":{"label":"Discord Auto Presence Exhausted Text","help":"Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder."},"guilds.*.presenceEvents":{"label":"Discord Online Presence Events","help":"Route selected human offline-to-online transitions into the configured guild channel as agent system events. Requires the Guild Presences privileged intent and an enabled agent heartbeat."},"guilds.*.presenceEvents.enabled":{"label":"Discord Online Presence Events Enabled","help":"Enable online-presence agent wakes for this guild. Defaults to true when presenceEvents is configured."},"guilds.*.presenceEvents.channelId":{"label":"Discord Online Presence Target Channel","help":"Numeric Discord channel ID whose routed agent session receives online-presence events and greeting delivery."},"guilds.*.presenceEvents.users":{"label":"Discord Online Presence User IDs","help":"Optional immutable Discord user ID allowlist. Omit to include all human members in the guild."},"guilds.*.presenceEvents.reconnectSuppressSeconds":{"label":"Discord Online Presence Reconnect Suppression","help":"Suppress online-presence events for this many seconds after a new Gateway session while guild presence state is rebuilt. Resumed sessions are unaffected. 0 disables. Default: 300."},"guilds.*.presenceEvents.burstLimit":{"label":"Discord Online Presence Burst Limit","help":"Maximum successfully queued online-presence events for this guild per burst window; the rest are suppressed and logged once. Default: 8."},"guilds.*.presenceEvents.burstWindowSeconds":{"label":"Discord Online Presence Burst Window","help":"Sliding window in seconds used for burst detection. Default: 60."},"activityType":{"label":"Discord Presence Activity Type","help":"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing)."},"activityUrl":{"label":"Discord Presence Activity URL","help":"Discord presence streaming URL (required for activityType=1)."},"allowBots":{"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \\"mentions\\" to only accept bot messages that mention the bot."},"botLoopProtection":{"label":"Discord Bot Loop Protection","help":"Sliding-window guard for bot-to-bot Discord loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Discord 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":"Discord Bot Pair Events Per Window","help":"Maximum messages a single Discord bot pair may exchange in the configured window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Discord Bot Loop Window Seconds","help":"Sliding window length in seconds for Discord bot-pair loop budgets. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Discord Bot Loop Cooldown Seconds","help":"Seconds to suppress a Discord bot pair after it exceeds the loop budget. Default: 60."},"mentionAliases":{"label":"Discord Mention Aliases","help":"Map outbound @handle text to stable Discord user IDs before sending. Set per account via channels.discord.accounts..mentionAliases."},"token":{"label":"Discord Bot Token","help":"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.","sensitive":true},"applicationId":{"label":"Discord Application ID","help":"Optional Discord application/client ID. Set this when hosted environments cannot reach Discord\'s application lookup endpoint during startup."},"activities":{"label":"Discord Activities","help":"Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."},"activities.clientSecret":{"label":"Discord Activities Client Secret","help":"OAuth2 client secret for the Discord application. DISCORD_CLIENT_SECRET is used when this field is unset.","sensitive":true},"activities.applicationId":{"label":"Discord Activities Application ID","help":"Optional Activity application ID. Defaults to the bot application ID learned at gateway startup."}},"unsupportedSecretRefSurfacePatterns":["channels.discord.accounts.*.threadBindings.webhookToken","channels.discord.threadBindings.webhookToken"]},{"pluginId":"feishu","channelId":"feishu","aliases":["lark"],"order":35,"channelEnvVars":["FEISHU_APP_ID","FEISHU_APP_SECRET","FEISHU_ENCRYPT_KEY","FEISHU_VERIFICATION_TOKEN"],"label":"Feishu","description":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"appId":{"type":"string"},"appSecret":{"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":"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}]}]},"encryptKey":{"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":"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}]}]},"verificationToken":{"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":"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}]}]},"domain":{"default":"feishu","anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"default":"websocket","type":"string","enum":["websocket","webhook"]},"webhookPath":{"default":"/feishu/events","type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"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"}},"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"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"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},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"default":"own","type":"string","enum":["off","own","all"]},"typingIndicator":{"default":true,"type":"boolean"},"resolveSenderNames":{"default":true,"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"dynamicAgentCreation":{"type":"object","properties":{"enabled":{"type":"boolean"},"workspaceTemplate":{"type":"string"},"agentDirTemplate":{"type":"string"},"maxAgents":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"appSecret":{"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":"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}]}]},"encryptKey":{"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":"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}]}]},"verificationToken":{"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":"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}]}]},"domain":{"anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"type":"string","enum":["websocket","webhook"]},"webhookPath":{"type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"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"}},"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"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"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},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"typingIndicator":{"type":"boolean"},"resolveSenderNames":{"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}}},"additionalProperties":false}},{"pluginId":"googlechat","channelId":"googlechat","aliases":["gchat","google-chat"],"order":55,"channelEnvVars":["GOOGLE_CHAT_SERVICE_ACCOUNT","GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"],"label":"Google Chat","description":"Google Workspace Chat app with HTTP webhook.","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":{"d', - 'efault":"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},"heartbeat":{"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"}]},"allowBots":{"type":"boolean"},"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"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"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"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"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":"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}]}]},"serviceAccountRef":{"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":"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}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]},"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"},"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},"heartbeat":{"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"}]},"allowBots":{"type":"boolean"},"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"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"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"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"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":"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}]}]},"serviceAccountRef":{"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":"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}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"imessage","channelId":"imessage","aliases":["imsg"],"label":"iMessage","description":"Local iMessage/SMS through the imsg bridge, including private API message actions when enabled.","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},"heartbeat":{"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},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"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}},"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","property', - 'Names":{"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},"heartbeat":{"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},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"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}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"iMessage","help":"iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations."},"dmPolicy":{"label":"iMessage DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.imessage.allowFrom=[\\"*\\"]."},"configWrites":{"label":"iMessage Config Writes","help":"Allow iMessage to write config in response to channel events/commands (default: true)."},"cliPath":{"label":"iMessage CLI Path","help":"Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments."},"sendTransport":{"label":"iMessage Send Transport","help":"Preferred imsg RPC send transport for normal outbound replies. \\"auto\\" uses the IMCore bridge when available, \\"bridge\\" requires it, and \\"applescript\\" forces Messages automation."}}},{"pluginId":"irc","channelId":"irc","aliases":["internet-relay-chat"],"channelEnvVars":["IRC_CHANNELS","IRC_HOST","IRC_NICK","IRC_NICKSERV_PASSWORD","IRC_NICKSERV_REGISTER_EMAIL","IRC_PASSWORD","IRC_PORT","IRC_REALNAME","IRC_TLS","IRC_USERNAME"],"label":"IRC","description":"classic IRC networks with DM/channel routing and pairing controls.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"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},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"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},"uiHints":{"":{"label":"IRC","help":"IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw."},"dmPolicy":{"label":"IRC DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.irc.allowFrom=[\\"*\\"]."},"nickserv.enabled":{"label":"IRC NickServ Enabled","help":"Enable NickServ identify/register after connect (defaults to enabled when password is configured)."},"nickserv.service":{"label":"IRC NickServ Service","help":"NickServ service nick (default: NickServ)."},"nickserv.password":{"label":"IRC NickServ Password","help":"NickServ password used for IDENTIFY/REGISTER (sensitive)."},"nickserv.passwordFile":{"label":"IRC NickServ Password File","help":"Optional file path containing NickServ password."},"nickserv.register":{"label":"IRC NickServ Register","help":"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable."},"nickserv.registerEmail":{"label":"IRC NickServ Register Email","help":"Email used with NickServ REGISTER (required when register=true)."},"configWrites":{"label":"IRC Config Writes","help":"Allow IRC to write config in response to channel events/commands (default: true)."}}},{"pluginId":"line","channelId":"line","order":75,"channelEnvVars":["LINE_CHANNEL_ACCESS_TOKEN","LINE_CHANNEL_SECRET"],"label":"LINE","description":"LINE Messaging API webhook bot.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"a', - 'dditionalProperties":false}},{"pluginId":"matrix","channelId":"matrix","order":70,"channelEnvVars":["MATRIX_ACCESS_TOKEN","MATRIX_DEVICE_ID","MATRIX_DEVICE_NAME","MATRIX_HOMESERVER","MATRIX_OPS_ACCESS_TOKEN","MATRIX_OPS_DEVICE_ID","MATRIX_OPS_DEVICE_NAME","MATRIX_OPS_HOMESERVER","MATRIX_PASSWORD","MATRIX_USER_ID"],"label":"Matrix","description":"open protocol; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"homeserver":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"userId":{"type":"string"},"accessToken":{"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":"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}]}]},"password":{"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":"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}]}]},"deviceId":{"type":"string"},"deviceName":{"type":"string"},"avatarUrl":{"type":"string"},"initialSyncLimit":{"type":"number"},"encryption":{"type":"boolean"},"allowlistOnly":{"type":"boolean"},"dangerouslyAllowNameMatching":{"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},"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"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["partial","quiet","progress","off"]},"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},"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},"toolProgress":{"type":"boolean"}},"additionalProperties":false},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]},"textChunkLimit":{"type":"number"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","none","off"]},"reactionNotifications":{"type":"string","enum":["off","own"]},"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},"startupVerification":{"type":"string","enum":["off","if-unverified"]},"startupVerificationCooldownHours":{"type":"number"},"mediaMaxMb":{"type":"number"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"autoJoin":{"type":"string","enum":["always","allowlist","off"]},"autoJoinAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"sessionScope":{"type":"string","enum":["per-user","per-room"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"type":"boolean"},"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},"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},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"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},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"rooms":{"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},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"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},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"profile":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"verification":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"uiHints":{"mentionPatterns":{"label":"Matrix Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Matrix room IDs. Native Matrix mention evidence still triggers even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Matrix Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Matrix Mention Pattern Allowlist","help":"Matrix room IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Matrix Mention Pattern Denylist","help":"Matrix room IDs where configured regex mention patterns are disabled. Native mention evidence still triggers."},"allowBots":{"label":"Matrix Allow Bot Messages","help":"Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set \\"mentions\\" to require a visible room mention."},"botLoopProtection":{"label":"Matrix Bot Loop Protection","help":"Sliding-window guard for accepted Matrix configured-bot loops. Default is enabled whenever allowBots lets configured bot messages reach dispatch."},"botLoopProtection.enabled":{"label":"Matrix Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when configured bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Matrix Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Matrix Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Matrix Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"dangerouslyAllowNameMatching":{"label":"Matrix Display Name Matching","help":"Compatibility opt-in for resolving Matrix display names and joined room names in allowlists. Prefer full @user:server IDs and room IDs or aliases because names are mutable."},"streaming.progress.label":{"label":"Matrix 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":"Matrix Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Matrix Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Matrix 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":"Matrix 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":"Matrix Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"mattermost","channelId":"mattermost","order":65,"channelEnvVars":["MATTERMOST_BOT_TOKEN","MATTERMOST_URL"],"label":"Mattermost","description":"self-hosted Slack-style chat; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"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":"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}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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":{"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},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additio', - 'nalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"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":"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}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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":{"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},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Mattermost","help":"Mattermost channel provider configuration for bot auth, access policy, slash commands, and preview streaming."},"dmPolicy":{"label":"Mattermost DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.mattermost.allowFrom=[\\"*\\"]."},"implicitMentions":{"label":"Mattermost Implicit Mentions","help":"Control which Mattermost reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Mattermost Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Mattermost Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Mattermost Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Mattermost Streaming Mode","help":"Unified Mattermost stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery."},"streaming.mode":{"label":"Mattermost Streaming Mode","help":"Canonical Mattermost preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.progress.label":{"label":"Mattermost 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":"Mattermost Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Mattermost Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Mattermost 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":"Mattermost 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":"Mattermost Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.preview.toolProgress":{"label":"Mattermost Draft Tool Progress","help":"Show tool/progress activity in the live draft preview post (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Mattermost Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.block.enabled":{"label":"Mattermost Block Streaming Enabled","help":"Enable chunked block-style Mattermost preview delivery when channels.mattermost.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Mattermost Block Streaming Coalesce","help":"Merge streamed Mattermost block replies before final delivery."}}},{"pluginId":"msteams","channelId":"msteams","aliases":["teams"],"order":60,"channelEnvVars":["MSTEAMS_APP_ID","MSTEAMS_APP_PASSWORD","MSTEAMS_TENANT_ID"],"label":"Microsoft Teams","description":"Teams SDK; enterprise support.","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"]},"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},"heartbeat":{"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},"dangerouslyAllowNameMatching":{"type":"boolean"},"appId":{"type":"string"},"appPassword":{"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":"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}]}]},"tenantId":{"type":"string"},"cloud":{"type":"string","enum":["Public","USGov","USGovDoD","China"]},"serviceUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["secret","federated"]},"certificatePath":{"type":"string"},"certificateThumbprint":{"type":"string"},"useManagedIdentity":{"type":"boolean"},"managedIdentityClientId":{"type":"string"},"webhook":{"type":"object","properties":{"port":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"path":{"type":"string"}},"additionalProperties":false},"typingIndicator":{"type":"boolean"},"mediaAllowHosts":{"type":"array","items":{"type":"string"}},"mediaAuthAllowHosts":{"type":"array","items":{"type":"string"}},"graphMediaFallback":{"type":"boolean"},"requireMention":{"type":"boolean"},"replyStyle":{"type":"string","enum":["thread","top-level"]},"teams":{"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}},"replyStyle":{"type":"string","enum":["thread","top-level"]},"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}},"replyStyle":{"type":"string","e', - 'num":["thread","top-level"]}},"additionalProperties":false}}},"additionalProperties":false}},"sharePointSiteId":{"type":"string"},"welcomeCard":{"type":"boolean"},"promptStarters":{"type":"array","items":{"type":"string"}},"groupWelcomeCard":{"type":"boolean"},"feedbackEnabled":{"type":"boolean"},"feedbackReflection":{"type":"boolean"},"feedbackReflectionCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"delegatedAuth":{"type":"object","properties":{"enabled":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"sso":{"type":"object","properties":{"enabled":{"type":"boolean"},"connectionName":{"type":"string"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"MS Teams","help":"Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers."},"configWrites":{"label":"MS Teams Config Writes","help":"Allow Microsoft Teams to write config in response to channel events/commands (default: true)."},"cloud":{"label":"MS Teams Cloud","help":"Teams SDK cloud environment for auth, token validation, and token services: \\"Public\\", \\"USGov\\", \\"USGovDoD\\", or \\"China\\" (default: Public)."},"serviceUrl":{"label":"MS Teams Service URL","help":"Bot Connector service URL for SDK proactive sends/edits/deletes. Set with cloud for USGov/DoD; set alone for GCC."},"graphMediaFallback":{"label":"MS Teams Graph Media Fallback","help":"Query Microsoft Graph for unresolved channel or group-chat HTML media. Adds one lookup per matching message when enabled (default: false)."},"streaming":{"label":"MS Teams Streaming","help":"Microsoft Teams preview/progress streaming mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Personal chats use Teams native streaminfo progress when available."},"streaming.progress.label":{"label":"MS Teams Progress Label","help":"Initial progress title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"MS Teams Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"MS Teams Progress Max Lines","help":"Maximum number of compact progress lines to keep below the progress title (default: 8)."},"streaming.progress.maxLineChars":{"label":"MS Teams 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":"MS Teams Progress Tool Lines","help":"Show compact tool/progress lines in progress mode (default: true). Set false to keep only the title until final delivery."},"streaming.progress.commandText":{"label":"MS Teams Progress Command Text","help":"Command/exec detail in progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"nextcloud-talk","channelId":"nextcloud-talk","aliases":["nc","nc-talk"],"order":65,"channelEnvVars":["NEXTCLOUD_TALK_API_PASSWORD","NEXTCLOUD_TALK_BOT_SECRET"],"label":"Nextcloud Talk","description":"Self-hosted chat via Nextcloud Talk webhook bots.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"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":"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}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"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":"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}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":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},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"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":"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}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"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":"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}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":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"},"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":"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"},"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"},"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":"qqbot","channelId":"qqbot","channelEnvVars":["QQBOT_APP_ID","QQBOT_CLIENT_SECRET"],"label":"QQ Bot","description":"connect to QQ via official QQ Bot API with group chat and direct message support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"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":"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}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"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}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}},"stt":{"type":"object","properties":{"enabled":{"type":"boolean"},"provider":{"type":"string"},"baseUrl":{"type":"string"},"apiKey":{"type":"string"},"model":{"type":"string"}},"additionalProperties":false},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"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":"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}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"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}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":{}}},"defaultAccount":{"type":"string"}},"additionalProperties":{}}},{"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"},"profile":{"type":"string","minLength":1},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"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"},"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; more setup (David Reagans: \\"Hop on Discord.\\").","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},"heartbeat":{"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"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"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},"apiMode":{"type":"string","enum":["auto","native","container"]},"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":{"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"},"coales', - 'ce":{"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},"heartbeat":{"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"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"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."},"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."}}},{"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":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"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},"heartbeat":{"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"}]},"identity":{"default":"bot","type":"string","enum":["bot","user"]},"mode":{"default":"socket","type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"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":"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":"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":"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":"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":"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":"arr', - 'ay","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":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"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},"heartbeat":{"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"}]},"identity":{"type":"string","enum":["bot","user"]},"mode":{"type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"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":"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":"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":"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":"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":"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","identity","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."},"enterpriseOrgInstall":{"label":"Slack Enterprise Grid Org Install","help":"Enable only for an Enterprise Grid org-wide bot installation. OpenClaw verifies the token with Slack auth.test at startup; DMs must be disabled or use dmPolicy=\\"open\\" with allowFrom=[\\"*\\"]."},"identity":{"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."},"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."},"socketMode":{"label":"Slack Socket Mode Transport","help":"Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior."},"socketMode.clientPingTimeout":{"label":"Slack Socket Mode Pong Timeout","help":"Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling."},"socketMode.serverPingTimeout":{"label":"Slack Socket Mode Server Ping Timeout","help":"Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale."},"socketMode.pingPongLoggingEnabled":{"label":"Slack Socket Mode Ping/Pong Logging","help":"Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health."},"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."},"capabilities.interactiveReplies":{"label":"Slack Interactive Replies","help":"Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false."},"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 workspace 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."},"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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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.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.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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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 with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"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":"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"},"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":"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 channel configuration for inbound webhooks and outbound text 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, for example +15551234567."},"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."},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly."},"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."},"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},"heartbeat":{"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":"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"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":"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"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":"s', - 'tring","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},"heartbeat":{"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":"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"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":"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"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"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 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: \\"partial\\"). \\"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: \\"partial\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.1 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable chunked block-style Telegram preview delivery when channels.telegram.streaming.mode=\\"block\\"."},"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 act', - 'ivity 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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"retry.attempts":{"label":"Telegram Retry Attempts","help":"Max retry attempts for outbound Telegram API calls (default: 3)."},"retry.minDelayMs":{"label":"Telegram Retry Min Delay (ms)","help":"Minimum retry delay in ms for Telegram outbound calls."},"retry.maxDelayMs":{"label":"Telegram Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Telegram outbound calls."},"retry.jitter":{"label":"Telegram Retry Jitter","help":"Jitter factor (0-1) applied to Telegram retry delays."},"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."},"timeoutSeconds":{"label":"Telegram API Timeout (seconds)","help":"Max seconds before Telegram API requests are aborted (default: 500 per grammY)."},"mediaGroupFlushMs":{"label":"Telegram Media Group Flush (ms)","help":"Milliseconds to buffer Telegram albums/media groups before dispatching them as one inbound message. Default: 500."},"pollingStallThresholdMs":{"label":"Telegram Polling Stall Threshold (ms)","help":"Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000."},"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"},"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"},"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"},"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"},"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"},"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},"heartbeat":{"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"},"messagePrefix":{"type":"string"},"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"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991},"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},"heartbeat":{"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"},"messagePrefix":{"type":"string"},"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"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"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","debounceMs"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"debounceMs":{"label":"WhatsApp Message Debounce (ms)","help":"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable)."},"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"},"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":"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":"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"},"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":"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":"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"},"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"},"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}}]', + '[{"pluginId":"clickclack","channelId":"clickclack","order":85,"channelEnvVars":["CLICKCLACK_BOT_TOKEN"],"label":"ClickClack","description":"self-hosted chat via first-class ClickClack bot tokens.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"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":"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"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"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":"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"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"discord","channelId":"discord","channelEnvVars":["DISCORD_BOT_TOKEN"],"label":"Discord","description":"very well supported right now.","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":{"type":"string"}},"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"}},"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},"heartbeat":{"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"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"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":"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}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"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"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"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}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"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"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"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},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"in', + 'teger","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"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":"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}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"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":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"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"}},"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},"heartbeat":{"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"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"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":"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}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"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"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"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}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"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"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"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":fals', + 'e},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"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":"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}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"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":"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}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Discord","help":"Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed."},"dmPolicy":{"label":"Discord DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.discord.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Discord Config Writes","help":"Allow Discord to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Discord Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Discord channel IDs. Native Discord @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Discord Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Discord Mention Pattern Allowlist","help":"Discord channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Discord Mention Pattern Denylist","help":"Discord channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"proxy":{"label":"Discord Proxy URL","help":"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts..proxy."},"commands.native":{"label":"Discord Native Commands","help":"Override native commands for Discord (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Discord Native Skill Commands","help":"Override native skill commands for Discord (bool or \\"auto\\")."},"streaming":{"label":"Discord Streaming Mode","help":"Unified Discord stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Discord Streaming Mode","help":"Canonical Discord preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Discord Chunk Mode","help":"Chunking mode for outbound Discord text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Discord Block Streaming Enabled","help":"Enable chunked block-style Discord preview delivery when channels.discord.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Discord Block Streaming Coalesce","help":"Merge streamed Discord block replies before final delivery."},"streaming.preview.chunk.minChars":{"label":"Discord Draft Chunk Min Chars","help":"Minimum chars before emitting a Discord stream preview update when channels.discord.streaming.mode=\\"block\\" (default: 200)."},"streaming.preview.chunk.maxChars":{"label":"Discord Draft Chunk Max Chars","help":"Target max size for a Discord stream preview chunk when channels.discord.streaming.mode=\\"block\\" (default: 800; clamped to channels.discord.textChunkLimit)."},"streaming.preview.chunk.breakPreference":{"label":"Discord Draft Chunk Break Preference","help":"Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph."},"streaming.preview.toolProgress":{"label":"Discord 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":"Discord Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Discord 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":"Discord Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Discord Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Discord 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":"Discord 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.commentary":{"label":"Discord Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"streaming.progress.commandText":{"label":"Discord Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"maxLinesPerMessage":{"label":"Discord Max Lines Per Message","help":"Soft max line count per Discord message (default: 17)."},"suppressEmbeds":{"label":"Discord Suppress Link Embeds","help":"Suppress Discord-generated link embeds on outbound messages by default. Explicit embeds still send normally. Default: true."},"thread.inheritParent":{"label":"Discord Thread Parent Inheritance","help":"If true, Discord thread sessions inherit the parent channel transcript (default: false)."},"threadBindings.enabled":{"label":"Discord Thread Binding Enabled","help":"Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Discord Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Discord Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Discord Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel."},"threadBindings.defaultSpawnContext":{"label":"Discord Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."},"subagentProgress":{"label":"Discord Subagent Progress","help":"Show active subagent count reactions and typing on the source message. Default: false."},"ui.components.accentColor":{"label":"Discord Component Accent Color","help":"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts..ui.components.accentColor."},"agentComponents.ttlMs":{"label":"Discord Component TTL (ms)","help":"How long sent Discord component callbacks remain registered. Default is 1800000 (30 minutes); maximum is 86400000 (24 hours)."},"intents.presence":{"label":"Discord Presence Intent","help":"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false."},"intents.guildMembers":{"label":"Discord Guild Members Intent","help":"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false."},"intents.voiceStates":{"label":"Discord Voice States Intent","help":"Enable the Guild Voice States intent. Defaults to the effective Discord voice setting; set true only for Dis', + 'cord voice channel conversations."},"voice.enabled":{"label":"Discord Voice Enabled","help":"Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent."},"voice.model":{"label":"Discord Voice Model","help":"Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai/gpt-5.6-sol). Leave unset to inherit the routed agent model."},"voice.mode":{"label":"Discord Voice Mode","help":"Conversation mode: agent-proxy (default) uses realtime voice as the microphone/speaker for the routed OpenClaw agent, stt-tts uses batch speech-to-text plus TTS, and bidi lets the realtime provider converse directly with the OpenClaw consult tool."},"voice.agentSession":{"label":"Discord Voice Agent Session","help":"Controls which OpenClaw conversation receives voice turns. Leave unset for the voice channel session, or set mode=\\"target\\" with a Discord target such as channel:123 to make voice an extension of an existing text channel session."},"voice.agentSession.target":{"label":"Discord Voice Agent Session Target","help":"Discord target used when voice.agentSession.mode=\\"target\\", for example channel:123."},"voice.followUsersEnabled":{"label":"Discord Voice Follow Users Enabled","help":"Toggle Discord voice follow-users behavior without removing the saved voice.followUsers list. Defaults to true when followUsers is configured."},"voice.followUsers":{"label":"Discord Voice Follow Users","help":"Discord user IDs to follow into voice channels. The bot joins when a followed user joins or moves, and leaves when that user disconnects."},"voice.realtime.provider":{"label":"Discord Realtime Provider","help":"Realtime voice provider for agent-proxy or bidi Discord voice modes, such as openai."},"voice.realtime.model":{"label":"Discord Realtime Model","help":"Provider realtime session model, such as gpt-realtime-2.1. This is separate from voice.model, which remains the OpenClaw agent brain model."},"voice.realtime.speakerVoice":{"label":"Discord Realtime Speaker Voice","help":"Provider realtime output voice name, such as cedar."},"voice.realtime.speakerVoiceId":{"label":"Discord Realtime Speaker Voice ID","help":"Provider realtime output voice id."},"voice.realtime.toolPolicy":{"label":"Discord Realtime Tool Policy","help":"Tool policy for the OpenClaw agent consult tool in realtime voice modes: safe-read-only, owner, or none. Default is owner for agent-proxy and safe-read-only for bidi."},"voice.realtime.consultPolicy":{"label":"Discord Realtime Consult Policy","help":"Use always to strongly prefer the OpenClaw agent brain for substantive realtime turns. agent-proxy defaults to always."},"voice.realtime.requireWakeName":{"label":"Discord Realtime Require Wake Name","help":"Control OpenAI agent-proxy wake-name gating. Unset listens naturally with one human and requires a wake name with two or more; true always requires one and false never does."},"voice.realtime.wakeNames":{"label":"Discord Realtime Wake Names","help":"One- or two-word activation names used whenever OpenAI agent-proxy Discord realtime voice has an active wake-name gate."},"voice.realtime.bootstrapContextFiles":{"label":"Discord Realtime Bootstrap Context Files","help":"Agent profile bootstrap files included in realtime provider instructions for direct voice identity/persona grounding. Defaults to IDENTITY.md, USER.md, and SOUL.md; set [] to disable."},"voice.realtime.bargeIn":{"label":"Discord Realtime Barge-In","help":"Allow Discord speaker-start events to interrupt active realtime playback. Set true to keep manual interruption when provider input-audio interruption is disabled for echo control."},"voice.realtime.minBargeInAudioEndMs":{"label":"Discord Realtime Minimum Barge-In Audio (ms)","help":"Minimum assistant playback duration before a Discord barge-in truncates realtime audio. Default: 250; set 0 for immediate interruption in low-echo rooms."},"voice.realtime.providers":{"label":"Discord Realtime Provider Settings","help":"Provider-specific realtime voice settings keyed by provider id.","advanced":true},"voice.autoJoin":{"label":"Discord Voice Auto-Join","help":"Voice channels to auto-join on startup (list of guildId/channelId entries)."},"voice.allowedChannels":{"label":"Discord Voice Allowed Channels","help":"Optional voice channel residency allowlist. When set, /vc join, auto-join, and bot voice-state moves are restricted to these guildId/channelId entries. Leave unset to allow any voice channel."},"voice.daveEncryption":{"label":"Discord Voice DAVE Encryption","help":"Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this)."},"voice.decryptionFailureTolerance":{"label":"Discord Voice Decrypt Failure Tolerance","help":"Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24)."},"voice.connectTimeoutMs":{"label":"Discord Voice Connect Timeout (ms)","help":"Initial @discordjs/voice Ready wait before a join is treated as failed. Default: 30000."},"voice.reconnectGraceMs":{"label":"Discord Voice Reconnect Grace (ms)","help":"Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000."},"voice.captureSilenceGraceMs":{"label":"Discord Voice Capture Silence Grace (ms)","help":"Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2000."},"voice.tts":{"label":"Discord Voice Text-to-Speech","help":"Optional TTS overrides for Discord voice playback (merged with messages.tts)."},"pluralkit.enabled":{"label":"Discord PluralKit Enabled","help":"Resolve PluralKit proxied messages and treat system members as distinct senders."},"pluralkit.token":{"label":"Discord PluralKit Token","help":"Optional PluralKit token for resolving private systems or members."},"activity":{"label":"Discord Presence Activity","help":"Discord presence activity text (defaults to custom status)."},"status":{"label":"Discord Presence Status","help":"Discord presence status (online, dnd, idle, invisible)."},"autoPresence.enabled":{"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd."},"autoPresence.intervalMs":{"label":"Discord Auto Presence Check Interval (ms)","help":"How often to evaluate Discord auto-presence state in milliseconds (default: 30000)."},"autoPresence.minUpdateIntervalMs":{"label":"Discord Auto Presence Min Update Interval (ms)","help":"Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes."},"autoPresence.healthyText":{"label":"Discord Auto Presence Healthy Text","help":"Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set."},"autoPresence.degradedText":{"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle)."},"autoPresence.exhaustedText":{"label":"Discord Auto Presence Exhausted Text","help":"Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder."},"guilds.*.presenceEvents":{"label":"Discord Online Presence Events","help":"Route selected human offline-to-online transitions into the configured guild channel as agent system events. Requires the Guild Presences privileged intent and an enabled agent heartbeat."},"guilds.*.presenceEvents.enabled":{"label":"Discord Online Presence Events Enabled","help":"Enable online-presence agent wakes for this guild. Defaults to true when presenceEvents is configured."},"guilds.*.presenceEvents.channelId":{"label":"Discord Online Presence Target Channel","help":"Numeric Discord channel ID whose routed agent session receives online-presence events and greeting delivery."},"guilds.*.presenceEvents.users":{"label":"Discord Online Presence User IDs","help":"Optional immutable Discord user ID allowlist. Omit to include all human members in the guild."},"guilds.*.presenceEvents.reconnectSuppressSeconds":{"label":"Discord Online Presence Reconnect Suppression","help":"Suppress online-presence events for this many seconds after a new Gateway session while guild presence state is rebuilt. Resumed sessions are unaffected. 0 disables. Default: 300."},"guilds.*.presenceEvents.burstLimit":{"label":"Discord Online Presence Burst Limit","help":"Maximum successfully queued online-presence events for this guild per burst window; the rest are suppressed and logged once. Default: 8."},"guilds.*.presenceEvents.burstWindowSeconds":{"label":"Discord Online Presence Burst Window","help":"Sliding window in seconds used for burst detection. Default: 60."},"activityType":{"label":"Discord Presence Activity Type","help":"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing)."},"activityUrl":{"label":"Discord Presence Activity URL","help":"Discord presence streaming URL (required for activityType=1)."},"allowBots":{"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \\"mentions\\" to only accept bot messages that mention the bot."},"botLoopProtection":{"label":"Discord Bot Loop Protection","help":"Sliding-window guard for bot-to-bot Discord loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Discord 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":"Discord Bot Pair Events Per Window","help":"Maximum messages a single Discord bot pair may exchange in the configured window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Discord Bot Loop Window Seconds","help":"Sliding window length in seconds for Discord bot-pair loop budgets. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Discord Bot Loop Cooldown Seconds","help":"Seconds to suppress a Discord bot pair after it exceeds the loop budget. Default: 60."},"mentionAliases":{"label":"Discord Mention Aliases","help":"Map outbound @handle text to stable Discord user IDs before sending. Set per account via channels.discord.accounts..mentionAliases."},"token":{"label":"Discord Bot Token","help":"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.","sensitive":true},"applicationId":{"label":"Discord Application ID","help":"Optional Discord application/client ID. Set this when hosted environments cannot reach Discord\'s application lookup endpoint during startup."},"activities":{"label":"Discord Activities","help":"Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."},"activities.clientSecret":{"label":"Discord Activities Client Secret","help":"OAuth2 client secret for the Discord application. DISCORD_CLIENT_SECRET is used when this field is unset.","sensitive":true},"activities.applicationId":{"label":"Discord Activities Application ID","help":"Optional Activity application ID. Defaults to the bot application ID learned at gateway startup."}},"unsupportedSecretRefSurfacePatterns":["channels.discord.accounts.*.threadBindings.webhookToken","channels.discord.threadBindings.webhookToken"]},{"pluginId":"feishu","channelId":"feishu","aliases":["lark"],"order":35,"channelEnvVars":["FEISHU_APP_ID","FEISHU_APP_SECRET","FEISHU_ENCRYPT_KEY","FEISHU_VERIFICATION_TOKEN"],"label":"Feishu","description":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"appId":{"type":"string"},"appSecret":{"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":"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}]}]},"encryptKey":{"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":"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}]}]},"verificationToken":{"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":"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}]}]},"domain":{"default":"feishu","anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"default":"websocket","type":"string","enum":["websocket","webhook"]},"webhookPath":{"default":"/feishu/events","type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"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"}},"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"}]}},"syste', + 'mPrompt":{"type":"string"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"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},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"default":"own","type":"string","enum":["off","own","all"]},"typingIndicator":{"default":true,"type":"boolean"},"resolveSenderNames":{"default":true,"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"dynamicAgentCreation":{"type":"object","properties":{"enabled":{"type":"boolean"},"workspaceTemplate":{"type":"string"},"agentDirTemplate":{"type":"string"},"maxAgents":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"appSecret":{"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":"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}]}]},"encryptKey":{"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":"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}]}]},"verificationToken":{"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":"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}]}]},"domain":{"anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"type":"string","enum":["websocket","webhook"]},"webhookPath":{"type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"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"}},"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"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"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},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"typingIndicator":{"type":"boolean"},"resolveSenderNames":{"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}}},"additionalProperties":false}},{"pluginId":"googlechat","channelId":"googlechat","aliases":["gchat","google-chat"],"order":55,"channelEnvVars":["GOOGLE_CHAT_SERVICE_ACCOUNT","GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"],"label":"Google Chat","description":"Google Workspace Chat app with HTTP webhook.","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},"heartbeat":{"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"}]},"allowBots":{"type":"boolean"},"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"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"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"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"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":"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}]}]},"serviceAccountRef":{"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","propertie', + 's":{"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}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]},"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"},"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},"heartbeat":{"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"}]},"allowBots":{"type":"boolean"},"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"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"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"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"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":"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}]}]},"serviceAccountRef":{"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":"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}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"imessage","channelId":"imessage","aliases":["imsg"],"label":"iMessage","description":"Local iMessage/SMS through the imsg bridge, including private API message actions when enabled.","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},"heartbeat":{"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},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"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}},"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":{"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},"heartbeat":{"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},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"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}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"iMessage","help":"iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations."},"dmPolicy":{"label":"iMessage DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.imessage.allowFrom=[\\"*\\"].', + '"},"configWrites":{"label":"iMessage Config Writes","help":"Allow iMessage to write config in response to channel events/commands (default: true)."},"cliPath":{"label":"iMessage CLI Path","help":"Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments."},"sendTransport":{"label":"iMessage Send Transport","help":"Preferred imsg RPC send transport for normal outbound replies. \\"auto\\" uses the IMCore bridge when available, \\"bridge\\" requires it, and \\"applescript\\" forces Messages automation."}}},{"pluginId":"irc","channelId":"irc","aliases":["internet-relay-chat"],"channelEnvVars":["IRC_CHANNELS","IRC_HOST","IRC_NICK","IRC_NICKSERV_PASSWORD","IRC_NICKSERV_REGISTER_EMAIL","IRC_PASSWORD","IRC_PORT","IRC_REALNAME","IRC_TLS","IRC_USERNAME"],"label":"IRC","description":"classic IRC networks with DM/channel routing and pairing controls.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"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},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"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},"uiHints":{"":{"label":"IRC","help":"IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw."},"dmPolicy":{"label":"IRC DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.irc.allowFrom=[\\"*\\"]."},"nickserv.enabled":{"label":"IRC NickServ Enabled","help":"Enable NickServ identify/register after connect (defaults to enabled when password is configured)."},"nickserv.service":{"label":"IRC NickServ Service","help":"NickServ service nick (default: NickServ)."},"nickserv.password":{"label":"IRC NickServ Password","help":"NickServ password used for IDENTIFY/REGISTER (sensitive)."},"nickserv.passwordFile":{"label":"IRC NickServ Password File","help":"Optional file path containing NickServ password."},"nickserv.register":{"label":"IRC NickServ Register","help":"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable."},"nickserv.registerEmail":{"label":"IRC NickServ Register Email","help":"Email used with NickServ REGISTER (required when register=true)."},"configWrites":{"label":"IRC Config Writes","help":"Allow IRC to write config in response to channel events/commands (default: true)."}}},{"pluginId":"line","channelId":"line","order":75,"channelEnvVars":["LINE_CHANNEL_ACCESS_TOKEN","LINE_CHANNEL_SECRET"],"label":"LINE","description":"LINE Messaging API webhook bot.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"matrix","channelId":"matrix","order":70,"channelEnvVars":["MATRIX_ACCESS_TOKEN","MATRIX_DEVICE_ID","MATRIX_DEVICE_NAME","MATRIX_HOMESERVER","MATRIX_OPS_ACCESS_TOKEN","MATRIX_OPS_DEVICE_ID","MATRIX_OPS_DEVICE_NAME","MATRIX_OPS_HOMESERVER","MATRIX_PASSWORD","MATRIX_USER_ID"],"label":"Matrix","description":"open protocol; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"homeserver":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"userId":{"type":"string"},"accessToken":{"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":"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}]}]},"password":{"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":"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}]}]},"deviceId":{"type":"string"},"deviceName":{"type":"string"},"avatarUrl":{"type":"string"},"initialSyncLimit":{"type":"number"},"encryption":{"type":"boolean"},"allowlistOnly":{"type":"boolean"},"dangerouslyAllowNameMatching":{"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},"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"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["partial","quiet","progress","off"]},"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},"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},"toolProgress":{"type":"boolean"}},"additionalProperties":false},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]},"textChunkLimit":{"type":"number"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","none","off"]},"reactionNotifications":{"type":"string","enum":["off","own"]},"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},"startupVerification":{"type":"string","enum":["off","if-unverified"]},"startupVerificationCooldownHours":{"type":"number"},"mediaMaxMb":{"type":"number"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"autoJoin":{"type":"string","enum":["always","allowlist","off"]},"autoJoinAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"sessionScope":{"type":"string","enum":["per-user","per-room"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"type":"boolean"},"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},"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},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"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},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"rooms":{"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},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"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},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"profile":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"verification":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"uiHints":{"mentionPatterns":{"label":"Matrix Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Matrix room IDs. Native Matrix mention evidence still triggers even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Matrix Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Matrix Mention Pattern Allowlist","help":"Matrix room IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Matrix Mention Pattern Denylist","help":"Matrix room IDs where configured regex mention patterns are disabled. Native mention evidence still triggers."},"allowBots":{"label":"Matrix Allow Bot Messages","help":"Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set \\"mentions\\" to require a visible room mention."},"botLoopProtection":{"label":"Matrix Bot Loop Protection","help":"Sliding-window guard for accepted Matrix configured-bot loops. Default is enabled whenever allowBots lets configured bot messages reach dispatch."},"botLoopProtection.enabled":{"label":"Matrix Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when configured bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Matrix Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Matrix Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Matrix Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"dangerouslyAllowNameMatching":{"label":"Matrix Display Name Matching","help":"Compatibility opt-in for resolving Matrix display names and joined room names in allowlists. Prefer full @user:server IDs and room IDs or aliases because names are mutable."},"streaming.progress.label":{"label":"Matrix 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":"Matrix Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Matrix Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Matrix 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":"Matrix 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":"Matrix Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"mattermost","channelId":"mattermost","order":65,"channelEnvVars":["MATTERMOST_BOT_TOKEN","MATTERMOST_URL"],"label":"Mattermost","description":"self-hosted Slack-style chat; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"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":"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}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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":{"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},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"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":"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}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"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":{"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},"toolProgress":{"type":"boolean"},"commandT', + 'ext":{"type":"string","enum":["raw","status"]}},"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Mattermost","help":"Mattermost channel provider configuration for bot auth, access policy, slash commands, and preview streaming."},"dmPolicy":{"label":"Mattermost DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.mattermost.allowFrom=[\\"*\\"]."},"implicitMentions":{"label":"Mattermost Implicit Mentions","help":"Control which Mattermost reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Mattermost Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Mattermost Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Mattermost Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Mattermost Streaming Mode","help":"Unified Mattermost stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery."},"streaming.mode":{"label":"Mattermost Streaming Mode","help":"Canonical Mattermost preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.progress.label":{"label":"Mattermost 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":"Mattermost Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Mattermost Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Mattermost 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":"Mattermost 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":"Mattermost Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.preview.toolProgress":{"label":"Mattermost Draft Tool Progress","help":"Show tool/progress activity in the live draft preview post (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Mattermost Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.block.enabled":{"label":"Mattermost Block Streaming Enabled","help":"Enable chunked block-style Mattermost preview delivery when channels.mattermost.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Mattermost Block Streaming Coalesce","help":"Merge streamed Mattermost block replies before final delivery."}}},{"pluginId":"msteams","channelId":"msteams","aliases":["teams"],"order":60,"channelEnvVars":["MSTEAMS_APP_ID","MSTEAMS_APP_PASSWORD","MSTEAMS_TENANT_ID"],"label":"Microsoft Teams","description":"Teams SDK; enterprise support.","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"]},"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},"heartbeat":{"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},"dangerouslyAllowNameMatching":{"type":"boolean"},"appId":{"type":"string"},"appPassword":{"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":"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}]}]},"tenantId":{"type":"string"},"cloud":{"type":"string","enum":["Public","USGov","USGovDoD","China"]},"serviceUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["secret","federated"]},"certificatePath":{"type":"string"},"certificateThumbprint":{"type":"string"},"useManagedIdentity":{"type":"boolean"},"managedIdentityClientId":{"type":"string"},"webhook":{"type":"object","properties":{"port":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"path":{"type":"string"}},"additionalProperties":false},"typingIndicator":{"type":"boolean"},"mediaAllowHosts":{"type":"array","items":{"type":"string"}},"mediaAuthAllowHosts":{"type":"array","items":{"type":"string"}},"graphMediaFallback":{"type":"boolean"},"requireMention":{"type":"boolean"},"replyStyle":{"type":"string","enum":["thread","top-level"]},"teams":{"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}},"replyStyle":{"type":"string","enum":["thread","top-level"]},"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}},"replyStyle":{"type":"string","enum":["thread","top-level"]}},"additionalProperties":false}}},"additionalProperties":false}},"sharePointSiteId":{"type":"string"},"welcomeCard":{"type":"boolean"},"promptStarters":{"type":"array","items":{"type":"string"}},"groupWelcomeCard":{"type":"boolean"},"feedbackEnabled":{"type":"boolean"},"feedbackReflection":{"type":"boolean"},"feedbackReflectionCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"delegatedAuth":{"type":"object","properties":{"enabled":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"sso":{"type":"object","properties":{"enabled":{"type":"boolean"},"connectionName":{"type":"string"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"MS Teams","help":"Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers."},"configWrites":{"label":"MS Teams Config Writes","help":"Allow Microsoft Teams to write config in response to channel events/commands (default: true)."},"cloud":{"label":"MS Teams Cloud","help":"Teams SDK cloud environment for auth, token validation, and token services: \\"Public\\", \\"USGov\\", \\"USGovDoD\\", or \\"China\\" (default: Public)."},"serviceUrl":{"label":"MS Teams Service URL","help":"Bot Connector service URL for SDK proactive sends/edits/deletes. Set with cloud for USGov/DoD; set alone for GCC."},"graphMediaFallback":{"label":"MS Teams Graph Media Fallback","help":"Query Microsoft Graph for unresolved channel or group-chat HTML media. Adds one lookup per matching message when enabled (default: false)."},"streaming":{"label":"MS Teams Streaming","help":"Microsoft Teams preview/progress streaming mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Personal chats use Teams native streaminfo progress when available."},"streaming.progress.label":{"label":"MS Teams Progress Label","help":"Initial progress title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"MS Teams Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"MS Teams Progress Max Lines","help":"Maximum number of compact progress lines to keep below the progress title (default: 8)."},"streaming.progress.maxLineChars":{"label":"MS Teams 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":"MS Teams Progress Tool Lines","help":"Show compact tool/progress lines in progress mode (default: true). Set false to keep only the title until final delivery."},"streaming.progress.commandText":{"label":"MS Teams Progress Command Text","help":"Command/exec detail in progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"nextcloud-talk","channelId":"nextcloud-talk","aliases":["nc","nc-talk"],"order":65,"channelEnvVars":["NEXTCLOUD_TALK_API_PASSWORD","NEXTCLOUD_TALK_BOT_SECRET"],"label":"Nextcloud Talk","description":"Self-hosted chat via Nextcloud Talk webhook bots.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"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":fals', + 'e},{"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}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"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":"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}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":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},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"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":"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}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"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":"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}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":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"},"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":"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"},"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"},"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":"qqbot","channelId":"qqbot","channelEnvVars":["QQBOT_APP_ID","QQBOT_CLIENT_SECRET"],"label":"QQ Bot","description":"connect to QQ via official QQ Bot API with group chat and direct message support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"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":"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}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"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}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}},"stt":{"type":"object","properties":{"enabled":{"type":"boolean"},"provider":{"type":"string"},"baseUrl":{"type":"string"},"apiKey":{"type":"string"},"model":{"type":"string"}},"additionalProperties":false},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"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":"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}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"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}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":{}}},"defaultAccount":{"type":"string"}},"additionalProperties":{}}},{"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"},"profile":{"type":"string","minLength":1},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"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"},"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; more setup (David Reagans: \\"Hop on Discord.\\").","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},"heartbeat":{"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"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"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},"apiMode":{"type":"string","enum":["auto","native","container"]},"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":{"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},"heartbeat":{"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"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"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."},"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."}}},{"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":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"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},"heartbeat":{"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"}]},"identity":{"default":"bot","type":"string","enum":["bot","user"]},"mode":{"default":"socket","type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"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":"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":"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":"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":"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":"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"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"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},"heartbeat":{"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"}]},"identity":{"type":"string","enum":["bot","user"]},"mode":{"type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"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":"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":"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":"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":"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":"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","identity","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."},"enterpriseOrgInstall":{"label":"Slack Enterprise Grid Org Install","help":"Enable only for an Enterprise Grid org-wide bot installation. OpenClaw verifies the token with Slack auth.test at startup; DMs must be disabled or use dmPolicy=\\"open\\" with allowFrom=[\\"*\\"]."},"identity":{"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."},"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."},"socketMode":{"label":"Slack Socket Mode Transport","help":"Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior."},"socketMode.clientPingTimeout":{"label":"Slack Socket Mode Pong Timeout","help":"Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling."},"socketMode.serverPingTimeout":{"label":"Slack Socket Mode Server Ping Timeout","help":"Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale."},"socketMode.pingPongLoggingEnabled":{"label":"Slack Socket Mode Ping/Pong Logging","help":"Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health."},"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."},"capabilities.interactiveReplies":{"label":"Slack Interactive Replies","help":"Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false."},"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 workspace 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."},"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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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.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.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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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 with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"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":"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"},"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":"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 channel configuration for inbound webhooks and outbound text 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, for example +15551234567."},"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."},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly."},"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."},"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},"heartbeat":{"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":"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":"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}]},"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},"heartbeat":{"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":"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","all', + 'owlist"]},"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":"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 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: \\"partial\\"). \\"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: \\"partial\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.1 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable chunked block-style Telegram preview delivery when channels.telegram.streaming.mode=\\"block\\"."},"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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"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: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"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"},"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"},"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"},"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"},"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"},"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},"heartbeat":{"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"},"messagePrefix":{"type":"string"},"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"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991},"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},"heartbeat":{"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"},"messagePrefix":{"type":"string"},"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"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"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","debounceMs"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"debounceMs":{"label":"WhatsApp Message Debounce (ms)","help":"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable)."},"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"},"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":"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":"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"},"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":"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":"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"},"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"},"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/config-misc.test.ts b/src/config/config-misc.test.ts index 20b2101f3c40..7c2534430ed0 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -491,20 +491,6 @@ describe("diagnostics.otel.captureContent", () => { }); }); -describe("auth.cooldowns auth_permanent backoff config", () => { - it("accepts auth_permanent backoff knobs", () => { - const result = OpenClawSchema.safeParse({ - auth: { - cooldowns: { - authPermanentBackoffMinutes: 10, - authPermanentMaxMinutes: 60, - }, - }, - }); - expect(result.success).toBe(true); - }); -}); - describe("ui.seamColor", () => { it("accepts hex colors", () => { const res = validateConfigObject({ ui: { seamColor: "#FF4500" } }); @@ -917,93 +903,6 @@ describe("gateway.tools config", () => { }); }); -describe("gateway.channelHealthCheckMinutes", () => { - it("accepts preauth handshake timeout tuning", () => { - const res = validateConfigObject({ - gateway: { - handshakeTimeoutMs: 30_000, - }, - }); - expect(res.ok).toBe(true); - }); - - it("rejects non-positive preauth handshake timeouts", () => { - const res = validateConfigObject({ - gateway: { - handshakeTimeoutMs: 0, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues[0]?.path).toBe("gateway.handshakeTimeoutMs"); - } - }); - - it("accepts zero to disable monitor", () => { - const res = validateConfigObject({ - gateway: { - channelHealthCheckMinutes: 0, - }, - }); - expect(res.ok).toBe(true); - }); - - it("rejects negative intervals", () => { - const res = validateConfigObject({ - gateway: { - channelHealthCheckMinutes: -1, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues[0]?.path).toBe("gateway.channelHealthCheckMinutes"); - } - }); - - it("rejects stale thresholds shorter than the health check interval", () => { - const res = validateConfigObject({ - gateway: { - channelHealthCheckMinutes: 5, - channelStaleEventThresholdMinutes: 4, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues[0]?.path).toBe("gateway.channelStaleEventThresholdMinutes"); - } - }); - - it("accepts stale thresholds that match or exceed the health check interval", () => { - const equal = validateConfigObject({ - gateway: { - channelHealthCheckMinutes: 5, - channelStaleEventThresholdMinutes: 5, - }, - }); - expect(equal.ok).toBe(true); - - const greater = validateConfigObject({ - gateway: { - channelHealthCheckMinutes: 5, - channelStaleEventThresholdMinutes: 6, - }, - }); - expect(greater.ok).toBe(true); - }); - - it("rejects stale thresholds shorter than the default health check interval", () => { - const res = validateConfigObject({ - gateway: { - channelStaleEventThresholdMinutes: 4, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues[0]?.path).toBe("gateway.channelStaleEventThresholdMinutes"); - } - }); -}); - describe("config identity/materialization regressions", () => { it("keeps explicit responsePrefix and group mention patterns", () => { const res = validateConfigObject({ @@ -1111,19 +1010,6 @@ describe("cron webhook schema", () => { expect(res.success).toBe(true); }); - - it("accepts cron.retry config", () => { - const res = OpenClawSchema.safeParse({ - cron: { - retry: { - maxAttempts: 5, - backoffMs: [60000, 120000, 300000], - retryOn: ["rate_limit", "overloaded", "network"], - }, - }, - }); - expect(res.success).toBe(true); - }); }); describe("broadcast", () => { diff --git a/src/config/config.agent-concurrency-defaults.test.ts b/src/config/config.agent-concurrency-defaults.test.ts index 849c2ae3e04b..d3a134112816 100644 --- a/src/config/config.agent-concurrency-defaults.test.ts +++ b/src/config/config.agent-concurrency-defaults.test.ts @@ -29,7 +29,6 @@ describe("agent concurrency defaults", () => { }; expect(resolveAgentMaxConcurrent(cfg)).toBe(1); expect(resolveSubagentMaxConcurrent(cfg)).toBe(1); - expect(resolveCronMaxConcurrentRuns({ maxConcurrentRuns: 0 })).toBe(1); }); it("accepts subagent spawn depth and per-agent child limits", () => { diff --git a/src/config/config.compaction-settings.test.ts b/src/config/config.compaction-settings.test.ts index 5fd9e70dfd55..6e422c3a0ebd 100644 --- a/src/config/config.compaction-settings.test.ts +++ b/src/config/config.compaction-settings.test.ts @@ -20,7 +20,6 @@ describe("config compaction settings", () => { it("preserves memory flush config values", () => { const compaction = materializeCompactionConfig({ mode: "safeguard", - reserveTokensFloor: 12_345, identifierPolicy: "custom", identifierInstructions: "Keep ticket IDs unchanged.", qualityGuard: { @@ -40,9 +39,7 @@ describe("config compaction settings", () => { maxActiveTranscriptBytes: "20mb", }); - expect(compaction?.reserveTokensFloor).toBe(12_345); expect(compaction?.mode).toBe("safeguard"); - expect(compaction?.reserveTokens).toBeUndefined(); expect(compaction?.keepRecentTokens).toBeUndefined(); expect(compaction?.identifierPolicy).toBe("custom"); expect(compaction?.identifierInstructions).toBe("Keep ticket IDs unchanged."); @@ -57,23 +54,10 @@ describe("config compaction settings", () => { expect(compaction?.maxActiveTranscriptBytes).toBe("20mb"); }); - it("preserves legacy compaction override values", () => { - const compaction = materializeCompactionConfig({ - reserveTokens: 15_000, - keepRecentTokens: 12_000, - }); - - expect(compaction?.reserveTokens).toBe(15_000); - expect(compaction?.keepRecentTokens).toBe(12_000); - }); - it("defaults compaction mode to safeguard", () => { - const compaction = materializeCompactionConfig({ - reserveTokensFloor: 9000, - }); + const compaction = materializeCompactionConfig({}); expect(compaction?.mode).toBe("safeguard"); - expect(compaction?.reserveTokensFloor).toBe(9000); }); it("preserves recent turn safeguard values during materialization", () => { diff --git a/src/config/config.schema-regressions.test.ts b/src/config/config.schema-regressions.test.ts index 4d9404667aa0..a8b5637818aa 100644 --- a/src/config/config.schema-regressions.test.ts +++ b/src/config/config.schema-regressions.test.ts @@ -3,41 +3,6 @@ import { describe, expect, it } from "vitest"; import { validateConfigObject } from "./validation.js"; describe("config schema regressions", () => { - it("accepts session write-lock acquire timeout", () => { - const res = validateConfigObject({ - session: { - writeLock: { - acquireTimeoutMs: 60_000, - }, - }, - }); - - expect(res.ok).toBe(true); - }); - - it("accepts worktree cleanup limits and rejects negative values", () => { - expect( - validateConfigObject({ - worktrees: { cleanup: { maxCount: 25, maxTotalSizeGb: 50 } }, - }).ok, - ).toBe(true); - expect( - validateConfigObject({ - worktrees: { cleanup: { maxCount: 0, maxTotalSizeGb: 0 } }, - }).ok, - ).toBe(true); - expect( - validateConfigObject({ - worktrees: { cleanup: { maxCount: -1 } }, - }).ok, - ).toBe(false); - expect( - validateConfigObject({ - worktrees: { cleanup: { maxTotalSizeGb: -0.5 } }, - }).ok, - ).toBe(false); - }); - it('accepts memorySearch fallback "voyage"', () => { const res = validateConfigObject({ agents: { @@ -361,28 +326,6 @@ describe("config schema regressions", () => { expect(res.ok).toBe(true); }); - it("accepts browser local startup timeout settings", () => { - const res = validateConfigObject({ - browser: { - localLaunchTimeoutMs: 45_000, - localCdpReadyTimeoutMs: 30_000, - }, - }); - - expect(res.ok).toBe(true); - }); - - it("rejects out-of-range browser local startup timeout settings", () => { - const res = validateConfigObject({ - browser: { - localLaunchTimeoutMs: 120_001, - localCdpReadyTimeoutMs: 0, - }, - }); - - expect(res.ok).toBe(false); - }); - it("rejects browser.extraArgs with non-array value", () => { const res = validateConfigObject({ browser: { @@ -393,33 +336,6 @@ describe("config schema regressions", () => { expect(res.ok).toBe(false); }); - it("accepts browser.tabCleanup overrides", () => { - const res = validateConfigObject({ - browser: { - tabCleanup: { - enabled: true, - idleMinutes: 10, - maxTabsPerSession: 10, - sweepMinutes: 5, - }, - }, - }); - - expect(res.ok).toBe(true); - }); - - it("rejects browser.tabCleanup.sweepMinutes when not positive", () => { - const res = validateConfigObject({ - browser: { - tabCleanup: { - sweepMinutes: 0, - }, - }, - }); - - expect(res.ok).toBe(false); - }); - it("rejects unknown keys under browser.tabCleanup", () => { const res = validateConfigObject({ browser: { diff --git a/src/config/cron-limits.ts b/src/config/cron-limits.ts index 9be828f62264..0f4f8d9ec9c0 100644 --- a/src/config/cron-limits.ts +++ b/src/config/cron-limits.ts @@ -1,26 +1,14 @@ // Computes cron scheduling limits from config. -import type { CronConfig } from "./types.cron.js"; - /** Default maximum number of cron jobs allowed to run at once. */ export const DEFAULT_CRON_MAX_CONCURRENT_RUNS = 8; const DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS = 30_000; /** Resolves cron concurrency config, flooring finite values and clamping to at least one. */ -export function resolveCronMaxConcurrentRuns( - cronConfig?: Pick, -): number { - const raw = cronConfig?.maxConcurrentRuns; - if (typeof raw === "number" && Number.isFinite(raw)) { - return Math.max(1, Math.floor(raw)); - } +export function resolveCronMaxConcurrentRuns(): number { return DEFAULT_CRON_MAX_CONCURRENT_RUNS; } /** Resolves the minimum cadence for trigger-bearing cron jobs. */ -export function resolveCronTriggerMinIntervalMs(cronConfig?: Pick): number { - const raw = cronConfig?.triggers?.minIntervalMs; - if (typeof raw === "number" && Number.isFinite(raw)) { - return Math.max(1, Math.floor(raw)); - } +export function resolveCronTriggerMinIntervalMs(): number { return DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS; } diff --git a/src/config/dead-config-keys.test.ts b/src/config/dead-config-keys.test.ts index e9b32bb8cb72..ac7fe9f65c5d 100644 --- a/src/config/dead-config-keys.test.ts +++ b/src/config/dead-config-keys.test.ts @@ -19,7 +19,123 @@ function expectUnknownKey(params: { config: Record; path: strin } } +function configWithPath(path: string): Record { + const segments = path.split("."); + const config = segments.reduceRight( + (value, segment) => (segment === "0" ? [value] : { [segment]: value }), + 1, + ) as Record; + const agent = (config.agents as { list?: Array> } | undefined)?.list?.[0]; + if (agent) { + agent.id = "test"; + } + return config; +} + describe("dead config keys", () => { + it.each([ + "auth.cooldowns", + "secrets.resolution", + "browser.remoteCdpTimeoutMs", + "browser.remoteCdpHandshakeTimeoutMs", + "browser.localLaunchTimeoutMs", + "browser.localCdpReadyTimeoutMs", + "browser.actionTimeoutMs", + "browser.cdpPortRangeStart", + "browser.tabCleanup.idleMinutes", + "browser.tabCleanup.maxTabsPerSession", + "browser.tabCleanup.sweepMinutes", + "tools.loopDetection.historySize", + "tools.loopDetection.warningThreshold", + "tools.loopDetection.detectors", + "tools.loopDetection.postCompactionGuard", + "agents.defaults.compaction.reserveTokens", + "agents.defaults.compaction.reserveTokensFloor", + "agents.defaults.compaction.maxHistoryShare", + "agents.defaults.contextPruning.keepLastAssistants", + "agents.defaults.contextPruning.softTrimRatio", + "agents.defaults.contextPruning.softTrim", + "agents.defaults.memorySearch.chunking", + "agents.defaults.memorySearch.sync.watchDebounceMs", + "agents.defaults.memorySearch.sync.intervalMinutes", + "agents.defaults.memorySearch.query.hybrid.vectorWeight", + "agents.defaults.memorySearch.query.hybrid.textWeight", + "agents.defaults.memorySearch.query.hybrid.candidateMultiplier", + "agents.defaults.memorySearch.query.hybrid.mmr.lambda", + "agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays", + "agents.defaults.memorySearch.cache.maxEntries", + "agents.defaults.cliBackends.codex.reliability.outputLimits", + "agents.defaults.cliBackends.codex.reliability.watchdog.fresh.noOutputTimeoutMs", + "agents.defaults.cliBackends.codex.reliability.watchdog.resume.noOutputTimeoutMs", + "agents.defaults.runRetries", + "agents.list.0.memorySearch.chunking", + "agents.list.0.runRetries", + "gateway.handshakeTimeoutMs", + "gateway.channelHealthCheckMinutes", + "gateway.channelStaleEventThresholdMinutes", + "gateway.channelMaxRestartsPerHour", + "gateway.reload.debounceMs", + "gateway.reload.deferralTimeoutMs", + "gateway.http.endpoints.chatCompletions.maxBodyBytes", + "gateway.http.endpoints.chatCompletions.maxImageParts", + "gateway.http.endpoints.chatCompletions.maxTotalImageBytes", + "gateway.http.endpoints.responses.maxBodyBytes", + "session.typingIntervalSeconds", + "session.writeLock", + "session.agentToAgent", + "cron.maxConcurrentRuns", + "cron.triggers.minIntervalMs", + "cron.retry", + "diagnostics.stuckSessionWarnMs", + "diagnostics.stuckSessionAbortMs", + "diagnostics.memoryPressureSnapshot", + "web.heartbeatSeconds", + "web.reconnect", + "web.whatsapp", + "messages.queue.debounceMs", + "messages.statusReactions.timing", + "acp.stream.coalesceIdleMs", + "acp.stream.maxChunkChars", + "acp.stream.maxOutputChars", + "acp.stream.maxSessionUpdateChars", + "acp.stream.hiddenBoundarySeparator", + "acp.maxConcurrentSessions", + "acp.runtime.ttlMinutes", + "mcp.sessionIdleTtlMs", + "worktrees", + "transcripts.maxUtterances", + "hooks.maxBodyBytes", + "update.auto.stableDelayHours", + "update.auto.stableJitterHours", + "update.auto.betaCheckIntervalHours", + "channels.telegram.timeoutSeconds", + "channels.telegram.mediaGroupFlushMs", + "channels.telegram.pollingStallThresholdMs", + "channels.telegram.retry", + "channels.telegram.errorCooldownMs", + "channels.telegram.accounts.work.timeoutSeconds", + "channels.telegram.accounts.work.retry", + "channels.telegram.groups.-100.errorCooldownMs", + "channels.telegram.groups.-100.topics.1.errorCooldownMs", + "channels.discord.gatewayInfoTimeoutMs", + "channels.discord.gatewayReadyTimeoutMs", + "channels.discord.gatewayRuntimeReadyTimeoutMs", + "channels.discord.eventQueue", + "channels.discord.retry", + "channels.discord.accounts.work.eventQueue", + "channels.discord.accounts.work.retry", + "channels.clickclack.timeoutSeconds", + "channels.clickclack.accounts.work.timeoutSeconds", + ] as const)("rejects retired tuning knob %s", (fullPath) => { + const segments = fullPath.split("."); + const key = segments.pop() ?? ""; + expectUnknownKey({ + config: configWithPath(fullPath), + path: segments.join("."), + key, + }); + }); + it.each([ ["Discord root", "discord", { dm: { policy: "pairing" } }, "channels.discord.dm", "policy"], [ diff --git a/src/config/defaults.test.ts b/src/config/defaults.test.ts index 99cfae45ee7d..92c341541981 100644 --- a/src/config/defaults.test.ts +++ b/src/config/defaults.test.ts @@ -6,11 +6,9 @@ import { DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, DEFAULT_SUBAGENT_MAX_CONCURRENT, } from "./agent-limits.js"; -import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "./cron-limits.js"; import { applyAgentDefaults, applyContextPruningDefaults, - applyCronDefaults, applyMessageDefaults, } from "./defaults.js"; @@ -131,18 +129,6 @@ describe("config defaults", () => { ); }); - it("fills missing cron concurrency default", () => { - const next = applyCronDefaults({ messages: {} } as never); - - expect(next.cron?.maxConcurrentRuns).toBe(DEFAULT_CRON_MAX_CONCURRENT_RUNS); - }); - - it("preserves explicit cron concurrency", () => { - const next = applyCronDefaults({ cron: { maxConcurrentRuns: 3 } } as never); - - expect(next.cron?.maxConcurrentRuns).toBe(3); - }); - it("preserves explicit subagent archive default", () => { const next = applyAgentDefaults({ agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 45c3720fcd4d..ab0a7e078600 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -13,7 +13,6 @@ import { DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, DEFAULT_SUBAGENT_MAX_CONCURRENT, } from "./agent-limits.js"; -import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "./cron-limits.js"; import { normalizeAgentModelMapForConfig, normalizeAgentModelRefForConfig } from "./model-input.js"; import { applyProviderConfigDefaultsForConfig, @@ -460,17 +459,7 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig { } export function applyCronDefaults(cfg: OpenClawConfig): OpenClawConfig { - const raw = cfg.cron?.maxConcurrentRuns; - if (typeof raw === "number" && Number.isFinite(raw)) { - return cfg; - } - return { - ...cfg, - cron: { - ...cfg.cron, - maxConcurrentRuns: DEFAULT_CRON_MAX_CONCURRENT_RUNS, - }, - }; + return cfg; } export function applyLoggingDefaults(cfg: OpenClawConfig): OpenClawConfig { diff --git a/src/config/schema.help.agents.ts b/src/config/schema.help.agents.ts index 69f3e611acce..9d70f078940b 100644 --- a/src/config/schema.help.agents.ts +++ b/src/config/schema.help.agents.ts @@ -121,21 +121,15 @@ export const AGENT_FIELD_HELP: Record = { 'Image-tool media compression preference: "auto" adapts to provider/model limits and image count, "efficient" saves tokens and bytes, "balanced" keeps the current middle ground, and "high" preserves more detail for screenshots and document images.', "agents.defaults.cliBackends": "Optional CLI backends for text-only fallback (claude-cli, etc.).", "agents.defaults.compaction": - "Compaction tuning for when context nears token limits, including history share, reserve headroom, and pre-compaction memory flush behavior. Use this when long-running sessions need stable continuity under tight context windows.", + "Compaction behavior for when context nears token limits, including strategy and pre-compaction memory flush behavior. Use this when long-running sessions need stable continuity under tight context windows.", "agents.defaults.compaction.mode": 'Compaction strategy mode: "default" uses baseline behavior, while "safeguard" applies stricter guardrails to preserve recent context. Keep "default" unless you observe aggressive history loss near limit boundaries.', "agents.defaults.compaction.provider": "Id of a registered compaction provider plugin used for summarization. When set and the provider is registered, its summarize() method is called instead of the built-in summarizeInStages pipeline. Falls back to built-in on provider failure. Leave unset to use the default built-in summarization.", "agents.defaults.compaction.thinkingLevel": 'Optional thinking level used only for embedded OpenClaw compaction summaries: "off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max", or "ultra". It overrides the session level and is clamped to the actual compaction model/runtime; leave unset to inherit the session level. Native Codex app-server compaction ignores this setting because its compact request has no per-operation thinking override, and OpenClaw logs a warning.', - "agents.defaults.compaction.reserveTokens": - "Token headroom reserved for reply generation and tool output after compaction runs. Use higher reserves for verbose/tool-heavy sessions, and lower reserves when maximizing retained history matters more. When the active model context window is known, the effective reserve is capped to preserve usable prompt space.", "agents.defaults.compaction.keepRecentTokens": "Minimum token budget preserved from the most recent conversation window during compaction. Use higher values to protect immediate context continuity and lower values to keep more long-tail history.", - "agents.defaults.compaction.reserveTokensFloor": - "Minimum floor enforced for reserveTokens in embedded OpenClaw compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.", - "agents.defaults.compaction.maxHistoryShare": - "Maximum fraction of total context budget allowed for retained history after compaction (range 0.1-0.9). Use lower shares for more generation headroom or higher shares for deeper historical continuity.", "agents.defaults.compaction.identifierPolicy": 'Identifier-preservation policy for compaction summaries: "strict" prepends built-in opaque-identifier retention guidance (default), "off" disables this prefix, and "custom" uses identifierInstructions. Keep "strict" unless you have a specific compatibility need.', "agents.defaults.compaction.identifierInstructions": @@ -180,23 +174,6 @@ export const AGENT_FIELD_HELP: Record = { "User-prompt template used for the pre-compaction memory flush turn when generating memory candidates. Use this only when you need custom extraction instructions beyond the default memory flush behavior.", "agents.defaults.compaction.memoryFlush.systemPrompt": "System-prompt override for the pre-compaction memory flush turn to control extraction style and safety constraints. Use carefully so custom instructions do not reduce memory quality or leak sensitive context.", - "agents.defaults.runRetries": - "Outer run loop retry iteration boundaries for the embedded OpenClaw runner to prevent infinite execution loops during failure recovery.", - "agents.defaults.runRetries.base": - "Base number of run retry iterations for the embedded OpenClaw runner's outer run loop (default: 24).", - "agents.defaults.runRetries.perProfile": - "Additional run retry iterations granted per fallback profile candidate (default: 8).", - "agents.defaults.runRetries.min": - "Minimum absolute limit for run retry iterations (default: 32).", - "agents.defaults.runRetries.max": - "Maximum absolute limit for run retry iterations to prevent runaway execution (default: 160).", - "agents.list[].runRetries": - "Optional per-agent override for the embedded OpenClaw runner's outer run loop retry iteration boundaries.", - "agents.list[].runRetries.base": "Base number of run retry iterations for this agent.", - "agents.list[].runRetries.perProfile": - "Additional run retry iterations granted per fallback profile candidate for this agent.", - "agents.list[].runRetries.min": "Minimum absolute limit for run retry iterations for this agent.", - "agents.list[].runRetries.max": "Maximum absolute limit for run retry iterations for this agent.", "agents.defaults.embeddedAgent": "Embedded OpenClaw runner hardening controls for how workspace-local agent settings are trusted and applied in OpenClaw sessions.", "agents.defaults.embeddedAgent.projectSettingsPolicy": @@ -257,6 +234,4 @@ export const AGENT_FIELD_HELP: Record = { 'Optional Codex MCP tool approval mode for this server: "auto", "prompt", or "approve". Use only for MCP servers you intentionally trust.', "mcp.servers.*.codex.default_tools_approval_mode": "Codex-native spelling for the same per-server MCP tool approval mode. Prefer defaultToolsApprovalMode in OpenClaw config.", - "mcp.sessionIdleTtlMs": - "Idle TTL in milliseconds for session-scoped bundled MCP runtimes. Defaults to 10 minutes; set 0 to disable idle eviction.", }; diff --git a/src/config/schema.help.automation.ts b/src/config/schema.help.automation.ts index 8da195c94ad6..8217c1a2f5db 100644 --- a/src/config/schema.help.automation.ts +++ b/src/config/schema.help.automation.ts @@ -32,8 +32,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Provides channel-specific reset overrides keyed by provider/channel id for fine-grained behavior control. Use this only when one channel needs exceptional reset behavior beyond type-level policies.", "session.store": "Sets the session storage file path used to persist session records across restarts. Use an explicit path only when you need custom disk layout, backup routing, or mounted-volume storage.", - "session.typingIntervalSeconds": - "Controls interval for repeated typing indicators while replies are being prepared in typing-capable channels. Increase to reduce chatty updates or decrease for more active typing feedback.", "session.typingMode": 'Controls typing behavior timing: "never", "instant", "thinking", or "message" based emission points. Keep conservative modes in high-volume channels to avoid unnecessary typing noise.', "session.mainKey": @@ -56,18 +54,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Matches a normalized session-key prefix after internal key normalization steps in policy consumers. Use this for general prefix controls, and prefer rawKeyPrefix when exact full-key matching is required.", "session.sendPolicy.rules[].match.rawKeyPrefix": "Matches the raw, unnormalized session-key prefix for exact full-key policy targeting. Use this when normalized keyPrefix is too broad and you need agent-prefixed or transport-specific precision.", - "session.writeLock": - "Groups session transcript write-lock controls. Tune only when legitimate transcript prep, cleanup, compaction, or mirror work contends longer than the default policies.", - "session.writeLock.acquireTimeoutMs": - "Milliseconds to wait while acquiring a session transcript write lock before reporting the session as busy. Default: 60000; env override: OPENCLAW_SESSION_WRITE_LOCK_ACQUIRE_TIMEOUT_MS.", - "session.writeLock.staleMs": - "Milliseconds before an existing session transcript lock can be treated as stale and reclaimed. Default: 1800000; env override: OPENCLAW_SESSION_WRITE_LOCK_STALE_MS.", - "session.writeLock.maxHoldMs": - "Milliseconds a held in-process session transcript lock may remain held before the watchdog releases it. Default: 300000; env override: OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS.", - "session.agentToAgent": - "Groups controls for inter-agent session exchanges, including loop prevention limits on reply chaining. Keep defaults unless you run advanced agent-to-agent automation with strict turn caps.", - "session.agentToAgent.maxPingPongTurns": - "Max reply-back turns between requester and target agents during agent-to-agent exchanges (0-20, default 5). Use lower values to hard-limit chatter loops and preserve predictable run completion.", "session.threadBindings": "Shared defaults for thread-bound session routing behavior across providers that support thread focus workflows. Configure global defaults here and override per channel only when behavior differs.", "session.threadBindings.enabled": @@ -99,34 +85,14 @@ export const AUTOMATION_FIELD_HELP: Record = { "Enables cron job execution for stored schedules managed by the gateway. Keep enabled for normal reminder/automation flows, and disable only to pause all cron execution without deleting jobs.", "cron.store": "Path to the cron job store file used to persist scheduled jobs across restarts. Set an explicit path only when you need custom storage layout, backups, or mounted volumes.", - "cron.maxConcurrentRuns": - "Defaults to 8. Limits how many cron jobs can execute at the same time when multiple schedules fire together, including isolated agent-turn LLM execution on the dedicated cron-nested lane. Use lower values to protect CPU/memory under heavy automation load, or raise carefully for higher throughput.", - "cron.retry": - "Overrides the default retry policy for one-shot jobs when they fail with transient errors (rate limit, overloaded, network, server_error). Omit to use defaults: maxAttempts 3, backoffMs [30000, 60000, 300000], retry all transient types.", - "cron.retry.maxAttempts": - "Max retries for one-shot jobs on transient errors before permanent disable (default: 3).", - "cron.retry.backoffMs": - "Backoff delays in ms for each retry attempt (default: [30000, 60000, 300000]). Use shorter values for faster retries.", - "cron.retry.retryOn": - "Error types to retry: rate_limit, overloaded, network, timeout, server_error. Use to restrict which errors trigger retries; omit to retry all transient types.", "cron.webhookToken": "Bearer token attached to cron webhook POST deliveries when webhook mode is used. Prefer secret/env substitution and rotate this token regularly if shared webhook endpoints are internet-reachable.", "cron.sessionRetention": "Controls how long completed cron run sessions are kept before pruning (`24h`, `7d`, `1h30m`, or `false` to disable pruning; default: `24h`). Use shorter retention to reduce storage growth on high-frequency schedules.", - worktrees: - "Managed worktree retention settings applied by hourly cleanup and manual `openclaw worktrees gc`. Keep defaults unless managed worktrees accumulate faster than idle cleanup reclaims them.", - "worktrees.cleanup": - "Retention limits for OpenClaw-managed worktrees across all repositories. Cleanup snapshots and removes the least recently active session- and Workboard-owned worktrees first; manual, locked, and recently active worktrees are never limit-evicted.", - "worktrees.cleanup.maxCount": - "Maximum number of managed worktrees to retain across all repositories. When exceeded, the least recently active evictable worktrees are snapshotted and removed until the count fits. 0 or unset disables the count limit.", - "worktrees.cleanup.maxTotalSizeGb": - "Maximum total disk size in GB across all managed worktrees, measured during cleanup. When exceeded, the least recently active evictable worktrees are snapshotted and removed until usage fits. 0 or unset disables the size limit.", transcripts: "Core transcript capture settings for recording-capable agent tools and configured live meeting auto-start sources. Keep disabled unless operators explicitly want agents to capture or import meeting transcripts.", "transcripts.enabled": "Enables the recording-capable transcripts agent tool and configured auto-start sources. Default: false. Enable only on hosts where operators have reviewed meeting capture policy and provider permissions.", - "transcripts.maxUtterances": - "Maximum utterances retained in a transcript summary operation before truncation. Use lower values to limit prompt/storage footprint, or raise carefully for long meetings where summary completeness matters.", "transcripts.autoStart": "Live transcript sources started automatically when the gateway starts. Each entry is enabled by being present; remove an entry to disable that source.", "transcripts.autoStart[].providerId": @@ -159,8 +125,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Allowlist of accepted session-key prefixes for inbound hook requests when caller-provided keys are enabled. Use narrow prefixes to prevent arbitrary session-key injection.", "hooks.allowedAgentIds": "Allowlist of effective agent IDs that hook requests and mappings are allowed to target, including default-agent routing when agentId is omitted. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.", - "hooks.maxBodyBytes": - "Maximum accepted webhook payload size in bytes before the request is rejected. Keep this bounded to reduce abuse risk and protect memory usage under bursty integrations.", "hooks.presets": "Named hook preset bundles applied at load time to seed standard mappings and behavior defaults. Keep preset usage explicit so operators can audit which automations are active.", "hooks.transformsDir": @@ -289,8 +253,6 @@ export const AUTOMATION_FIELD_HELP: Record = { 'Queue mode for active runs. Use "steer" to inject prompts into the active run, "followup" to run later, "collect" to batch compatible messages later, or "interrupt" to abort the active run before starting the newest prompt.', "messages.queue.byChannel": "Per-channel queue mode overrides keyed by provider id (for example telegram, discord, slack). Use this when one channel's traffic pattern needs different behavior than global defaults.", - "messages.queue.debounceMs": - "Global fallback followup queue debounce window in milliseconds before draining buffered inbound messages. Default is 500ms; higher values coalesce bursts, lower values reduce latency.", "messages.queue.debounceMsByChannel": "Per-channel debounce overrides for queue behavior keyed by provider id. Use this to tune burst handling independently for chat surfaces with different pacing.", "messages.queue.cap": @@ -382,8 +344,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Enable lifecycle status reactions on supported channels. Discord treats unset as enabled when ack reactions are active; Slack, Signal, Telegram, and WhatsApp require this to be true before lifecycle reactions are used. Slack uses native assistant thread status for progress by default.", "messages.statusReactions.emojis": "Override default status reaction emojis. Keys: queued, thinking, compacting, tool, coding, web, deploy, build, concierge, done, error, stallSoft, stallHard. Telegram chooses the first supported fallback when a configured emoji is not available in the chat.", - "messages.statusReactions.timing": - "Override default timing. Keys: debounceMs (700), stallSoftMs (10000), stallHardMs (30000), doneHoldMs (1500), errorHoldMs (2500).", "messages.inbound.debounceMs": "Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).", }; diff --git a/src/config/schema.help.core.ts b/src/config/schema.help.core.ts index 9be9a12a76f8..5782722082c6 100644 --- a/src/config/schema.help.core.ts +++ b/src/config/schema.help.core.ts @@ -69,8 +69,6 @@ export const CORE_FIELD_HELP: Record = { 'Controls content-free message lifecycle records: "off" (default), "direct" for known direct conversations only, or "all" for direct, group, channel, and unknown conversation kinds. Both audit.enabled and audit.messages are startup-scoped; restart the Gateway after changing either setting.', diagnostics: "Diagnostics controls for targeted tracing, telemetry export, and cache inspection during debugging. Keep baseline diagnostics minimal in production and enable deeper signals only when investigating issues.", - "diagnostics.memoryPressureSnapshot": - "Whether critical memory pressure automatically captures a redacted stability snapshot. Default: false. Enable only when this host can tolerate the extra file-system scan and snapshot write during critical memory pressure.", "diagnostics.otel": "OpenTelemetry export settings for traces, metrics, and logs emitted by gateway components. Use this when integrating with centralized observability backends and distributed tracing pipelines.", "diagnostics.cacheTrace": @@ -102,11 +100,6 @@ export const CORE_FIELD_HELP: Record = { "Check for npm updates when the gateway starts, including read-only extended-stable hints (default: true).", "update.auto.enabled": "Enable background auto-update for stable and beta package installs; extended-stable never auto-applies (default: false).", - "update.auto.stableDelayHours": - "Minimum delay before stable-channel auto-apply starts (default: 6).", - "update.auto.stableJitterHours": - "Extra stable-channel rollout spread window in hours (default: 12).", - "update.auto.betaCheckIntervalHours": "How often beta-channel checks run in hours (default: 1).", cloudWorkers: "Opt-in cloud worker profiles for disposable remote environments. When this section is omitted or has no profiles, cloud worker creation remains unavailable and existing gateway/node status behavior is unchanged.", "cloudWorkers.profiles": @@ -173,14 +166,6 @@ export const CORE_FIELD_HELP: Record = { "Explicit gateway-level tool allowlist when you want a narrow set of tools available at runtime. Use this for locked-down environments where tool scope must be tightly controlled.", "gateway.tools.deny": "Explicit gateway-level tool denylist to block risky tools even if lower-level policies allow them. Use deny rules for emergency response and defense-in-depth hardening.", - "gateway.handshakeTimeoutMs": - "Pre-auth Gateway WebSocket handshake timeout in milliseconds. Use higher values on loaded or low-powered hosts where local clients can connect during startup warmup. OPENCLAW_HANDSHAKE_TIMEOUT_MS still takes precedence.", - "gateway.channelHealthCheckMinutes": - "Interval in minutes for automatic channel health probing and status updates. Use lower intervals for faster detection, or higher intervals to reduce periodic probe noise.", - "gateway.channelStaleEventThresholdMinutes": - "How many minutes a connected channel can go without provider-proven transport activity before the health monitor treats it as a stale socket and triggers a restart. Default: 30.", - "gateway.channelMaxRestartsPerHour": - "Maximum number of health-monitor-initiated channel restarts allowed within a rolling one-hour window. Once hit, further restarts are skipped until the window expires. Default: 10.", "gateway.tailscale": "Tailscale integration settings for Serve/Funnel exposure and lifecycle handling on gateway start/exit. Keep off unless your deployment intentionally relies on Tailscale ingress.", "gateway.tailscale.mode": @@ -272,7 +257,7 @@ export const CORE_FIELD_HELP: Record = { "talk.interruptOnSpeech": "If true (default), stop assistant speech when the user starts speaking in Talk mode. Keep enabled for conversational turn-taking.", "talk.silenceTimeoutMs": `Milliseconds of user silence before Talk mode finalizes and sends the current transcript. Leave unset to keep the platform default pause window (${describeTalkSilenceTimeoutDefaults()}).`, - acp: "ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.", + acp: "ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and selecting streamed turn projection behavior.", "acp.enabled": "Global ACP feature gate. Keep disabled unless ACP runtime + policy are configured.", "acp.dispatch.enabled": @@ -285,28 +270,14 @@ export const CORE_FIELD_HELP: Record = { "Fallback ACP target agent id used when ACP spawns do not specify an explicit target.", "acp.allowedAgents": "Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.", - "acp.maxConcurrentSessions": - "Maximum concurrently active ACP sessions across this gateway process.", "acp.stream": "ACP streaming projection controls for chunk sizing, metadata visibility, and deduped delivery behavior.", - "acp.stream.coalesceIdleMs": - "Coalescer idle flush window in milliseconds for ACP streamed text before block replies are emitted.", - "acp.stream.maxChunkChars": - "Maximum chunk size for ACP streamed block projection before splitting into multiple block replies.", "acp.stream.repeatSuppression": "When true (default), suppress repeated ACP status/tool projection lines in a turn while keeping raw ACP events unchanged.", "acp.stream.deliveryMode": "ACP delivery style: live streams projected output incrementally, final_only buffers all projected ACP output until terminal turn events.", - "acp.stream.hiddenBoundarySeparator": - "Separator inserted before next visible assistant text when hidden ACP tool lifecycle events occurred (none|space|newline|paragraph). Default: paragraph.", - "acp.stream.maxOutputChars": - "Maximum assistant output characters projected per ACP turn before truncation notice is emitted.", - "acp.stream.maxSessionUpdateChars": - "Maximum characters for projected ACP session/update lines (tool/status updates).", "acp.stream.tagVisibility": "Per-sessionUpdate visibility overrides for ACP projection (for example usage_update, available_commands_update).", - "acp.runtime.ttlMinutes": - "Idle runtime TTL in minutes for ACP session workers before eligible cleanup.", "acp.runtime.installCommand": "Optional operator install/setup command shown by `/acp install` and `/acp doctor` when ACP backend wiring is missing.", commitments: diff --git a/src/config/schema.help.models.ts b/src/config/schema.help.models.ts index 41e4c2922da9..32312bf3027a 100644 --- a/src/config/schema.help.models.ts +++ b/src/config/schema.help.models.ts @@ -153,24 +153,6 @@ export const MODEL_FIELD_HELP: Record = { "Require @mention in channels before responding (default: true).", "auth.profiles": "Named auth profiles (provider + mode + optional email).", "auth.order": "Ordered auth profile IDs per provider (used for automatic failover).", - "auth.cooldowns": - "Cooldown/backoff controls for temporary profile suppression after billing-related failures and retry windows. Use these to prevent rapid re-selection of profiles that are still blocked.", - "auth.cooldowns.billingBackoffHours": - "Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).", - "auth.cooldowns.billingBackoffHoursByProvider": - "Optional per-provider overrides for billing backoff (hours).", - "auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).", - "auth.cooldowns.authPermanentBackoffMinutes": - "Base backoff (minutes) for high-confidence auth_permanent failures (default: 10). Keep this shorter than billing so providers recover automatically after transient upstream auth incidents.", - "auth.cooldowns.authPermanentMaxMinutes": - "Cap (minutes) for auth_permanent backoff (default: 60).", - "auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).", - "auth.cooldowns.overloadedProfileRotations": - "Maximum same-provider auth-profile rotations allowed for overloaded errors before switching to model fallback (default: 1).", - "auth.cooldowns.overloadedBackoffMs": - "Fixed delay in milliseconds before retrying an overloaded provider/profile rotation (default: 0).", - "auth.cooldowns.rateLimitedProfileRotations": - "Maximum same-provider auth-profile rotations allowed for rate-limit errors before switching to model fallback (default: 1).", "agents.defaults.workspace": "Default workspace path exposed to agent runtime tools for filesystem context and repo-aware behavior. Set this explicitly when running from wrappers so path resolution stays deterministic.", "agents.defaults.skipOptionalBootstrapFiles": @@ -294,30 +276,16 @@ export const MODEL_FIELD_HELP: Record = { "Enables the sqlite-vec extension used for vector similarity queries in memory search (default: true). Keep this enabled for normal semantic recall; disable only for debugging or fallback-only operation.", "agents.defaults.memorySearch.store.vector.extensionPath": "Overrides the auto-discovered sqlite-vec extension library path (`.dylib`, `.so`, or `.dll`). Use this when your runtime cannot find sqlite-vec automatically or you pin a known-good build.", - "agents.defaults.memorySearch.chunking.tokens": - "Chunk size in tokens used when splitting memory sources before embedding/indexing. Increase for broader context per chunk, or lower to improve precision on pinpoint lookups.", - "agents.defaults.memorySearch.chunking.overlap": - "Token overlap between adjacent memory chunks to preserve context continuity near split boundaries. Use modest overlap to reduce boundary misses without inflating index size too aggressively.", "agents.defaults.memorySearch.query.maxResults": "Maximum number of memory hits returned from search before downstream reranking and prompt injection. Raise for broader recall, or lower for tighter prompts and faster responses.", "agents.defaults.memorySearch.query.minScore": "Minimum relevance score threshold for including memory results in final recall output. Increase to reduce weak/noisy matches, or lower when you need more permissive retrieval.", "agents.defaults.memorySearch.query.hybrid.enabled": "Combines BM25 keyword matching with vector similarity for better recall on mixed exact + semantic queries. Keep enabled unless you are isolating ranking behavior for troubleshooting.", - "agents.defaults.memorySearch.query.hybrid.vectorWeight": - "Controls how strongly semantic similarity influences hybrid ranking (0-1). Increase when paraphrase matching matters more than exact terms; decrease for stricter keyword emphasis.", - "agents.defaults.memorySearch.query.hybrid.textWeight": - "Controls how strongly BM25 keyword relevance influences hybrid ranking (0-1). Increase for exact-term matching; decrease when semantic matches should rank higher.", - "agents.defaults.memorySearch.query.hybrid.candidateMultiplier": - "Expands the candidate pool before reranking (default: 4). Raise this for better recall on noisy corpora, but expect more compute and slightly slower searches.", "agents.defaults.memorySearch.query.hybrid.mmr.enabled": "Adds MMR reranking to diversify results and reduce near-duplicate snippets in a single answer window. Enable when recall looks repetitive; keep off for strict score ordering.", - "agents.defaults.memorySearch.query.hybrid.mmr.lambda": - "Sets MMR relevance-vs-diversity balance (0 = most diverse, 1 = most relevant, default: 0.7). Lower values reduce repetition; higher values keep tightly relevant but may duplicate.", "agents.defaults.memorySearch.query.hybrid.temporalDecay.enabled": "Applies recency decay so newer memory can outrank older memory when scores are close. Enable when timeliness matters; keep off for timeless reference knowledge.", - "agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays": - "Controls how fast older memory loses rank when temporal decay is enabled (half-life in days, default: 30). Lower values prioritize recent context more aggressively.", "agents.defaults.memorySearch.cache.enabled": "Caches computed chunk embeddings in SQLite so reindexing and incremental updates run faster (default: true). Keep this enabled unless investigating cache correctness or minimizing disk usage.", memory: "Memory backend configuration (global).", @@ -387,16 +355,12 @@ export const MODEL_FIELD_HELP: Record = { "Sets per-query QMD search timeout in milliseconds (default: 4000). Increase for larger indexes or slower environments, and lower to keep request latency bounded.", "memory.qmd.scope": "Defines which sessions/channels are eligible for QMD recall using session.sendPolicy-style rules. Keep default direct-only scope unless you intentionally want cross-chat memory sharing.", - "agents.defaults.memorySearch.cache.maxEntries": - "Sets a best-effort upper bound on cached embeddings kept in SQLite for memory search. Use this when controlling disk growth matters more than peak reindex speed.", "agents.defaults.memorySearch.sync.onSessionStart": "Triggers a memory index sync when a session starts so early turns see fresh memory content. Keep enabled when startup freshness matters more than initial turn latency.", "agents.defaults.memorySearch.sync.onSearch": "Uses lazy sync by scheduling reindex on search after content changes are detected. Keep enabled for lower idle overhead, or disable if you require pre-synced indexes before any query.", "agents.defaults.memorySearch.sync.watch": "Watches memory files and schedules index updates from file-change events (chokidar). Enable for near-real-time freshness; disable on very large workspaces if watch churn is too noisy.", - "agents.defaults.memorySearch.sync.watchDebounceMs": - "Debounce window in milliseconds for coalescing rapid file-watch events before reindex runs. Increase to reduce churn on frequently-written files, or lower for faster freshness.", "agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds": "Overrides the timeout for inline embedding batches during memory indexing. Leave unset to use provider defaults: 600 seconds for local/self-hosted providers such as local, Ollama, and LM Studio, and 120 seconds for hosted providers.", "agents.defaults.memorySearch.sync.sessions.deltaBytes": diff --git a/src/config/schema.help.quality.test-fixtures.ts b/src/config/schema.help.quality.test-fixtures.ts index 7c0b85cbebc8..286a727ac2b2 100644 --- a/src/config/schema.help.quality.test-fixtures.ts +++ b/src/config/schema.help.quality.test-fixtures.ts @@ -104,15 +104,9 @@ export const TARGET_KEYS = [ "agents.defaults.memorySearch.store.vector.enabled", "agents.defaults.memorySearch.store.vector.extensionPath", "agents.defaults.memorySearch.query.hybrid.enabled", - "agents.defaults.memorySearch.query.hybrid.vectorWeight", - "agents.defaults.memorySearch.query.hybrid.textWeight", - "agents.defaults.memorySearch.query.hybrid.candidateMultiplier", "agents.defaults.memorySearch.query.hybrid.mmr.enabled", - "agents.defaults.memorySearch.query.hybrid.mmr.lambda", "agents.defaults.memorySearch.query.hybrid.temporalDecay.enabled", - "agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays", "agents.defaults.memorySearch.cache.enabled", - "agents.defaults.memorySearch.cache.maxEntries", "agents.defaults.memorySearch.sync.onSearch", "agents.defaults.memorySearch.sync.watch", "agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds", @@ -130,11 +124,6 @@ export const TARGET_KEYS = [ "cron", "cron.enabled", "cron.store", - "cron.maxConcurrentRuns", - "cron.retry", - "cron.retry.maxAttempts", - "cron.retry.backoffMs", - "cron.retry.retryOn", "cron.webhookToken", "cron.sessionRetention", "session", @@ -153,7 +142,6 @@ export const TARGET_KEYS = [ "session.resetByType.thread", "session.resetByChannel", "session.store", - "session.typingIntervalSeconds", "session.typingMode", "session.mainKey", "session.sendPolicy", @@ -165,8 +153,6 @@ export const TARGET_KEYS = [ "session.sendPolicy.rules[].match.chatType", "session.sendPolicy.rules[].match.keyPrefix", "session.sendPolicy.rules[].match.rawKeyPrefix", - "session.agentToAgent", - "session.agentToAgent.maxPingPongTurns", "session.threadBindings", "session.threadBindings.enabled", "session.threadBindings.idleHours", @@ -228,7 +214,6 @@ export const TARGET_KEYS = [ "hooks.allowRequestSessionKey", "hooks.allowedSessionKeyPrefixes", "hooks.allowedAgentIds", - "hooks.maxBodyBytes", "hooks.transformsDir", "hooks.mappings", "hooks.mappings[].action", @@ -252,7 +237,6 @@ export const TARGET_KEYS = [ "messages.queue", "messages.queue.mode", "messages.queue.byChannel", - "messages.queue.debounceMs", "messages.queue.debounceMsByChannel", "messages.queue.cap", "messages.queue.drop", @@ -313,17 +297,6 @@ export const TARGET_KEYS = [ "tools.sandbox.tools", "web", "web.enabled", - "web.heartbeatSeconds", - "web.reconnect", - "web.reconnect.initialMs", - "web.reconnect.maxMs", - "web.reconnect.factor", - "web.reconnect.jitter", - "web.reconnect.maxAttempts", - "web.whatsapp", - "web.whatsapp.keepAliveIntervalMs", - "web.whatsapp.connectTimeoutMs", - "web.whatsapp.defaultQueryTimeoutMs", "discovery", "discovery.wideArea.domain", "discovery.wideArea.enabled", @@ -385,7 +358,6 @@ export const TARGET_KEYS = [ "plugins.entries.*.env", "plugins.entries.*.config", "auth", - "auth.cooldowns", "models", "models.providers", "models.providers.*.baseUrl", @@ -404,10 +376,7 @@ export const TARGET_KEYS = [ "agents.defaults.compaction.mode", "agents.defaults.compaction.provider", "agents.defaults.compaction.thinkingLevel", - "agents.defaults.compaction.reserveTokens", "agents.defaults.compaction.keepRecentTokens", - "agents.defaults.compaction.reserveTokensFloor", - "agents.defaults.compaction.maxHistoryShare", "agents.defaults.compaction.identifierPolicy", "agents.defaults.compaction.identifierInstructions", "agents.defaults.compaction.recentTurnsPreserve", @@ -542,14 +511,11 @@ export const TOOLS_HOOKS_TARGET_KEYS = [ ] as const; export const CHANNELS_AGENTS_TARGET_KEYS = [ - "agents.defaults.memorySearch.chunking.overlap", - "agents.defaults.memorySearch.chunking.tokens", "agents.defaults.memorySearch.enabled", "agents.defaults.memorySearch.model", "agents.defaults.memorySearch.query.maxResults", "agents.defaults.memorySearch.query.minScore", "agents.defaults.memorySearch.sync.onSessionStart", - "agents.defaults.memorySearch.sync.watchDebounceMs", "agents.defaults.workspace", "agents.list[].tools.alsoAllow", "agents.list[].tools.byProvider", @@ -561,8 +527,6 @@ export const CHANNELS_AGENTS_TARGET_KEYS = [ export const FINAL_BACKLOG_TARGET_KEYS = [ "browser.evaluateEnabled", - "browser.remoteCdpHandshakeTimeoutMs", - "browser.remoteCdpTimeoutMs", "browser.snapshotDefaults", "browser.snapshotDefaults.mode", "browser.ssrfPolicy", diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 74d04443a246..af5895352e11 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -221,29 +221,6 @@ describe("config help copy quality", () => { expect(/raw|unnormalized/i.test(rawKeyPrefix)).toBe(true); }); - it("documents session write-lock policy defaults", () => { - const acquireTimeout = expectDefined( - FIELD_HELP["session.writeLock.acquireTimeoutMs"], - 'FIELD_HELP["session.writeLock.acquireTimeoutMs"] test invariant', - ); - expect(acquireTimeout.includes("60000")).toBe(true); - expect(/transcript|lock/i.test(acquireTimeout)).toBe(true); - - const stale = expectDefined( - FIELD_HELP["session.writeLock.staleMs"], - 'FIELD_HELP["session.writeLock.staleMs"] test invariant', - ); - expect(stale.includes("1800000")).toBe(true); - expect(stale.includes("OPENCLAW_SESSION_WRITE_LOCK_STALE_MS")).toBe(true); - - const maxHold = expectDefined( - FIELD_HELP["session.writeLock.maxHoldMs"], - 'FIELD_HELP["session.writeLock.maxHoldMs"] test invariant', - ); - expect(maxHold.includes("300000")).toBe(true); - expect(maxHold.includes("OPENCLAW_SESSION_WRITE_LOCK_MAX_HOLD_MS")).toBe(true); - }); - it("documents session maintenance duration/size examples and deprecations", () => { const pruneAfter = expectDefined( FIELD_HELP["session.maintenance.pruneAfter"], @@ -321,20 +298,13 @@ describe("config help copy quality", () => { expect(queueMode.includes('"steer"')).toBe(true); }); - it("documents gateway bind modes and web reconnect semantics", () => { + it("documents gateway bind modes", () => { const bind = expectDefined( FIELD_HELP["gateway.bind"], 'FIELD_HELP["gateway.bind"] test invariant', ); expect(bind.includes('"loopback"')).toBe(true); expect(bind.includes('"tailnet"')).toBe(true); - - const reconnect = expectDefined( - FIELD_HELP["web.reconnect.maxAttempts"], - 'FIELD_HELP["web.reconnect.maxAttempts"] test invariant', - ); - expect(/0 means no retries|no retries/i.test(reconnect)).toBe(true); - expect(/failure sequence|retry/i.test(reconnect)).toBe(true); }); it("documents metadata/admin semantics for logging, wizard, and plugins", () => { @@ -409,12 +379,6 @@ describe("config help copy quality", () => { ); expect(modelsMode.includes("SecretRef-managed")).toBe(true); expect(modelsMode.includes("preserve")).toBe(true); - - const authCooldowns = expectDefined( - FIELD_HELP["auth.cooldowns"], - 'FIELD_HELP["auth.cooldowns"] test invariant', - ); - expect(/cooldown|backoff|retry/i.test(authCooldowns)).toBe(true); }); it("documents agent compaction safeguards and memory flush behavior", () => { @@ -432,12 +396,6 @@ describe("config help copy quality", () => { expect(/session level|inherit/i.test(thinkingLevel)).toBe(true); expect(/Codex app-server|no per-operation thinking override/i.test(thinkingLevel)).toBe(true); - const historyShare = expectDefined( - FIELD_HELP["agents.defaults.compaction.maxHistoryShare"], - 'FIELD_HELP["agents.defaults.compaction.maxHistoryShare"] test invariant', - ); - expect(/0\\.1-0\\.9|fraction|share/i.test(historyShare)).toBe(true); - const identifierPolicy = expectDefined( FIELD_HELP["agents.defaults.compaction.identifierPolicy"], 'FIELD_HELP["agents.defaults.compaction.identifierPolicy"] test invariant', diff --git a/src/config/schema.help.runtime.ts b/src/config/schema.help.runtime.ts index f7f91d44ac4a..8fd881324637 100644 --- a/src/config/schema.help.runtime.ts +++ b/src/config/schema.help.runtime.ts @@ -11,12 +11,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Allows macOS hosts to import cookies from a local Chrome-family system profile into a managed OpenClaw browser profile. Disable this to prevent browser profile cookie import and its macOS Keychain consent prompt.", "browser.cdpUrl": "CDP/DevTools endpoint URL used to attach to an externally managed browser instance. Use this for centralized browser hosts, tunnels, or existing-session attachment, and keep URL access restricted to trusted network paths.", - "browser.actionTimeoutMs": - "Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.", - "browser.localLaunchTimeoutMs": - "Timeout in milliseconds for locally launched managed Chrome to expose its CDP HTTP endpoint after process start. Raise this on slow single-board computers or older hosts.", - "browser.localCdpReadyTimeoutMs": - "Timeout in milliseconds for a locally launched managed browser to finish CDP websocket readiness after the process is discovered. Raise this when Chrome starts but browser start still reports CDP not reachable.", "browser.color": "Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.", "browser.executablePath": @@ -27,8 +21,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Disables Chromium sandbox isolation flags for environments where sandboxing fails at runtime. Keep this off whenever possible because process isolation protections are reduced.", "browser.attachOnly": "Restricts browser mode to attach-only behavior without starting local browser processes. Use this when all browser sessions are externally managed by a remote CDP provider.", - "browser.cdpPortRangeStart": - "Starting local CDP port used for auto-allocated browser profile ports. Increase this when host-level port defaults conflict with other local services.", "browser.defaultProfile": "Default browser profile name selected when callers do not explicitly choose a profile. Use a stable low-privilege profile as the default to reduce accidental cross-context state use.", "browser.profiles": @@ -63,12 +55,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Best-effort cleanup policy for browser tabs opened by primary-agent sessions. Keep enabled to avoid stale sandbox or managed-browser tabs accumulating across long-lived gateways.", "browser.tabCleanup.enabled": "Enables cleanup of idle tracked browser tabs for primary-agent sessions. Disable only when external tooling owns tab lifecycle completely.", - "browser.tabCleanup.idleMinutes": - "Minutes of inactivity before a tracked primary-agent browser tab is eligible for closure. Set 0 to disable idle-time cleanup while keeping the per-session tab cap.", - "browser.tabCleanup.maxTabsPerSession": - "Maximum tracked browser tabs kept per primary-agent session. Oldest inactive tabs are closed first. Set 0 to disable the cap.", - "browser.tabCleanup.sweepMinutes": - "Minutes between browser tab cleanup sweeps. Keep this modest so idle tabs are reclaimed without adding frequent background work.", "browser.ssrfPolicy": "Server-side request forgery guardrail settings for browser/network fetch paths that could reach internal hosts. Keep restrictive defaults in production and open only explicitly approved targets.", "browser.ssrfPolicy.dangerouslyAllowPrivateNetwork": @@ -77,10 +63,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Explicit hostname allowlist exceptions for SSRF policy checks on browser/network requests. Keep this list minimal and review entries regularly to avoid stale broad access.", "browser.ssrfPolicy.hostnameAllowlist": "Legacy/alternate hostname allowlist field used by SSRF policy consumers for explicit host exceptions. Use stable exact hostnames and avoid wildcard-like broad patterns.", - "browser.remoteCdpTimeoutMs": - "Timeout in milliseconds for connecting to a remote CDP endpoint before failing the browser attach attempt. The larger of this value and remoteCdpHandshakeTimeoutMs also bounds persistent remote tab enumeration. Increase for high-latency tunnels, or lower for faster failure detection.", - "browser.remoteCdpHandshakeTimeoutMs": - "Timeout in milliseconds for post-connect CDP handshake readiness checks against remote browser targets. The larger of this value and remoteCdpTimeoutMs also bounds persistent remote tab enumeration. Raise this for slow-start remote browsers and lower to fail fast in automation loops.", "discovery.mdns.mode": 'mDNS broadcast mode ("minimal" default, "full" includes cliPath/sshPort, "off" disables mDNS).', discovery: @@ -186,31 +168,9 @@ export const RUNTIME_FIELD_HELP: Record = { "Tool policy wrapper for sandboxed agent executions so sandbox runs can have distinct capability boundaries. Use this to enforce stronger safety in sandbox contexts.", "tools.sandbox.tools": "Allow/deny tool policy applied when agents run in sandboxed execution environments. Keep policies minimal so sandbox tasks cannot escalate into unnecessary external actions.", - web: "Web channel runtime settings for heartbeat and reconnect behavior when operating web-based chat surfaces. Use reconnect values tuned to your network reliability profile and expected uptime needs.", + web: "Web channel runtime settings for operating web-based chat surfaces. Use this section to control the built-in web channel.", "web.enabled": "Enables the web channel runtime and related websocket lifecycle behavior. Keep disabled when web chat is unused to reduce active connection management overhead.", - "web.heartbeatSeconds": - "Heartbeat interval in seconds for web channel connectivity and liveness maintenance. Use shorter intervals for faster detection, or longer intervals to reduce keepalive chatter.", - "web.reconnect": - "Reconnect backoff policy for web channel reconnect attempts after transport failure. Keep bounded retries and jitter tuned to avoid thundering-herd reconnect behavior.", - "web.reconnect.initialMs": - "Initial reconnect delay in milliseconds before the first retry after disconnection. Use modest delays to recover quickly without immediate retry storms.", - "web.reconnect.maxMs": - "Maximum reconnect backoff cap in milliseconds to bound retry delay growth over repeated failures. Use a reasonable cap so recovery remains timely after prolonged outages.", - "web.reconnect.factor": - "Exponential backoff multiplier used between reconnect attempts in web channel retry loops. Keep factor above 1 and tune with jitter for stable large-fleet reconnect behavior.", - "web.reconnect.jitter": - "Randomization factor (0-1) applied to reconnect delays to desynchronize clients after outage events. Keep non-zero jitter in multi-client deployments to reduce synchronized spikes.", - "web.reconnect.maxAttempts": - "Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.", - "web.whatsapp": - "WhatsApp Web socket timing controls used by Baileys and OpenClaw's local outbound and inbound read-receipt operation bounds. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.", - "web.whatsapp.keepAliveIntervalMs": - "Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.", - "web.whatsapp.connectTimeoutMs": - "Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.", - "web.whatsapp.defaultQueryTimeoutMs": - "Default Baileys query timeout and OpenClaw outbound send/presence and inbound read-receipt operation bound in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows socket operations need longer.", talk: "Talk-mode voice synthesis settings for voice identity, model selection, output format, and interruption behavior. Use this section to tune human-facing voice UX while controlling latency and cost.", "gateway.auth.token": "Required by default for gateway access (unless using Tailscale Serve identity); required for non-loopback binds.", @@ -270,12 +230,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Timeout in milliseconds for relay send requests from the gateway to the APNs relay (default: 10000). Increase for slower relays or networks, or lower to fail wake attempts faster.", "gateway.http.endpoints.chatCompletions.enabled": "Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).", - "gateway.http.endpoints.chatCompletions.maxBodyBytes": - "Max request body size in bytes for `/v1/chat/completions` (default: 20MB).", - "gateway.http.endpoints.chatCompletions.maxImageParts": - "Max number of `image_url` parts accepted from the latest user message (default: 8).", - "gateway.http.endpoints.chatCompletions.maxTotalImageBytes": - "Max cumulative decoded bytes across all `image_url` parts in one request (default: 20MB).", "gateway.http.endpoints.chatCompletions.images": "Image fetch/validation controls for OpenAI-compatible `image_url` parts.", "gateway.http.endpoints.chatCompletions.images.allowUrl": @@ -292,9 +246,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Timeout in milliseconds for `image_url` URL fetches (default: 10000).", "gateway.reload.mode": 'Controls how config edits are applied: "off" ignores live edits, "restart" always restarts, "hot" applies in-process, and "hybrid" tries hot then restarts if required. Keep "hybrid" for safest routine updates.', - "gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.", - "gateway.reload.deferralTimeoutMs": - "Optional maximum time (ms) to wait for in-flight operations before forcing a restart. Omit to use the default bounded wait; set 0 to wait indefinitely with periodic still-pending warnings. Lower positive values risk aborting active subagent LLM calls.", "gateway.nodes.browser.mode": 'Node browser routing ("auto" = pick single connected browser node, "manual" = require node param, "off" = disable).', "gateway.nodes.browser.node": "Pin browser routing to a specific node id or name (optional).", @@ -383,10 +334,6 @@ export const RUNTIME_FIELD_HELP: Record = { 'Enable targeted diagnostics logs by flag (e.g. ["telegram.http"]). Supports wildcards like "telegram.*" or "*".', "diagnostics.enabled": "Master toggle for diagnostics instrumentation output in logs and telemetry wiring paths. Defaults to enabled; set false only in tightly constrained environments.", - "diagnostics.stuckSessionWarnMs": - "No-progress age threshold in milliseconds for classifying long processing sessions as long-running, stalled, or stuck. Reply, tool, status, block, and ACP progress reset the timer; repeated stuck diagnostics back off while unchanged.", - "diagnostics.stuckSessionAbortMs": - "No-progress age threshold in milliseconds before eligible stalled active work may be abort-drained for recovery. Defaults to the safer extended embedded-run recovery window.", "diagnostics.otel.enabled": "Enables OpenTelemetry export pipeline for traces, metrics, and logs based on configured endpoint/protocol settings. Keep disabled unless your collector endpoint and auth are fully configured.", "diagnostics.otel.endpoint": @@ -447,22 +394,6 @@ export const RUNTIME_FIELD_HELP: Record = { 'Optional allowlist of model ids (e.g. "gpt-5.4" or "openai/gpt-5.4").', "tools.loopDetection.enabled": "Enable repetitive tool-call loop detection and backoff safety checks (default: false).", - "tools.loopDetection.historySize": "Tool history window size for loop detection (default: 30).", - "tools.loopDetection.warningThreshold": - "Warning threshold for repetitive patterns when detector is enabled (default: 10).", - "tools.loopDetection.unknownToolThreshold": - "Block repeated calls to the same unavailable tool after this many misses (default: 10).", - "tools.loopDetection.criticalThreshold": - "Critical threshold for repetitive patterns when detector is enabled (default: 20).", - "tools.loopDetection.globalCircuitBreakerThreshold": - "Global no-progress breaker threshold (default: 30).", - "tools.loopDetection.detectors.genericRepeat": - "Enable generic repeated same-tool/same-params loop detection (default: true).", - "tools.loopDetection.detectors.knownPollNoProgress": - "Enable known poll tool no-progress loop detection (default: true).", - "tools.loopDetection.detectors.pingPong": "Enable ping-pong loop detection (default: true).", - "tools.loopDetection.postCompactionGuard.windowSize": - "Number of post-compaction attempts during which the guard stays armed (default: 3). Lower values are stricter; higher values give the agent more attempts before abort.", "tools.exec.notifyOnExit": "When true (default), backgrounded exec sessions on exit and node exec lifecycle events enqueue a system event and request a heartbeat.", "tools.exec.notifyOnExitEmptySuccess": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 5f5cdc51faa2..d5a2b1a24365 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -55,17 +55,11 @@ export const FIELD_LABELS: Record = { "update.channel": "Update Channel", "update.checkOnStart": "Update Check on Start", "update.auto.enabled": "Auto Update Enabled", - "update.auto.stableDelayHours": "Auto Update Stable Delay (hours)", - "update.auto.stableJitterHours": "Auto Update Stable Jitter (hours)", - "update.auto.betaCheckIntervalHours": "Auto Update Beta Check Interval (hours)", commitments: "Commitments", "commitments.enabled": "Commitments Enabled", "commitments.maxPerDay": "Commitments per Day", "diagnostics.enabled": "Diagnostics Enabled", "diagnostics.flags": "Diagnostics Flags", - "diagnostics.stuckSessionWarnMs": "Session Liveness Threshold (ms)", - "diagnostics.stuckSessionAbortMs": "Session Abort Threshold (ms)", - "diagnostics.memoryPressureSnapshot": "Memory Pressure Snapshot Enabled", "diagnostics.otel.enabled": "OpenTelemetry Enabled", "diagnostics.otel.endpoint": "OpenTelemetry Endpoint", "diagnostics.otel.tracesEndpoint": "OpenTelemetry Traces Endpoint", @@ -166,10 +160,6 @@ export const FIELD_LABELS: Record = { "gateway.tools": "Gateway Tool Exposure Policy", "gateway.tools.allow": "Gateway Tool Allowlist", "gateway.tools.deny": "Gateway Tool Denylist", - "gateway.handshakeTimeoutMs": "Gateway Handshake Timeout", - "gateway.channelHealthCheckMinutes": "Gateway Channel Health Check Interval (min)", - "gateway.channelStaleEventThresholdMinutes": "Gateway Channel Stale Event Threshold (min)", - "gateway.channelMaxRestartsPerHour": "Gateway Channel Max Restarts Per Hour", "gateway.tailscale": "Gateway Tailscale", "gateway.tailscale.mode": "Gateway Tailscale Mode", "gateway.tailscale.resetOnExit": "Gateway Tailscale Reset on Exit", @@ -201,15 +191,11 @@ export const FIELD_LABELS: Record = { "browser.enabled": "Browser Enabled", "browser.allowSystemProfileImport": "Allow System Profile Import", "browser.cdpUrl": "Browser CDP URL", - "browser.actionTimeoutMs": "Browser Action Timeout (ms)", - "browser.localLaunchTimeoutMs": "Browser Local Launch Timeout (ms)", - "browser.localCdpReadyTimeoutMs": "Browser Local CDP Ready Timeout (ms)", "browser.color": "Browser Accent Color", "browser.executablePath": "Browser Executable Path", "browser.headless": "Browser Headless Mode", "browser.noSandbox": "Browser No-Sandbox Mode", "browser.attachOnly": "Browser Attach-only Mode", - "browser.cdpPortRangeStart": "Browser CDP Port Range Start", "browser.defaultProfile": "Browser Default Profile", "browser.profiles": "Browser Profiles", "browser.profiles.*.cdpPort": "Browser Profile CDP Port", @@ -268,15 +254,6 @@ export const FIELD_LABELS: Record = { "tools.exec.applyPatch.workspaceOnly": "apply_patch Workspace-Only", "tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist", "tools.loopDetection.enabled": "Tool-loop Detection", - "tools.loopDetection.historySize": "Tool-loop History Size", - "tools.loopDetection.warningThreshold": "Tool-loop Warning Threshold", - "tools.loopDetection.unknownToolThreshold": "Unknown-tool Loop Threshold", - "tools.loopDetection.criticalThreshold": "Tool-loop Critical Threshold", - "tools.loopDetection.globalCircuitBreakerThreshold": "Tool-loop Global Circuit Breaker Threshold", - "tools.loopDetection.postCompactionGuard.windowSize": "Post-compaction Loop Guard Window Size", - "tools.loopDetection.detectors.genericRepeat": "Tool-loop Generic Repeat Detection", - "tools.loopDetection.detectors.knownPollNoProgress": "Tool-loop Poll No-Progress Detection", - "tools.loopDetection.detectors.pingPong": "Tool-loop Ping-Pong Detection", "tools.fs.workspaceOnly": "Workspace-only FS tools", "tools.sessions.visibility": "Session Tools Visibility", "tools.exec.notifyOnExit": "Exec Notify On Exit", @@ -412,10 +389,6 @@ export const FIELD_LABELS: Record = { "gateway.push.apns.relay.baseUrl": "Gateway APNs Relay Base URL", "gateway.push.apns.relay.timeoutMs": "Gateway APNs Relay Timeout (ms)", "gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint", - "gateway.http.endpoints.chatCompletions.maxBodyBytes": "OpenAI Chat Completions Max Body Bytes", - "gateway.http.endpoints.chatCompletions.maxImageParts": "OpenAI Chat Completions Max Image Parts", - "gateway.http.endpoints.chatCompletions.maxTotalImageBytes": - "OpenAI Chat Completions Max Total Image Bytes", "gateway.http.endpoints.chatCompletions.images": "OpenAI Chat Completions Image Limits", "gateway.http.endpoints.chatCompletions.images.allowUrl": "OpenAI Chat Completions Allow Image URLs", @@ -430,8 +403,6 @@ export const FIELD_LABELS: Record = { "gateway.http.endpoints.chatCompletions.images.timeoutMs": "OpenAI Chat Completions Image Timeout (ms)", "gateway.reload.mode": "Config Reload Mode", - "gateway.reload.debounceMs": "Config Reload Debounce (ms)", - "gateway.reload.deferralTimeoutMs": "Restart Deferral Timeout (ms)", "gateway.nodes.browser.mode": "Gateway Node Browser Mode", "gateway.nodes.browser.node": "Gateway Node Browser Pin", "gateway.nodes.pairing": "Gateway Node Pairing", @@ -542,12 +513,9 @@ export const FIELD_LABELS: Record = { "agents.defaults.memorySearch.local.contextSize": "Local Embedding Context Size", "agents.defaults.memorySearch.store.vector.enabled": "Memory Search Vector Index", "agents.defaults.memorySearch.store.vector.extensionPath": "Memory Search Vector Extension Path", - "agents.defaults.memorySearch.chunking.tokens": "Memory Chunk Tokens", - "agents.defaults.memorySearch.chunking.overlap": "Memory Chunk Overlap Tokens", "agents.defaults.memorySearch.sync.onSessionStart": "Index on Session Start", "agents.defaults.memorySearch.sync.onSearch": "Index on Search (Lazy)", "agents.defaults.memorySearch.sync.watch": "Watch Memory Files", - "agents.defaults.memorySearch.sync.watchDebounceMs": "Memory Watch Debounce (ms)", "agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds": "Embedding Batch Timeout (s)", "agents.defaults.memorySearch.sync.sessions.deltaBytes": "Session Delta Bytes", "agents.defaults.memorySearch.sync.sessions.deltaMessages": "Session Delta Messages", @@ -556,17 +524,9 @@ export const FIELD_LABELS: Record = { "agents.defaults.memorySearch.query.maxResults": "Memory Search Max Results", "agents.defaults.memorySearch.query.minScore": "Memory Search Min Score", "agents.defaults.memorySearch.query.hybrid.enabled": "Memory Search Hybrid", - "agents.defaults.memorySearch.query.hybrid.vectorWeight": "Memory Search Vector Weight", - "agents.defaults.memorySearch.query.hybrid.textWeight": "Memory Search Text Weight", - "agents.defaults.memorySearch.query.hybrid.candidateMultiplier": - "Memory Search Hybrid Candidate Multiplier", "agents.defaults.memorySearch.query.hybrid.mmr.enabled": "Memory Search MMR Re-ranking", - "agents.defaults.memorySearch.query.hybrid.mmr.lambda": "Memory Search MMR Lambda", "agents.defaults.memorySearch.query.hybrid.temporalDecay.enabled": "Memory Search Temporal Decay", - "agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays": - "Memory Search Temporal Decay Half-life (Days)", "agents.defaults.memorySearch.cache.enabled": "Memory Search Embedding Cache", - "agents.defaults.memorySearch.cache.maxEntries": "Memory Search Embedding Cache Max Entries", memory: "Memory", "memory.backend": "Memory Backend", "memory.citations": "Memory Citations Mode", @@ -604,7 +564,6 @@ export const FIELD_LABELS: Record = { auth: "Auth", "auth.profiles": "Auth Profiles", "auth.order": "Auth Profile Order", - "auth.cooldowns": "Auth Cooldowns", acp: "ACP", "acp.enabled": "ACP Enabled", "acp.dispatch.enabled": "ACP Dispatch Enabled", @@ -612,17 +571,10 @@ export const FIELD_LABELS: Record = { "acp.fallbacks": "ACP Fallback Backends", "acp.defaultAgent": "ACP Default Agent", "acp.allowedAgents": "ACP Allowed Agents", - "acp.maxConcurrentSessions": "ACP Max Concurrent Sessions", "acp.stream": "ACP Stream", - "acp.stream.coalesceIdleMs": "ACP Stream Coalesce Idle (ms)", - "acp.stream.maxChunkChars": "ACP Stream Max Chunk Chars", "acp.stream.repeatSuppression": "ACP Stream Repeat Suppression", "acp.stream.deliveryMode": "ACP Stream Delivery Mode", - "acp.stream.hiddenBoundarySeparator": "ACP Stream Hidden Boundary Separator", - "acp.stream.maxOutputChars": "ACP Stream Max Output Chars", - "acp.stream.maxSessionUpdateChars": "ACP Stream Max Session Update Chars", "acp.stream.tagVisibility": "ACP Stream Tag Visibility", - "acp.runtime.ttlMinutes": "ACP Runtime TTL (minutes)", "acp.runtime.installCommand": "ACP Runtime Install Command", models: "Models", "models.mode": "Model Catalog Mode", @@ -695,15 +647,6 @@ export const FIELD_LABELS: Record = { "models.providers.*.models[].mediaInput.image.maxSidePx": "Model Image Max Side", "models.providers.*.models[].mediaInput.image.preferredSidePx": "Model Image Preferred Side", "models.providers.*.models[].mediaInput.image.tokenMode": "Model Image Token Mode", - "auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)", - "auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides", - "auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)", - "auth.cooldowns.authPermanentBackoffMinutes": "Auth-Permanent Backoff (minutes)", - "auth.cooldowns.authPermanentMaxMinutes": "Auth-Permanent Backoff Cap (minutes)", - "auth.cooldowns.failureWindowHours": "Failover Window (hours)", - "auth.cooldowns.overloadedProfileRotations": "Overloaded Profile Rotations", - "auth.cooldowns.overloadedBackoffMs": "Overloaded Backoff (ms)", - "auth.cooldowns.rateLimitedProfileRotations": "Rate-Limited Profile Rotations", "agents.defaults.models": "Models", "agents.defaults.modelPolicy": "Model Policy", "agents.defaults.modelPolicy.allow": "Allowed Models", @@ -741,10 +684,7 @@ export const FIELD_LABELS: Record = { "agents.defaults.compaction.mode": "Compaction Mode", "agents.defaults.compaction.provider": "Compaction Provider", "agents.defaults.compaction.thinkingLevel": "Compaction Thinking Level", - "agents.defaults.compaction.reserveTokens": "Compaction Reserve Tokens", "agents.defaults.compaction.keepRecentTokens": "Compaction Keep Recent Tokens", - "agents.defaults.compaction.reserveTokensFloor": "Compaction Reserve Token Floor", - "agents.defaults.compaction.maxHistoryShare": "Compaction Max History Share", "agents.defaults.compaction.identifierPolicy": "Compaction Identifier Policy", "agents.defaults.compaction.identifierInstructions": "Compaction Identifier Instructions", "agents.defaults.compaction.recentTurnsPreserve": "Compaction Preserve Recent Turns", @@ -770,16 +710,6 @@ export const FIELD_LABELS: Record = { "Compaction Memory Flush Transcript Size Threshold", "agents.defaults.compaction.memoryFlush.prompt": "Compaction Memory Flush Prompt", "agents.defaults.compaction.memoryFlush.systemPrompt": "Compaction Memory Flush System Prompt", - "agents.defaults.runRetries": "Run Retries", - "agents.defaults.runRetries.base": "Run Retries Base", - "agents.defaults.runRetries.perProfile": "Run Retries Per Profile", - "agents.defaults.runRetries.min": "Run Retries Minimum", - "agents.defaults.runRetries.max": "Run Retries Maximum", - "agents.list[].runRetries": "Agent Run Retries", - "agents.list[].runRetries.base": "Agent Run Retries Base", - "agents.list[].runRetries.perProfile": "Agent Run Retries Per Profile", - "agents.list[].runRetries.min": "Agent Run Retries Minimum", - "agents.list[].runRetries.max": "Agent Run Retries Maximum", "agents.defaults.embeddedAgent": "Embedded OpenClaw", "agents.defaults.embeddedAgent.projectSettingsPolicy": "Embedded OpenClaw Project Settings Policy", @@ -843,7 +773,6 @@ export const FIELD_LABELS: Record = { "mcp.servers.*.codex.agents": "Codex MCP Agents", "mcp.servers.*.codex.defaultToolsApprovalMode": "Codex MCP Tool Approval", "mcp.servers.*.codex.default_tools_approval_mode": "Codex MCP Tool Approval", - "mcp.sessionIdleTtlMs": "MCP Runtime Idle TTL", ui: "UI", "ui.seamColor": "Accent Color", "ui.assistant": "Assistant Appearance", @@ -856,15 +785,10 @@ export const FIELD_LABELS: Record = { "browser.snapshotDefaults.mode": "Browser Snapshot Mode", "browser.tabCleanup": "Browser Tab Cleanup", "browser.tabCleanup.enabled": "Browser Tab Cleanup Enabled", - "browser.tabCleanup.idleMinutes": "Browser Tab Cleanup Idle Minutes", - "browser.tabCleanup.maxTabsPerSession": "Browser Tab Cleanup Max Tabs Per Session", - "browser.tabCleanup.sweepMinutes": "Browser Tab Cleanup Sweep Minutes", "browser.ssrfPolicy": "Browser SSRF Policy", "browser.ssrfPolicy.dangerouslyAllowPrivateNetwork": "Browser Dangerously Allow Private Network", "browser.ssrfPolicy.allowedHostnames": "Browser Allowed Hostnames", "browser.ssrfPolicy.hostnameAllowlist": "Browser Hostname Allowlist", - "browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)", - "browser.remoteCdpHandshakeTimeoutMs": "Remote CDP Handshake Timeout (ms)", session: "Session", "session.scope": "Session Scope", "session.dmScope": "DM Session Scope", @@ -881,7 +805,6 @@ export const FIELD_LABELS: Record = { "session.resetByType.thread": "Session Reset (Thread)", "session.resetByChannel": "Session Reset by Channel", "session.store": "Session Store Path", - "session.typingIntervalSeconds": "Session Typing Interval (seconds)", "session.typingMode": "Session Typing Mode", "session.mainKey": "Session Main Key", "session.sendPolicy": "Session Send Policy", @@ -893,12 +816,6 @@ export const FIELD_LABELS: Record = { "session.sendPolicy.rules[].match.chatType": "Session Send Rule Chat Type", "session.sendPolicy.rules[].match.keyPrefix": "Session Send Rule Key Prefix", "session.sendPolicy.rules[].match.rawKeyPrefix": "Session Send Rule Raw Key Prefix", - "session.writeLock": "Session Write Lock", - "session.writeLock.acquireTimeoutMs": "Session Write Lock Acquire Timeout", - "session.writeLock.staleMs": "Session Write Lock Stale Timeout", - "session.writeLock.maxHoldMs": "Session Write Lock Max Hold", - "session.agentToAgent": "Session Agent-to-Agent", - "session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns", "session.threadBindings": "Session Thread Bindings", "session.threadBindings.enabled": "Thread Binding Enabled", "session.threadBindings.idleHours": "Thread Binding Idle Timeout (hours)", @@ -915,20 +832,10 @@ export const FIELD_LABELS: Record = { cron: "Cron", "cron.enabled": "Cron Enabled", "cron.store": "Cron Store Path", - "cron.maxConcurrentRuns": "Cron Max Concurrent Runs", - "cron.retry": "Cron Retry Policy", - "cron.retry.maxAttempts": "Cron Retry Max Attempts", - "cron.retry.backoffMs": "Cron Retry Backoff (ms)", - "cron.retry.retryOn": "Cron Retry Error Types", "cron.webhookToken": "Cron Webhook Bearer Token", "cron.sessionRetention": "Cron Session Retention", - worktrees: "Worktrees", - "worktrees.cleanup": "Worktree Cleanup Limits", - "worktrees.cleanup.maxCount": "Max Worktrees", - "worktrees.cleanup.maxTotalSizeGb": "Max Total Worktree Size (GB)", transcripts: "Transcripts", "transcripts.enabled": "Transcripts Enabled", - "transcripts.maxUtterances": "Transcripts Max Utterances", "transcripts.autoStart": "Transcripts Auto-start Sources", "transcripts.autoStart[].providerId": "Transcript Source Provider ID", "transcripts.autoStart[].sessionId": "Transcript Session ID", @@ -945,7 +852,6 @@ export const FIELD_LABELS: Record = { "hooks.allowRequestSessionKey": "Hooks Allow Request Session Key", "hooks.allowedSessionKeyPrefixes": "Hooks Allowed Session Key Prefixes", "hooks.allowedAgentIds": "Hooks Allowed Agent IDs", - "hooks.maxBodyBytes": "Hooks Max Body Bytes", "hooks.presets": "Hooks Presets", "hooks.transformsDir": "Hooks Transforms Directory", "hooks.mappings": "Hook Mappings", @@ -999,17 +905,6 @@ export const FIELD_LABELS: Record = { "hooks.internal.installs": "Internal Hook Install Records", web: "Web Channel", "web.enabled": "Web Channel Enabled", - "web.heartbeatSeconds": "Web Channel Heartbeat Interval (sec)", - "web.reconnect": "Web Channel Reconnect Policy", - "web.reconnect.initialMs": "Web Reconnect Initial Delay (ms)", - "web.reconnect.maxMs": "Web Reconnect Max Delay (ms)", - "web.reconnect.factor": "Web Reconnect Backoff Factor", - "web.reconnect.jitter": "Web Reconnect Jitter", - "web.reconnect.maxAttempts": "Web Reconnect Max Attempts", - "web.whatsapp": "WhatsApp Web Socket Timing", - "web.whatsapp.keepAliveIntervalMs": "WhatsApp Web Keepalive Interval (ms)", - "web.whatsapp.connectTimeoutMs": "WhatsApp Web Connect Timeout (ms)", - "web.whatsapp.defaultQueryTimeoutMs": "WhatsApp Web Query/Operation Timeout (ms)", discovery: "Discovery", "discovery.wideArea": "Wide-area Discovery", "discovery.wideArea.enabled": "Wide-area Discovery Enabled", @@ -1034,7 +929,6 @@ export const FIELD_LABELS: Record = { "messages.queue": "Inbound Queue", "messages.queue.mode": "Queue Mode", "messages.queue.byChannel": "Queue Mode by Channel", - "messages.queue.debounceMs": "Queue Fallback Debounce (ms)", "messages.queue.debounceMsByChannel": "Queue Fallback Debounce by Channel (ms)", "messages.queue.cap": "Queue Capacity", "messages.queue.drop": "Queue Drop Strategy", @@ -1046,7 +940,6 @@ export const FIELD_LABELS: Record = { "messages.statusReactions": "Status Reactions", "messages.statusReactions.enabled": "Enable Status Reactions", "messages.statusReactions.emojis": "Status Reaction Emojis", - "messages.statusReactions.timing": "Status Reaction Timing", "messages.inbound.debounceMs": "Inbound Message Debounce (ms)", "messages.inbound.byChannel": "Inbound Debounce by Channel (ms)", "messages.tts": "Message Text-to-Speech", diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index eb1c73e8b1f6..82e96407e4ef 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -950,24 +950,6 @@ describe("config schema", () => { expect(parsed?.web?.fetch?.maxResponseBytes).toBe(2_000_000); }); - it("accepts WhatsApp Web Baileys socket timing in the runtime zod schema", () => { - const parsed = OpenClawSchema.parse({ - web: { - whatsapp: { - keepAliveIntervalMs: 15_000, - connectTimeoutMs: 60_000, - defaultQueryTimeoutMs: 90_000, - }, - }, - }); - - expect(parsed.web?.whatsapp).toEqual({ - keepAliveIntervalMs: 15_000, - connectTimeoutMs: 60_000, - defaultQueryTimeoutMs: 90_000, - }); - }); - it("accepts web fetch ssrfPolicy in the runtime zod schema", () => { const parsed = ToolsSchema.parse({ web: { @@ -1081,7 +1063,7 @@ describe("config schema", () => { it("includes reload metadata when a resolver is provided", () => { const lookup = lookupConfigSchema(baseSchema, "gateway", (path) => { - if (path === "gateway.channelHealthCheckMinutes") { + if (path === "gateway.auth.mode") { return { kind: "hot" }; } if (path.startsWith("gateway")) { @@ -1091,13 +1073,12 @@ describe("config schema", () => { }); expect(lookup?.reloadKind).toBe("restart"); - expect( - lookup?.children.find((child) => child.path === "gateway.handshakeTimeoutMs")?.reloadKind, - ).toBe("restart"); - expect( - lookup?.children.find((child) => child.path === "gateway.channelHealthCheckMinutes") - ?.reloadKind, - ).toBe("hot"); + expect(lookup?.children.find((child) => child.path === "gateway.port")?.reloadKind).toBe( + "restart", + ); + expect(lookup?.children.find((child) => child.path === "gateway.auth")?.reloadKind).toBe( + "restart", + ); }); it("returns a shallow lookup schema without nested composition keywords", () => { diff --git a/src/config/sessions/transcript-append-redact.test.ts b/src/config/sessions/transcript-append-redact.test.ts index fd3bf9b6cf49..758c3a86df67 100644 --- a/src/config/sessions/transcript-append-redact.test.ts +++ b/src/config/sessions/transcript-append-redact.test.ts @@ -250,11 +250,7 @@ describe("appendSessionTranscriptMessage - redaction", () => { content: [{ type: "text", text: "email peter@dc.io and key sk-abcdef1234567890xyz ok" }], }, config: { - session: { - writeLock: { - acquireTimeoutMs: 25_000, - }, - }, + session: {}, }, }); diff --git a/src/config/types.acp.ts b/src/config/types.acp.ts index 37263efd2df0..7a362c0674f5 100644 --- a/src/config/types.acp.ts +++ b/src/config/types.acp.ts @@ -7,20 +7,10 @@ export type AcpDispatchConfig = { }; export type AcpStreamConfig = { - /** Coalescer idle flush window in milliseconds for ACP streamed text. */ - coalesceIdleMs?: number; - /** Maximum text size per streamed chunk. */ - maxChunkChars?: number; /** Suppresses repeated ACP status/tool projection lines within a turn. */ repeatSuppression?: boolean; /** Live streams chunks or waits for terminal event before delivery. */ deliveryMode?: "live" | "final_only"; - /** Separator inserted before visible text when hidden tool events occurred. */ - hiddenBoundarySeparator?: "none" | "space" | "newline" | "paragraph"; - /** Maximum assistant output characters forwarded per turn. */ - maxOutputChars?: number; - /** Maximum visible characters for projected session/update lines. */ - maxSessionUpdateChars?: number; /** * Per-sessionUpdate visibility overrides. * Keys not listed here fall back to OpenClaw defaults. @@ -29,8 +19,6 @@ export type AcpStreamConfig = { }; export type AcpRuntimeConfig = { - /** Idle runtime TTL in minutes for ACP session workers. */ - ttlMinutes?: number; /** Optional operator install/setup command shown by `/acp install` and `/acp doctor`. */ installCommand?: string; }; @@ -45,7 +33,6 @@ export type AcpConfig = { fallbacks?: string[]; defaultAgent?: string; allowedAgents?: string[]; - maxConcurrentSessions?: number; stream?: AcpStreamConfig; runtime?: AcpRuntimeConfig; }; diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 1f759f9e4f8e..9416a4c85569 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -74,28 +74,12 @@ export type AgentContextPruningConfig = { mode?: "off" | "cache-ttl"; /** TTL to consider cache expired (duration string, default unit: minutes). */ ttl?: string; - /** Number of most recent assistant turns preserved from pruning. */ - keepLastAssistants?: number; - /** Context pressure ratio where soft trimming starts. */ - softTrimRatio?: number; - /** Context pressure ratio where hard clearing starts. */ - hardClearRatio?: number; - /** Minimum tool-result size before pruning considers it worthwhile. */ - minPrunableToolChars?: number; tools?: { /** Tool names eligible for context pruning. */ allow?: string[]; /** Tool names excluded from context pruning. */ deny?: string[]; }; - softTrim?: { - /** Maximum retained characters for softly trimmed tool results. */ - maxChars?: number; - /** Leading characters retained during soft trim. */ - headChars?: number; - /** Trailing characters retained during soft trim. */ - tailChars?: number; - }; hardClear?: { /** Replace oversized old tool results with a placeholder at high pressure. */ enabled?: boolean; @@ -130,17 +114,6 @@ export type AgentContextLimitsConfig = { postCompactionMaxChars?: number; }; -export type AgentRunRetriesConfig = { - /** Base number of run retry iterations (default: 24). */ - base?: number; - /** Additional run retry iterations per fallback profile (default: 8). */ - perProfile?: number; - /** Minimum limit for run retry iterations (default: 32). */ - min?: number; - /** Maximum limit for run retry iterations (default: 160). */ - max?: number; -}; - export type CliBackendConfig = { /** CLI command to execute (absolute path or on PATH). */ command: string; @@ -202,19 +175,10 @@ export type CliBackendConfig = { reseedFromRawTranscriptWhenUncompacted?: boolean; /** Runtime reliability tuning for this backend's process lifecycle. */ reliability?: { - /** Live-session output caps for CLIs that stream JSONL through a long-lived process. */ - outputLimits?: { - /** Max raw JSONL characters retained for one live CLI turn. */ - maxTurnRawChars?: number; - /** Max raw JSONL lines retained for one live CLI turn. */ - maxTurnLines?: number; - }; /** No-output watchdog tuning (fresh vs resumed runs). */ watchdog?: { /** Fresh/new sessions (non-resume). */ fresh?: { - /** Fixed watchdog timeout in ms (overrides ratio when set). */ - noOutputTimeoutMs?: number; /** Fraction of overall timeout used when fixed timeout is not set. */ noOutputTimeoutRatio?: number; /** Lower bound for computed watchdog timeout. */ @@ -224,8 +188,6 @@ export type CliBackendConfig = { }; /** Resume sessions. */ resume?: { - /** Fixed watchdog timeout in ms (overrides ratio when set). */ - noOutputTimeoutMs?: number; /** Fraction of overall timeout used when fixed timeout is not set. */ noOutputTimeoutRatio?: number; /** Lower bound for computed watchdog timeout. */ @@ -351,8 +313,6 @@ export type AgentDefaultsConfig = { contextPruning?: AgentContextPruningConfig; /** Compaction tuning and pre-compaction memory flush behavior. */ compaction?: AgentCompactionConfig; - /** Outer run loop retry iteration boundaries. */ - runRetries?: AgentRunRetriesConfig; /** Embedded OpenClaw runner hardening and compatibility controls. */ embeddedAgent?: { /** @@ -532,14 +492,8 @@ export type AgentCompactionConfig = { mode?: AgentCompactionMode; /** Override the session thinking level for embedded OpenClaw compaction summaries. */ thinkingLevel?: AgentThinkingLevel; - /** Embedded OpenClaw reserve target before floor and context-window caps. */ - reserveTokens?: number; /** Embedded OpenClaw keepRecentTokens budget used for cut-point selection. */ keepRecentTokens?: number; - /** Minimum reserve tokens enforced for embedded OpenClaw compaction (0 disables the floor). */ - reserveTokensFloor?: number; - /** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */ - maxHistoryShare?: number; /** Additional compaction-summary instructions that can preserve language or persona continuity. */ customInstructions?: string; /** Preserve this many most-recent user/assistant turns verbatim in compaction summary context. */ diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index 7d1ee38fdc5e..cf8cc47fff3d 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -147,8 +147,6 @@ export type AgentConfig = { /** Require explicit agentId in sessions_spawn (no default same-as-caller). */ requireAgentId?: boolean; }; - /** Optional outer run loop retry boundaries. */ - runRetries?: AgentDefaultsConfig["runRetries"]; /** Optional per-agent embedded OpenClaw overrides. */ embeddedAgent?: { /** Optional per-agent execution contract override. */ diff --git a/src/config/types.auth.ts b/src/config/types.auth.ts index ec365337efa1..12ad51c3c8b4 100644 --- a/src/config/types.auth.ts +++ b/src/config/types.auth.ts @@ -21,42 +21,4 @@ export type AuthConfig = { profiles?: Record; /** Preferred profile order per provider id. */ order?: Record; - /** Backoff and same-provider rotation policy for auth/profile failures. */ - cooldowns?: { - /** Default billing backoff (hours). Default: 5. */ - billingBackoffHours?: number; - /** Optional per-provider billing backoff (hours). */ - billingBackoffHoursByProvider?: Record; - /** Billing backoff cap (hours). Default: 24. */ - billingMaxHours?: number; - /** - * Base backoff for high-confidence permanent-auth failures (minutes). - * Default: 10. - */ - authPermanentBackoffMinutes?: number; - /** - * Cap for high-confidence permanent-auth backoff (minutes). Default: 60. - */ - authPermanentMaxMinutes?: number; - /** - * Failure window for backoff counters (hours). If no failures occur within - * this window, counters reset. Default: 24. - */ - failureWindowHours?: number; - /** - * Maximum same-provider auth-profile rotations to allow for overloaded - * errors before escalating to cross-provider model fallback. Default: 1. - */ - overloadedProfileRotations?: number; - /** - * Fixed delay before retrying an overloaded provider/profile rotation. - * Default: 0. - */ - overloadedBackoffMs?: number; - /** - * Maximum same-provider auth-profile rotations to allow for rate-limit - * errors before escalating to cross-provider model fallback. Default: 1. - */ - rateLimitedProfileRotations?: number; - }; }; diff --git a/src/config/types.base.ts b/src/config/types.base.ts index 502980bcbd5a..f3a108e7cec6 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -226,31 +226,15 @@ export type SessionConfig = { /** Channel-specific reset overrides (e.g. { discord: { mode: "idle", idleMinutes: 10080 } }). */ resetByChannel?: Record; store?: string; - typingIntervalSeconds?: number; typingMode?: TypingMode; mainKey?: string; sendPolicy?: SessionSendPolicyConfig; - /** Session transcript write-lock acquisition policy. */ - writeLock?: SessionWriteLockConfig; - agentToAgent?: { - /** Max ping-pong turns between requester/target (0-20). Default: 5. */ - maxPingPongTurns?: number; - }; /** Shared defaults for thread-bound session routing across channels/providers. */ threadBindings?: SessionThreadBindingsConfig; /** Automatic session store maintenance (pruning, capping, archive retention, disk budget). */ maintenance?: SessionMaintenanceConfig; }; -export type SessionWriteLockConfig = { - /** How long to wait while acquiring a session transcript write lock. Default: 60000. */ - acquireTimeoutMs?: number; - /** When an existing lock can be treated as stale and reclaimed. Default: 1800000. */ - staleMs?: number; - /** Maximum in-process hold time before the watchdog releases the lock. Default: 300000. */ - maxHoldMs?: number; -}; - export type SessionMaintenanceMode = "enforce" | "warn"; /** Session-store cleanup policy for transcript count, age, archives, and disk budget. */ @@ -362,39 +346,13 @@ export type DiagnosticsConfig = { enabled?: boolean; /** Optional ad-hoc diagnostics flags (e.g. "telegram.http"). */ flags?: string[]; - /** Threshold in ms before a processing session with no observed progress logs diagnostics. */ - stuckSessionWarnMs?: number; - /** Threshold in ms before eligible stalled active work may be aborted for recovery. */ - stuckSessionAbortMs?: number; - /** Capture a redacted stability snapshot when memory pressure reaches critical. Default: false. */ - memoryPressureSnapshot?: boolean; otel?: DiagnosticsOtelConfig; cacheTrace?: DiagnosticsCacheTraceConfig; }; -export type WebReconnectConfig = { - initialMs?: number; - maxMs?: number; - factor?: number; - jitter?: number; - maxAttempts?: number; // 0 = unlimited -}; - -export type WebWhatsAppConfig = { - /** Baileys application ping interval in milliseconds. Default: 25000. */ - keepAliveIntervalMs?: number; - /** WebSocket opening handshake timeout in milliseconds. Default: 60000. */ - connectTimeoutMs?: number; - /** Baileys query and WhatsApp outbound/read-receipt operation timeout in milliseconds. Default: 60000. */ - defaultQueryTimeoutMs?: number; -}; - export type WebConfig = { /** If false, do not start the WhatsApp web provider. Default: true. */ enabled?: boolean; - heartbeatSeconds?: number; - reconnect?: WebReconnectConfig; - whatsapp?: WebWhatsAppConfig; }; // Provider docking: allowlists keyed by provider id (and internal "webchat"). diff --git a/src/config/types.browser.ts b/src/config/types.browser.ts index 506dc459aa4b..19fed151e35a 100644 --- a/src/config/types.browser.ts +++ b/src/config/types.browser.ts @@ -31,12 +31,6 @@ export type BrowserSnapshotDefaults = { export type BrowserTabCleanupConfig = { /** Enable best-effort cleanup for tracked primary-agent browser tabs. Default: true */ enabled?: boolean; - /** Close tracked tabs after this many idle minutes. Set 0 to disable idle cleanup. Default: 120 */ - idleMinutes?: number; - /** Keep at most this many tracked tabs per primary session. Set 0 to disable the cap. Default: 8 */ - maxTabsPerSession?: number; - /** Cleanup sweep interval in minutes. Default: 5 */ - sweepMinutes?: number; }; export type BrowserSsrFPolicyConfig = { /** If true, permit browser navigation to private/internal networks. Default: false */ @@ -60,16 +54,6 @@ export type BrowserConfig = { evaluateEnabled?: boolean; /** Base URL of the CDP endpoint (for remote browsers). Default: loopback CDP on the derived port. */ cdpUrl?: string; - /** Remote CDP HTTP timeout and persistent tab-enumeration budget input (ms). Default: 1500. */ - remoteCdpTimeoutMs?: number; - /** Remote CDP handshake timeout and tab-enumeration budget input (ms). Default: max(remoteCdpTimeoutMs * 2, 2000). */ - remoteCdpHandshakeTimeoutMs?: number; - /** Local managed browser launch discovery timeout (ms). Default: 15000. */ - localLaunchTimeoutMs?: number; - /** Local managed browser post-launch CDP readiness timeout (ms). Default: 8000. */ - localCdpReadyTimeoutMs?: number; - /** Default browser act timeout (ms). Default: 60000. */ - actionTimeoutMs?: number; /** Accent color for the openclaw browser profile (hex). Default: #FF4500 */ color?: string; /** Override the browser executable path (all platforms). */ @@ -80,8 +64,6 @@ export type BrowserConfig = { noSandbox?: boolean; /** If true: never launch; only attach to an existing browser. Default: false */ attachOnly?: boolean; - /** Starting local CDP port for auto-assigned browser profiles. Default derives from gateway port. */ - cdpPortRangeStart?: number; /** Default profile to use when profile param is omitted. Default: "chrome" */ defaultProfile?: string; /** Named browser profiles with explicit CDP ports or URLs. */ diff --git a/src/config/types.cron.ts b/src/config/types.cron.ts index aa35f5f7d848..1b51b726f042 100644 --- a/src/config/types.cron.ts +++ b/src/config/types.cron.ts @@ -1,18 +1,6 @@ // Defines cron scheduling configuration types. import type { SecretInput } from "./types.secrets.js"; -/** Error types that can trigger retries for one-shot jobs. */ -export type CronRetryOn = "rate_limit" | "overloaded" | "network" | "timeout" | "server_error"; - -export type CronRetryConfig = { - /** Max retries for transient errors before permanent disable (default: 3). */ - maxAttempts?: number; - /** Backoff delays in ms for each retry attempt (default: [30000, 60000, 300000]). */ - backoffMs?: number[]; - /** Error types to retry; omit to retry all transient types. */ - retryOn?: CronRetryOn[]; -}; - export type CronFailureAlertConfig = { enabled?: boolean; after?: number; @@ -32,13 +20,9 @@ export type CronFailureDestinationConfig = { export type CronConfig = { enabled?: boolean; store?: string; - maxConcurrentRuns?: number; triggers?: { enabled?: boolean; - minIntervalMs?: number; }; - /** Override default retry policy for one-shot jobs on transient errors. */ - retry?: CronRetryConfig; /** Bearer token for cron webhook POST delivery. */ webhookToken?: SecretInput; /** diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 22d1d5f9a546..fd76e83799db 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -1,7 +1,6 @@ import type { ChannelPreviewStreamingConfig, ChannelStreamingProgressConfig, - OutboundRetryConfig, } from "./types.base.js"; import type { ChannelBotInteractionConfig, @@ -306,12 +305,6 @@ export type DiscordAccountConfig = Omit< activities?: { clientSecret?: string; applicationId?: string }; /** HTTP(S) proxy URL for Discord gateway WebSocket connections. */ proxy?: string; - /** Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default: 30000. */ - gatewayInfoTimeoutMs?: number; - /** Startup wait for the gateway READY event before restarting the socket. Default: 15000. */ - gatewayReadyTimeoutMs?: number; - /** Runtime reconnect wait for the gateway READY event before force-stopping the lifecycle. Default: 30000. */ - gatewayRuntimeReadyTimeoutMs?: number; /** * Deterministic outbound @handle rewrites for known Discord users. * Keys are handles without the leading @; values are Discord user IDs. @@ -328,8 +321,6 @@ export type DiscordAccountConfig = Omit< * keeps replies readable in-channel. Default: 17. */ maxLinesPerMessage?: number; - /** Retry policy for outbound Discord API calls. */ - retry?: OutboundRetryConfig; /** Per-action tool gating (default: true for all). */ actions?: DiscordActionConfig; /** Thread session behavior. */ @@ -378,19 +369,6 @@ export type DiscordAccountConfig = Omit< */ runTimeoutMs?: number; }; - /** - * Discord EventQueue configuration. Controls how Discord gateway events are processed. - * `listenerTimeout` only covers gateway listener work such as normalization and enqueue. - * It does not control the lifetime of queued inbound agent turns. - */ - eventQueue?: { - /** Max time (ms) a single listener can run before being killed. Default: 120000. */ - listenerTimeout?: number; - /** Max events queued before backpressure is applied. Default: 10000. */ - maxQueueSize?: number; - /** Max concurrent event processing operations. Default: 50. */ - maxConcurrency?: number; - }; }; export type DiscordConfig = { diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index 22901017ee60..1b7a0213e7b3 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -315,16 +315,6 @@ export type GatewayReloadMode = "off" | "restart" | "hot" | "hybrid"; export type GatewayReloadConfig = { /** Reload strategy for config changes (default: hybrid). */ mode?: GatewayReloadMode; - /** Debounce window for config reloads (ms). Default: 300. */ - debounceMs?: number; - /** - * Optional maximum time (ms) to wait for in-flight operations to complete - * before forcing a restart. Absent uses the gateway's default bounded wait; - * 0 waits indefinitely and logs periodic still-pending warnings. - * Lower positive values risk aborting active subagent LLM calls. - * @see https://github.com/openclaw/openclaw/issues/65485 - */ - deferralTimeoutMs?: number; }; export type GatewayHttpChatCompletionsConfig = { @@ -333,21 +323,6 @@ export type GatewayHttpChatCompletionsConfig = { * Default: false when absent. */ enabled?: boolean; - /** - * Max request body size in bytes for `/v1/chat/completions`. - * Default: 20MB. - */ - maxBodyBytes?: number; - /** - * Max number of `image_url` parts processed from the latest user message. - * Default: 8. - */ - maxImageParts?: number; - /** - * Max cumulative decoded image bytes for all `image_url` parts in one request. - * Default: 20MB. - */ - maxTotalImageBytes?: number; /** Image input controls for `image_url` parts. */ images?: GatewayHttpChatCompletionsImagesConfig; }; @@ -376,11 +351,6 @@ export type GatewayHttpResponsesConfig = { * Default: false when absent. */ enabled?: boolean; - /** - * Max request body size in bytes for `/v1/responses`. - * Default: 20MB. - */ - maxBodyBytes?: number; /** * Max number of URL-based `input_file` + `input_image` parts per request. * Default: 8. @@ -588,27 +558,4 @@ export type GatewayConfig = { allowRealIpFallback?: boolean; /** Tool access restrictions for HTTP /tools/invoke endpoint. */ tools?: GatewayToolsConfig; - /** - * Pre-auth Gateway WebSocket handshake timeout in milliseconds. - * Env var OPENCLAW_HANDSHAKE_TIMEOUT_MS takes precedence. Default: 15000. - */ - handshakeTimeoutMs?: number; - /** - * Channel health monitor interval in minutes. - * Periodically checks channel health and restarts unhealthy channels. - * Set to 0 to disable. Default: 5. - */ - channelHealthCheckMinutes?: number; - /** - * Stale transport-activity threshold in minutes for the channel health monitor. - * A connected channel that reports no provider-proven transport activity for - * this duration is treated as a stale socket and restarted. Default: 30. - */ - channelStaleEventThresholdMinutes?: number; - /** - * Maximum number of health-monitor-initiated channel restarts per hour. - * Once this limit is reached, the monitor skips further restarts until - * the rolling window expires. Default: 10. - */ - channelMaxRestartsPerHour?: number; }; diff --git a/src/config/types.hooks.ts b/src/config/types.hooks.ts index e7e90af20da9..57c7d345e5a0 100644 --- a/src/config/types.hooks.ts +++ b/src/config/types.hooks.ts @@ -116,7 +116,6 @@ export type HooksConfig = { * allow any agent. Set `[]` to deny all agent routing. */ allowedAgentIds?: string[]; - maxBodyBytes?: number; presets?: string[]; transformsDir?: string; mappings?: HookMappingConfig[]; diff --git a/src/config/types.mcp.ts b/src/config/types.mcp.ts index a48160e1ba36..3f90250aa271 100644 --- a/src/config/types.mcp.ts +++ b/src/config/types.mcp.ts @@ -91,5 +91,4 @@ export type McpConfig = { * * Defaults to 10 minutes. Set to 0 to disable idle eviction. */ - sessionIdleTtlMs?: number; }; diff --git a/src/config/types.messages.ts b/src/config/types.messages.ts index e0fcca2ee994..961b3ed62f9e 100644 --- a/src/config/types.messages.ts +++ b/src/config/types.messages.ts @@ -35,7 +35,6 @@ export type DmConfig = { export type QueueConfig = { mode?: QueueMode; byChannel?: QueueModeByProvider; - debounceMs?: number; /** Per-channel debounce overrides (ms). */ debounceMsByChannel?: InboundDebounceByProvider; cap?: number; @@ -78,26 +77,11 @@ export type StatusReactionsEmojiConfig = { compacting?: string; }; -export type StatusReactionsTimingConfig = { - /** Debounce interval for intermediate states (ms). Default: 700. */ - debounceMs?: number; - /** Soft stall warning timeout (ms). Default: 10000. */ - stallSoftMs?: number; - /** Hard stall warning timeout (ms). Default: 30000. */ - stallHardMs?: number; - /** How long to hold done emoji before cleanup (ms). Default: 1500. */ - doneHoldMs?: number; - /** How long to hold error emoji before cleanup (ms). Default: 2500. */ - errorHoldMs?: number; -}; - export type StatusReactionsConfig = { /** Enable lifecycle status reactions (default: false). */ enabled?: boolean; /** Override default emojis. */ emojis?: StatusReactionsEmojiConfig; - /** Override default timing. */ - timing?: StatusReactionsTimingConfig; }; export type MessagesConfig = { diff --git a/src/config/types.openclaw.ts b/src/config/types.openclaw.ts index e99b6e63f14a..ec6a38230f03 100644 --- a/src/config/types.openclaw.ts +++ b/src/config/types.openclaw.ts @@ -81,16 +81,6 @@ export type SecurityConfig = { }; }; -export type WorktreesConfig = { - /** Retention limits enforced by hourly managed-worktree cleanup. */ - cleanup?: { - /** Max managed worktrees to retain across all repositories; oldest evictable ones are snapshotted and removed first. 0 or unset disables the count limit. */ - maxCount?: number; - /** Max total size in GB across all managed worktrees. 0 or unset disables the size limit. */ - maxTotalSizeGb?: number; - }; -}; - export type SurfaceConfigEntry = { /** Surface-specific silent reply policy for channels or UI integrations. */ silentReply?: SilentReplyPolicyShape; @@ -174,12 +164,6 @@ export type OpenClawConfig = { auto?: { /** Enable background auto-update checks and apply logic. Default: false. */ enabled?: boolean; - /** Stable channel minimum delay before auto-apply. Default: 6. */ - stableDelayHours?: number; - /** Additional stable-channel jitter window. Default: 12. */ - stableJitterHours?: number; - /** Beta channel check cadence. Default: 1 hour. */ - betaCheckIntervalHours?: number; }; }; /** Browser automation and browser plugin integration settings. */ @@ -263,8 +247,6 @@ export type OpenClawConfig = { channels?: ChannelsConfig; /** Cron schedule and retention settings. */ cron?: CronConfig; - /** Managed worktree retention settings. */ - worktrees?: WorktreesConfig; /** Transcript persistence and export settings. */ transcripts?: TranscriptsConfig; /** Commitment/reminder extraction settings. */ diff --git a/src/config/types.secrets.ts b/src/config/types.secrets.ts index e5989ce0ce7d..d1232d286f46 100644 --- a/src/config/types.secrets.ts +++ b/src/config/types.secrets.ts @@ -353,9 +353,4 @@ export type SecretsConfig = { file?: string; exec?: string; }; - resolution?: { - maxProviderConcurrency?: number; - maxRefsPerProvider?: number; - maxBatchBytes?: number; - }; }; diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 00fc4a269626..e197c0fecae0 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -4,7 +4,6 @@ import type { ChannelStreamingPreviewConfig, DmPolicy, GroupPolicy, - OutboundRetryConfig, SessionThreadBindingsConfig, } from "./types.base.js"; import type { @@ -102,14 +101,6 @@ export type TelegramAccountConfig = CommonChannelMessagingConfig< * Default: false. */ richMessages?: boolean; - /** Telegram API client timeout in seconds (grammY ApiClientOptions). */ - timeoutSeconds?: number; - /** Buffer window for Telegram media groups/albums before dispatching them as one inbound message. Default: 500ms. */ - mediaGroupFlushMs?: number; - /** Telegram polling watchdog threshold in milliseconds. Default: 120000. */ - pollingStallThresholdMs?: number; - /** Retry policy for outbound Telegram API calls. */ - retry?: OutboundRetryConfig; /** Network transport overrides for Telegram. */ network?: TelegramNetworkConfig; proxy?: string; @@ -145,8 +136,6 @@ export type TelegramAccountConfig = CommonChannelMessagingConfig< silentErrorReplies?: boolean; /** Controls outbound error reporting: always, once per cooldown window, or silent. */ errorPolicy?: "always" | "once" | "silent"; - /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ - errorCooldownMs?: number; /** * Per-channel outbound response prefix override. * @@ -186,8 +175,6 @@ export type TelegramTopicConfig = { agentId?: string; /** Controls outbound error reporting for this topic. */ errorPolicy?: "always" | "once" | "silent"; - /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ - errorCooldownMs?: number; }; export type TelegramGroupConfig = { @@ -213,8 +200,6 @@ export type TelegramGroupConfig = { disableAudioPreflight?: boolean; /** Controls outbound error reporting for this group. */ errorPolicy?: "always" | "once" | "silent"; - /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ - errorCooldownMs?: number; }; /** Config for LLM-based auto-topic labeling. */ @@ -246,8 +231,6 @@ export type TelegramDirectConfig = { systemPrompt?: string; /** Controls outbound error reporting for this DM. */ errorPolicy?: "always" | "once" | "silent"; - /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ - errorCooldownMs?: number; /** Auto-rename DM forum topics on first message using LLM. Default: true. */ autoTopicLabel?: AutoTopicLabelConfig; }; diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index 87ada31bf5da..8b1b8dcdb67d 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -153,37 +153,9 @@ export type MediaToolsConfig = { export type ToolProfileId = "minimal" | "coding" | "messaging" | "full"; -export type ToolLoopDetectionDetectorConfig = { - /** Enable warning/blocking for repeated identical calls to the same tool/params. */ - genericRepeat?: boolean; - /** Enable warning/blocking for known no-progress polling loops. */ - knownPollNoProgress?: boolean; - /** Enable warning/blocking for no-progress ping-pong alternating patterns. */ - pingPong?: boolean; -}; - -export type ToolLoopPostCompactionGuardConfig = { - /** How many attempts post-compaction the guard remains armed (default: 3). */ - windowSize?: number; -}; - export type ToolLoopDetectionConfig = { /** Enable tool-loop protection (default: false). */ enabled?: boolean; - /** Maximum tool call history entries retained for loop detection (default: 30). */ - historySize?: number; - /** Warning threshold before a warning-only loop classification (default: 10). */ - warningThreshold?: number; - /** Block repeated calls to the same unavailable tool after this many misses (default: 10). */ - unknownToolThreshold?: number; - /** Critical threshold for blocking repetitive loops (default: 20). */ - criticalThreshold?: number; - /** Global no-progress breaker threshold (default: 30). */ - globalCircuitBreakerThreshold?: number; - /** Detector toggles. */ - detectors?: ToolLoopDetectionDetectorConfig; - /** Post-compaction loop guard: aborts when the agent repeats the same (tool, args, result) immediately after auto-compaction-retry. */ - postCompactionGuard?: ToolLoopPostCompactionGuardConfig; }; export type ToolSearchConfig = @@ -536,18 +508,11 @@ export type MemorySearchConfig = { maxEntries?: number; }; }; - /** Chunking configuration. */ - chunking?: { - tokens?: number; - overlap?: number; - }; /** Sync behavior. */ sync?: { onSessionStart?: boolean; onSearch?: boolean; watch?: boolean; - watchDebounceMs?: number; - intervalMinutes?: number; /** * Timeout in seconds for inline embedding batches during memory indexing. * Unset uses provider defaults: 600s for local/self-hosted providers, 120s for hosted providers. @@ -569,25 +534,15 @@ export type MemorySearchConfig = { hybrid?: { /** Enable hybrid BM25 + vector search (default: true). */ enabled?: boolean; - /** Weight for vector similarity when merging results (0-1). */ - vectorWeight?: number; - /** Weight for BM25 text relevance when merging results (0-1). */ - textWeight?: number; - /** Multiplier for candidate pool size (default: 4). */ - candidateMultiplier?: number; /** Optional MMR re-ranking for result diversity. */ mmr?: { /** Enable MMR re-ranking (default: false). */ enabled?: boolean; - /** Lambda: 0 = max diversity, 1 = max relevance (default: 0.7). */ - lambda?: number; }; /** Optional temporal decay to boost recency in hybrid scoring. */ temporalDecay?: { /** Enable temporal decay (default: false). */ enabled?: boolean; - /** Half-life in days for exponential decay (default: 30). */ - halfLifeDays?: number; }; }; }; @@ -595,8 +550,6 @@ export type MemorySearchConfig = { cache?: { /** Cache chunk embeddings in SQLite (default: true). */ enabled?: boolean; - /** Optional cap on cached embeddings (best-effort). */ - maxEntries?: number; }; }; diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 6206c3caf016..49dcad445147 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -342,38 +342,6 @@ describe("agent defaults schema", () => { expect(result.embeddedAgent?.projectSettingsPolicy).toBe("sanitize"); }); - it("accepts runRetries configuration on defaults and agent entries", () => { - const result = AgentDefaultsSchema.parse({ - runRetries: { - base: 24, - max: 160, - }, - }); - expect(result?.runRetries?.base).toBe(24); - expect(result?.runRetries?.max).toBe(160); - - const agentResult = AgentEntrySchema.parse({ - id: "test", - runRetries: { - min: 10, - max: 50, - }, - }); - expect(agentResult?.runRetries?.min).toBe(10); - expect(agentResult?.runRetries?.max).toBe(50); - }); - - it("rejects runRetries with max < min", () => { - expectSchemaFailurePath( - AgentDefaultsSchema.safeParse({ runRetries: { min: 100, max: 50 } }), - "runRetries.max", - ); - expectSchemaFailurePath( - AgentEntrySchema.safeParse({ id: "test", runRetries: { min: 100, max: 50 } }), - "runRetries.max", - ); - }); - it("accepts compaction.truncateAfterCompaction", () => { const result = AgentDefaultsSchema.parse({ compaction: { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index fdd69765c7ce..55c898926dfc 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -10,7 +10,6 @@ import { AgentModelSchema, AgentToolModelSchema, MemorySearchSchema, - AgentRunRetriesConfigSchema, } from "./zod-schema.agent-runtime.js"; import { BlockStreamingChunkSchema, @@ -141,10 +140,6 @@ export const AgentDefaultsSchema = z .object({ mode: z.union([z.literal("off"), z.literal("cache-ttl")]).optional(), ttl: z.string().optional(), - keepLastAssistants: z.number().int().nonnegative().optional(), - softTrimRatio: z.number().min(0).max(1).optional(), - hardClearRatio: z.number().min(0).max(1).optional(), - minPrunableToolChars: z.number().int().nonnegative().optional(), tools: z .object({ allow: z.array(z.string()).optional(), @@ -152,14 +147,6 @@ export const AgentDefaultsSchema = z }) .strict() .optional(), - softTrim: z - .object({ - maxChars: z.number().int().nonnegative().optional(), - headChars: z.number().int().nonnegative().optional(), - tailChars: z.number().int().nonnegative().optional(), - }) - .strict() - .optional(), hardClear: z .object({ enabled: z.boolean().optional(), @@ -175,10 +162,7 @@ export const AgentDefaultsSchema = z mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(), provider: z.string().optional(), thinkingLevel: AgentThinkingLevelSchema.optional(), - reserveTokens: z.number().int().nonnegative().optional(), keepRecentTokens: z.number().int().positive().optional(), - reserveTokensFloor: z.number().int().nonnegative().optional(), - maxHistoryShare: z.number().min(0.1).max(0.9).optional(), customInstructions: z.string().optional(), identifierPolicy: z .union([z.literal("strict"), z.literal("off"), z.literal("custom")]) @@ -219,7 +203,6 @@ export const AgentDefaultsSchema = z }) .strict() .optional(), - runRetries: AgentRunRetriesConfigSchema.optional(), embeddedAgent: EmbeddedAgentConfigSchema.optional(), thinkingDefault: AgentThinkingLevelSchema.optional(), verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 140a88ceca48..0123f1f2dc55 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -56,24 +56,6 @@ function validateSandboxBindEntries( } } -export const AgentRunRetriesConfigSchema = z - .object({ - base: z.number().int().positive().optional(), - perProfile: z.number().int().nonnegative().optional(), - min: z.number().int().positive().optional(), - max: z.number().int().positive().optional(), - }) - .strict() - .refine( - (data) => { - if (data.min !== undefined && data.max !== undefined) { - return data.max >= data.min; - } - return true; - }, - { message: "max must be greater than or equal to min", path: ["max"] }, - ); - const AgentEntryEmbeddedAgentConfigSchema = z .object({ executionContract: z.union([z.literal("default"), z.literal("strict-agentic")]).optional(), @@ -615,59 +597,11 @@ const ToolFsSchema = z .strict() .optional(); -const ToolLoopDetectionDetectorSchema = z - .object({ - genericRepeat: z.boolean().optional(), - knownPollNoProgress: z.boolean().optional(), - pingPong: z.boolean().optional(), - }) - .strict() - .optional(); - -const ToolLoopPostCompactionGuardSchema = z - .object({ - windowSize: z.number().int().positive().optional(), - }) - .strict() - .optional(); - const ToolLoopDetectionSchema = z .object({ enabled: z.boolean().optional(), - historySize: z.number().int().positive().optional(), - warningThreshold: z.number().int().positive().optional(), - unknownToolThreshold: z.number().int().positive().optional(), - criticalThreshold: z.number().int().positive().optional(), - globalCircuitBreakerThreshold: z.number().int().positive().optional(), - detectors: ToolLoopDetectionDetectorSchema, - postCompactionGuard: ToolLoopPostCompactionGuardSchema, }) .strict() - .superRefine((value, ctx) => { - if ( - value.warningThreshold !== undefined && - value.criticalThreshold !== undefined && - value.warningThreshold >= value.criticalThreshold - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["criticalThreshold"], - message: "tools.loopDetection.warningThreshold must be lower than criticalThreshold.", - }); - } - if ( - value.criticalThreshold !== undefined && - value.globalCircuitBreakerThreshold !== undefined && - value.criticalThreshold >= value.globalCircuitBreakerThreshold - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["globalCircuitBreakerThreshold"], - message: - "tools.loopDetection.criticalThreshold must be lower than globalCircuitBreakerThreshold.", - }); - } - }) .optional(); const ToolSearchSchema = z @@ -937,20 +871,11 @@ export const MemorySearchSchema = z }) .strict() .optional(), - chunking: z - .object({ - tokens: z.number().int().positive().optional(), - overlap: z.number().int().nonnegative().optional(), - }) - .strict() - .optional(), sync: z .object({ onSessionStart: z.boolean().optional(), onSearch: z.boolean().optional(), watch: z.boolean().optional(), - watchDebounceMs: z.number().int().nonnegative().optional(), - intervalMinutes: z.number().int().nonnegative().optional(), embeddingBatchTimeoutSeconds: z.number().int().positive().optional(), sessions: z .object({ @@ -970,20 +895,15 @@ export const MemorySearchSchema = z hybrid: z .object({ enabled: z.boolean().optional(), - vectorWeight: z.number().min(0).max(1).optional(), - textWeight: z.number().min(0).max(1).optional(), - candidateMultiplier: z.number().int().positive().optional(), mmr: z .object({ enabled: z.boolean().optional(), - lambda: z.number().min(0).max(1).optional(), }) .strict() .optional(), temporalDecay: z .object({ enabled: z.boolean().optional(), - halfLifeDays: z.number().int().positive().optional(), }) .strict() .optional(), @@ -996,7 +916,6 @@ export const MemorySearchSchema = z cache: z .object({ enabled: z.boolean().optional(), - maxEntries: z.number().int().positive().optional(), }) .strict() .optional(), @@ -1103,7 +1022,6 @@ export const AgentEntrySchema = z }) .strict() .optional(), - runRetries: AgentRunRetriesConfigSchema.optional(), embeddedAgent: AgentEntryEmbeddedAgentConfigSchema.optional(), sandbox: AgentSandboxSchema, params: z.record(z.string(), z.unknown()).optional(), diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 0ed90a22a374..36364f628eb5 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -189,19 +189,6 @@ export const SecretsConfigSchema = z }) .strict() .optional(), - resolution: z - .object({ - maxProviderConcurrency: z.number().int().positive().max(16).optional(), - maxRefsPerProvider: z.number().int().positive().max(4096).optional(), - maxBatchBytes: z - .number() - .int() - .positive() - .max(5 * 1024 * 1024) - .optional(), - }) - .strict() - .optional(), }) .strict() .optional(); @@ -794,7 +781,6 @@ export const HumanDelaySchema = z const CliBackendWatchdogModeSchema = z .object({ - noOutputTimeoutMs: z.number().int().min(1000).optional(), noOutputTimeoutRatio: z.number().min(0.05).max(0.95).optional(), minMs: z.number().int().min(1000).optional(), maxMs: z.number().int().min(1000).optional(), @@ -802,19 +788,6 @@ const CliBackendWatchdogModeSchema = z .strict() .optional(); -const CliBackendOutputLimitsSchema = z - .object({ - maxTurnRawChars: z - .number() - .int() - .min(1024) - .max(64 * 1024 * 1024) - .optional(), - maxTurnLines: z.number().int().min(100).max(100_000).optional(), - }) - .strict() - .optional(); - export const CliBackendSchema = z .object({ command: z.string(), @@ -854,7 +827,6 @@ export const CliBackendSchema = z reseedFromRawTranscriptWhenUncompacted: z.boolean().optional(), reliability: z .object({ - outputLimits: CliBackendOutputLimitsSchema, watchdog: z .object({ fresh: CliBackendWatchdogModeSchema, @@ -943,16 +915,6 @@ export const requireAllowlistAllowFrom = (params: { export const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]); -export const RetryConfigSchema = z - .object({ - attempts: z.number().int().min(1).optional(), - minDelayMs: z.number().int().min(0).optional(), - maxDelayMs: z.number().int().min(0).optional(), - jitter: z.number().min(0).max(1).optional(), - }) - .strict() - .optional(); - const QueueModeBySurfaceSchema = z .object({ whatsapp: QueueModeSchema.optional(), @@ -977,7 +939,6 @@ export const QueueSchema = z .object({ mode: QueueModeSchema.optional(), byChannel: QueueModeBySurfaceSchema, - debounceMs: z.number().int().nonnegative().optional(), debounceMsByChannel: DebounceMsBySurfaceSchema, cap: z.number().int().positive().optional(), drop: QueueDropSchema.optional(), diff --git a/src/config/zod-schema.cron-triggers.test.ts b/src/config/zod-schema.cron-triggers.test.ts index 3fd5df98b651..161ed2d28ef3 100644 --- a/src/config/zod-schema.cron-triggers.test.ts +++ b/src/config/zod-schema.cron-triggers.test.ts @@ -2,17 +2,13 @@ import { describe, expect, it } from "vitest"; import { OpenClawSchema } from "./zod-schema.js"; describe("OpenClawSchema cron triggers", () => { - it("accepts the strict trigger gate and interval floor", () => { - expect( - OpenClawSchema.parse({ cron: { triggers: { enabled: true, minIntervalMs: 45_000 } } }).cron - ?.triggers, - ).toEqual({ enabled: true, minIntervalMs: 45_000 }); + it("accepts the strict trigger gate", () => { + expect(OpenClawSchema.parse({ cron: { triggers: { enabled: true } } }).cron?.triggers).toEqual({ + enabled: true, + }); }); it("rejects invalid and unknown trigger settings", () => { - expect(OpenClawSchema.safeParse({ cron: { triggers: { minIntervalMs: 0 } } }).success).toBe( - false, - ); expect( OpenClawSchema.safeParse({ cron: { triggers: { enabled: true, extra: true } } }).success, ).toBe(false); diff --git a/src/config/zod-schema.gateway.ts b/src/config/zod-schema.gateway.ts index 0f91a7e12997..f370598825b2 100644 --- a/src/config/zod-schema.gateway.ts +++ b/src/config/zod-schema.gateway.ts @@ -107,10 +107,6 @@ export const GatewayConfigSchema = z allow: z.array(z.string()).optional(), }) .optional(), - handshakeTimeoutMs: z.number().int().min(1).optional(), - channelHealthCheckMinutes: z.number().int().min(0).optional(), - channelStaleEventThresholdMinutes: z.number().int().min(1).optional(), - channelMaxRestartsPerHour: z.number().int().min(1).optional(), tailscale: z .strictObject({ mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(), @@ -125,8 +121,6 @@ export const GatewayConfigSchema = z mode: z .union([z.literal("off"), z.literal("restart"), z.literal("hot"), z.literal("hybrid")]) .optional(), - debounceMs: z.number().int().min(0).optional(), - deferralTimeoutMs: z.number().int().min(0).optional(), }) .optional(), tls: z @@ -155,9 +149,6 @@ export const GatewayConfigSchema = z chatCompletions: z .strictObject({ enabled: z.boolean().optional(), - maxBodyBytes: z.number().int().positive().optional(), - maxImageParts: z.number().int().nonnegative().optional(), - maxTotalImageBytes: z.number().int().positive().optional(), images: z .strictObject({ ...ResponsesEndpointUrlFetchShape, @@ -168,7 +159,6 @@ export const GatewayConfigSchema = z responses: z .strictObject({ enabled: z.boolean().optional(), - maxBodyBytes: z.number().int().positive().optional(), maxUrlParts: z.number().int().nonnegative().optional(), files: z .strictObject({ @@ -252,19 +242,4 @@ export const GatewayConfigSchema = z }) .optional(), }) - .superRefine((gateway, ctx) => { - const effectiveHealthCheckMinutes = gateway.channelHealthCheckMinutes ?? 5; - if ( - gateway.channelStaleEventThresholdMinutes != null && - effectiveHealthCheckMinutes !== 0 && - gateway.channelStaleEventThresholdMinutes < effectiveHealthCheckMinutes - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["channelStaleEventThresholdMinutes"], - message: - "channelStaleEventThresholdMinutes should be >= channelHealthCheckMinutes to avoid delayed stale detection", - }); - } - }) .optional(); diff --git a/src/config/zod-schema.post-compaction-guard.test.ts b/src/config/zod-schema.post-compaction-guard.test.ts deleted file mode 100644 index f0f5c7f853c2..000000000000 --- a/src/config/zod-schema.post-compaction-guard.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Guards post-compaction config schema behavior against regressions. -import { describe, expect, it } from "vitest"; -import { ToolsSchema } from "./zod-schema.agent-runtime.js"; -import { OpenClawSchema } from "./zod-schema.js"; - -describe("OpenClawSchema tools.loopDetection.postCompactionGuard validation", () => { - it("accepts tools.loopDetection.postCompactionGuard configuration", () => { - const result = OpenClawSchema.safeParse({ - tools: { - loopDetection: { - enabled: true, - postCompactionGuard: { - windowSize: 5, - }, - }, - }, - }); - expect(result.success).toBe(true); - }); - - it("accepts an empty postCompactionGuard object", () => { - const result = OpenClawSchema.safeParse({ - tools: { - loopDetection: { - postCompactionGuard: {}, - }, - }, - }); - expect(result.success).toBe(true); - }); - - it("rejects unknown keys under tools.loopDetection.postCompactionGuard", () => { - const result = OpenClawSchema.safeParse({ - tools: { - loopDetection: { - postCompactionGuard: { - windowSize: 3, - bogus: "key", - }, - }, - }, - }); - expect(result.success).toBe(false); - }); - - it("rejects non-positive windowSize", () => { - const result = OpenClawSchema.safeParse({ - tools: { - loopDetection: { - postCompactionGuard: { - windowSize: 0, - }, - }, - }, - }); - expect(result.success).toBe(false); - }); - - it("rejects non-integer windowSize", () => { - const result = OpenClawSchema.safeParse({ - tools: { - loopDetection: { - postCompactionGuard: { - windowSize: 2.5, - }, - }, - }, - }); - expect(result.success).toBe(false); - }); - - it("validates via ToolsSchema directly", () => { - const result = ToolsSchema.safeParse({ - loopDetection: { - postCompactionGuard: { windowSize: 4 }, - }, - }); - expect(result.success).toBe(true); - }); -}); diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 5aafc7b383f9..f1d895c0af2a 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -32,7 +32,6 @@ import { ProviderCommandsSchema, SecretInputSchema, ReplyToModeSchema, - RetryConfigSchema, TextChunkModeSchema, TtsConfigSchema, requireAllowlistAllowFrom, @@ -135,7 +134,6 @@ const TelegramTopicSchema = z systemPrompt: z.string().optional(), agentId: z.string().optional(), errorPolicy: TelegramErrorPolicySchema, - errorCooldownMs: z.number().int().nonnegative().optional(), }) .strict(); @@ -145,7 +143,6 @@ const TelegramGroupSchema = buildGroupEntrySchema({ groupPolicy: GroupPolicySchema.optional(), topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(), errorPolicy: TelegramErrorPolicySchema, - errorCooldownMs: z.number().int().nonnegative().optional(), }); const AutoTopicLabelSchema = z @@ -171,7 +168,6 @@ const TelegramDirectSchema = z systemPrompt: z.string().optional(), topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(), errorPolicy: TelegramErrorPolicySchema, - errorCooldownMs: z.number().int().nonnegative().optional(), requireTopic: z.boolean().optional(), autoTopicLabel: AutoTopicLabelSchema, }) @@ -222,18 +218,6 @@ const TelegramAccountSchemaBase = z groups: z.record(z.string(), TelegramGroupSchema.optional()).optional(), direct: z.record(z.string(), TelegramDirectSchema.optional()).optional(), richMessages: z.boolean().optional(), - timeoutSeconds: z.number().int().positive().optional(), - mediaGroupFlushMs: z - .number() - .int() - .min(10) - .max(60_000) - .optional() - .describe( - "Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.", - ), - pollingStallThresholdMs: z.number().int().min(30_000).max(600_000).optional(), - retry: RetryConfigSchema, network: z .object({ autoSelectFamily: z.boolean().optional(), @@ -316,7 +300,6 @@ const TelegramAccountSchemaBase = z linkPreview: z.boolean().optional(), silentErrorReplies: z.boolean().optional(), errorPolicy: TelegramErrorPolicySchema, - errorCooldownMs: z.number().int().nonnegative().optional(), apiRoot: z.string().url().optional(), trustedLocalFileRoots: z .array(z.string()) @@ -577,16 +560,12 @@ const DiscordAccountSchema = z .strict() .optional(), proxy: z.string().optional(), - gatewayInfoTimeoutMs: z.number().int().positive().max(120_000).optional(), - gatewayReadyTimeoutMs: z.number().int().positive().max(120_000).optional(), - gatewayRuntimeReadyTimeoutMs: z.number().int().positive().max(120_000).optional(), allowBots: buildChannelAllowBotsSchema({ allowMentions: true }), botLoopProtection: ChannelBotLoopProtectionSchema.optional(), dangerouslyAllowNameMatching: ChannelDangerouslyAllowNameMatchingSchema, mentionAliases: z.record(z.string(), DiscordSnowflakeStringSchema).optional(), suppressEmbeds: z.boolean().optional(), maxLinesPerMessage: z.number().int().positive().optional(), - retry: RetryConfigSchema, actions: z .object({ reactions: z.boolean().optional(), @@ -690,14 +669,6 @@ const DiscordAccountSchema = z }) .strict() .optional(), - eventQueue: z - .object({ - listenerTimeout: z.number().int().positive().optional(), - maxQueueSize: z.number().int().positive().optional(), - maxConcurrency: z.number().int().positive().optional(), - }) - .strict() - .optional(), }) .strict() .superRefine((value, ctx) => { diff --git a/src/config/zod-schema.root-shape.ts b/src/config/zod-schema.root-shape.ts index 0188018638e9..e51122979c25 100644 --- a/src/config/zod-schema.root-shape.ts +++ b/src/config/zod-schema.root-shape.ts @@ -93,9 +93,6 @@ export const OpenClawSchemaShape = { .strictObject({ enabled: z.boolean().optional(), flags: z.array(z.string()).optional(), - stuckSessionWarnMs: z.number().int().positive().optional(), - stuckSessionAbortMs: z.number().int().positive().optional(), - memoryPressureSnapshot: z.boolean().optional(), otel: z .strictObject({ enabled: z.boolean().optional(), @@ -186,9 +183,6 @@ export const OpenClawSchemaShape = { auto: z .strictObject({ enabled: z.boolean().optional(), - stableDelayHours: z.number().nonnegative().max(168).optional(), - stableJitterHours: z.number().nonnegative().max(168).optional(), - betaCheckIntervalHours: z.number().positive().max(24).optional(), }) .optional(), }) @@ -199,17 +193,11 @@ export const OpenClawSchemaShape = { allowSystemProfileImport: z.boolean().optional(), evaluateEnabled: z.boolean().optional(), cdpUrl: z.string().optional(), - remoteCdpTimeoutMs: z.number().int().nonnegative().optional(), - remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(), - localLaunchTimeoutMs: z.number().int().positive().max(120_000).optional(), - localCdpReadyTimeoutMs: z.number().int().positive().max(120_000).optional(), - actionTimeoutMs: z.number().int().positive().optional(), color: z.string().optional(), executablePath: z.string().optional(), headless: z.boolean().optional(), noSandbox: z.boolean().optional(), attachOnly: z.boolean().optional(), - cdpPortRangeStart: z.number().int().min(1).max(65535).optional(), defaultProfile: z.string().optional(), snapshotDefaults: BrowserSnapshotDefaultsSchema, ssrfPolicy: z @@ -265,9 +253,6 @@ export const OpenClawSchemaShape = { tabCleanup: z .strictObject({ enabled: z.boolean().optional(), - idleMinutes: z.number().int().nonnegative().optional(), - maxTabsPerSession: z.number().int().nonnegative().optional(), - sweepMinutes: z.number().int().positive().optional(), }) .optional(), }) @@ -327,19 +312,6 @@ export const OpenClawSchemaShape = { ) .optional(), order: z.record(z.string(), z.array(z.string())).optional(), - cooldowns: z - .strictObject({ - billingBackoffHours: z.number().positive().optional(), - billingBackoffHoursByProvider: z.record(z.string(), z.number().positive()).optional(), - billingMaxHours: z.number().positive().optional(), - authPermanentBackoffMinutes: z.number().positive().optional(), - authPermanentMaxMinutes: z.number().positive().optional(), - failureWindowHours: z.number().positive().optional(), - overloadedProfileRotations: z.number().int().nonnegative().optional(), - overloadedBackoffMs: z.number().int().nonnegative().optional(), - rateLimitedProfileRotations: z.number().int().nonnegative().optional(), - }) - .optional(), }) .optional(), accessGroups: AccessGroupsSchema, @@ -355,29 +327,15 @@ export const OpenClawSchemaShape = { fallbacks: z.array(z.string()).optional(), defaultAgent: z.string().optional(), allowedAgents: z.array(z.string()).optional(), - maxConcurrentSessions: z.number().int().positive().optional(), stream: z .strictObject({ - coalesceIdleMs: z.number().int().nonnegative().optional(), - maxChunkChars: z.number().int().positive().optional(), repeatSuppression: z.boolean().optional(), deliveryMode: z.union([z.literal("live"), z.literal("final_only")]).optional(), - hiddenBoundarySeparator: z - .union([ - z.literal("none"), - z.literal("space"), - z.literal("newline"), - z.literal("paragraph"), - ]) - .optional(), - maxOutputChars: z.number().int().positive().optional(), - maxSessionUpdateChars: z.number().int().positive().optional(), tagVisibility: z.record(z.string(), z.boolean()).optional(), }) .optional(), runtime: z .strictObject({ - ttlMinutes: z.number().int().positive().optional(), installCommand: z.string().optional(), }) .optional(), @@ -409,21 +367,9 @@ export const OpenClawSchemaShape = { .strictObject({ enabled: z.boolean().optional(), store: z.string().optional(), - maxConcurrentRuns: z.number().int().positive().optional(), triggers: z .strictObject({ enabled: z.boolean().optional(), - minIntervalMs: z.number().int().positive().optional(), - }) - .optional(), - retry: z - .strictObject({ - maxAttempts: z.number().int().min(0).max(10).optional(), - backoffMs: z.array(z.number().int().nonnegative()).min(1).max(10).optional(), - retryOn: z - .array(z.enum(["rate_limit", "overloaded", "network", "timeout", "server_error"])) - .min(1) - .optional(), }) .optional(), webhookToken: SecretInputSchema.optional().register(sensitive), @@ -463,20 +409,9 @@ export const OpenClawSchemaShape = { } }) .optional(), - worktrees: z - .strictObject({ - cleanup: z - .strictObject({ - maxCount: z.number().int().min(0).optional(), - maxTotalSizeGb: z.number().min(0).optional(), - }) - .optional(), - }) - .optional(), transcripts: z .strictObject({ enabled: z.boolean().optional(), - maxUtterances: z.number().int().min(1).max(10_000).optional(), autoStart: z .array( z.strictObject({ @@ -502,7 +437,6 @@ export const OpenClawSchemaShape = { allowRequestSessionKey: z.boolean().optional(), allowedSessionKeyPrefixes: z.array(z.string()).optional(), allowedAgentIds: z.array(z.string()).optional(), - maxBodyBytes: z.number().int().positive().optional(), presets: z.array(z.string()).optional(), transformsDir: z.string().optional(), mappings: z.array(HookMappingSchema).optional(), @@ -513,23 +447,6 @@ export const OpenClawSchemaShape = { web: z .strictObject({ enabled: z.boolean().optional(), - heartbeatSeconds: z.number().int().positive().optional(), - reconnect: z - .strictObject({ - initialMs: z.number().positive().optional(), - maxMs: z.number().positive().optional(), - factor: z.number().positive().optional(), - jitter: z.number().min(0).max(1).optional(), - maxAttempts: z.number().int().min(0).optional(), - }) - .optional(), - whatsapp: z - .strictObject({ - keepAliveIntervalMs: z.number().int().positive().optional(), - connectTimeoutMs: z.number().int().positive().optional(), - defaultQueryTimeoutMs: z.number().int().positive().optional(), - }) - .optional(), }) .optional(), channels: ChannelsSchema, diff --git a/src/config/zod-schema.root-support.ts b/src/config/zod-schema.root-support.ts index 40e3e3922486..3229614e47be 100644 --- a/src/config/zod-schema.root-support.ts +++ b/src/config/zod-schema.root-support.ts @@ -426,7 +426,6 @@ export const McpConfigSchema = z sandboxPort: z.number().int().min(1).max(65535).optional(), }) .optional(), - sessionIdleTtlMs: z.number().finite().min(0).optional(), }) .optional(); diff --git a/src/config/zod-schema.session-maintenance-extensions.test.ts b/src/config/zod-schema.session-maintenance-extensions.test.ts index 265ffeff16da..d90f8011f56e 100644 --- a/src/config/zod-schema.session-maintenance-extensions.test.ts +++ b/src/config/zod-schema.session-maintenance-extensions.test.ts @@ -3,43 +3,6 @@ import { describe, expect, it } from "vitest"; import { SessionSchema } from "./zod-schema.session.js"; describe("SessionSchema maintenance extensions", () => { - it("accepts session write-lock acquire timeout", () => { - const result = SessionSchema.safeParse({ - writeLock: { - acquireTimeoutMs: 60_000, - staleMs: 1_800_000, - maxHoldMs: 300_000, - }, - }); - expect(result.success).toBe(true); - }); - - it("rejects invalid session write-lock timeout values", () => { - expect(() => - SessionSchema.parse({ - writeLock: { - acquireTimeoutMs: 0, - }, - }), - ).toThrow(/acquireTimeoutMs|number/i); - - expect(() => - SessionSchema.parse({ - writeLock: { - staleMs: 0, - }, - }), - ).toThrow(/staleMs|number/i); - - expect(() => - SessionSchema.parse({ - writeLock: { - maxHoldMs: 0, - }, - }), - ).toThrow(/maxHoldMs|number/i); - }); - it("accepts valid maintenance extensions", () => { const result = SessionSchema.safeParse({ maintenance: { diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index 0e9832cc5592..079ab9cb90e1 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -70,24 +70,9 @@ export const SessionSchema = z .optional(), resetByChannel: z.record(z.string(), SessionResetConfigSchema).optional(), store: z.string().optional(), - typingIntervalSeconds: z.number().int().positive().optional(), typingMode: TypingModeSchema.optional(), mainKey: z.string().optional(), sendPolicy: SessionSendPolicySchema.optional(), - writeLock: z - .object({ - acquireTimeoutMs: z.number().int().positive().optional(), - staleMs: z.number().int().positive().optional(), - maxHoldMs: z.number().int().positive().optional(), - }) - .strict() - .optional(), - agentToAgent: z - .object({ - maxPingPongTurns: z.number().int().min(0).max(20).optional(), - }) - .strict() - .optional(), threadBindings: z .object({ enabled: z.boolean().optional(), @@ -180,16 +165,6 @@ export const MessagesSchema = z }) .strict() .optional(), - timing: z - .object({ - debounceMs: z.number().int().min(0).optional(), - stallSoftMs: z.number().int().min(0).optional(), - stallHardMs: z.number().int().min(0).optional(), - doneHoldMs: z.number().int().min(0).optional(), - errorHoldMs: z.number().int().min(0).optional(), - }) - .strict() - .optional(), }) .strict() .optional(), diff --git a/src/cron/active-jobs-manual-run.test.ts b/src/cron/active-jobs-manual-run.test.ts index 3b9d51c930ef..45394e767b88 100644 --- a/src/cron/active-jobs-manual-run.test.ts +++ b/src/cron/active-jobs-manual-run.test.ts @@ -203,7 +203,6 @@ describe("cron activeJobIds — manual-run mark/clear", () => { const cron = new CronService({ storePath: store.storePath, cronEnabled: true, - cronConfig: { maxConcurrentRuns: 2 }, log: logger, enqueueSystemEvent: () => {}, requestHeartbeat: () => {}, diff --git a/src/cron/active-jobs.test.ts b/src/cron/active-jobs.test.ts index 322d53832ef9..7b1a9be098ff 100644 --- a/src/cron/active-jobs.test.ts +++ b/src/cron/active-jobs.test.ts @@ -25,7 +25,7 @@ describe("hasActiveCronJobsExceptMarker", () => { markCronJobActive("different-job"); // The owning job must not be waved through while another run holds a marker: - // cron executes jobs concurrently (cron.maxConcurrentRuns). + // Cron executes jobs up to the built-in concurrency limit. expect(hasActiveCronJobsExceptMarker(marker!)).toBe(true); }); diff --git a/src/cron/retry-hint.ts b/src/cron/retry-hint.ts index e5401cf8db17..e6b070f716bd 100644 --- a/src/cron/retry-hint.ts +++ b/src/cron/retry-hint.ts @@ -1,5 +1,5 @@ /** Classifies cron run failures for retry policy decisions. */ -import type { CronRetryOn } from "../config/types.cron.js"; +export type CronRetryOn = "rate_limit" | "overloaded" | "network" | "timeout" | "server_error"; /** Cron retry classifier output consumed by scheduler retry policy. */ type CronRetryHint = { diff --git a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts index fec0249debcb..7b58da8e688f 100644 --- a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts +++ b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts @@ -597,41 +597,6 @@ describe("CronService", () => { await stopCronAndCleanup(cron, store); }); - it("disables exhausted disabled-heartbeat one-shots without leaving queued events", async () => { - resetSystemEventsForTest(); - const atMs = Date.parse("2025-12-13T00:00:02.000Z"); - const runHeartbeatOnce = vi.fn(async () => ({ - status: "skipped" as const, - reason: "disabled", - })); - const { store, cron, enqueueSystemEvent, requestHeartbeat } = await createCronHarness({ - runHeartbeatOnce, - nowMs: () => atMs, - cronConfig: { retry: { maxAttempts: 0, backoffMs: [30_000] } }, - useRemovableSystemEventQueue: true, - withEvents: false, - }); - const job = await addMainOneShotHelloJob(cron, { - atMs, - name: "one-shot disabled heartbeat exhausted", - }); - - await cron.run(job.id, "due"); - - const jobs = await cron.list({ includeDisabled: true }); - const updated = jobs.find((j) => j.id === job.id); - expect(updated?.enabled).toBe(false); - expect(updated?.state.lastStatus).toBe("skipped"); - expect(updated?.state.lastError).toBe("disabled"); - expect(updated?.state.consecutiveSkipped).toBe(1); - expect(updated?.state.nextRunAtMs).toBeUndefined(); - expect(runHeartbeatOnce).toHaveBeenCalledTimes(1); - expect(requestHeartbeat).not.toHaveBeenCalled(); - expectNoQueuedEvents(getPostedSystemEventSessionKeys(enqueueSystemEvent)); - - await stopCronAndCleanup(cron, store); - }); - it("runs an isolated job without posting a fallback summary to main", async () => { const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const, summary: "done" })); const { store, cron, enqueueSystemEvent, requestHeartbeat, events } = diff --git a/src/cron/service.trigger-eval.test.ts b/src/cron/service.trigger-eval.test.ts index 7f076fafcba5..5de1c3bd7f6e 100644 --- a/src/cron/service.trigger-eval.test.ts +++ b/src/cron/service.trigger-eval.test.ts @@ -39,7 +39,7 @@ async function createHarness(params: { const cron = new CronService({ storePath, cronEnabled: true, - cronConfig: { triggers: { enabled: true, minIntervalMs: 30_000 } }, + cronConfig: { triggers: { enabled: true } }, log: logger, enqueueSystemEvent, requestHeartbeat: vi.fn(), diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 6b844a3d5d00..c3dba6fba81b 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -219,11 +219,8 @@ function isPendingErrorBackoffSlot(params: { nextRunAtMs: number; nowMs: number; }): boolean { - const { state, job, nextRunAtMs, nowMs } = params; - const backoffUntilMs = resolveJobErrorBackoffUntilMs( - job, - state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, - ); + const { job, nextRunAtMs, nowMs } = params; + const backoffUntilMs = resolveJobErrorBackoffUntilMs(job, DEFAULT_ERROR_BACKOFF_SCHEDULE_MS); return backoffUntilMs !== undefined && nowMs < backoffUntilMs && nextRunAtMs <= backoffUntilMs; } @@ -377,7 +374,7 @@ function assertTriggerSupport( if (job.schedule.kind !== "every" && job.schedule.kind !== "cron") { throw new Error("cron triggers require an every or cron schedule"); } - const minIntervalMs = resolveCronTriggerMinIntervalMs(opts?.cronConfig); + const minIntervalMs = resolveCronTriggerMinIntervalMs(); if (job.schedule.kind === "every" && job.schedule.everyMs < minIntervalMs) { throw new Error(`cron trigger every interval must be at least ${minIntervalMs}ms`); } @@ -797,7 +794,7 @@ function recomputeJobNextRunAtMs(params: { ) { const backoffFloor = resolveJobErrorBackoffUntilMs( params.job, - params.state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, + DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, ); if (newNext !== undefined) { newNext = backoffFloor !== undefined ? Math.max(newNext, backoffFloor) : newNext; @@ -939,7 +936,7 @@ export function recomputeNextRunsForMaintenance( const alreadyExecutedSlot = isFiniteTimestamp(lastRun) && lastRun >= job.state.nextRunAtMs; const backoffUntilMs = resolveJobErrorBackoffUntilMs( job, - state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, + DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, ); const isStaleBackoffSlot = backoffUntilMs !== undefined && diff --git a/src/cron/service/ops.regression.test.ts b/src/cron/service/ops.regression.test.ts index 52cb4ba09f5a..b13586addfa6 100644 --- a/src/cron/service/ops.regression.test.ts +++ b/src/cron/service/ops.regression.test.ts @@ -8,6 +8,7 @@ import { noopLogger, setupCronRegressionFixtures, } from "../../../test/helpers/cron/service-regression-fixtures.js"; +import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "../../config/cron-limits.js"; import { clearCommandLane, enqueueCommandInLane, @@ -660,7 +661,6 @@ describe("cron service ops regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -672,6 +672,7 @@ describe("cron service ops regressions", () => { } }, }); + state.runAdmission.active = DEFAULT_CRON_MAX_CONCURRENT_RUNS - 1; const firstAck = await enqueueRun(state, first.id, "force"); const secondAck = await enqueueRun(state, second.id, "force"); @@ -723,7 +724,7 @@ describe("cron service ops regressions", () => { const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const })); const state = createCronServiceState({ cronEnabled: true, - cronConfig: { triggers: { enabled: true, minIntervalMs: 30_000 } }, + cronConfig: { triggers: { enabled: true } }, storePath: store.storePath, log: noopLogger, nowMs: () => dueAt, @@ -903,7 +904,6 @@ describe("cron service ops regressions", () => { let peakInFlight = 0; const state = createCronServiceState({ cronEnabled: true, - cronConfig: { maxConcurrentRuns: 2 }, storePath: store.storePath, log: noopLogger, nowMs: () => now, @@ -957,7 +957,6 @@ describe("cron service ops regressions", () => { let dispatchCount = 0; const state = createCronServiceState({ cronEnabled: true, - cronConfig: { maxConcurrentRuns: 2 }, storePath: store.storePath, log: noopLogger, nowMs: () => now, diff --git a/src/cron/service/ops.run-admission.test.ts b/src/cron/service/ops.run-admission.test.ts index 2c9272b45581..041326eacca2 100644 --- a/src/cron/service/ops.run-admission.test.ts +++ b/src/cron/service/ops.run-admission.test.ts @@ -6,6 +6,7 @@ import { noopLogger, setupCronRegressionFixtures, } from "../../../test/helpers/cron/service-regression-fixtures.js"; +import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "../../config/cron-limits.js"; import { clearCommandLane, enqueueCommandInLane, @@ -24,6 +25,19 @@ const opsRegressionFixtures = setupCronRegressionFixtures({ prefix: "cron-service-run-admission-", }); +type CronStateParams = Parameters[0] & { + testAdmissionLimit?: number; +}; + +function createAdmissionTestState(params: CronStateParams) { + const { testAdmissionLimit, ...stateParams } = params; + const state = createCronServiceState(stateParams); + if (testAdmissionLimit !== undefined) { + state.runAdmission.active = DEFAULT_CRON_MAX_CONCURRENT_RUNS - testAdmissionLimit; + } + return state; +} + function expectQueuedRunAck(result: unknown) { const ack = result as { ok?: unknown; enqueued?: unknown; runId?: unknown }; expect(ack.ok).toBe(true); @@ -56,7 +70,7 @@ describe("cron service run admission", () => { await blockerStarted.promise; const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const })); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, log: noopLogger, @@ -76,55 +90,6 @@ describe("cron service run admission", () => { clearCommandLane(CommandLane.Cron); }); - it("shares maxConcurrentRuns between direct manual and scheduled jobs", async () => { - const store = opsRegressionFixtures.makeStorePath(); - const dueAt = Date.parse("2026-02-06T10:05:05.000Z"); - const scheduledJob = createDueIsolatedJob({ - id: "scheduled-shared-admission", - nowMs: dueAt, - nextRunAtMs: dueAt, - }); - const manualJob = createDueIsolatedJob({ - id: "manual-shared-admission", - nowMs: dueAt, - nextRunAtMs: dueAt + 3_600_000, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [scheduledJob, manualJob] }); - - const manualStarted = createDeferred(); - const scheduledStarted = createDeferred(); - const releaseManual = createDeferred<{ status: "ok"; summary: string }>(); - const runIsolatedAgentJob = vi.fn(async ({ job: runningJob }: { job: { id: string } }) => { - if (runningJob.id === manualJob.id) { - manualStarted.resolve(); - return await releaseManual.promise; - } - scheduledStarted.resolve(); - return { status: "ok" as const, summary: "scheduled" }; - }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, - log: noopLogger, - nowMs: () => dueAt, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - }); - - const manualRun = run(state, manualJob.id, "force"); - await manualStarted.promise; - const timerRun = onTimer(state); - await Promise.resolve(); - await Promise.resolve(); - - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1); - releaseManual.resolve({ status: "ok", summary: "manual" }); - await scheduledStarted.promise; - await Promise.all([manualRun, timerRun]); - }); - it("drains a burst of scheduled jobs without exceeding shared admission", async () => { vi.useRealTimers(); const store = opsRegressionFixtures.makeStorePath(); @@ -141,10 +106,10 @@ describe("cron service run admission", () => { let active = 0; let peakActive = 0; const completed = new Set(); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 4 }, + testAdmissionLimit: 4, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -204,10 +169,10 @@ describe("cron service run admission", () => { completingStarted.resolve(); return await releaseCompleting.promise; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 2 }, + testAdmissionLimit: 2, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -286,10 +251,10 @@ describe("cron service run admission", () => { } return { status: "ok" as const, summary: "should not run" }; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -350,10 +315,10 @@ describe("cron service run admission", () => { } return { status: "ok" as const, summary: "should not run" }; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -413,10 +378,10 @@ describe("cron service run admission", () => { replacementStarted.resolve(); return { status: "ok" as const, summary: "replacement" }; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -462,7 +427,7 @@ describe("cron service run admission", () => { await saveCronStore(store.storePath, { version: 1, jobs: [job] }); const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const })); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, log: noopLogger, @@ -504,10 +469,10 @@ describe("cron service run admission", () => { waitingStarted.resolve(); return await releaseWaiting.promise; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -561,10 +526,10 @@ describe("cron service run admission", () => { const releaseActive = createDeferred<{ status: "ok"; summary: string }>(); const waitingStarted = createDeferred(); const releaseWaiting = createDeferred<{ status: "ok"; summary: string }>(); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -626,7 +591,7 @@ describe("cron service run admission", () => { }); await saveCronStore(store.storePath, { version: 1, jobs: [job] }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, log: noopLogger, @@ -673,7 +638,7 @@ describe("cron service run admission", () => { }); await saveCronStore(store.storePath, { version: 1, jobs: [job] }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, log: noopLogger, @@ -784,10 +749,10 @@ describe("cron service run admission", () => { const activeStarted = createDeferred(); const releaseActive = createDeferred<{ status: "ok"; summary: string }>(); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -843,10 +808,10 @@ describe("cron service run admission", () => { } return { status: "ok" as const, summary: "should not run" }; }); - const state = createCronServiceState({ + const state = createAdmissionTestState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), diff --git a/src/cron/service/run-admission.ts b/src/cron/service/run-admission.ts index b86f0ca4efc1..a40130a96df5 100644 --- a/src/cron/service/run-admission.ts +++ b/src/cron/service/run-admission.ts @@ -1,9 +1,9 @@ // Shared execution admission for scheduled, manual, and on-exit cron runs. -import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion"; +import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "../../config/cron-limits.js"; import type { CronServiceState } from "./state.js"; -export function resolveRunConcurrency(state: CronServiceState): number { - return resolveIntegerOption(state.deps.cronConfig?.maxConcurrentRuns, 1, { min: 1 }); +export function resolveRunConcurrency(): number { + return DEFAULT_CRON_MAX_CONCURRENT_RUNS; } function dispatchWaiters(state: CronServiceState): void { @@ -12,7 +12,7 @@ function dispatchWaiters(state: CronServiceState): void { cancelCronRunAdmissionWaiters(state); return; } - const maxConcurrentRuns = resolveRunConcurrency(state); + const maxConcurrentRuns = resolveRunConcurrency(); while (admission.active < maxConcurrentRuns) { const waiter = admission.waiters.shift(); if (!waiter) { @@ -36,7 +36,7 @@ async function acquireCronRunAdmission(state: CronServiceState): Promise<(() => if (state.stopped) { return null; } - if (admission.waiters.length === 0 && admission.active < resolveRunConcurrency(state)) { + if (admission.waiters.length === 0 && admission.active < resolveRunConcurrency()) { admission.active += 1; let released = false; return () => { diff --git a/src/cron/service/timer.backoff-config-readback.test.ts b/src/cron/service/timer.backoff-config-readback.test.ts deleted file mode 100644 index 31daa202f199..000000000000 --- a/src/cron/service/timer.backoff-config-readback.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Verifies the configured retry.backoffMs floor for a recurring job survives a -// real cron service run and is persisted to the SQLite-backed store, not just -// computed in memory by applyJobResult. -import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; -import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; -import { withEnvAsync } from "../../test-utils/env.js"; -import { setupCronServiceSuite, writeCronStoreSnapshot } from "../service.test-harness.js"; -import { loadCronStore } from "../store.js"; -import type { CronJob } from "../types.js"; -import { run } from "./ops.js"; -import { createCronServiceState } from "./state.js"; - -const { logger, makeStorePath } = setupCronServiceSuite({ - prefix: "cron-backoff-config-readback", -}); - -function createDueRecurringJob(now: number): CronJob { - return { - id: "recurring-backoff-readback", - name: "recurring backoff readback", - enabled: true, - createdAtMs: now - 60_000, - updatedAtMs: now - 60_000, - schedule: { kind: "every", everyMs: 1_000, anchorMs: now - 60_000 }, - sessionTarget: "isolated", - wakeMode: "next-heartbeat", - payload: { kind: "agentTurn", message: "ping" }, - sessionKey: "agent:main:main", - state: { nextRunAtMs: now - 1 }, - }; -} - -describe("recurring error backoff floor persistence", () => { - it("persists the configured retry.backoffMs floor across a real run and SQLite readback", async () => { - const now = Date.parse("2026-03-02T12:00:00.000Z"); - const { storePath } = await makeStorePath(); - const stateRoot = path.dirname(path.dirname(storePath)); - const job = createDueRecurringJob(now); - - let persistedJob: CronJob | undefined; - resetTaskRegistryForTests(); - try { - await withEnvAsync({ OPENCLAW_STATE_DIR: stateRoot }, async () => { - await writeCronStoreSnapshot({ storePath, jobs: [job] }); - - const state = createCronServiceState({ - storePath, - cronEnabled: true, - log: logger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - // Permanent (non-retryable) error -> recurring safety-net backoff - // floor, the branch that must honor the configured backoffMs. - runIsolatedAgentJob: vi.fn(async () => { - throw new Error("permanent: bad request"); - }), - cronConfig: { retry: { backoffMs: [300_000] } }, - }); - - // mode "due" (not "force") advances cadence, so the error - // path computes the safety-net backoff floor rather than preserving the - // recurring anchor. - await expect(run(state, job.id, "due")).resolves.toEqual({ ok: true, ran: true }); - - const persisted = (await loadCronStore(storePath)) as { jobs: CronJob[] }; - persistedJob = persisted.jobs.find((entry) => entry.id === job.id); - }); - } finally { - resetTaskRegistryForTests(); - } - - // The floor read back from the SQLite-backed store must be endedAt(=now) + - // the configured backoffMs[0], not the hardcoded 30000 default. - expect(persistedJob?.state.nextRunAtMs).toBe(now + 300_000); - expect(persistedJob?.state.lastStatus).toBe("error"); - expect(persistedJob?.state.consecutiveErrors).toBe(1); - }); -}); diff --git a/src/cron/service/timer.pacing.test.ts b/src/cron/service/timer.pacing.test.ts index b0dc97e7d5b1..c1ce36e59239 100644 --- a/src/cron/service/timer.pacing.test.ts +++ b/src/cron/service/timer.pacing.test.ts @@ -9,7 +9,7 @@ import { applyJobResult } from "./timer.js"; const ENDED_AT = Date.parse("2026-07-18T12:00:00.000Z"); const STARTED_AT = ENDED_AT - 1_000; -function makeState(triggerMinIntervalMs?: number) { +function makeState() { return createCronServiceState({ storePath: "/tmp/cron-pacing-timer/jobs.json", cronEnabled: true, @@ -18,9 +18,6 @@ function makeState(triggerMinIntervalMs?: number) { enqueueSystemEvent: vi.fn(), requestHeartbeat: vi.fn(), runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })), - ...(triggerMinIntervalMs !== undefined - ? { cronConfig: { triggers: { enabled: true, minIntervalMs: triggerMinIntervalMs } } } - : {}), }); } @@ -94,34 +91,19 @@ describe("applyJobResult dynamic cadence", () => { expect(job.state.pacedNextRunAtMs).toBe(pendingSlot); }); - it("applies the operator trigger floor after the job-local pacing clamp", () => { + it("applies the built-in trigger floor after the job-local pacing clamp", () => { const job = makePacedJob({ min: "1s", max: "2m" }); job.trigger = { script: "return true" }; - applyJobResult(makeState(60_000), job, { + applyJobResult(makeState(), job, { status: "ok", startedAt: STARTED_AT, endedAt: ENDED_AT, nextCheck: { delayMs: 1_000 }, }); - expect(job.state.nextRunAtMs).toBe(ENDED_AT + 60_000); - expect(job.state.pacedNextRunAtMs).toBe(ENDED_AT + 60_000); - }); - - it("keeps the cron anti-spin floor when the configured trigger floor is smaller", () => { - const job = makePacedJob({ min: "1ms", max: "2m" }); - job.trigger = { script: "return true" }; - - applyJobResult(makeState(1), job, { - status: "ok", - startedAt: STARTED_AT, - endedAt: ENDED_AT, - nextCheck: { delayMs: 1 }, - }); - - expect(job.state.nextRunAtMs).toBe(ENDED_AT + 2_000); - expect(job.state.pacedNextRunAtMs).toBe(ENDED_AT + 2_000); + expect(job.state.nextRunAtMs).toBe(ENDED_AT + 30_000); + expect(job.state.pacedNextRunAtMs).toBe(ENDED_AT + 30_000); }); it("discards proposals on error so normal backoff wins", () => { diff --git a/src/cron/service/timer.regression.test.ts b/src/cron/service/timer.regression.test.ts index df2cbc6868a5..56c321492cce 100644 --- a/src/cron/service/timer.regression.test.ts +++ b/src/cron/service/timer.regression.test.ts @@ -10,6 +10,7 @@ import { noopLogger, setupCronRegressionFixtures, } from "../../../test/helpers/cron/service-regression-fixtures.js"; +import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "../../config/cron-limits.js"; import { HEARTBEAT_SKIP_LANES_BUSY, type HeartbeatRunResult } from "../../infra/heartbeat-wake.js"; import { enqueueCommandInLane } from "../../process/command-queue.js"; import { CommandLane } from "../../process/lanes.js"; @@ -38,7 +39,7 @@ import { cancelActiveCronTaskRun } from "./active-run-cancellation.js"; import { resetActiveCronTaskRunsForTests } from "./active-run-cancellation.test-support.js"; import { computeJobNextRunAtMs, recomputeNextRunsForMaintenance } from "./jobs.js"; import { run as runManualCronJob, stop } from "./ops.js"; -import { createCronServiceState, type CronEvent } from "./state.js"; +import { createCronServiceState as createBaseCronServiceState, type CronEvent } from "./state.js"; import { applyJobResult, executeJobCoreWithTimeout, runMissedJobs } from "./timer.js"; import { executeJobCore, onTimer } from "./timer.test-support.js"; @@ -47,6 +48,19 @@ const timerRegressionFixtures = setupCronRegressionFixtures({ prefix: "cron-service-timer-regressions-", }); +type CronStateParams = Parameters[0] & { + testAdmissionLimit?: number; +}; + +function createCronServiceState(params: CronStateParams) { + const { testAdmissionLimit, ...stateParams } = params; + const state = createBaseCronServiceState(stateParams); + if (testAdmissionLimit !== undefined) { + state.runAdmission.active = DEFAULT_CRON_MAX_CONCURRENT_RUNS - testAdmissionLimit; + } + return state; +} + function requireJob(state: { store?: { jobs?: CronJob[] } | null }, id: string): CronJob { const job = state.store?.jobs?.find((candidate) => candidate.id === id); if (!job) { @@ -284,376 +298,6 @@ describe("cron service timer regressions", () => { expect(runIsolatedAgentJob).toHaveBeenCalledTimes(4); }); - it("#24355: one-shot job respects cron.retry config", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-custom-retry", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi.fn().mockResolvedValue({ - status: "error", - error: "429 rate limit exceeded", - }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 2, backoffMs: [1000, 2000] }, - }, - }); - - for (let i = 0; i < 4; i += 1) { - await onTimer(state); - const job = requireJob(state, "oneshot-custom-retry"); - if (i < 2) { - expect(job.enabled).toBe(true); - now = requireTimestamp(job.state.nextRunAtMs, "custom-retry next run") + 1; - } else { - expect(job.enabled).toBe(false); - } - } - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(3); - }); - - it("#24355: one-shot job retries status-only 529 failures when retryOn only includes overloaded", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-overloaded-529-only", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ status: "error", error: "FailoverError: HTTP 529" }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["overloaded"] }, - }, - }); - - await onTimer(state); - const jobAfterRetry = requireJob(state, "oneshot-overloaded-529-only"); - expect(jobAfterRetry.enabled).toBe(true); - expect(jobAfterRetry.state.lastStatus).toBe("error"); - expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - - now = requireTimestamp(jobAfterRetry.state.nextRunAtMs, "529 retry next run") + 1; - await onTimer(state); - - const finishedJob = requireJob(state, "oneshot-overloaded-529-only"); - expect(finishedJob.state.lastStatus).toBe("ok"); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - - it("retries OpenAI-compatible server_error payloads when retryOn only includes server_error", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-03-14T00:00:00.000Z"); - const serverErrorPayload = - 'Codex error: {"type":"error","error":{"type":"server_error","code":"server_error","message":"An error occurred while processing your request."},"sequence_number":2}'; - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-server-error-only", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ status: "error", error: serverErrorPayload }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["server_error"] }, - }, - }); - - await onTimer(state); - const jobAfterRetry = requireJob(state, "oneshot-server-error-only"); - expect(jobAfterRetry.enabled).toBe(true); - expect(jobAfterRetry.state.lastStatus).toBe("error"); - expect(jobAfterRetry.state.lastErrorReason).toBe("server_error"); - expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - - now = requireTimestamp(jobAfterRetry.state.nextRunAtMs, "server_error retry next run") + 1; - await onTimer(state); - - const finishedJob = requireJob(state, "oneshot-server-error-only"); - expect(finishedJob.state.lastStatus).toBe("ok"); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - - it("#38822: one-shot job retries Bedrock too-many-tokens-per-day errors", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-03-08T10:00:00.000Z"); - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-bedrock-too-many-tokens-per-day", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ - status: "error", - error: "AWS Bedrock: Too many tokens per day. Please try again tomorrow.", - }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["rate_limit"] }, - }, - }); - - await onTimer(state); - const jobAfterRetry = requireJob(state, "oneshot-bedrock-too-many-tokens-per-day"); - expect(jobAfterRetry.enabled).toBe(true); - expect(jobAfterRetry.state.lastStatus).toBe("error"); - expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - - now = requireTimestamp(jobAfterRetry.state.nextRunAtMs, "Bedrock retry next run") + 1; - await onTimer(state); - - const finishedJob = requireJob(state, "oneshot-bedrock-too-many-tokens-per-day"); - expect(finishedJob.state.lastStatus).toBe("ok"); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - - it("#24355: one-shot job disabled immediately on permanent error", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-permanent-error", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - const now = scheduledAt; - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob: vi.fn().mockResolvedValue({ - status: "error", - error: "invalid API key", - }), - }); - - await onTimer(state); - - const job = requireJob(state, "oneshot-permanent-error"); - expect(job.enabled).toBe(false); - expect(job.state.lastStatus).toBe("error"); - expect(job.state.nextRunAtMs).toBeUndefined(); - }); - - it("retries recurring jobs after transient model rate limits before the next scheduled slot", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-05-29T02:28:00.000Z"); - const everySixHoursMs = 6 * 60 * 60 * 1_000; - - const cronJob = createIsolatedRegressionJob({ - id: "recurring-rate-limit-retry", - name: "Clawsweeper 6h closure report", - scheduledAt, - schedule: { kind: "every", everyMs: everySixHoursMs, anchorMs: scheduledAt }, - payload: { kind: "agentTurn", message: "closure report" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ - status: "error", - error: - "FailoverError: stream disconnected before completion: Rate limit reached for gpt-5.5 in organization org-test on tokens per min (TPM): Limit 40000000, Used 40000000, Requested 15773. Please try again in 23ms.", - }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["rate_limit"] }, - }, - }); - - await onTimer(state); - const jobAfterRetry = requireJob(state, "recurring-rate-limit-retry"); - expect(jobAfterRetry.enabled).toBe(true); - expect(jobAfterRetry.state.lastStatus).toBe("error"); - expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - expect(jobAfterRetry.state.nextRunAtMs).toBeLessThan(scheduledAt + everySixHoursMs); - expect(Object.keys(jobAfterRetry.state)).not.toContain("recurringRetryNextRunAtMs"); - expect(Object.keys(jobAfterRetry.state)).not.toContain("recurringRetryScheduleIdentity"); - - now = requireTimestamp(jobAfterRetry.state.nextRunAtMs, "recurring retry next run") + 1; - await onTimer(state); - - const finishedJob = requireJob(state, "recurring-rate-limit-retry"); - expect(finishedJob.state.lastStatus).toBe("ok"); - expect(finishedJob.state.nextRunAtMs).toBe(scheduledAt + everySixHoursMs); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - - it("retries recurring jobs after isolated setup timeouts before the next scheduled slot", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-06-08T13:00:00.000Z"); - const everySixHoursMs = 6 * 60 * 60 * 1_000; - - const cronJob = createIsolatedRegressionJob({ - id: "recurring-setup-timeout-retry", - name: "ShadowTrader Auto Channel Bug Monitor", - scheduledAt, - schedule: { kind: "every", everyMs: everySixHoursMs, anchorMs: scheduledAt }, - payload: { kind: "agentTurn", message: "monitor bugs" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - const now = scheduledAt; - const runIsolatedAgentJob = vi.fn().mockResolvedValueOnce({ - status: "error", - error: "cron: isolated agent setup timed out before runner start", - }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["timeout"] }, - }, - }); - - await onTimer(state); - const jobAfterRetry = requireJob(state, "recurring-setup-timeout-retry"); - expect(jobAfterRetry.enabled).toBe(true); - expect(jobAfterRetry.state.lastStatus).toBe("error"); - expect(jobAfterRetry.state.lastError).toContain("setup timed out before runner start"); - expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - expect(jobAfterRetry.state.nextRunAtMs).toBeLessThan(scheduledAt + everySixHoursMs); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1); - }); - - it("uses the normal recurring schedule after transient retry attempts are exhausted", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const scheduledAt = Date.parse("2026-05-29T02:28:00.000Z"); - const everySixHoursMs = 6 * 60 * 60 * 1_000; - - const cronJob = createIsolatedRegressionJob({ - id: "recurring-rate-limit-exhausted", - name: "Clawsweeper 6h closure report", - scheduledAt, - schedule: { kind: "every", everyMs: everySixHoursMs, anchorMs: scheduledAt }, - payload: { kind: "agentTurn", message: "closure report" }, - state: { nextRunAtMs: scheduledAt }, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); - - let now = scheduledAt; - const runIsolatedAgentJob = vi.fn().mockResolvedValue({ - status: "error", - error: "429 rate limit exceeded", - }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - cronConfig: { - retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["rate_limit"] }, - }, - }); - - await onTimer(state); - let job = requireJob(state, "recurring-rate-limit-exhausted"); - expect(job.state.nextRunAtMs).toBeLessThan(scheduledAt + everySixHoursMs); - - now = requireTimestamp(job.state.nextRunAtMs, "recurring exhausted retry") + 1; - await onTimer(state); - - job = requireJob(state, "recurring-rate-limit-exhausted"); - expect(job.enabled).toBe(true); - expect(job.state.lastStatus).toBe("error"); - expect(job.state.nextRunAtMs).toBe(scheduledAt + everySixHoursMs); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - it("preserves every cadence after a transient recurring retry succeeds", () => { const scheduledAt = Date.parse("2026-05-29T02:28:00.000Z"); const everyTwelveHoursMs = 12 * 60 * 60 * 1_000; @@ -2186,7 +1830,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2216,70 +1860,6 @@ describe("cron service timer regressions", () => { expect(startedAtEvents).toEqual([dueAt, dueAt + 50]); }); - it("honors cron maxConcurrentRuns for due jobs", async () => { - vi.useRealTimers(); - const store = timerRegressionFixtures.makeStorePath(); - const dueAt = Date.parse("2026-02-06T10:05:01.000Z"); - const first = createDueIsolatedJob({ id: "parallel-first", nowMs: dueAt, nextRunAtMs: dueAt }); - const second = createDueIsolatedJob({ - id: "parallel-second", - nowMs: dueAt, - nextRunAtMs: dueAt, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [first, second] }); - - let now = dueAt; - let activeRuns = 0; - let peakActiveRuns = 0; - const bothRunsStarted = createDeferred(); - const firstRun = createDeferred<{ status: "ok"; summary: string }>(); - const secondRun = createDeferred<{ status: "ok"; summary: string }>(); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 2 }, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob: vi.fn(async (params: { job: { id: string } }) => { - activeRuns += 1; - peakActiveRuns = Math.max(peakActiveRuns, activeRuns); - if (peakActiveRuns >= 2) { - bothRunsStarted.resolve(); - } - try { - const result = - params.job.id === first.id ? await firstRun.promise : await secondRun.promise; - now += 10; - return result; - } finally { - activeRuns -= 1; - } - }), - }); - - const timerPromise = onTimer(state); - const startTimeout = setTimeout(() => { - bothRunsStarted.reject(new Error("timed out waiting for concurrent job starts")); - }, 250); - try { - await bothRunsStarted.promise; - } finally { - clearTimeout(startTimeout); - } - - expect(peakActiveRuns).toBe(2); - - firstRun.resolve({ status: "ok", summary: "first done" }); - secondRun.resolve({ status: "ok", summary: "second done" }); - await timerPromise; - - const jobs = state.store?.jobs ?? []; - expect(jobs.find((job) => job.id === first.id)?.state.lastStatus).toBe("ok"); - expect(jobs.find((job) => job.id === second.id)?.state.lastStatus).toBe("ok"); - }); - it("keeps queued scheduled reservations out of stuck-marker cleanup", async () => { const store = timerRegressionFixtures.makeStorePath(); const dueAt = Date.parse("2026-02-06T10:05:01.250Z"); @@ -2303,7 +1883,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2353,56 +1933,6 @@ describe("cron service timer regressions", () => { expect(state.queuedRunReservationsByJobId.has(second.id)).toBe(false); }); - it("shares maxConcurrentRuns with startup catch-up", async () => { - const store = timerRegressionFixtures.makeStorePath(); - const dueAt = Date.parse("2026-02-06T10:05:01.375Z"); - const activeManualJob = createDueIsolatedJob({ - id: "manual-before-startup-catchup", - nowMs: dueAt, - nextRunAtMs: dueAt + 3_600_000, - }); - const catchupJob = createDueIsolatedJob({ - id: "startup-catchup-shared-admission", - nowMs: dueAt, - nextRunAtMs: dueAt, - }); - await saveCronStore(store.storePath, { version: 1, jobs: [activeManualJob, catchupJob] }); - - const activeStarted = createDeferred(); - const releaseActive = createDeferred<{ status: "ok"; summary: string }>(); - const runIsolatedAgentJob = vi.fn(async ({ job }: { job: { id: string } }) => { - if (job.id === activeManualJob.id) { - activeStarted.resolve(); - return await releaseActive.promise; - } - return { status: "ok" as const, summary: "catch-up" }; - }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, - log: noopLogger, - nowMs: () => dueAt, - enqueueSystemEvent: vi.fn(), - requestHeartbeat: vi.fn(), - runIsolatedAgentJob, - }); - - const activeRun = runManualCronJob(state, activeManualJob.id, "force"); - await activeStarted.promise; - const catchupRun = runMissedJobs(state); - await vi.waitFor(() => { - expect(state.store?.jobs.find((job) => job.id === catchupJob.id)?.state.queuedAtMs).toBe( - dueAt, - ); - }); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1); - - releaseActive.resolve({ status: "ok", summary: "manual" }); - await Promise.all([activeRun, catchupRun]); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - it("rechecks startup catch-up eligibility after an admission wait", async () => { const store = timerRegressionFixtures.makeStorePath(); const dueAt = Date.parse("2026-02-06T10:05:01.437Z"); @@ -2430,7 +1960,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -2543,7 +2073,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => dueAt, enqueueSystemEvent: vi.fn(), @@ -2601,7 +2131,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 2 }, + testAdmissionLimit: 2, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2665,7 +2195,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2727,7 +2257,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2796,7 +2326,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), @@ -2821,7 +2351,7 @@ describe("cron service timer regressions", () => { await Promise.resolve(); await Promise.resolve(); - expect(state.runAdmission.active).toBe(1); + expect(state.runAdmission.active).toBe(DEFAULT_CRON_MAX_CONCURRENT_RUNS); await vi.advanceTimersByTimeAsync(60_100); now += 60_100; @@ -3102,7 +2632,7 @@ describe("cron service timer regressions", () => { const state = createCronServiceState({ cronEnabled: true, storePath: store.storePath, - cronConfig: { maxConcurrentRuns: 1 }, + testAdmissionLimit: 1, log: noopLogger, nowMs: () => now, enqueueSystemEvent: vi.fn(), diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index c13f32fe3f44..fafb39b014b5 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -3,7 +3,7 @@ import pMap, { pMapSkip } from "p-map"; import { resolveFailoverReasonFromError } from "../../agents/failover-error.js"; import { resolveCronTriggerMinIntervalMs } from "../../config/cron-limits.js"; import { loadSessionEntry } from "../../config/sessions/session-accessor.js"; -import type { CronConfig, CronRetryOn } from "../../config/types.cron.js"; +import type { CronConfig } from "../../config/types.cron.js"; import { formatErrorMessage } from "../../infra/errors.js"; import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js"; import { @@ -29,7 +29,7 @@ import { } from "../active-jobs.js"; import { resolveCronDeliveryPlan, resolveFailureDestination } from "../delivery-plan.js"; import { resolvePacedNextRunAtMs } from "../pacing.js"; -import { resolveCronExecutionRetryHint } from "../retry-hint.js"; +import { resolveCronExecutionRetryHint, type CronRetryOn } from "../retry-hint.js"; import { createCronRunDiagnosticsFromError, normalizeCronRunDiagnostics, @@ -526,16 +526,11 @@ function resolveCronNextRunWithLowerBound(params: { return Math.max(params.naturalNext, params.lowerBoundMs); } -function resolveRetryConfig(cronConfig?: CronConfig) { - const retry = cronConfig?.retry; +function resolveRetryConfig() { return { - maxAttempts: - typeof retry?.maxAttempts === "number" ? retry.maxAttempts : DEFAULT_MAX_TRANSIENT_RETRIES, - backoffMs: - Array.isArray(retry?.backoffMs) && retry.backoffMs.length > 0 - ? retry.backoffMs - : DEFAULT_ERROR_BACKOFF_SCHEDULE_MS.slice(0, 3), - retryOn: Array.isArray(retry?.retryOn) && retry.retryOn.length > 0 ? retry.retryOn : undefined, + maxAttempts: DEFAULT_MAX_TRANSIENT_RETRIES, + backoffMs: DEFAULT_ERROR_BACKOFF_SCHEDULE_MS.slice(0, 3), + retryOn: undefined, }; } @@ -546,7 +541,7 @@ function resolveTransientCronRetryDecision(params: { executionStarted?: boolean; consecutiveErrors: number | undefined; }): TransientCronRetryDecision { - const retryConfig = resolveRetryConfig(params.cronConfig); + const retryConfig = resolveRetryConfig(); const retryHint = resolveCronExecutionRetryHint({ error: params.error, retryOn: retryConfig.retryOn, @@ -583,7 +578,7 @@ function resolveDisabledHeartbeatOneShotRetryDecision(params: { cronConfig?: CronConfig; consecutiveSkipped: number | undefined; }): DisabledHeartbeatOneShotRetryDecision { - const retryConfig = resolveRetryConfig(params.cronConfig); + const retryConfig = resolveRetryConfig(); const consecutiveSkipped = params.consecutiveSkipped ?? 0; if (consecutiveSkipped > retryConfig.maxAttempts) { return { @@ -1000,7 +995,7 @@ export function applyJobResult( // Apply exponential backoff for errored jobs to prevent retry storms. const backoff = errorBackoffMs( job.state.consecutiveErrors ?? 1, - state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, + DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, ); normalNext = computeNormalNext(); const backoffNext = result.endedAt + backoff; @@ -1044,8 +1039,7 @@ export function applyJobResult( const nextRunAtMs = job.trigger ? Math.max( pacedNextRunAtMs, - result.endedAt + - Math.max(MIN_REFIRE_GAP_MS, resolveCronTriggerMinIntervalMs(state.deps.cronConfig)), + result.endedAt + Math.max(MIN_REFIRE_GAP_MS, resolveCronTriggerMinIntervalMs()), ) : pacedNextRunAtMs; job.state.nextRunAtMs = nextRunAtMs; @@ -1072,10 +1066,7 @@ export function applyJobResult( // would otherwise refire sooner after a successful payload run. const minNext = result.endedAt + - Math.max( - MIN_REFIRE_GAP_MS, - job.trigger ? resolveCronTriggerMinIntervalMs(state.deps.cronConfig) : 0, - ); + Math.max(MIN_REFIRE_GAP_MS, job.trigger ? resolveCronTriggerMinIntervalMs() : 0); job.state.nextRunAtMs = resolveCronNextRunWithLowerBound({ state, job, @@ -1086,10 +1077,7 @@ export function applyJobResult( } else { job.state.nextRunAtMs = naturalNext !== undefined && job.trigger - ? Math.max( - naturalNext, - result.endedAt + resolveCronTriggerMinIntervalMs(state.deps.cronConfig), - ) + ? Math.max(naturalNext, result.endedAt + resolveCronTriggerMinIntervalMs()) : naturalNext; } } else { @@ -1186,10 +1174,7 @@ export function applyTriggerNoFireResult( // Job-level computation keeps per-job cron staggering intact on quiet // ticks; raw schedule math would collapse watchers onto exact boundaries. const naturalNext = computeJobNextRunAtMs(job, result.endedAt); - const floorMs = Math.max( - MIN_REFIRE_GAP_MS, - resolveCronTriggerMinIntervalMs(state.deps.cronConfig), - ); + const floorMs = Math.max(MIN_REFIRE_GAP_MS, resolveCronTriggerMinIntervalMs()); // Quiet ticks still advance the schedule; the floor prevents scripts from // becoming a headless hot loop even when cron resolves inside the window. job.state.nextRunAtMs = @@ -1608,7 +1593,7 @@ async function onAdmittedTimer(state: CronServiceState) { } }; - const concurrency = Math.min(resolveRunConcurrency(state), Math.max(1, dueJobs.length)); + const concurrency = Math.min(resolveRunConcurrency(), Math.max(1, dueJobs.length)); const claimedIndexes = new Set(); let reservationReleaseError: unknown; let setupTimeoutNotified = false; @@ -1941,14 +1926,11 @@ function isRunnableJob(params: { return previousRunAtMs > lastRunAtMs; } -function isErrorBackoffPending(state: CronServiceState, job: CronJob, nowMs: number): boolean { +function isErrorBackoffPending(_state: CronServiceState, job: CronJob, nowMs: number): boolean { if (job.schedule.kind === "at" || resolveJobLastRunStatus(job) !== "error") { return false; } - const backoffUntilMs = resolveJobErrorBackoffUntilMs( - job, - state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, - ); + const backoffUntilMs = resolveJobErrorBackoffUntilMs(job, DEFAULT_ERROR_BACKOFF_SCHEDULE_MS); return backoffUntilMs !== undefined && nowMs < backoffUntilMs; } @@ -1995,10 +1977,7 @@ function deferPendingBackoffMissedCronSlots( ) { continue; } - const backoffUntilMs = resolveJobErrorBackoffUntilMs( - job, - state.deps.cronConfig?.retry?.backoffMs ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, - ); + const backoffUntilMs = resolveJobErrorBackoffUntilMs(job, DEFAULT_ERROR_BACKOFF_SCHEDULE_MS); if (backoffUntilMs === undefined || nowMs >= backoffUntilMs) { continue; } diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index 41f7c9478e7d..22d062e48210 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -1139,7 +1139,6 @@ describe("buildGatewayConnectionDetails", () => { mode: "local", bind: "loopback", tls: { enabled: true }, - handshakeTimeoutMs: 4321, }, } satisfies OpenClawConfig; resolveGatewayPort.mockReturnValue(18800); @@ -1157,7 +1156,7 @@ describe("buildGatewayConnectionDetails", () => { expect(details.url).toBe("wss://127.0.0.1:18800"); expect(details.tlsFingerprint).toBe("sha256:test-local-gateway-fingerprint"); - expect(details.preauthHandshakeTimeoutMs).toBe(4321); + expect(details.preauthHandshakeTimeoutMs).toBeUndefined(); }); it("lets probe details local port override bypass gateway env URL and port", async () => { @@ -1777,27 +1776,6 @@ describe("callGateway error details", () => { expect(startCalls).toBe(0); }); - it("keeps the default wrapper timeout aligned with configured handshake timeout", async () => { - startMode = "silent"; - getRuntimeConfig.mockReturnValue({ - gateway: { mode: "local", bind: "loopback", handshakeTimeoutMs: 30_000 }, - }); - setGatewayNetworkDefaults(); - - vi.useFakeTimers(); - let errMessage = ""; - const promise = callGateway({ method: "health" }).catch((caught: unknown) => { - errMessage = caught instanceof Error ? caught.message : String(caught); - }); - - await vi.advanceTimersByTimeAsync(10_000); - expect(errMessage).toBe(""); - await vi.advanceTimersByTimeAsync(20_000); - await promise; - - expect(errMessage).toContain("gateway timeout after 30000ms"); - }); - it("keeps the default wrapper timeout aligned with env handshake timeout", async () => { const envSnapshot = captureEnv(["OPENCLAW_HANDSHAKE_TIMEOUT_MS"]); try { @@ -2090,17 +2068,6 @@ describe("callGateway error details", () => { expect(stopStarted).toBe(true); }); - it("passes configured gateway handshake timeout to the client watchdog", async () => { - getRuntimeConfig.mockReturnValue({ - gateway: { mode: "local", bind: "loopback", handshakeTimeoutMs: 30_000 }, - }); - setGatewayNetworkDefaults(); - - await callGateway({ method: "health" }); - - expect(lastClientOptions?.preauthHandshakeTimeoutMs).toBe(30_000); - }); - it("does not inject wrapper timeout defaults into expectFinal requests", async () => { setLocalLoopbackGatewayConfig(); diff --git a/src/gateway/call.ts b/src/gateway/call.ts index ca193eb341d9..819624b10e47 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -660,25 +660,17 @@ type ResolvedGatewayCallContext = { remotePasswordFallback?: GatewayRemoteCredentialFallback; }; -function resolveGatewayCallTimeout( - timeoutValue: unknown, - configuredHandshakeTimeoutMs?: number | null, -): { +function resolveGatewayCallTimeout(timeoutValue: unknown): { timeoutMs: number | null; startupTimeoutMs: number; safeTimerTimeoutMs: number; } { - const hasConfiguredHandshakeTimeout = - typeof configuredHandshakeTimeoutMs === "number" && - Number.isFinite(configuredHandshakeTimeoutMs) && - configuredHandshakeTimeoutMs > 0; const hasEnvHandshakeTimeout = Boolean(process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS) || Boolean(process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS); - const resolvedHandshakeTimeoutMs = - hasConfiguredHandshakeTimeout || hasEnvHandshakeTimeout - ? resolvePreauthHandshakeTimeoutMs({ configuredTimeoutMs: configuredHandshakeTimeoutMs }) - : undefined; + const resolvedHandshakeTimeoutMs = hasEnvHandshakeTimeout + ? resolvePreauthHandshakeTimeoutMs() + : undefined; const defaultTimeoutMs = typeof resolvedHandshakeTimeoutMs === "number" && resolvedHandshakeTimeoutMs > 10_000 ? resolvedHandshakeTimeoutMs @@ -1134,7 +1126,6 @@ async function callGatewayWithScopes>( const context = await resolveGatewayCallContext(opts); const { timeoutMs, startupTimeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout( opts.timeoutMs, - context.config.gateway?.handshakeTimeoutMs, ); if (opts.requireLocalBackendSharedAuth && (context.urlOverride || context.isRemoteMode)) { throw new GatewayLocalBackendSharedAuthUnavailableError( @@ -1232,7 +1223,6 @@ async function callGatewayWithScopes>( token, password, tlsFingerprint, - preauthHandshakeTimeoutMs: context.config.gateway?.handshakeTimeoutMs, timeoutMs, startupTimeoutMs, safeTimerTimeoutMs, @@ -1281,9 +1271,6 @@ export async function buildGatewayProbeConnectionDetails( return { ...connectionDetails, ...(tlsFingerprint ? { tlsFingerprint } : {}), - ...(context.config.gateway?.handshakeTimeoutMs - ? { preauthHandshakeTimeoutMs: context.config.gateway.handshakeTimeoutMs } - : {}), }; } diff --git a/src/gateway/client-bootstrap.test.ts b/src/gateway/client-bootstrap.test.ts index a0bb116a7c04..1eebef5d5e83 100644 --- a/src/gateway/client-bootstrap.test.ts +++ b/src/gateway/client-bootstrap.test.ts @@ -88,18 +88,4 @@ describe("resolveGatewayClientBootstrap", () => { urlOverrideSource: undefined, }); }); - - it("carries configured preauth handshake timeout for GatewayClient callers", async () => { - mockState.buildGatewayConnectionDetails.mockReturnValue({ - url: "ws://127.0.0.1:18789", - urlSource: "local loopback", - }); - - const result = await resolveGatewayClientBootstrap({ - config: { gateway: { handshakeTimeoutMs: 30_000 } } as never, - env: process.env, - }); - - expect(result.preauthHandshakeTimeoutMs).toBe(30_000); - }); }); diff --git a/src/gateway/client-bootstrap.ts b/src/gateway/client-bootstrap.ts index 489b22faca86..3daf40ea98d0 100644 --- a/src/gateway/client-bootstrap.ts +++ b/src/gateway/client-bootstrap.ts @@ -52,7 +52,6 @@ export async function resolveGatewayClientBootstrap(params: { return { url: connection.url, urlSource: connection.urlSource, - preauthHandshakeTimeoutMs: params.config.gateway?.handshakeTimeoutMs, auth, }; } diff --git a/src/gateway/config-reload-plan.ts b/src/gateway/config-reload-plan.ts index 71354a0961a6..060fd3bacd0d 100644 --- a/src/gateway/config-reload-plan.ts +++ b/src/gateway/config-reload-plan.ts @@ -73,25 +73,6 @@ const BASE_RELOAD_RULES: ReloadRule[] = [ // permissions) and the bootstrap availability flag, both fixed at document // load, plus live PTYs — none can hot-update a connected client, so a change // must restart the gateway (clients reconnect with a fresh page and CSP). - { - prefix: "gateway.channelHealthCheckMinutes", - kind: "hot", - actions: ["restart-health-monitor"], - }, - { - prefix: "gateway.channelStaleEventThresholdMinutes", - kind: "hot", - actions: ["restart-health-monitor"], - }, - { - prefix: "gateway.channelMaxRestartsPerHour", - kind: "hot", - actions: ["restart-health-monitor"], - }, - // Diagnostics heartbeat reads these from current runtime config. - { prefix: "diagnostics.stuckSessionWarnMs", kind: "none" }, - { prefix: "diagnostics.stuckSessionAbortMs", kind: "none" }, - { prefix: "diagnostics.memoryPressureSnapshot", kind: "hot" }, { prefix: "hooks.gmail", kind: "hot", actions: ["restart-gmail-watcher"] }, { prefix: "hooks", kind: "hot", actions: ["reload-hooks"] }, { @@ -123,14 +104,6 @@ const BASE_RELOAD_RULES: ReloadRule[] = [ kind: "hot", actions: ["restart-heartbeat"], }, - // Auth cooldown readers resolve values from the active runtime config for each - // auth failure decision, so cooldown tuning needs a snapshot refresh but not - // a gateway restart. - { prefix: "auth.cooldowns", kind: "hot" }, - // Worktree cleanup limits are read from the runtime config at each gc pass - // (hourly sweep and worktrees.gc), so a Settings stepper edit needs only a - // snapshot refresh; a restart here would drop live sessions per click. - { prefix: "worktrees", kind: "hot" }, { prefix: "agents.list", kind: "hot", diff --git a/src/gateway/config-reload-settings.ts b/src/gateway/config-reload-settings.ts index 4a596438cf7d..65bd25fd0a76 100644 --- a/src/gateway/config-reload-settings.ts +++ b/src/gateway/config-reload-settings.ts @@ -20,10 +20,5 @@ export function resolveGatewayReloadSettings(cfg: OpenClawConfig): GatewayReload rawMode === "off" || rawMode === "restart" || rawMode === "hot" || rawMode === "hybrid" ? rawMode : DEFAULT_RELOAD_SETTINGS.mode; - const debounceRaw = cfg.gateway?.reload?.debounceMs; - const debounceMs = - typeof debounceRaw === "number" && Number.isFinite(debounceRaw) - ? Math.max(0, Math.floor(debounceRaw)) - : DEFAULT_RELOAD_SETTINGS.debounceMs; - return { mode, debounceMs }; + return { mode, debounceMs: DEFAULT_RELOAD_SETTINGS.debounceMs }; } diff --git a/src/gateway/config-reload.test.ts b/src/gateway/config-reload.test.ts index dba7d89486d7..c31843ee752f 100644 --- a/src/gateway/config-reload.test.ts +++ b/src/gateway/config-reload.test.ts @@ -282,27 +282,12 @@ describe("buildGatewayReloadPlan", () => { restart: true, reason: "models.pricing.enabled", }, - { - path: "auth.cooldowns.billingBackoffHours", - restart: false, - hot: "auth.cooldowns.billingBackoffHours", - }, { path: "agents.defaults.model", restart: false, hot: "agents.defaults.model", restartHeartbeat: true, }, - { - path: "worktrees.cleanup.maxCount", - restart: false, - hot: "worktrees.cleanup.maxCount", - }, - { - path: "worktrees.cleanup.maxTotalSizeGb", - restart: false, - hot: "worktrees.cleanup.maxTotalSizeGb", - }, { path: "unknownField", restart: true, @@ -343,10 +328,6 @@ describe("buildGatewayReloadPlan", () => { path: "hooks.gmail.account", expected: { restartGmailWatcher: true, reloadHooks: true }, }, - { - path: "gateway.channelHealthCheckMinutes", - expected: { restartHealthMonitor: true }, - }, { path: "mcp.servers.context7.command", expected: { disposeMcpRuntimes: true }, @@ -371,10 +352,6 @@ describe("buildGatewayReloadPlan", () => { path: "plugins.entries.lossless-claw.config.mode", expected: { reloadPlugins: true, disposeMcpRuntimes: true }, }, - { - path: "diagnostics.memoryPressureSnapshot", - expected: {}, - }, ])("keeps hot-reload actions for $path", ({ path, expected }) => { const plan = buildGatewayReloadPlan([path]); @@ -387,20 +364,17 @@ describe("buildGatewayReloadPlan", () => { }); }); - it.each([ - "gateway.remote.url", - "secrets.providers.default.path", - "tui.footer.showRemoteHost", - "diagnostics.stuckSessionWarnMs", - "diagnostics.stuckSessionAbortMs", - ])("keeps runtime-irrelevant path as a no-op: %s", (path) => { - const plan = buildGatewayReloadPlan([path]); + it.each(["gateway.remote.url", "secrets.providers.default.path", "tui.footer.showRemoteHost"])( + "keeps runtime-irrelevant path as a no-op: %s", + (path) => { + const plan = buildGatewayReloadPlan([path]); - expect(plan.restartGateway).toBe(false); - expect(plan.restartReasons).toStrictEqual([]); - expect(plan.hotReasons).toStrictEqual([]); - expect(plan.noopPaths).toEqual([path]); - }); + expect(plan.restartGateway).toBe(false); + expect(plan.restartReasons).toStrictEqual([]); + expect(plan.hotReasons).toStrictEqual([]); + expect(plan.noopPaths).toEqual([path]); + }, + ); it("treats plugin install timestamp-only changes as no-ops", () => { const paths = [ @@ -644,15 +618,15 @@ function makeSnapshot(partial: Partial = {}): ConfigFileSnap function makeZeroDebounceHookSnapshot(hash: string): ConfigFileSnapshot { return makeSnapshot({ sourceConfig: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, runtimeConfig: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, hash, @@ -662,9 +636,9 @@ function makeZeroDebounceHookSnapshot(hash: string): ConfigFileSnapshot { function makeZeroDebounceHookWrite(persistedHash: string): ConfigWriteNotification { return { configPath: "/tmp/openclaw.json", - sourceConfig: { gateway: { reload: { debounceMs: 0 } }, hooks: { enabled: true } }, + sourceConfig: { gateway: { reload: {} }, hooks: { enabled: true } }, runtimeConfig: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, persistedHash, @@ -786,8 +760,9 @@ function createReloaderHarness( warn: vi.fn(), error: vi.fn(), }; - const initialConfig = options.initialConfig ?? { gateway: { reload: { debounceMs: 0 } } }; + const initialConfig = options.initialConfig ?? { gateway: { reload: {} } }; const reloader = startGatewayConfigReloader({ + testDebounceMs: 0, initialConfig, initialCompareConfig: options.initialCompareConfig, initialSnapshotRawHash: @@ -886,10 +861,10 @@ describe("startGatewayConfigReloader", () => { it("journals valid external watcher edits and advances the snapshot slot", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, parsed: nextConfig, hash: "next-raw-hash" }), @@ -921,10 +896,10 @@ describe("startGatewayConfigReloader", () => { it("does not duplicate another OpenClaw process's journaled write", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }; const harness = createReloaderHarness( vi.fn(async () => @@ -954,7 +929,7 @@ describe("startGatewayConfigReloader", () => { }); it("journals invalid external watcher edits without advancing the snapshot slot", async () => { - const initialConfig: OpenClawConfig = { gateway: { reload: { debounceMs: 0 } } }; + const initialConfig: OpenClawConfig = { gateway: { reload: {} } }; const invalid = makeSnapshot({ valid: false, hash: "invalid-raw-hash", @@ -1018,7 +993,7 @@ describe("startGatewayConfigReloader", () => { it("uses the last observed hash when a valid edit follows an invalid one", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; const invalid = makeSnapshot({ valid: false, @@ -1026,7 +1001,7 @@ describe("startGatewayConfigReloader", () => { issues: [{ path: "gateway.port", message: "expected number" }], }); const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }; let activeSnapshot = invalid; const harness = createReloaderHarness( @@ -1057,7 +1032,7 @@ describe("startGatewayConfigReloader", () => { it("journals a return to the accepted bytes after an invalid edit", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; const invalid = makeSnapshot({ valid: false, @@ -1093,7 +1068,7 @@ describe("startGatewayConfigReloader", () => { it("journals restoration after startup observed a missing config", async () => { const acceptedConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; configAuditMocks.readSnapshot.mockReturnValue({ configPath: "/tmp/openclaw.json", @@ -1301,7 +1276,7 @@ describe("startGatewayConfigReloader", () => { }); it("ignores valid watcher events whose source hash did not change", async () => { - const initialConfig: OpenClawConfig = { gateway: { reload: { debounceMs: 0 } } }; + const initialConfig: OpenClawConfig = { gateway: { reload: {} } }; const snapshot = makeSnapshot({ config: initialConfig, hash: "unchanged-raw-hash" }); const harness = createReloaderHarness( vi.fn(async () => snapshot), @@ -1321,7 +1296,7 @@ describe("startGatewayConfigReloader", () => { }); it("journals opaque watcher edits when only the authored bytes changed", async () => { - const initialConfig: OpenClawConfig = { gateway: { reload: { debounceMs: 0 } } }; + const initialConfig: OpenClawConfig = { gateway: { reload: {} } }; const snapshot = makeSnapshot({ config: initialConfig, sourceConfig: initialConfig, @@ -1383,12 +1358,12 @@ describe("startGatewayConfigReloader", () => { it("notifies change listeners for every accepted external edit, including runtime-skipped ones", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, }; // ui.* is a no-op reload class: the runtime snapshot refreshes without a // hot reload or restart — exactly the agent-changes-theme case. const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, ui: { prefs: { themeMode: "dark" } }, }; const readSnapshot = vi.fn(async () => @@ -1416,10 +1391,10 @@ describe("startGatewayConfigReloader", () => { it("notifies change listeners when reload mode off skips the runtime apply", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { mode: "off", debounceMs: 0 } }, + gateway: { reload: { mode: "off" } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { mode: "off", debounceMs: 0 } }, + gateway: { reload: { mode: "off" } }, ui: { prefs: { themeMode: "light" } }, }; const readSnapshot = vi.fn(async () => @@ -1438,7 +1413,7 @@ describe("startGatewayConfigReloader", () => { it("notifies lifecycle owners when a persisted edit reverts to the current baseline", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18789 }, + gateway: { reload: {}, port: 18789 }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: initialConfig, hash: "reverted-restart-edit" }), @@ -1460,7 +1435,7 @@ describe("startGatewayConfigReloader", () => { it("reaccepts a same-hash watcher echo after synchronously pausing lifecycle work", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, } satisfies OpenClawConfig; const onConfigCandidateObserved = vi.fn(); const readSnapshot = vi.fn(async () => @@ -1485,7 +1460,7 @@ describe("startGatewayConfigReloader", () => { it("revalidates changed effective config when an accepted write hash is unchanged", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 }, port: 18_789 }, + gateway: { reload: {}, port: 18_789 }, } satisfies OpenClawConfig; const unavailableSecret = { source: "env" as const, @@ -1494,7 +1469,7 @@ describe("startGatewayConfigReloader", () => { }; const effectiveConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, port: 19_001, auth: { mode: "token" as const, token: unavailableSecret }, }, @@ -1551,11 +1526,11 @@ describe("startGatewayConfigReloader", () => { it("applies a superseded runtime plan before baseline-only acceptance", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, agents: { defaults: { sandbox: { mode: "off" as const } } }, } satisfies OpenClawConfig; const appliedConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, agents: { defaults: { sandbox: { mode: "all" as const } } }, } satisfies OpenClawConfig; const terminalPolicy = createTerminalLaunchPolicy(initialConfig); @@ -1622,7 +1597,7 @@ describe("startGatewayConfigReloader", () => { "applies a committed runtime owner before rejecting a superseding %s snapshot", async (_, rejectedSnapshot) => { const initialConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, agents: { defaults: { sandbox: { mode: "off" as const } } }, } satisfies OpenClawConfig; const appliedConfig = { @@ -1673,11 +1648,11 @@ describe("startGatewayConfigReloader", () => { it("applies a superseded runtime owner before preparing a restart candidate", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, agents: { defaults: { sandbox: { mode: "off" as const } } }, } satisfies OpenClawConfig; const appliedConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, agents: { defaults: { sandbox: { mode: "all" as const } } }, } satisfies OpenClawConfig; const restartConfig = { @@ -1745,7 +1720,7 @@ describe("startGatewayConfigReloader", () => { it("does not reaccept an invalid snapshot whose root hash matches the startup write", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, } satisfies OpenClawConfig; const readSnapshot = vi.fn(async () => makeSnapshot({ config: initialConfig, valid: false, hash: "accepted-write" }), @@ -1768,11 +1743,11 @@ describe("startGatewayConfigReloader", () => { "revokes a slow external %s transaction when a newer watcher burst reverts it", async (kind) => { const initialConfig = { - gateway: { reload: { mode: "off" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "off" as const } }, hooks: { enabled: true, token: "test-token", path: "/old" }, } satisfies OpenClawConfig; const configA = { - gateway: { reload: { mode: "hot" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "hot" as const } }, hooks: { enabled: true, token: "test-token", @@ -1850,7 +1825,7 @@ describe("startGatewayConfigReloader", () => { it("plans the reverse hot reload when config A commits before config B supersedes its tail", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/old" }, } satisfies OpenClawConfig; const configA = { @@ -1916,7 +1891,7 @@ describe("startGatewayConfigReloader", () => { const envKey = "OPENCLAW_TEST_COMMITTED_ENV_SOURCE"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "old" }; const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/old" }, env: { vars: { [envKey]: "old" } }, } satisfies OpenClawConfig; @@ -1977,11 +1952,11 @@ describe("startGatewayConfigReloader", () => { it("rereads the filesystem when a watcher event supersedes a queued in-process write", async () => { const initialConfig = { - gateway: { reload: { mode: "off" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "off" as const } }, hooks: { enabled: true, token: "test-token", path: "/old" }, } satisfies OpenClawConfig; const queuedConfig = { - gateway: { reload: { mode: "hot" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "hot" as const } }, hooks: { enabled: true, token: "test-token", path: "/queued" }, } satisfies OpenClawConfig; const externalConfig = structuredClone(initialConfig); @@ -2019,14 +1994,14 @@ describe("startGatewayConfigReloader", () => { it("does not restart stale external config A before rejecting invalid SecretRef config B", async () => { const initialConfig = { - gateway: { reload: { mode: "off" as const, debounceMs: 0 }, port: 18789 }, + gateway: { reload: { mode: "off" as const }, port: 18789 }, } satisfies OpenClawConfig; const configA = { - gateway: { reload: { mode: "restart" as const, debounceMs: 0 }, port: 18790 }, + gateway: { reload: { mode: "restart" as const }, port: 18790 }, } satisfies OpenClawConfig; const configB = { gateway: { - reload: { mode: "restart" as const, debounceMs: 0 }, + reload: { mode: "restart" as const }, port: 18791, auth: { mode: "token" as const, @@ -2103,11 +2078,11 @@ describe("startGatewayConfigReloader", () => { it("keeps an unlink epoch through a missing-file retry before accepting config B", async () => { const initialConfig = { - gateway: { reload: { mode: "off" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "off" as const } }, hooks: { enabled: true, token: "test-token", path: "/old" }, } satisfies OpenClawConfig; const configA = { - gateway: { reload: { mode: "hot" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "hot" as const } }, hooks: { enabled: true, token: "test-token", path: "/a" }, } satisfies OpenClawConfig; const configB = structuredClone(initialConfig); @@ -2170,10 +2145,10 @@ describe("startGatewayConfigReloader", () => { it("does not accept stale config A when config B arrives during plugin-index discovery", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, } satisfies OpenClawConfig; const invalidConfigB = { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, } satisfies OpenClawConfig; const readSnapshot = vi .fn<() => Promise>() @@ -2235,11 +2210,11 @@ describe("startGatewayConfigReloader", () => { it("waits for an active reload transaction before stop resolves", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/old" }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/next" }, }; const readSnapshot = vi.fn(async () => @@ -2283,11 +2258,11 @@ describe("startGatewayConfigReloader", () => { it("notifies lifecycle owners for no-op sandbox policy changes", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, agents: { defaults: { sandbox: { mode: "off" } } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, agents: { defaults: { sandbox: { mode: "all" } } }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "sandbox" })); @@ -2309,11 +2284,11 @@ describe("startGatewayConfigReloader", () => { it("commits runtime snapshot changes for no-op visible reply reloads", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { visibleReplies: "automatic" }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { visibleReplies: "message_tool" }, }; const readSnapshot = vi.fn(async () => @@ -2361,11 +2336,11 @@ describe("startGatewayConfigReloader", () => { }, ]); const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, channels: { mattermost: { accounts: { alpha: { enabled: false } } } }, } as OpenClawConfig; const nextConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, channels: { mattermost: { accounts: { alpha: { enabled: true } } } }, } as OpenClawConfig; const harness = createReloaderHarness( @@ -2389,7 +2364,7 @@ describe("startGatewayConfigReloader", () => { it("plans one immutable runtime override snapshot per candidate", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, meta: { lastTouchedVersion: "initial" }, messages: { visibleReplies: "automatic" }, }; @@ -2459,7 +2434,7 @@ describe("startGatewayConfigReloader", () => { it("does not publish a restart-only hot-mode candidate through a later safe edit", async () => { const initialConfig: OpenClawConfig = { gateway: { - reload: { mode: "hot", debounceMs: 0 }, + reload: { mode: "hot" }, auth: { mode: "token", token: "old-token" }, }, logging: { level: "info" }, @@ -2511,12 +2486,12 @@ describe("startGatewayConfigReloader", () => { it("notifies lifecycle owners before hot reload and commits after success", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, agents: { defaults: { sandbox: { mode: "off" } } }, hooks: { enabled: false }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, agents: { defaults: { sandbox: { mode: "all" } } }, hooks: { enabled: true }, }; @@ -2540,10 +2515,10 @@ describe("startGatewayConfigReloader", () => { it("notifies lifecycle owners before queuing a terminal disable restart", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: false } }, + gateway: { reload: {}, terminal: { enabled: false } }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "terminal" })); const harness = createReloaderHarness(readSnapshot, { initialConfig }); @@ -2572,10 +2547,10 @@ describe("startGatewayConfigReloader", () => { releaseRestart = resolve; }); const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: {}, terminal: { enabled: true } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, terminal: { enabled: false } }, + gateway: { reload: {}, terminal: { enabled: false } }, }; const harness = createReloaderHarness( async () => makeSnapshot({ config: nextConfig, hash: "restart-root" }), @@ -2601,10 +2576,10 @@ describe("startGatewayConfigReloader", () => { it("does not notify lifecycle owners when reload mode ignores the change", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { mode: "off", debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: { mode: "off" }, terminal: { enabled: true } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { mode: "off", debounceMs: 0 }, terminal: { enabled: false } }, + gateway: { reload: { mode: "off" }, terminal: { enabled: false } }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "off" })); const harness = createReloaderHarness(readSnapshot, { initialConfig }); @@ -2620,10 +2595,10 @@ describe("startGatewayConfigReloader", () => { it("does not notify lifecycle owners when hot mode ignores a restart-only change", async () => { const initialConfig: OpenClawConfig = { - gateway: { reload: { mode: "hot", debounceMs: 0 }, terminal: { enabled: true } }, + gateway: { reload: { mode: "hot" }, terminal: { enabled: true } }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { mode: "hot", debounceMs: 0 }, terminal: { enabled: false } }, + gateway: { reload: { mode: "hot" }, terminal: { enabled: false } }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "hot" })); const harness = createReloaderHarness(readSnapshot, { initialConfig }); @@ -2644,7 +2619,7 @@ describe("startGatewayConfigReloader", () => { .mockResolvedValueOnce( makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, hash: "next-1", @@ -2685,7 +2660,7 @@ describe("startGatewayConfigReloader", () => { it("contains restart callback failures and retries the same persisted config", async () => { const snapshot = makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }, hash: "restart-1", }); @@ -2733,7 +2708,7 @@ describe("startGatewayConfigReloader", () => { it("logs expected restart supersession without reporting a reload failure", async () => { const snapshot = makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }, hash: "restart-superseded-1", }); @@ -2787,7 +2762,7 @@ describe("startGatewayConfigReloader", () => { it("skips plugin-local invalid reloads without degraded mode", async () => { const activeConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, agents: { defaults: { model: "gpt-5.4" } }, plugins: { entries: { @@ -2854,7 +2829,7 @@ describe("startGatewayConfigReloader", () => { it("promotes valid external config edits after they are accepted", async () => { const acceptedSnapshot = makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, hash: "external-good-1", @@ -2876,42 +2851,10 @@ describe("startGatewayConfigReloader", () => { await reloader.stop(); }); - it("hot-reloads direct diagnostics memory pressure snapshot edits", async () => { - const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, - diagnostics: { memoryPressureSnapshot: false }, - }; - const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( - makeSnapshot({ - config: nextConfig, - sourceConfig: nextConfig, - runtimeConfig: nextConfig, - hash: "diagnostics-memory-pressure-snapshot-1", - }), - ); - const previousConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, - diagnostics: { memoryPressureSnapshot: true }, - }; - const harness = createReloaderHarness(readSnapshot, { - initialCompareConfig: previousConfig, - }); - - harness.watcher.emit("change"); - await vi.runAllTimersAsync(); - - expect(harness.onRestart).not.toHaveBeenCalled(); - const [plan, hotConfig] = getOnlyHotReloadCall(harness); - expect(plan.hotReasons).toEqual(["diagnostics.memoryPressureSnapshot"]); - expect(hotConfig).toBe(nextConfig); - - await harness.reloader.stop(); - }); - it("does not promote external config edits when hot reload rejects them", async () => { const acceptedSnapshot = makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, hash: "external-rejected-1", @@ -2972,7 +2915,7 @@ describe("startGatewayConfigReloader", () => { it("keeps accepted external config reloads applied when last-known-good promotion fails", async () => { const acceptedSnapshot = makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }, hash: "external-promotion-fails-1", @@ -3007,13 +2950,13 @@ describe("startGatewayConfigReloader", () => { .mockResolvedValueOnce( makeSnapshot({ sourceConfig: { - gateway: { reload: { debounceMs: 0 }, port: 19001 }, + gateway: { reload: {}, port: 19001 }, }, runtimeConfig: { - gateway: { reload: { debounceMs: 0 }, port: 19001 }, + gateway: { reload: {}, port: 19001 }, }, config: { - gateway: { reload: { debounceMs: 0 }, port: 19001 }, + gateway: { reload: {}, port: 19001 }, }, hash: "external-1", }), @@ -3088,7 +3031,7 @@ describe("startGatewayConfigReloader", () => { const envKey = "OPENCLAW_TEST_RELOAD_TRANSACTION_ENV"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "old" }; const initialConfig = { - gateway: { reload: { debounceMs: 0, mode: testCase.reloadMode } }, + gateway: { reload: { mode: testCase.reloadMode } }, env: { vars: { [envKey]: "old" } }, } satisfies OpenClawConfig; const nextConfig = { @@ -3131,7 +3074,7 @@ describe("startGatewayConfigReloader", () => { const envKey = "OPENCLAW_TEST_RELOAD_ENV_COMMIT_EDGE"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "old" }; const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test", path: "/old" }, env: { vars: { [envKey]: "old" } }, } satisfies OpenClawConfig; @@ -3183,7 +3126,7 @@ describe("startGatewayConfigReloader", () => { const envKey = "OPENCLAW_TEST_SUPERSEDED_RELOAD_ENV"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "old" }; const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, env: { vars: { [envKey]: "old" } }, } satisfies OpenClawConfig; const nextConfig = { @@ -3235,7 +3178,7 @@ describe("startGatewayConfigReloader", () => { const envKey = "OPENCLAW_TEST_INTERLEAVED_RELOAD_ENV"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "a" }; const makeConfig = (value: string, port: number): OpenClawConfig => ({ - gateway: { reload: { debounceMs: 0 }, port }, + gateway: { reload: {}, port }, env: { vars: { [envKey]: value } }, }); const configA = makeConfig("a", 18_789); @@ -3313,7 +3256,7 @@ describe("startGatewayConfigReloader", () => { expect(plan.restartGateway).toBe(true); expect(plan.restartReasons).toEqual(["plugin runtime contract changed"]); expect(nextConfig).toEqual({ - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true }, }); @@ -3384,7 +3327,7 @@ describe("startGatewayConfigReloader", () => { it("discards slow in-process intent when the watcher proves different bytes", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, } satisfies OpenClawConfig; let releasePluginRead = () => {}; let recordPluginReadStarted: (() => void) | undefined; @@ -3429,7 +3372,7 @@ describe("startGatewayConfigReloader", () => { it("uses a freshly resolved snapshot when the root hash still matches writer intent", async () => { const freshConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: false }, } satisfies OpenClawConfig; const readSnapshot = vi.fn(async () => @@ -3472,13 +3415,13 @@ describe("startGatewayConfigReloader", () => { }; const sourceConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: secretRef }, }, } satisfies OpenClawConfig; const runtimeConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: "resolved-test-token" }, }, } satisfies OpenClawConfig; @@ -3523,7 +3466,7 @@ describe("startGatewayConfigReloader", () => { it("publishes a managed source edit when runtime overlays mask every effective change", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, logging: { level: "info" as const }, } satisfies OpenClawConfig; const sourceConfig = { @@ -3561,7 +3504,7 @@ describe("startGatewayConfigReloader", () => { it("does not publish a masked source edit when acceptance fails", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, logging: { level: "info" as const }, } satisfies OpenClawConfig; const sourceConfig = { @@ -3602,7 +3545,7 @@ describe("startGatewayConfigReloader", () => { it("rolls back masked source publication when superseded after acceptance", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, logging: { level: "info" as const }, } satisfies OpenClawConfig; const sourceConfig = { @@ -3742,13 +3685,13 @@ describe("startGatewayConfigReloader", () => { }; const sourceConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: secretRef }, }, } satisfies OpenClawConfig; const runtimeConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: "resolved-direct-token" }, }, } satisfies OpenClawConfig; @@ -3782,13 +3725,13 @@ describe("startGatewayConfigReloader", () => { }; const sourceConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: secretRef }, }, } satisfies OpenClawConfig; const runtimeConfig = { gateway: { - reload: { debounceMs: 0 }, + reload: {}, auth: { mode: "token" as const, token: "resolved-replay-token" }, }, } satisfies OpenClawConfig; @@ -3984,7 +3927,7 @@ describe("startGatewayConfigReloader", () => { resolvedAt: "2026-04-22T00:00:00.000Z", }; const sourceConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 }, auth: { mode: "token" } }, + gateway: { reload: {}, auth: { mode: "token" } }, plugins: { installs: { "lossless-claw": baseInstall, @@ -4026,7 +3969,7 @@ describe("startGatewayConfigReloader", () => { }, runtimeConfig: { ...sourceConfig, - gateway: { reload: { debounceMs: 0 }, auth: { mode: "token", token: "runtime" } }, + gateway: { reload: {}, auth: { mode: "token", token: "runtime" } }, plugins: { ...sourceConfig.plugins, entries: { @@ -4064,7 +4007,7 @@ describe("startGatewayConfigReloader", () => { it("does not suppress functional install changes that collide with timestamp paths", async () => { const sourceConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { installs: { lossless: { @@ -4075,7 +4018,7 @@ describe("startGatewayConfigReloader", () => { }, }; const nextSourceConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { installs: { lossless: { @@ -4128,7 +4071,7 @@ describe("startGatewayConfigReloader", () => { it("queues restart when an external plugin source write only changes the managed index", async () => { const activeConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { allow: ["lossless-claw"], entries: { @@ -4173,7 +4116,7 @@ describe("startGatewayConfigReloader", () => { it("reloads explicitly signaled plugin metadata when config bytes stay identical", async () => { const activeConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, }; const readSnapshot = vi.fn(async () => makeSnapshot({ @@ -4213,7 +4156,7 @@ describe("startGatewayConfigReloader", () => { it("keeps external plugin policy-only writes on the hot reload path", async () => { const previousConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { entries: { telegram: { enabled: false }, @@ -4221,7 +4164,7 @@ describe("startGatewayConfigReloader", () => { }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { entries: { telegram: { enabled: true }, @@ -4264,66 +4207,15 @@ describe("startGatewayConfigReloader", () => { await harness.reloader.stop(); }); - it("keeps external auth cooldown writes on the hot reload path", async () => { - const previousConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, - auth: { - cooldowns: { - billingBackoffHours: 1, - billingBackoffHoursByProvider: { anthropic: 2 }, - }, - }, - }; - const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, - auth: { - cooldowns: { - billingBackoffHours: 3, - billingBackoffHoursByProvider: { anthropic: 4 }, - }, - }, - }; - const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( - makeSnapshot({ - sourceConfig: nextConfig, - runtimeConfig: nextConfig, - config: nextConfig, - hash: "external-auth-cooldowns-1", - }), - ); - const harness = createReloaderHarness(readSnapshot, { - initialCompareConfig: previousConfig, - }); - - harness.watcher.emit("change"); - await vi.runOnlyPendingTimersAsync(); - - expect(harness.onRestart).not.toHaveBeenCalled(); - const [plan, hotConfig] = getOnlyHotReloadCall(harness); - expect(plan.changedPaths).toEqual([ - "auth.cooldowns.billingBackoffHours", - "auth.cooldowns.billingBackoffHoursByProvider.anthropic", - ]); - expect(plan.restartGateway).toBe(false); - expect(plan.hotReasons).toEqual([ - "auth.cooldowns.billingBackoffHours", - "auth.cooldowns.billingBackoffHoursByProvider.anthropic", - ]); - expect(plan.noopPaths).toStrictEqual([]); - expect(hotConfig).toBe(nextConfig); - - await harness.reloader.stop(); - }); - it("queues restart when an external plugin source write also changes plugin config", async () => { const previousConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { allow: ["lossless-claw"], }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, plugins: { allow: ["lossless-claw"], entries: { @@ -4370,13 +4262,13 @@ describe("startGatewayConfigReloader", () => { const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( makeSnapshot({ sourceConfig: { - gateway: { reload: { debounceMs: 0 }, port: 19002 }, + gateway: { reload: {}, port: 19002 }, }, runtimeConfig: { - gateway: { reload: { debounceMs: 0 }, port: 19002 }, + gateway: { reload: {}, port: 19002 }, }, config: { - gateway: { reload: { debounceMs: 0 }, port: 19002 }, + gateway: { reload: {}, port: 19002 }, }, hash: "racing-external-edit", }), @@ -4397,7 +4289,7 @@ describe("startGatewayConfigReloader", () => { it("dedupes only the first watcher reread for startup internal writes", async () => { const startupConfig = { - gateway: { reload: { debounceMs: 0 }, auth: { mode: "token" as const, token: "startup" } }, + gateway: { reload: {}, auth: { mode: "token" as const, token: "startup" } }, } satisfies OpenClawConfig; const readSnapshot = vi .fn<() => Promise>() @@ -4410,7 +4302,7 @@ describe("startGatewayConfigReloader", () => { .mockResolvedValueOnce( makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 }, port: 19001 }, + gateway: { reload: {}, port: 19001 }, }, hash: "startup-internal-1", }), @@ -4461,7 +4353,7 @@ describe("startGatewayConfigReloader", () => { const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 }, auth: { mode: "token", token: "startup" } }, + gateway: { reload: {}, auth: { mode: "token", token: "startup" } }, }, hash: "startup-internal-1", }), @@ -4508,7 +4400,7 @@ describe("startGatewayConfigReloader watcher error recovery", () => { const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const readSnapshot = vi.fn(async () => makeSnapshot()); const reloader = startGatewayConfigReloader({ - initialConfig: { gateway: { reload: { debounceMs: 0 } } }, + initialConfig: { gateway: { reload: {} } }, initialSnapshotRawHash: "initial-raw-hash", initialAuthoredConfig: {}, initialSnapshotValid: true, @@ -4876,7 +4768,7 @@ describe("startGatewayConfigReloader skills invalidation", () => { const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, skills: { allowBundled: ["gog"] }, }, hash: "skills-change-1", @@ -4899,7 +4791,7 @@ describe("startGatewayConfigReloader skills invalidation", () => { const readSnapshot = vi.fn<() => Promise>().mockResolvedValueOnce( makeSnapshot({ config: { - gateway: { reload: { debounceMs: 0 }, port: 18790 }, + gateway: { reload: {}, port: 18790 }, }, hash: "unrelated-change-1", }), diff --git a/src/gateway/config-reload.ts b/src/gateway/config-reload.ts index a097dba80c0a..a9890f4cf182 100644 --- a/src/gateway/config-reload.ts +++ b/src/gateway/config-reload.ts @@ -164,6 +164,8 @@ export function startGatewayConfigReloader(opts: { initialAuthoredConfig: unknown; initialSnapshotValid: boolean; initialSnapshotIssues: ConfigFileSnapshot["issues"]; + /** Keeps watcher-heavy tests immediate without reopening config-level debounce tuning. */ + testDebounceMs?: number; prepareConfigCandidate?: (params: { runtimeConfig: OpenClawConfig; sourceConfig: OpenClawConfig; @@ -259,7 +261,13 @@ export function startGatewayConfigReloader(opts: { let currentReapplyRuntimeOverlays = initialCandidate?.reapplyRuntimeOverlays ?? ((config: OpenClawConfig) => config); let currentRuntimeRefresh: RuntimeConfigSnapshotRefreshOptions | undefined; - let settings = resolveGatewayReloadSettings(currentConfig); + const resolveSettings = (config: OpenClawConfig) => { + const resolved = resolveGatewayReloadSettings(config); + return opts.testDebounceMs === undefined + ? resolved + : { ...resolved, debounceMs: opts.testDebounceMs }; + }; + let settings = resolveSettings(currentConfig); let debounceTimer: ReturnType | null = null; let pending = false; let running = false; @@ -467,7 +475,7 @@ export function startGatewayConfigReloader(opts: { let committedRuntimeConfig: OpenClawConfig | null = null; let publishedRuntimeEnv: ConfigRuntimeEnvPublication | undefined; let runtimeEnvCommitted = false; - const nextSettings = resolveGatewayReloadSettings(nextConfig); + const nextSettings = resolveSettings(nextConfig); const isCurrent = () => configWriteEpoch === transactionEpoch; const assertCurrent = () => { if (!isCurrent()) { @@ -513,7 +521,7 @@ export function startGatewayConfigReloader(opts: { currentReapplyRuntimeOverlays = ownership.reapplyRuntimeOverlays; currentRuntimeRefresh = ownership.runtimeRefresh; currentPluginInstallRecords = nextPluginInstallRecords; - settings = resolveGatewayReloadSettings(runtimeConfig); + settings = resolveSettings(runtimeConfig); appliedRevision.defer(plan, nextConfigRevisionHash); }, }; @@ -636,9 +644,7 @@ export function startGatewayConfigReloader(opts: { currentReapplyRuntimeOverlays = ownership.reapplyRuntimeOverlays; currentRuntimeRefresh = ownership.runtimeRefresh; currentPluginInstallRecords = nextPluginInstallRecords; - settings = committedRuntimeConfig - ? resolveGatewayReloadSettings(committedRuntimeConfig) - : nextSettings; + settings = committedRuntimeConfig ? resolveSettings(committedRuntimeConfig) : nextSettings; commitPublishedRuntimeEnv(); } catch (error) { ownership.rollbackRuntimeEnv(); diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index f7573796db8b..961236726924 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -4439,7 +4439,7 @@ async function sanitizeAuthConfig(params: { } } - if (!profiles && !order && !auth.cooldowns) { + if (!profiles && !order) { return undefined; } return { diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index 6601e1eab649..d66744d3c36d 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -63,10 +63,6 @@ export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | n if (trimmed === "/") { throw new Error("hooks.path may not be '/'"); } - const maxBodyBytes = - cfg.hooks?.maxBodyBytes && cfg.hooks.maxBodyBytes > 0 - ? cfg.hooks.maxBodyBytes - : DEFAULT_HOOKS_MAX_BODY_BYTES; const mappings = resolveHookMappings(cfg.hooks); const defaultAgentId = resolveDefaultAgentId(cfg); const knownAgentIds = resolveKnownAgentIds(cfg, defaultAgentId); @@ -99,7 +95,7 @@ export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | n return { basePath: trimmed, token, - maxBodyBytes, + maxBodyBytes: DEFAULT_HOOKS_MAX_BODY_BYTES, mappings, agentPolicy: { defaultAgentId, diff --git a/src/gateway/openai-http.image-budget.test.ts b/src/gateway/openai-http.image-budget.test.ts deleted file mode 100644 index b8cf70d7f19d..000000000000 --- a/src/gateway/openai-http.image-budget.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Tests image budget handling for OpenAI HTTP gateway requests. - */ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const extractImageContentFromSourceMock = vi.fn(); - -vi.mock("../media/input-files.js", async () => { - const actual = - await vi.importActual("../media/input-files.js"); - return { - ...actual, - extractImageContentFromSource: (...args: unknown[]) => - extractImageContentFromSourceMock(...args), - }; -}); - -import { testOnlyOpenAiHttp } from "./openai-http.js"; - -describe("openai image budget accounting", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("counts normalized base64 image bytes against maxTotalImageBytes", async () => { - extractImageContentFromSourceMock.mockResolvedValueOnce({ - type: "image", - data: Buffer.alloc(10, 1).toString("base64"), - mimeType: "image/jpeg", - }); - - const limits = testOnlyOpenAiHttp.resolveOpenAiChatCompletionsLimits({ - maxTotalImageBytes: 5, - }); - - await expect( - testOnlyOpenAiHttp.resolveImagesForRequest( - { - urls: ["data:image/heic;base64,QUJD"], - }, - limits, - ), - ).rejects.toThrow(/Total image payload too large/); - }); - - it("uses default image limits for non-finite configured caps", () => { - const limits = testOnlyOpenAiHttp.resolveOpenAiChatCompletionsLimits({ - maxImageParts: Number.NaN, - maxTotalImageBytes: Number.POSITIVE_INFINITY, - }); - - expect(limits.maxImageParts).toBe(8); - expect(limits.maxTotalImageBytes).toBe(20 * 1024 * 1024); - }); - - it("does not double-count unchanged base64 image payloads", async () => { - extractImageContentFromSourceMock.mockResolvedValueOnce({ - type: "image", - data: "QUJDRA==", - mimeType: "image/jpeg", - }); - - const limits = testOnlyOpenAiHttp.resolveOpenAiChatCompletionsLimits({ - maxTotalImageBytes: 4, - }); - - await expect( - testOnlyOpenAiHttp.resolveImagesForRequest( - { - urls: ["data:image/jpeg;base64,QUJDRA=="], - }, - limits, - ), - ).resolves.toEqual([ - { - type: "image", - data: "QUJDRA==", - mimeType: "image/jpeg", - }, - ]); - }); -}); diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 9c11161e1870..f8a675410406 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -3,7 +3,6 @@ import { randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import { estimateBase64DecodedBytes } from "@openclaw/media-core/base64"; -import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -133,15 +132,9 @@ function resolveOpenAiChatCompletionsLimits( ): ResolvedOpenAiChatCompletionsLimits { const imageConfig = config?.images; return { - maxBodyBytes: config?.maxBodyBytes ?? DEFAULT_OPENAI_CHAT_COMPLETIONS_BODY_BYTES, - maxImageParts: resolveIntegerOption(config?.maxImageParts, DEFAULT_OPENAI_MAX_IMAGE_PARTS, { - min: 0, - }), - maxTotalImageBytes: resolveIntegerOption( - config?.maxTotalImageBytes, - DEFAULT_OPENAI_MAX_TOTAL_IMAGE_BYTES, - { min: 1 }, - ), + maxBodyBytes: DEFAULT_OPENAI_CHAT_COMPLETIONS_BODY_BYTES, + maxImageParts: DEFAULT_OPENAI_MAX_IMAGE_PARTS, + maxTotalImageBytes: DEFAULT_OPENAI_MAX_TOTAL_IMAGE_BYTES, images: { allowUrl: imageConfig?.allowUrl ?? DEFAULT_OPENAI_IMAGE_LIMITS.allowUrl, urlAllowlist: normalizeInputHostnameAllowlist(imageConfig?.urlAllowlist), diff --git a/src/gateway/openresponses-http.ts b/src/gateway/openresponses-http.ts index a8b707697691..1ce5d91be5ac 100644 --- a/src/gateway/openresponses-http.ts +++ b/src/gateway/openresponses-http.ts @@ -262,7 +262,7 @@ function resolveResponsesLimits( const images = config?.images; const fileLimits = resolveInputFileLimits(files); return { - maxBodyBytes: config?.maxBodyBytes ?? DEFAULT_BODY_BYTES, + maxBodyBytes: DEFAULT_BODY_BYTES, maxUrlParts: resolveIntegerOption(config?.maxUrlParts, DEFAULT_MAX_URL_PARTS, { min: 0 }), files: { ...fileLimits, @@ -454,9 +454,7 @@ export async function handleOpenResponsesHttpRequest( const limits = resolveResponsesLimits(opts.config); const maxBodyBytes = opts.maxBodyBytes ?? - (opts.config?.maxBodyBytes - ? limits.maxBodyBytes - : Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2)); + Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2); const handled = await handleGatewayPostJsonEndpoint(req, res, { pathname: "/v1/responses", requiredOperatorMethod: "chat.send", diff --git a/src/gateway/operator-approvals-client.ts b/src/gateway/operator-approvals-client.ts index 6626e0487a77..6f7ea7a5556f 100644 --- a/src/gateway/operator-approvals-client.ts +++ b/src/gateway/operator-approvals-client.ts @@ -120,7 +120,7 @@ export async function withOperatorApprovalsGatewayClient( try { const readiness = await startGatewayClientWhenEventLoopReady(gatewayClient, { - clientOptions: { preauthHandshakeTimeoutMs: params.config.gateway?.handshakeTimeoutMs }, + clientOptions: {}, }); if (!readiness.ready) { throw new Error( diff --git a/src/gateway/server-cron.test.ts b/src/gateway/server-cron.test.ts index 968dff7e854b..a4ce0e4d89cf 100644 --- a/src/gateway/server-cron.test.ts +++ b/src/gateway/server-cron.test.ts @@ -341,7 +341,7 @@ describe("buildGatewayCronService", () => { const cfg = createCronConfig("server-cron-trigger-tool-cap"); cfg.cron = { ...cfg.cron, - triggers: { enabled: true, minIntervalMs: 30_000 }, + triggers: { enabled: true }, }; loadConfigMock.mockReturnValue(cfg); const state = buildGatewayCronService({ diff --git a/src/gateway/server-lanes.test.ts b/src/gateway/server-lanes.test.ts index 2ab2273a18ef..77ed57b7fe92 100644 --- a/src/gateway/server-lanes.test.ts +++ b/src/gateway/server-lanes.test.ts @@ -32,7 +32,7 @@ describe("applyGatewayLaneConcurrency", () => { resetCommandQueueStateForTest(); }); - it("uses the higher cron default when maxConcurrentRuns is unset", async () => { + it("uses the built-in cron concurrency", async () => { applyConfigLaneConcurrency({} as OpenClawConfig); let activeRuns = 0; @@ -70,49 +70,8 @@ describe("applyGatewayLaneConcurrency", () => { } }); - it("applies cron maxConcurrentRuns to the cron-nested lane used by cron agent turns", async () => { - applyConfigLaneConcurrency({ cron: { maxConcurrentRuns: 2 } } as OpenClawConfig); - - let activeRuns = 0; - let peakActiveRuns = 0; - const bothRunsStarted = createDeferred(); - const releaseRuns = createDeferred(); - - const run = async () => { - activeRuns += 1; - peakActiveRuns = Math.max(peakActiveRuns, activeRuns); - if (peakActiveRuns >= 2) { - bothRunsStarted.resolve(); - } - try { - await releaseRuns.promise; - } finally { - activeRuns -= 1; - } - }; - - const first = enqueueCommandInLane(CommandLane.CronNested, run, { warnAfterMs: 10_000 }); - const second = enqueueCommandInLane(CommandLane.CronNested, run, { warnAfterMs: 10_000 }); - const timeout = setTimeout(() => { - bothRunsStarted.reject( - new Error("timed out waiting for nested cron work to run in parallel"), - ); - }, 250); - - try { - await bothRunsStarted.promise; - expect(peakActiveRuns).toBe(2); - } finally { - clearTimeout(timeout); - releaseRuns.resolve(); - await Promise.all([first, second]); - } - }); - it("keeps the shared nested lane at its default concurrency", async () => { - applyConfigLaneConcurrency({ cron: { maxConcurrentRuns: 2 } } as OpenClawConfig, { - gatewayStart: true, - }); + applyConfigLaneConcurrency({} as OpenClawConfig, { gatewayStart: true }); let startedRuns = 0; const releaseRuns = createDeferred(); diff --git a/src/gateway/server-lanes.ts b/src/gateway/server-lanes.ts index 2881257d7281..eb909a57b8c0 100644 --- a/src/gateway/server-lanes.ts +++ b/src/gateway/server-lanes.ts @@ -18,7 +18,7 @@ type GatewayLaneConcurrency = { export function resolveGatewayLaneConcurrency(cfg: OpenClawConfig): GatewayLaneConcurrency { return { - cron: resolveCronMaxConcurrentRuns(cfg.cron), + cron: resolveCronMaxConcurrentRuns(), main: resolveAgentMaxConcurrent(cfg), subagent: resolveSubagentMaxConcurrent(cfg), }; diff --git a/src/gateway/server-maintenance.ts b/src/gateway/server-maintenance.ts index 77d8eda68fb6..a8bf2f7c1ec1 100644 --- a/src/gateway/server-maintenance.ts +++ b/src/gateway/server-maintenance.ts @@ -132,7 +132,7 @@ export function startGatewayMaintenanceTimers(params: { // worktree activity so idle GC cannot remove a checkout still used by the session. shouldProtectOwner: createManagedWorktreeOwnerProtection(cfg), // Read limits per run so a config edit applies at the next hourly sweep. - limits: resolveWorktreeCleanupLimits(cfg.worktrees), + limits: resolveWorktreeCleanupLimits(), }); }); const performWorktreeGc = () => diff --git a/src/gateway/server-methods/config.shared-auth.test.ts b/src/gateway/server-methods/config.shared-auth.test.ts index 6d99a35e71e4..b324492e8dec 100644 --- a/src/gateway/server-methods/config.shared-auth.test.ts +++ b/src/gateway/server-methods/config.shared-auth.test.ts @@ -529,34 +529,6 @@ describe("config shared auth disconnects", () => { expect(payload?.stats?.requiresRestart).toBe(true); }); - it("marks hot-reloaded config.patch writes as not restart required", async () => { - const prevConfig: OpenClawConfig = { - gateway: { - channelHealthCheckMinutes: 10, - }, - }; - readConfigFileSnapshotForWriteMock.mockResolvedValue(createConfigWriteSnapshot(prevConfig)); - - const { options } = createConfigHandlerHarness({ - method: "config.patch", - params: { - baseHash: "base-hash", - raw: JSON.stringify({ gateway: { channelHealthCheckMinutes: 15 } }), - restartDelayMs: 1_000, - }, - }); - - await expectDefined( - configHandlers["config.patch"], - 'configHandlers["config.patch"] test invariant', - )(options); - await flushConfigHandlerMicrotasks(); - - expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled(); - const payload = restartSentinelMocks.writeRestartSentinel.mock.calls.at(-1)?.[0]; - expect(payload?.stats?.requiresRestart).toBe(false); - }); - it("does not schedule a direct restart for hot-mode browser profile config.patch writes", async () => { installBrowserReloadRegistry(); mockPreviousConfig({ diff --git a/src/gateway/server-methods/update.test.ts b/src/gateway/server-methods/update.test.ts index 684111724df5..bafe812ef3b8 100644 --- a/src/gateway/server-methods/update.test.ts +++ b/src/gateway/server-methods/update.test.ts @@ -475,7 +475,7 @@ describe("update.run restart scheduling", () => { mockGlobalInstallSurface(); const payload = await withProcessEnv({ OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway" }, () => - captureUpdateRunPayload({}, { gateway: { reload: { deferralTimeoutMs: 60_000 } } }), + captureUpdateRunPayload({}, {}), ); expect(runGatewayUpdateMock).not.toHaveBeenCalled(); @@ -483,7 +483,7 @@ describe("update.run restart scheduling", () => { expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledWith( expect.objectContaining({ root: "/tmp/openclaw", - restartDrainTimeoutMs: 60_000, + restartDrainTimeoutMs: 300_000, restartDelayMs: 2000, handoffId: expect.any(String), supervisor: "launchd", @@ -536,19 +536,6 @@ describe("update.run restart scheduling", () => { ); }); - it("preserves an indefinite restart drain for managed updates", async () => { - detectRespawnSupervisorMock.mockReturnValueOnce("launchd"); - mockGlobalInstallSurface(); - - await withProcessEnv({ OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway" }, () => - captureUpdateRunPayload({}, { gateway: { reload: { deferralTimeoutMs: 0 } } }), - ); - - expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledWith( - expect.objectContaining({ restartDrainTimeoutMs: undefined }), - ); - }); - it("rejects a joining request instead of dropping its restart continuation", async () => { detectRespawnSupervisorMock.mockReturnValueOnce("launchd"); mockGlobalInstallSurface(); diff --git a/src/gateway/server-methods/update.ts b/src/gateway/server-methods/update.ts index baec7c00ce8b..a38746c1c02e 100644 --- a/src/gateway/server-methods/update.ts +++ b/src/gateway/server-methods/update.ts @@ -267,9 +267,7 @@ export const updateHandlers: GatewayRequestHandlers = { const started = await startManagedServiceUpdateHandoff({ root, timeoutMs, - restartDrainTimeoutMs: resolveGatewayRestartDeferralTimeoutMs( - config.gateway?.reload?.deferralTimeoutMs, - ), + restartDrainTimeoutMs: resolveGatewayRestartDeferralTimeoutMs(), ...(handoffChannel ? { channel: handoffChannel } : {}), restartDelayMs: managedRestartDelayMs, meta: sentinelMeta, diff --git a/src/gateway/server-methods/worktrees.test.ts b/src/gateway/server-methods/worktrees.test.ts index 555425998d7f..a80def928306 100644 --- a/src/gateway/server-methods/worktrees.test.ts +++ b/src/gateway/server-methods/worktrees.test.ts @@ -149,20 +149,16 @@ describe("worktrees gateway methods", () => { } }); - it("passes configured cleanup limits to gc", async () => { + it("uses the built-in cleanup policy for gc", async () => { const service = { gc: vi.fn(async () => ({ removed: [], orphansDeleted: 0, snapshotsPruned: 0 })), }; const handlers = createWorktreesHandlers(service as never); - const context = { - getRuntimeConfig: () => ({ - worktrees: { cleanup: { maxCount: 25, maxTotalSizeGb: 50 } }, - }), - }; + const context = { getRuntimeConfig: () => ({}) }; const response = await call(handlers, "worktrees.gc", {}, { context }); expect(response?.[0]).toBe(true); expect(service.gc).toHaveBeenCalledWith({ - limits: { maxCount: 25, maxTotalSizeBytes: 50 * 1024 ** 3 }, + limits: {}, shouldProtectOwner: expect.any(Function), }); }); diff --git a/src/gateway/server-methods/worktrees.ts b/src/gateway/server-methods/worktrees.ts index ca727c847621..99bdb61a4bfc 100644 --- a/src/gateway/server-methods/worktrees.ts +++ b/src/gateway/server-methods/worktrees.ts @@ -149,7 +149,7 @@ export function createWorktreesHandlers(service: WorktreeService): GatewayReques } try { const cfg = context.getRuntimeConfig(); - const limits = resolveWorktreeCleanupLimits(cfg.worktrees); + const limits = resolveWorktreeCleanupLimits(); respond( true, await service.gc({ diff --git a/src/gateway/server-reload-handlers.test.ts b/src/gateway/server-reload-handlers.test.ts index 81d843afffa8..765b3b088ce5 100644 --- a/src/gateway/server-reload-handlers.test.ts +++ b/src/gateway/server-reload-handlers.test.ts @@ -461,7 +461,7 @@ function createManagedRestartSequenceHarness( const initialConfig = { gateway: { port: 18789, - reload: { debounceMs: 0 }, + reload: {}, terminal: { enabled: true }, }, } as OpenClawConfig; @@ -477,7 +477,7 @@ function createManagedRestartSequenceHarness( const deferredConfig = { gateway: { port: 18790, - reload: { debounceMs: 0 }, + reload: {}, terminal: { enabled: true }, auth: { mode: "token", @@ -783,14 +783,13 @@ async function runManagedOwnershipScenario(params: { queueRevert: boolean; }) { const initialConfig = { - gateway: { reload: { mode: "off" as const, debounceMs: 0 } }, + gateway: { reload: { mode: "off" as const } }, hooks: { enabled: true, token: "test-token", path: "/old" }, } satisfies OpenClawConfig; const configA = { gateway: { reload: { mode: params.kind === "restart" ? ("restart" as const) : ("hot" as const), - debounceMs: 0, }, }, hooks: { @@ -2530,65 +2529,6 @@ describe("gateway restart deferral preflight", () => { } }); - it("retries a timed-out deferral with its original force intent", async () => { - const requestRecoveryRestart = vi - .fn>() - .mockReturnValueOnce({ status: "failed" }) - .mockReturnValueOnce({ status: "emitted" }); - const logReload = { info: vi.fn(), warn: vi.fn() }; - const { requestGatewayRestart, stopRestartRetries } = createReloadHandlersForTest( - logReload, - undefined, - undefined, - undefined, - requestRecoveryRestart, - ); - hoisted.activeTaskBlockers.push({ - taskId: "force-intent-blocker", - status: "running", - runtime: "subagent", - }); - vi.useFakeTimers(); - - try { - const transaction = requestGatewayRestart( - { - changedPaths: ["gateway.port"], - restartGateway: true, - restartReasons: ["gateway.port"], - hotReasons: [], - reloadHooks: false, - restartGmailWatcher: false, - restartCron: false, - restartHeartbeat: false, - restartHealthMonitor: false, - reloadPlugins: false, - restartChannels: new Set(), - disposeMcpRuntimes: false, - noopPaths: [], - }, - { gateway: { reload: { deferralTimeoutMs: 500 } } }, - ); - transaction.settle("committed"); - - await vi.advanceTimersByTimeAsync(500); - await vi.advanceTimersByTimeAsync(1_000); - - expect(requestRecoveryRestart.mock.calls).toEqual([ - ["config reload: gateway.port", { force: true, reason: "config reload forced restart" }], - ["config reload: gateway.port", { force: true, reason: "config reload forced restart" }], - ]); - expect( - logReload.warn.mock.calls.filter(([message]) => - message.includes("deferring until 1 background task run(s) complete"), - ), - ).toHaveLength(1); - } finally { - stopRestartRetries(); - hoisted.activeTaskBlockers.length = 0; - } - }); - it("defers config restart across an admitted process handoff", async () => { restartTesting.resetSigusr1State(); resetGatewayWorkAdmission(); @@ -2695,7 +2635,7 @@ describe("gateway restart deferral preflight", () => { noopPaths: [], }, { - gateway: { reload: { deferralTimeoutMs: 60_000 } }, + gateway: { reload: {} }, channels: { discord: { token: "token" } }, }, { @@ -2740,97 +2680,6 @@ describe("gateway restart deferral preflight", () => { expect(setState).toHaveBeenCalledTimes(1); }); - it("forces channel hot reload after the configured deferral timeout", async () => { - const previousSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS; - const previousSkipProviders = process.env.OPENCLAW_SKIP_PROVIDERS; - delete process.env.OPENCLAW_SKIP_CHANNELS; - delete process.env.OPENCLAW_SKIP_PROVIDERS; - const startChannel = vi.fn(async () => {}); - const stopChannel = vi.fn(async () => {}); - const logReload = { info: vi.fn(), warn: vi.fn() }; - const { applyHotReload } = createGatewayReloadHandlers({ - deps: {} as never, - broadcast: vi.fn(), - getState: () => ({ - hooksConfig: {} as never, - hookClientIpConfig: {} as never, - heartbeatRunner: { stop: vi.fn(), updateConfig: vi.fn() } as never, - cronState: { - cron: { start: vi.fn(async () => {}), stop: vi.fn() }, - storePath: "/tmp/cron.json", - cronEnabled: false, - } as never, - channelHealthMonitor: null, - }), - setState: vi.fn(), - startChannel, - stopChannel, - reloadPlugins: vi.fn( - async (): Promise => ({ - restartChannels: new Set(), - activeChannels: new Set(), - }), - ), - logHooks: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - logChannels: { info: vi.fn(), error: vi.fn() }, - logCron: { error: vi.fn() }, - logReload, - createHealthMonitor: () => null, - }); - hoisted.activeEmbeddedRunCount.value = 1; - vi.useFakeTimers(); - const reloadPromise = applyHotReload( - { - changedPaths: ["channels.discord.token"], - restartGateway: false, - restartReasons: [], - hotReasons: ["channels.discord.token"], - reloadHooks: false, - restartGmailWatcher: false, - restartCron: false, - restartHeartbeat: false, - restartHealthMonitor: false, - reloadPlugins: false, - restartChannels: new Set(["discord"]), - disposeMcpRuntimes: false, - noopPaths: [], - }, - { - gateway: { reload: { deferralTimeoutMs: 1_000 } }, - channels: { discord: { token: "token" } }, - }, - ); - try { - await Promise.resolve(); - expect(stopChannel).not.toHaveBeenCalled(); - expect(startChannel).not.toHaveBeenCalled(); - - await vi.advanceTimersByTimeAsync(1_000); - await reloadPromise; - } finally { - hoisted.activeEmbeddedRunCount.value = 0; - await vi.advanceTimersByTimeAsync(500).catch(() => {}); - vi.useRealTimers(); - await reloadPromise.catch(() => {}); - if (previousSkipChannels === undefined) { - delete process.env.OPENCLAW_SKIP_CHANNELS; - } else { - process.env.OPENCLAW_SKIP_CHANNELS = previousSkipChannels; - } - if (previousSkipProviders === undefined) { - delete process.env.OPENCLAW_SKIP_PROVIDERS; - } else { - process.env.OPENCLAW_SKIP_PROVIDERS = previousSkipProviders; - } - } - - expect(stopChannel).toHaveBeenCalledWith("discord", undefined, { manual: false }); - expect(startChannel).toHaveBeenCalledWith("discord"); - expect(logReload.warn).toHaveBeenCalledWith( - expect.stringContaining("channel reload timeout after"), - ); - }); - it("uses the default channel reload deferral timeout when config omits deferralTimeoutMs", async () => { const previousSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS; const previousSkipProviders = process.env.OPENCLAW_SKIP_PROVIDERS; @@ -2922,99 +2771,6 @@ describe("gateway restart deferral preflight", () => { ); }); - it("waits indefinitely for channel hot reload when deferral timeout is 0", async () => { - const previousSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS; - const previousSkipProviders = process.env.OPENCLAW_SKIP_PROVIDERS; - delete process.env.OPENCLAW_SKIP_CHANNELS; - delete process.env.OPENCLAW_SKIP_PROVIDERS; - const startChannel = vi.fn(async () => {}); - const stopChannel = vi.fn(async () => {}); - const logReload = { info: vi.fn(), warn: vi.fn() }; - const { applyHotReload } = createGatewayReloadHandlers({ - deps: {} as never, - broadcast: vi.fn(), - getState: () => ({ - hooksConfig: {} as never, - hookClientIpConfig: {} as never, - heartbeatRunner: { stop: vi.fn(), updateConfig: vi.fn() } as never, - cronState: { - cron: { start: vi.fn(async () => {}), stop: vi.fn() }, - storePath: "/tmp/cron.json", - cronEnabled: false, - } as never, - channelHealthMonitor: null, - }), - setState: vi.fn(), - startChannel, - stopChannel, - reloadPlugins: vi.fn( - async (): Promise => ({ - restartChannels: new Set(), - activeChannels: new Set(), - }), - ), - logHooks: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - logChannels: { info: vi.fn(), error: vi.fn() }, - logCron: { error: vi.fn() }, - logReload, - createHealthMonitor: () => null, - }); - hoisted.activeEmbeddedRunCount.value = 1; - vi.useFakeTimers(); - const reloadPromise = applyHotReload( - { - changedPaths: ["channels.discord.token"], - restartGateway: false, - restartReasons: [], - hotReasons: ["channels.discord.token"], - reloadHooks: false, - restartGmailWatcher: false, - restartCron: false, - restartHeartbeat: false, - restartHealthMonitor: false, - reloadPlugins: false, - restartChannels: new Set(["discord"]), - disposeMcpRuntimes: false, - noopPaths: [], - }, - { - gateway: { reload: { deferralTimeoutMs: 0 } }, - channels: { discord: { token: "token" } }, - }, - ); - try { - await Promise.resolve(); - await vi.advanceTimersByTimeAsync(500); - expect(stopChannel).not.toHaveBeenCalled(); - expect(startChannel).not.toHaveBeenCalled(); - expect(logReload.warn).not.toHaveBeenCalledWith( - expect.stringContaining("channel reload timeout after"), - ); - - hoisted.activeEmbeddedRunCount.value = 0; - await vi.advanceTimersByTimeAsync(500); - await reloadPromise; - } finally { - hoisted.activeEmbeddedRunCount.value = 0; - await vi.advanceTimersByTimeAsync(500).catch(() => {}); - vi.useRealTimers(); - await reloadPromise.catch(() => {}); - if (previousSkipChannels === undefined) { - delete process.env.OPENCLAW_SKIP_CHANNELS; - } else { - process.env.OPENCLAW_SKIP_CHANNELS = previousSkipChannels; - } - if (previousSkipProviders === undefined) { - delete process.env.OPENCLAW_SKIP_PROVIDERS; - } else { - process.env.OPENCLAW_SKIP_PROVIDERS = previousSkipProviders; - } - } - - expect(stopChannel).toHaveBeenCalledWith("discord", undefined, { manual: false }); - expect(startChannel).toHaveBeenCalledWith("discord"); - }); - it("logs active task run ids before waiting and when forcing after timeout", async () => { restartTesting.resetSigusr1State(); const logReload = { info: vi.fn(), warn: vi.fn() }; @@ -3052,20 +2808,22 @@ describe("gateway restart deferral preflight", () => { noopPaths: [], }, { - gateway: { reload: { deferralTimeoutMs: 1_000 } }, + gateway: { reload: {} }, }, ); - expect(logReload.warn.mock.calls).toEqual([ - [ - "config change requires gateway restart (gateway.port) — deferring until 1 background task run(s) complete", - ], - [ - "restart blocked by active background task run(s): taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts", - ], - ]); + expect(logReload.warn.mock.calls).toEqual( + expect.arrayContaining([ + [ + "config change requires gateway restart (gateway.port) — deferring until 1 background task run(s) complete", + ], + [ + "restart blocked by active background task run(s): taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts", + ], + ]), + ); - await vi.advanceTimersByTimeAsync(1_000); + await vi.advanceTimersByTimeAsync(300_000); await Promise.resolve(); expect(signalSpy).toHaveBeenCalledTimes(1); @@ -3074,17 +2832,19 @@ describe("gateway restart deferral preflight", () => { reason: "config reload forced restart", }); expect(hoisted.markRestartAbortedMainSessions).not.toHaveBeenCalled(); - expect(logReload.warn.mock.calls).toEqual([ - [ - "config change requires gateway restart (gateway.port) — deferring until 1 background task run(s) complete", - ], - [ - "restart blocked by active background task run(s): taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts", - ], - [ - "restart timeout after 1000ms with 1 background task run(s) still active (taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts); forcing restart", - ], - ]); + expect(logReload.warn.mock.calls).toEqual( + expect.arrayContaining([ + [ + "config change requires gateway restart (gateway.port) — deferring until 1 background task run(s) complete", + ], + [ + "restart blocked by active background task run(s): taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts", + ], + [ + "restart timeout after 300000ms with 1 background task run(s) still active (taskId=task-nightly runId=run-nightly status=running runtime=cron label=nightly sync title=refresh all accounts); forcing restart", + ], + ]), + ); } finally { hoisted.activeTaskCount.value = 0; vi.useRealTimers(); @@ -3778,7 +3538,7 @@ describe("gateway Gmail hot reload handlers", () => { function createGmailConfig(account: string): OpenClawConfig { return { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", gmail: { account } }, }; } @@ -3934,11 +3694,11 @@ describe("gateway Gmail hot reload handlers", () => { current: null, }; const initialConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { visibleReplies: "automatic" }, }; const nextConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { visibleReplies: "message_tool" }, }; const snapshot = (config: OpenClawConfig): PreparedSecretsRuntimeSnapshot => ({ @@ -4087,11 +3847,11 @@ describe("gateway Gmail hot reload handlers", () => { const thirdRef = { source: "env" as const, provider: "default", id: "TTS_THIRD" }; const fourthRef = { source: "env" as const, provider: "default", id: "TTS_FOURTH" }; const sourceConfig = (ref: typeof firstRef): OpenClawConfig => ({ - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { tts: { providers: { elevenlabs: { apiKey: ref } } } }, }); const runtimeConfig: OpenClawConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, messages: { tts: { providers: { elevenlabs: { apiKey: String(42) } } } }, }; const ttsContractDigest = "tts-source-only-contract"; @@ -4468,7 +4228,7 @@ describe("gateway Gmail hot reload handlers", () => { const initialConfig: OpenClawConfig = { gateway: { port: 18789, - reload: { debounceMs: 0 }, + reload: {}, terminal: { enabled: true }, }, hooks: { @@ -4603,14 +4363,6 @@ describe("gateway Gmail hot reload handlers", () => { config: { ...initialConfig, cron: { enabled: true } }, surface: "irreversible hot reload", }, - { - label: "health-monitor", - config: { - ...initialConfig, - gateway: { ...initialConfig.gateway, channelHealthCheckMinutes: 10 }, - }, - surface: "irreversible hot reload", - }, { label: "gmail", config: { @@ -4665,14 +4417,14 @@ describe("gateway Gmail hot reload handlers", () => { const initialConfig = { gateway: { port: 18789, - reload: { debounceMs: 0 }, + reload: {}, terminal: { enabled: true }, }, } as OpenClawConfig; const rejectedConfig = { gateway: { port: 18790, - reload: { debounceMs: 0 }, + reload: {}, terminal: { enabled: false }, }, } as OpenClawConfig; @@ -5267,11 +5019,11 @@ describe("gateway Gmail hot reload handlers", () => { current: null, }; const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/old" }, } as OpenClawConfig; const nextConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, hooks: { enabled: true, token: "test-token", path: "/next" }, } as OpenClawConfig; const initialSnapshot: PreparedSecretsRuntimeSnapshot = { @@ -5969,7 +5721,7 @@ describe("gateway plugin hot reload handlers", () => { const envKey = "OPENCLAW_TEST_HOT_RELOAD_SERVICE_ENV"; const targetEnv: NodeJS.ProcessEnv = { [envKey]: "old" }; const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, cron: { enabled: false }, plugins: { enabled: false }, env: { vars: { [envKey]: "old" } }, @@ -7043,11 +6795,11 @@ describe("deferred channel reload abort generation", () => { it("does not mark a managed reload applied when restart aborts its deferral", async () => { const initialConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, channels: { whatsapp: { enabled: true, selfChatMode: false } }, } as OpenClawConfig; const nextConfig = { - gateway: { reload: { debounceMs: 0 } }, + gateway: { reload: {} }, channels: { whatsapp: { enabled: true, selfChatMode: true } }, } as OpenClawConfig; const whatsappPlugin = { diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index 2689e65c9303..a24590e346b8 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -501,7 +501,6 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) }; const waitForActiveWorkBeforeChannelReload = async ( channels: Iterable, - nextConfig: OpenClawConfig, isTransactionCurrent: () => boolean, ): Promise => { // Returns true when the wait was cancelled (restart or config supersession), @@ -520,9 +519,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) ", ", )} complete`, ); - const timeoutMs = resolveGatewayRestartDeferralTimeoutMs( - nextConfig.gateway?.reload?.deferralTimeoutMs, - ); + const timeoutMs = resolveGatewayRestartDeferralTimeoutMs(); const startedAt = Date.now(); let nextStillPendingAt = startedAt + CHANNEL_RELOAD_STILL_PENDING_WARN_MS; while (true) { @@ -816,7 +813,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) if (targets.size === 0 || shouldSkipChannelRestart) { return; } - if (await waitForActiveWorkBeforeChannelReload(targets, nextConfig, isTransactionCurrent)) { + if (await waitForActiveWorkBeforeChannelReload(targets, isTransactionCurrent)) { params.logChannels.info( "channel reload before plugin replace cancelled by config supersession or restart", ); @@ -929,7 +926,6 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) if (!pluginReloadAborted && hasLiveChannelTargets && !shouldSkipChannelRestart) { pluginReloadAborted = await waitForActiveWorkBeforeChannelReload( channelTargets, - nextConfig, isTransactionCurrent, ); } @@ -1516,9 +1512,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) let failedEmission: { reason: string; intent?: GatewayRestartIntent } | undefined; restartDeferral = deferGatewayRestartUntilIdle({ getPendingCount: () => getActiveCounts().totalActive, - maxWaitMs: resolveGatewayRestartDeferralTimeoutMs( - nextConfig.gateway?.reload?.deferralTimeoutMs, - ), + maxWaitMs: resolveGatewayRestartDeferralTimeoutMs(undefined), timeoutIntent: { force: true, reason: "config reload forced restart" }, reason: restartReason, emitHooks: { diff --git a/src/gateway/server-runtime-startup-services.ts b/src/gateway/server-runtime-startup-services.ts index e4fed0efe1cc..045ffb63659d 100644 --- a/src/gateway/server-runtime-startup-services.ts +++ b/src/gateway/server-runtime-startup-services.ts @@ -31,19 +31,8 @@ export function startGatewayChannelHealthMonitor(params: { ) { return null; } - const healthCheckMinutes = params.cfg.gateway?.channelHealthCheckMinutes; - if (healthCheckMinutes === 0) { - return null; - } - const staleEventThresholdMinutes = params.cfg.gateway?.channelStaleEventThresholdMinutes; - const maxRestartsPerHour = params.cfg.gateway?.channelMaxRestartsPerHour; return startChannelHealthMonitor({ channelManager: params.channelManager, - checkIntervalMs: (healthCheckMinutes ?? 5) * 60_000, - ...(staleEventThresholdMinutes != null && { - timing: { staleEventThresholdMs: staleEventThresholdMinutes * 60_000 }, - }), - ...(maxRestartsPerHour != null && { maxRestartsPerHour }), }); } diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index b24fca8fda94..19b51fc744eb 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1085,8 +1085,7 @@ export async function startGatewayServer( current: resolveCurrentSharedGatewaySessionGeneration(), required: null, }; - const preauthHandshakeTimeoutMs = - cfgAtStart.gateway?.handshakeTimeoutMs ?? getRuntimeConfig().gateway?.handshakeTimeoutMs; + const preauthHandshakeTimeoutMs = undefined; const initialHooksConfig = runtimeConfig.hooksConfig; const initialHookClientIpConfig = resolveHookClientIpConfig(cfgAtStart); diff --git a/src/gateway/server.preauth-hardening.test.ts b/src/gateway/server.preauth-hardening.test.ts index 9479bcae7780..3cce6ab91376 100644 --- a/src/gateway/server.preauth-hardening.test.ts +++ b/src/gateway/server.preauth-hardening.test.ts @@ -1,7 +1,6 @@ /** * Gateway pre-auth hardening tests. */ -import { writeFile } from "node:fs/promises"; import http from "node:http"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; @@ -219,31 +218,6 @@ describe("gateway pre-auth hardening", () => { await expectIdlePreauthSocketClose(); }); - it("uses gateway.handshakeTimeoutMs for idle unauthenticated sockets", async () => { - const configPath = process.env.OPENCLAW_CONFIG_PATH; - if (!configPath) { - throw new Error("OPENCLAW_CONFIG_PATH missing in gateway preauth test"); - } - await writeFile( - configPath, - JSON.stringify( - { - gateway: { - handshakeTimeoutMs: 250, - }, - }, - null, - 2, - ), - "utf-8", - ); - try { - await expectIdlePreauthSocketClose(); - } finally { - await writeFile(configPath, "{}\n", "utf-8"); - } - }); - it("rejects oversized pre-auth connect frames before application-level auth responses", async () => { resetDiagnosticEventsForTest(); const events: DiagnosticEventPayload[] = []; diff --git a/src/infra/exec-approval-channel-runtime.test.ts b/src/infra/exec-approval-channel-runtime.test.ts index 285246228c6d..0d417efaef66 100644 --- a/src/infra/exec-approval-channel-runtime.test.ts +++ b/src/infra/exec-approval-channel-runtime.test.ts @@ -416,7 +416,7 @@ describe("createExecApprovalChannelRuntime", () => { const runtime = createExecApprovalChannelRuntime({ label: "test/exec-approvals", clientDisplayName: "Test Exec Approvals", - cfg: { gateway: { handshakeTimeoutMs: 30_000 } } as never, + cfg: {}, isConfigured: () => true, shouldHandle: () => true, deliverRequested: async () => [], @@ -429,7 +429,7 @@ describe("createExecApprovalChannelRuntime", () => { expect(mockGatewayClientStarts).not.toHaveBeenCalled(); expect(mockGatewayClientStops).toHaveBeenCalledTimes(1); - expectStartGatewayClientCall(30_000); + expectStartGatewayClientCall(); }); it("can retry start after gateway client creation fails", async () => { diff --git a/src/infra/exec-approval-channel-runtime.ts b/src/infra/exec-approval-channel-runtime.ts index b6f3183225d1..1eb8c1638061 100644 --- a/src/infra/exec-approval-channel-runtime.ts +++ b/src/infra/exec-approval-channel-runtime.ts @@ -423,9 +423,7 @@ export function createExecApprovalChannelRuntime< gatewayClient = client; try { const readiness = await startGatewayClientWhenEventLoopReady(client, { - clientOptions: { - preauthHandshakeTimeoutMs: adapter.cfg.gateway?.handshakeTimeoutMs, - }, + clientOptions: {}, }); if (!readiness.ready) { throw new Error( diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 25cc7f87c752..350ce69d0c8b 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -567,7 +567,7 @@ function isHeartbeatTypingEnabled(params: { cfg: OpenClawConfig; hasChatDelivery function resolveHeartbeatTypingIntervalSeconds(cfg: OpenClawConfig) { const agentCfg = cfg.agents?.defaults; - const configured = agentCfg?.typingIntervalSeconds ?? cfg.session?.typingIntervalSeconds; + const configured = agentCfg?.typingIntervalSeconds; return typeof configured === "number" && configured > 0 ? configured : undefined; } diff --git a/src/infra/infra-runtime.test.ts b/src/infra/infra-runtime.test.ts index e3905b0e18a3..93b448b44c34 100644 --- a/src/infra/infra-runtime.test.ts +++ b/src/infra/infra-runtime.test.ts @@ -1,7 +1,7 @@ import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; // Tests infra runtime loading and platform-dependent helpers. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js"; +import { clearRuntimeConfigSnapshot } from "../config/config.js"; import { beginGatewayRestartSignalAdmission, isGatewayWorkAdmissionClosed, @@ -10,7 +10,6 @@ import { } from "../process/gateway-work-admission.js"; type RestartModule = typeof import("./restart.js"); -let consumeGatewaySigusr1RestartIntent: RestartModule["consumeGatewaySigusr1RestartIntent"]; let consumeGatewaySigusr1RestartAuthorization: RestartModule["consumeGatewaySigusr1RestartAuthorization"]; let deferGatewayRestartUntilIdle: RestartModule["deferGatewayRestartUntilIdle"]; let isGatewaySigusr1RestartExternallyAllowed: RestartModule["isGatewaySigusr1RestartExternallyAllowed"]; @@ -101,7 +100,6 @@ describe("infra runtime", () => { `./restart.js?infra-runtime=${freshRestartModuleId++}`, ); ({ - consumeGatewaySigusr1RestartIntent, consumeGatewaySigusr1RestartAuthorization, deferGatewayRestartUntilIdle, isGatewaySigusr1RestartExternallyAllowed, @@ -1285,47 +1283,6 @@ describe("infra runtime", () => { } }); - it("keeps SIGUSR1 deferred when deferral timeout is explicitly disabled", async () => { - const emitSpy = vi.spyOn(process, "emit"); - const handler = () => {}; - process.on("SIGUSR1", handler); - try { - setRuntimeConfigSnapshot({ gateway: { reload: { deferralTimeoutMs: 0 } } }); - setPreRestartDeferralCheck(() => 5); // always pending - scheduleGatewaySigusr1Restart({ delayMs: 0 }); - - await vi.advanceTimersByTimeAsync(0); - expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); - - await vi.advanceTimersByTimeAsync(300_000); - expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); - } finally { - process.removeListener("SIGUSR1", handler); - } - }); - - it("emits SIGUSR1 after explicit deferral timeout even if still pending", async () => { - const emitSpy = vi.spyOn(process, "emit"); - const handler = () => {}; - process.on("SIGUSR1", handler); - try { - setRuntimeConfigSnapshot({ gateway: { reload: { deferralTimeoutMs: 1_000 } } }); - setPreRestartDeferralCheck(() => 5); // always pending - scheduleGatewaySigusr1Restart({ delayMs: 0 }); - - await vi.advanceTimersByTimeAsync(0); - expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); - - await vi.advanceTimersByTimeAsync(1_000); - expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); - expect(consumeGatewaySigusr1RestartIntent()).toEqual({ - force: true, - }); - } finally { - process.removeListener("SIGUSR1", handler); - } - }); - it("emits SIGUSR1 if deferral check throws", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; diff --git a/src/infra/restart.ts b/src/infra/restart.ts index ffad724427ec..ce6bc193fb26 100644 --- a/src/infra/restart.ts +++ b/src/infra/restart.ts @@ -2,7 +2,6 @@ import { spawnSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; -import { getRuntimeConfig } from "../config/config.js"; import { resolveGatewayLaunchAgentLabel, resolveGatewaySystemdServiceName, @@ -107,10 +106,7 @@ function armPendingRestartTimer(requestedDueAt: number, nowMs: number): void { void emitPreparedGatewayRestart(undefined, scheduledReason); return; } - const cfg = getRuntimeConfig(); - const deferralTimeoutMs = resolveGatewayRestartDeferralTimeoutMs( - cfg.gateway?.reload?.deferralTimeoutMs, - ); + const deferralTimeoutMs = resolveGatewayRestartDeferralTimeoutMs(); deferGatewayRestartUntilIdle({ getPendingCount: pendingCheck, maxWaitMs: deferralTimeoutMs, @@ -409,7 +405,7 @@ type GatewayRestartEmitResult = | { status: "coalesced" } | { status: "failed" }; -export function resolveGatewayRestartDeferralTimeoutMs(timeoutMs: unknown): number | undefined { +export function resolveGatewayRestartDeferralTimeoutMs(timeoutMs?: unknown): number | undefined { if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) { return DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS; } diff --git a/src/infra/update-startup.test.ts b/src/infra/update-startup.test.ts index 4ab138e6d085..7a31ca385b6d 100644 --- a/src/infra/update-startup.test.ts +++ b/src/infra/update-startup.test.ts @@ -334,7 +334,6 @@ describe("update-startup", () => { channel: "beta" as const, auto: { enabled: true, - betaCheckIntervalHours: 1, }, }, }; @@ -914,8 +913,6 @@ describe("update-startup", () => { channel: "stable" as const, auto: { enabled: true, - stableDelayHours: 6, - stableJitterHours: 12, }, }, }; @@ -950,10 +947,6 @@ describe("update-startup", () => { it("runs beta auto-update checks hourly when enabled", async () => { mockPackageUpdateStatus("beta", "2.0.0-beta.1"); const runAutoUpdate = createAutoUpdateSuccessMock(); - getRuntimeConfigMock.mockReturnValue({ - gateway: { reload: { deferralTimeoutMs: 90_000 } }, - }); - await runAutoUpdateCheckWithDefaults({ cfg: createBetaAutoUpdateConfig(), runAutoUpdate, @@ -963,7 +956,7 @@ describe("update-startup", () => { expect(runAutoUpdate).toHaveBeenCalledWith({ channel: "beta", timeoutMs: 45 * 60 * 1000, - restartDrainTimeoutMs: 90_000, + restartDrainTimeoutMs: 300_000, root: "/opt/openclaw", }); }); diff --git a/src/infra/update-startup.ts b/src/infra/update-startup.ts index ec315635f76b..ba2df2d5ba69 100644 --- a/src/infra/update-startup.ts +++ b/src/infra/update-startup.ts @@ -9,7 +9,6 @@ import { } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { formatCliCommand } from "../cli/command-format.js"; -import { getRuntimeConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { runCommandWithTimeout } from "../process/exec.js"; import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; @@ -116,24 +115,11 @@ function shouldSkipCheck(allowInTests: boolean): boolean { function resolveAutoUpdatePolicy(cfg: OpenClawConfig): AutoUpdatePolicy { const auto = cfg.update?.auto; - const stableDelayHours = - typeof auto?.stableDelayHours === "number" && Number.isFinite(auto.stableDelayHours) - ? Math.max(0, auto.stableDelayHours) - : AUTO_STABLE_DELAY_HOURS_DEFAULT; - const stableJitterHours = - typeof auto?.stableJitterHours === "number" && Number.isFinite(auto.stableJitterHours) - ? Math.max(0, auto.stableJitterHours) - : AUTO_STABLE_JITTER_HOURS_DEFAULT; - const betaCheckIntervalHours = - typeof auto?.betaCheckIntervalHours === "number" && Number.isFinite(auto.betaCheckIntervalHours) - ? Math.max(0.25, auto.betaCheckIntervalHours) - : AUTO_BETA_CHECK_INTERVAL_HOURS_DEFAULT; - return { enabled: Boolean(auto?.enabled), - stableDelayHours, - stableJitterHours, - betaCheckIntervalHours, + stableDelayHours: AUTO_STABLE_DELAY_HOURS_DEFAULT, + stableJitterHours: AUTO_STABLE_JITTER_HOURS_DEFAULT, + betaCheckIntervalHours: AUTO_BETA_CHECK_INTERVAL_HOURS_DEFAULT, }; } @@ -731,9 +717,7 @@ export async function runGatewayUpdateCheck(params: { const outcome = await runAuto({ channel, timeoutMs: AUTO_UPDATE_COMMAND_TIMEOUT_MS, - restartDrainTimeoutMs: resolveGatewayRestartDeferralTimeoutMs( - getRuntimeConfig().gateway?.reload?.deferralTimeoutMs, - ), + restartDrainTimeoutMs: resolveGatewayRestartDeferralTimeoutMs(), root: root ?? status.root ?? undefined, }); if (outcome.ok && outcome.reason === CONTROL_PLANE_UPDATE_HANDOFF_STARTED_REASON) { diff --git a/src/logging/diagnostic-memory.test.ts b/src/logging/diagnostic-memory.test.ts index e90e0e068887..dce15fead67b 100644 --- a/src/logging/diagnostic-memory.test.ts +++ b/src/logging/diagnostic-memory.test.ts @@ -359,8 +359,7 @@ describe("diagnostic memory", () => { }), expect.objectContaining({ level: "WARN", - message: - "critical memory pressure snapshot disabled: diagnostics.memoryPressureSnapshot=false", + message: "critical memory pressure snapshot disabled", attributes: expect.objectContaining({ subsystem: "gateway/diagnostics/memory", }), diff --git a/src/logging/diagnostic-memory.ts b/src/logging/diagnostic-memory.ts index 7feb2bba8af6..26fc94eeb759 100644 --- a/src/logging/diagnostic-memory.ts +++ b/src/logging/diagnostic-memory.ts @@ -313,9 +313,7 @@ export function emitDiagnosticMemorySample(options?: { log.warn(`critical memory pressure bundle failed: ${String(result.error)}`); } } else if (pressure.level === "critical") { - log.warn( - "critical memory pressure snapshot disabled: diagnostics.memoryPressureSnapshot=false", - ); + log.warn("critical memory pressure snapshot disabled"); } } return memory; diff --git a/src/logging/diagnostic-session-recovery.ts b/src/logging/diagnostic-session-recovery.ts index 0322305d6fc4..e9e04df5f630 100644 --- a/src/logging/diagnostic-session-recovery.ts +++ b/src/logging/diagnostic-session-recovery.ts @@ -25,7 +25,7 @@ export type StuckSessionRecoveryRequest = { expectedState?: DiagnosticSessionState; stateGeneration?: number; /** - * Resolved no-forward-progress age (from `diagnostics.stuckSessionAbortMs`) after + * Built-in no-forward-progress age after * which an "active" run with queued work is treated as a leaked/dead handle and * reclaimed. Honors an operator-raised threshold; falls back to a safe floor. */ diff --git a/src/logging/diagnostic.test.ts b/src/logging/diagnostic.test.ts index 2d6cb58687ef..4f593494b968 100644 --- a/src/logging/diagnostic.test.ts +++ b/src/logging/diagnostic.test.ts @@ -50,9 +50,19 @@ import { resetDiagnosticStateForTest, resolveStuckSessionAbortMs, resolveStuckSessionWarnMs, - startDiagnosticHeartbeat, + startDiagnosticHeartbeat as startDiagnosticHeartbeatImpl, } from "./diagnostic.js"; +function startDiagnosticHeartbeat( + config?: Parameters[0], + opts?: Parameters[1], +) { + return startDiagnosticHeartbeatImpl(config, { + testTimings: { stuckSessionWarnMs: 30_000, stuckSessionAbortMs: 60_000 }, + ...opts, + }); +} + function createEmitMemorySampleMock() { return vi.fn(() => ({ rssBytes: 100, @@ -324,7 +334,7 @@ describe("stuck session diagnostics threshold", () => { vi.useRealTimers(); }); - it("uses the configured diagnostics.stuckSessionWarnMs threshold", () => { + it("uses the heartbeat test timing without runtime config tuning", () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); const unsubscribe = onDiagnosticEvent((event) => { @@ -335,7 +345,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -371,7 +380,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -406,7 +414,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -435,7 +442,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -457,83 +463,6 @@ describe("stuck session diagnostics threshold", () => { ); }); - it.each([ - { stuckSessionAbortMs: 1_000, heartbeatOverdueMs: 1_000 }, - { stuckSessionAbortMs: 30_000, heartbeatOverdueMs: 1_000 }, - { stuckSessionAbortMs: 31_000, heartbeatOverdueMs: 1_000 }, - { stuckSessionAbortMs: 45_000, heartbeatOverdueMs: 15_000 }, - { stuckSessionAbortMs: 89_000, heartbeatOverdueMs: 59_000 }, - ])( - "defers a liveness-delayed heartbeat with a $stuckSessionAbortMs ms abort threshold", - ({ stuckSessionAbortMs, heartbeatOverdueMs }) => { - const recoverStuckSession = vi.fn(); - const warnSpy = vi.spyOn(diagnosticLogger, "warn").mockImplementation(() => undefined); - - vi.setSystemTime(0); - startDiagnosticHeartbeat( - { - diagnostics: { - enabled: true, - stuckSessionWarnMs: 1_000, - stuckSessionAbortMs, - }, - }, - { recoverStuckSession }, - ); - logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); - markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); - - vi.setSystemTime(heartbeatOverdueMs); - vi.advanceTimersByTime(30_000); - - expectLoggerMessageContaining(warnSpy, "liveness heartbeat delayed"); - expect(recoverStuckSession).not.toHaveBeenCalled(); - - vi.advanceTimersByTime(30_000); - - expectRecoveryCall( - recoverStuckSession, - { sessionId: "s1", sessionKey: "main", queueDepth: 0, allowActiveAbort: true }, - ["ageMs", "stateGeneration"], - ); - }, - ); - - it.each([ - { stuckSessionAbortMs: 1_000, schedulingJitterMs: 1 }, - { stuckSessionAbortMs: 30_000, schedulingJitterMs: 999 }, - ])( - "recovers at a $stuckSessionAbortMs ms abort threshold with ordinary scheduling jitter", - ({ stuckSessionAbortMs, schedulingJitterMs }) => { - const recoverStuckSession = vi.fn(); - const warnSpy = vi.spyOn(diagnosticLogger, "warn").mockImplementation(() => undefined); - - vi.setSystemTime(0); - startDiagnosticHeartbeat( - { - diagnostics: { - enabled: true, - stuckSessionWarnMs: 1_000, - stuckSessionAbortMs, - }, - }, - { recoverStuckSession }, - ); - logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); - markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); - - vi.setSystemTime(schedulingJitterMs); - vi.advanceTimersByTime(30_000); - - expectNoLoggerMessageContaining(warnSpy, "liveness heartbeat delayed"); - expectRecoveryCall( - recoverStuckSession, - { sessionId: "s1", sessionKey: "main", queueDepth: 0, allowActiveAbort: true }, - ["ageMs", "stateGeneration"], - ); - }, - ); - it("defers a material heartbeat stall even when elapsed time is below the abort threshold", () => { const recoverStuckSession = vi.fn(); const warnSpy = vi.spyOn(diagnosticLogger, "warn").mockImplementation(() => undefined); @@ -543,8 +472,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 1_000, - stuckSessionAbortMs: 45_000, }, }, { recoverStuckSession }, @@ -585,8 +512,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 1_000, - stuckSessionAbortMs: 45_000, }, }, { recoverStuckSession }, @@ -627,7 +552,6 @@ describe("stuck session diagnostics threshold", () => { startDiagnosticHeartbeat({ diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }); logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); @@ -656,7 +580,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -694,7 +617,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -735,10 +657,12 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, - { recoverStuckSession }, + { + recoverStuckSession, + testTimings: { stuckSessionWarnMs: 30_000, stuckSessionAbortMs: 5 * 60_000 }, + }, ); logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); @@ -777,7 +701,6 @@ describe("stuck session diagnostics threshold", () => { startDiagnosticHeartbeat({ diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }); @@ -803,7 +726,7 @@ describe("stuck session diagnostics threshold", () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); const stuckSessionWarnMs = 30_000; - const stuckSessionAbortMs = resolveStuckSessionAbortMs(undefined, stuckSessionWarnMs); + const stuckSessionAbortMs = resolveStuckSessionAbortMs(stuckSessionWarnMs); const unsubscribe = onDiagnosticEvent((event) => { events.push(event); }); @@ -812,11 +735,12 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, - { recoverStuckSession }, + { + recoverStuckSession, + testTimings: { stuckSessionWarnMs, stuckSessionAbortMs }, + }, ); logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); @@ -845,8 +769,6 @@ describe("stuck session diagnostics threshold", () => { it("aborts stale embedded runs when queued work refreshes session activity", () => { const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 120_000; - const stuckSessionAbortMs = 360_000; logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); @@ -858,8 +780,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -876,8 +796,6 @@ describe("stuck session diagnostics threshold", () => { it("does not abort embedded runs with recent progress just because session activity is old", () => { const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; - const stuckSessionAbortMs = 60_000; logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); @@ -892,8 +810,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -916,7 +832,6 @@ describe("stuck session diagnostics threshold", () => { it("recovers stale native tool calls through the active-run abort path", async () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 60_000; const unsubscribe = onDiagnosticEvent((event) => { events.push(event); @@ -926,8 +841,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -981,8 +894,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1018,7 +929,6 @@ describe("stuck session diagnostics threshold", () => { it("recovers stale model calls through the active embedded-run abort path", async () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 60_000; const unsubscribe = onDiagnosticEvent((event) => { events.push(event); @@ -1028,8 +938,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -1084,11 +992,12 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, - { recoverStuckSession }, + { + recoverStuckSession, + testTimings: { stuckSessionWarnMs, stuckSessionAbortMs }, + }, ); logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); markDiagnosticEmbeddedRunStarted({ sessionId: "s1", sessionKey: "main" }); @@ -1124,7 +1033,6 @@ describe("stuck session diagnostics threshold", () => { it("does not actively abort model calls with recent stream progress", async () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 60_000; const unsubscribe = onDiagnosticEvent((event) => { events.push(event); @@ -1134,8 +1042,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -1178,7 +1084,6 @@ describe("stuck session diagnostics threshold", () => { it("actively aborts silent local model calls after the stuck timeout", async () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 60_000; const unsubscribe = onDiagnosticEvent((event) => { events.push(event); @@ -1188,8 +1093,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -1231,7 +1134,6 @@ describe("stuck session diagnostics threshold", () => { it("recovers stale model calls without active embedded-run ownership", async () => { const events: DiagnosticEventPayload[] = []; const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 60_000; const unsubscribe = onDiagnosticEvent((event) => { events.push(event); @@ -1241,8 +1143,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -1282,15 +1182,12 @@ describe("stuck session diagnostics threshold", () => { it("does not recover a recent native tool call just because the session is old", async () => { const recoverStuckSession = vi.fn(); - const stuckSessionWarnMs = 30_000; const stuckSessionAbortMs = 90_000; startDiagnosticHeartbeat( { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -1320,15 +1217,13 @@ describe("stuck session diagnostics threshold", () => { ); }); - it("uses diagnostics.stuckSessionAbortMs for stalled active-work recovery", () => { + it("uses the built-in abort threshold for stalled active-work recovery", () => { const recoverStuckSession = vi.fn(); startDiagnosticHeartbeat( { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1378,8 +1273,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1437,8 +1330,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1489,8 +1380,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1558,8 +1447,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1614,8 +1501,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1689,8 +1574,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -1737,7 +1620,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -1778,7 +1660,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -1822,7 +1703,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -1877,7 +1757,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -1946,8 +1825,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, - stuckSessionAbortMs: 60_000, }, }, { recoverStuckSession }, @@ -2007,7 +1884,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -2044,7 +1920,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -2086,7 +1961,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs: 30_000, }, }, { recoverStuckSession }, @@ -2127,8 +2001,6 @@ describe("stuck session diagnostics threshold", () => { { diagnostics: { enabled: true, - stuckSessionWarnMs, - stuckSessionAbortMs, }, }, { recoverStuckSession }, @@ -2636,20 +2508,9 @@ describe("stuck session diagnostics threshold", () => { expect(events.some((event) => event.type === "session.stuck")).toBe(false); }); - it("uses default threshold for invalid values", () => { - expect(resolveStuckSessionWarnMs({ diagnostics: { stuckSessionWarnMs: -1 } })).toBe(120_000); - expect(resolveStuckSessionWarnMs({ diagnostics: { stuckSessionWarnMs: 0 } })).toBe(120_000); + it("uses fixed session attention thresholds", () => { expect(resolveStuckSessionWarnMs()).toBe(120_000); - expect( - resolveStuckSessionAbortMs({ diagnostics: { stuckSessionAbortMs: 5_000 } }, 30_000), - ).toBe(30_000); - expect( - resolveStuckSessionAbortMs( - { diagnostics: { stuckSessionAbortMs: 48 * 60 * 60_000 } }, - 30_000, - ), - ).toBe(48 * 60 * 60_000); - expect(resolveStuckSessionAbortMs(undefined, 30_000)).toBe(5 * 60_000); + expect(resolveStuckSessionAbortMs(30_000)).toBe(5 * 60_000); }); }); diff --git a/src/logging/diagnostic.ts b/src/logging/diagnostic.ts index d8ceb65bad6d..4840038442ec 100644 --- a/src/logging/diagnostic.ts +++ b/src/logging/diagnostic.ts @@ -81,8 +81,6 @@ const webhookStats = { }; const DEFAULT_STUCK_SESSION_WARN_MS = 120_000; -const MIN_STUCK_SESSION_WARN_MS = 1_000; -const MAX_STUCK_SESSION_WARN_MS = 24 * 60 * 60 * 1000; const MIN_STALLED_EMBEDDED_RUN_ABORT_MS = 5 * 60_000; const STALLED_EMBEDDED_RUN_ABORT_WARN_MULTIPLIER = 3; const RECENT_DIAGNOSTIC_ACTIVITY_MS = 120_000; @@ -132,6 +130,11 @@ type StartDiagnosticHeartbeatOptions = { sampleLiveness?: SampleDiagnosticLiveness; recoverStuckSession?: RecoverStuckSession; startupGraceMs?: number; + /** Keeps fake-timer recovery tests fast without reopening runtime config tuning. */ + testTimings?: { + stuckSessionWarnMs: number; + stuckSessionAbortMs: number; + }; }; function resolveDiagnosticSessionStorePaths(config?: OpenClawConfig): string[] | undefined { @@ -146,10 +149,6 @@ function resolveDiagnosticSessionStorePaths(config?: OpenClawConfig): string[] | } } -function shouldWriteCriticalMemoryPressureBundle(config?: OpenClawConfig): boolean { - return config?.diagnostics?.memoryPressureSnapshot === true; -} - let diagnosticLivenessMonitor: EventLoopDelayMonitor | null = null; let lastDiagnosticLivenessWallAt = 0; let lastDiagnosticLivenessCpuUsage: CpuUsage | null = null; @@ -476,31 +475,12 @@ function formatDiagnosticWorkLabels(work: DiagnosticWorkSnapshot): string { return parts.join(" "); } -export function resolveStuckSessionWarnMs(config?: OpenClawConfig): number { - const raw = config?.diagnostics?.stuckSessionWarnMs; - if (typeof raw !== "number" || !Number.isFinite(raw)) { - return DEFAULT_STUCK_SESSION_WARN_MS; - } - const rounded = Math.floor(raw); - if (rounded < MIN_STUCK_SESSION_WARN_MS || rounded > MAX_STUCK_SESSION_WARN_MS) { - return DEFAULT_STUCK_SESSION_WARN_MS; - } - return rounded; +export function resolveStuckSessionWarnMs(): number { + return DEFAULT_STUCK_SESSION_WARN_MS; } -export function resolveStuckSessionAbortMs( - config: OpenClawConfig | undefined, - stuckSessionWarnMs: number, -): number { - const raw = config?.diagnostics?.stuckSessionAbortMs; - if (typeof raw !== "number" || !Number.isFinite(raw)) { - return resolveStalledEmbeddedRunAbortMs(stuckSessionWarnMs); - } - const rounded = Math.floor(raw); - if (rounded <= 0) { - return resolveStalledEmbeddedRunAbortMs(stuckSessionWarnMs); - } - return Math.max(stuckSessionWarnMs, rounded); +export function resolveStuckSessionAbortMs(stuckSessionWarnMs: number): number { + return resolveStalledEmbeddedRunAbortMs(stuckSessionWarnMs); } function resolveStalledEmbeddedRunAbortMs(stuckSessionWarnMs: number): number { @@ -1237,8 +1217,9 @@ export function startDiagnosticHeartbeat( heartbeatConfig = undefined; } } - const stuckSessionWarnMs = resolveStuckSessionWarnMs(heartbeatConfig); - const stuckSessionAbortMs = resolveStuckSessionAbortMs(heartbeatConfig, stuckSessionWarnMs); + const stuckSessionWarnMs = opts?.testTimings?.stuckSessionWarnMs ?? resolveStuckSessionWarnMs(); + const stuckSessionAbortMs = + opts?.testTimings?.stuckSessionAbortMs ?? resolveStuckSessionAbortMs(stuckSessionWarnMs); const compactionSafetyTimeoutMs = resolveCompactionTimeoutMs(heartbeatConfig); const now = Date.now(); const heartbeatElapsedMs = @@ -1273,7 +1254,7 @@ export function startDiagnosticHeartbeat( } else { emitDiagnosticMemorySample({ emitSample: shouldRecordMemorySample, - writeCriticalBundle: shouldWriteCriticalMemoryPressureBundle(heartbeatConfig), + writeCriticalBundle: false, resolveSessionStorePaths: () => resolveDiagnosticSessionStorePaths(heartbeatConfig), }); } diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index 262439b609ce..66b6201cfc6a 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -239,7 +239,6 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { url, token: token || undefined, password: password || undefined, - preauthHandshakeTimeoutMs: cfg.gateway?.handshakeTimeoutMs, instanceId: nodeId, clientName: GATEWAY_CLIENT_NAMES.NODE_HOST, clientDisplayName: displayName, @@ -356,9 +355,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { process.once("SIGINT", onSigint); process.once("SIGTERM", onSigterm); - const readinessPromise = startGatewayClientWhenEventLoopReady(client, { - clientOptions: { preauthHandshakeTimeoutMs: cfg.gateway?.handshakeTimeoutMs }, - }); + const readinessPromise = startGatewayClientWhenEventLoopReady(client); let readiness; try { readiness = await readinessPromise; diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index cd6b9aeacfa7..5cca0e6cb3c9 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -492,6 +492,18 @@ describe("secret ref resolver", () => { }, ); + it("enforces the built-in per-provider reference limit", async () => { + const refs = Array.from({ length: 513 }, (_, index) => ({ + source: "env" as const, + provider: "default", + id: `SECRET_${index}`, + })); + + await expect(resolveSecretRefValues(refs, { config: {} })).rejects.toThrow( + 'Secret provider "default" exceeded maxRefsPerProvider (512).', + ); + }); + itPosix("rejects symlink command paths unless allowSymlinkCommand is enabled", async () => { const root = await createCaseDir("exec-link-reject"); const symlinkPath = path.join(root, "resolver-link.mjs"); diff --git a/src/secrets/resolve.ts b/src/secrets/resolve.ts index 0bde25e8552d..a26f6cedc552 100644 --- a/src/secrets/resolve.ts +++ b/src/secrets/resolve.ts @@ -126,18 +126,11 @@ function isAbsolutePathname(value: string): boolean { ); } -function resolveResolutionLimits(config: OpenClawConfig): ResolutionLimits { - const resolution = config.secrets?.resolution; +function resolveResolutionLimits(): ResolutionLimits { return { - maxProviderConcurrency: normalizePositiveInt( - resolution?.maxProviderConcurrency, - DEFAULT_PROVIDER_CONCURRENCY, - ), - maxRefsPerProvider: normalizePositiveInt( - resolution?.maxRefsPerProvider, - DEFAULT_MAX_REFS_PER_PROVIDER, - ), - maxBatchBytes: normalizePositiveInt(resolution?.maxBatchBytes, DEFAULT_MAX_BATCH_BYTES), + maxProviderConcurrency: DEFAULT_PROVIDER_CONCURRENCY, + maxRefsPerProvider: DEFAULT_MAX_REFS_PER_PROVIDER, + maxBatchBytes: DEFAULT_MAX_BATCH_BYTES, }; } @@ -807,7 +800,7 @@ async function resolveSecretRefProviderGroups(params: { errorMode: "continue" | "stop"; }) { const groups = normalizeAndGroupSecretRefs(params.refs); - const limits = resolveResolutionLimits(params.options.config); + const limits = resolveResolutionLimits(); const errorsByIndex = new Map(); const taskResults = await runTasksWithConcurrency({ tasks: createProviderResolutionTasks({ groups, options: params.options, limits }), diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 0b8b76c7da92..1ccad59cb84b 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -821,41 +821,6 @@ describe("secrets runtime snapshot", () => { ).rejects.toThrow("Env secret reference id must match"); }); - it("keeps provider resolution limit violations fail-closed", async () => { - await expect( - prepareSecretsRuntimeSnapshot({ - config: asConfig({ - secrets: { - resolution: { maxRefsPerProvider: 1 }, - }, - models: { - providers: { - openai: { - apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, - baseUrl: "https://api.openai.com/v1", - models: [], - }, - }, - }, - messages: { - tts: { - providers: { - elevenlabs: { apiKey: TTS_REF }, - }, - }, - }, - }), - env: { - OPENAI_API_KEY: "test-openai-api-key", - ELEVENLABS_API_KEY: "test-elevenlabs-api-key", - }, - includeAuthStoreRefs: false, - allowUnavailableSecretOwners: true, - loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, - }), - ).rejects.toThrow('Secret provider "default" exceeded maxRefsPerProvider (1).'); - }); - it("keeps unconfigured SecretRef provider aliases fail-closed", async () => { await expect( prepareSecretsRuntimeSnapshot({ diff --git a/src/system-agent/inference-route.ts b/src/system-agent/inference-route.ts index 22018f580ab4..1804bb2f65e4 100644 --- a/src/system-agent/inference-route.ts +++ b/src/system-agent/inference-route.ts @@ -267,7 +267,6 @@ export async function projectInferenceRoute( auth: { profiles: authProfiles, order: authOrder, - cooldowns: structuredClone(config.auth?.cooldowns), }, models: { mode: config.models?.mode, diff --git a/src/transcripts/config.ts b/src/transcripts/config.ts index ea84559f7e26..03ac4fb212ee 100644 --- a/src/transcripts/config.ts +++ b/src/transcripts/config.ts @@ -32,7 +32,6 @@ export type ResolvedTranscriptsAutoStartConfig = { /** Raw transcripts config block. */ export type TranscriptsConfig = { enabled?: boolean; - maxUtterances?: number; autoStart?: TranscriptsAutoStartConfig[]; }; @@ -43,6 +42,8 @@ type ResolvedTranscriptsConfig = { autoStart: ResolvedTranscriptsAutoStartConfig[]; }; +const DEFAULT_TRANSCRIPTS_MAX_UTTERANCES = 2_000; + function resolveAutoStart(raw: unknown): ResolvedTranscriptsAutoStartConfig[] { if (!Array.isArray(raw)) { return []; @@ -70,13 +71,9 @@ function resolveAutoStart(raw: unknown): ResolvedTranscriptsAutoStartConfig[] { /** Normalize raw transcripts config into runtime settings. */ export function resolveTranscriptsConfig(raw: unknown): ResolvedTranscriptsConfig { const config = raw && typeof raw === "object" ? (raw as Record) : {}; - const maxUtterances = - typeof config.maxUtterances === "number" && Number.isFinite(config.maxUtterances) - ? Math.max(1, Math.min(10_000, Math.floor(config.maxUtterances))) - : 2_000; return { enabled: config.enabled === true, - maxUtterances, + maxUtterances: DEFAULT_TRANSCRIPTS_MAX_UTTERANCES, autoStart: resolveAutoStart(config.autoStart), }; } diff --git a/src/tui/embedded-backend.test.ts b/src/tui/embedded-backend.test.ts index e4032dca619c..c8cb761b2cbd 100644 --- a/src/tui/embedded-backend.test.ts +++ b/src/tui/embedded-backend.test.ts @@ -1300,7 +1300,7 @@ describe("EmbeddedTuiBackend", () => { agentCommandFromIngressMock.mockReturnValueOnce(first.promise); resolveActiveEmbeddedRunSessionIdMock.mockReturnValue("active-session"); loadSessionEntryMock.mockImplementation((sessionKey: string) => ({ - cfg: { messages: { queue: { debounceMs: 125 } } }, + cfg: {}, canonicalKey: sessionKey, storePath: "/tmp/openclaw-sessions.json", store: {}, @@ -1331,7 +1331,7 @@ describe("EmbeddedTuiBackend", () => { expect(queueEmbeddedAgentMessageWithOutcomeAsyncMock).toHaveBeenCalledWith( "active-session", "steer this turn", - { steeringMode: "all", debounceMs: 125 }, + { steeringMode: "all", debounceMs: 500 }, ); expect(agentCommandFromIngressMock).toHaveBeenCalledTimes(1); @@ -1354,7 +1354,7 @@ describe("EmbeddedTuiBackend", () => { .mockReturnValueOnce(second.promise); resolveActiveEmbeddedRunSessionIdMock.mockReturnValue("active-session"); loadSessionEntryMock.mockImplementation((sessionKey: string) => ({ - cfg: { messages: { queue: { debounceMs: 0 } } }, + cfg: {}, canonicalKey: sessionKey, storePath: "/tmp/openclaw-sessions.json", store: {}, @@ -1449,7 +1449,7 @@ describe("EmbeddedTuiBackend", () => { .mockReturnValueOnce(first.promise) .mockReturnValueOnce(collected.promise); loadSessionEntryMock.mockImplementation((sessionKey: string) => ({ - cfg: { messages: { queue: { mode: "collect", debounceMs: 0 } } }, + cfg: { messages: { queue: { mode: "collect" } } }, canonicalKey: sessionKey, storePath: "/tmp/openclaw-sessions.json", store: {}, @@ -1508,7 +1508,7 @@ describe("EmbeddedTuiBackend", () => { .mockReturnValueOnce(second.promise); loadSessionEntryMock.mockImplementation((sessionKey: string) => ({ cfg: { - messages: { queue: { mode: "followup", debounceMs: 0, cap: 1, drop: "new" } }, + messages: { queue: { mode: "followup", cap: 1, drop: "new" } }, }, canonicalKey: sessionKey, storePath: "/tmp/openclaw-sessions.json", diff --git a/src/tui/gateway-chat.test.ts b/src/tui/gateway-chat.test.ts index e600d656c65a..6ec4f9c16cc1 100644 --- a/src/tui/gateway-chat.test.ts +++ b/src/tui/gateway-chat.test.ts @@ -172,7 +172,6 @@ describe("resolveGatewayConnection", () => { gateway: { mode: "remote", remote: { url: "wss://selected.example/ws" }, - handshakeTimeoutMs: 12_345, }, }, url: "wss://selected.example/ws", @@ -184,7 +183,6 @@ describe("resolveGatewayConnection", () => { token: undefined, password: undefined, tlsFingerprint: "sha256:selected", - preauthHandshakeTimeoutMs: 12_345, allowInsecureLocalOperatorUi: false, }); expect(loadConfig).not.toHaveBeenCalled(); @@ -227,20 +225,6 @@ describe("resolveGatewayConnection", () => { }); }); - it("carries configured handshake timeout to the TUI client connection", async () => { - loadConfig.mockReturnValue({ - gateway: { - mode: "local", - handshakeTimeoutMs: 30_000, - auth: { token: "config-token" }, - }, - }); - - const result = await resolveGatewayConnection({}); - - expect(result.preauthHandshakeTimeoutMs).toBe(30_000); - }); - it("keeps the TLS pin on an explicit Gateway target", async () => { loadConfig.mockReturnValue({ gateway: { mode: "local" } }); diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index d850359c814b..56c992fb179d 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -445,7 +445,6 @@ function resolveBoundGatewayConnection( token: explicitAuth.token, password: explicitAuth.password, ...(opts.tlsFingerprint ? { tlsFingerprint: opts.tlsFingerprint } : {}), - preauthHandshakeTimeoutMs: opts.config.gateway?.handshakeTimeoutMs, allowInsecureLocalOperatorUi: false, }; } @@ -499,7 +498,6 @@ async function resolveGatewayConnection( token: explicitAuth.token, password: explicitAuth.password, ...(opts.tlsFingerprint ? { tlsFingerprint: opts.tlsFingerprint } : {}), - preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs, allowInsecureLocalOperatorUi, }; } @@ -522,7 +520,6 @@ async function resolveGatewayConnection( ...((opts.tlsFingerprint ?? config.gateway?.remote?.tlsFingerprint) ? { tlsFingerprint: opts.tlsFingerprint ?? config.gateway?.remote?.tlsFingerprint } : {}), - preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs, allowInsecureLocalOperatorUi: false, }; } @@ -539,7 +536,6 @@ async function resolveGatewayConnection( token: resolved.token, password: resolved.password, ...(opts.tlsFingerprint ? { tlsFingerprint: opts.tlsFingerprint } : {}), - preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs, allowInsecureLocalOperatorUi, }; } @@ -565,7 +561,6 @@ async function resolveGatewayConnection( token: resolved.token, password: resolved.password, ...(opts.tlsFingerprint ? { tlsFingerprint: opts.tlsFingerprint } : {}), - preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs, allowInsecureLocalOperatorUi, }; } diff --git a/src/tui/tui-pty-local.e2e.test.ts b/src/tui/tui-pty-local.e2e.test.ts index 10977921365b..40320221854b 100644 --- a/src/tui/tui-pty-local.e2e.test.ts +++ b/src/tui/tui-pty-local.e2e.test.ts @@ -641,7 +641,6 @@ function buildGatewayModeConfig(params: { tempDir: string; providerBaseUrl: stri messages: { queue: { mode: "followup", - debounceMs: 25, }, }, } satisfies OpenClawConfig; diff --git a/test/gateway-queued-session-rotation.e2e.test.ts b/test/gateway-queued-session-rotation.e2e.test.ts index 9a964d5988c5..35c7b8456ec1 100644 --- a/test/gateway-queued-session-rotation.e2e.test.ts +++ b/test/gateway-queued-session-rotation.e2e.test.ts @@ -242,7 +242,7 @@ describe("Gateway queued session rotation", () => { }, }, }, - messages: { queue: { mode: "followup", debounceMs: 0 } }, + messages: { queue: { mode: "followup" } }, } satisfies OpenClawConfig; const instance = await createOpenClawTestInstance({ name: "queued-session-rotation", diff --git a/test/scripts/config-reload-log-scanner.test.ts b/test/scripts/config-reload-log-scanner.test.ts index 3841663f0109..b07b90887e0c 100644 --- a/test/scripts/config-reload-log-scanner.test.ts +++ b/test/scripts/config-reload-log-scanner.test.ts @@ -57,16 +57,15 @@ describe("config reload log scanner", () => { writeFileSync(logPath, "config change detected"); expect(scanner.scan().reloadLines).toEqual([]); - appendFileSync(logPath, "; evaluating reload: gateway.channelHealthCheckMinutes\n"); + appendFileSync(logPath, "; evaluating reload: ui.seamColor\n"); expect(scanner.scan().reloadLines).toEqual([ - "config change detected; evaluating reload: gateway.channelHealthCheckMinutes", + "config change detected; evaluating reload: ui.seamColor", ]); }); it("starts from a bounded tail of oversized logs", () => { const logPath = path.join(makeTempRoot(), "gateway.log"); - const reloadLine = - "config change detected; evaluating reload: gateway.channelHealthCheckMinutes\n"; + const reloadLine = "config change detected; evaluating reload: ui.seamColor\n"; writeFileSync(logPath, `${"x".repeat(4096)}\n${reloadLine}`); const scanner = createConfigReloadLogScanner(logPath, { @@ -75,7 +74,7 @@ describe("config reload log scanner", () => { }); expect(scanner.scan().reloadLines).toEqual([ - "config change detected; evaluating reload: gateway.channelHealthCheckMinutes", + "config change detected; evaluating reload: ui.seamColor", ]); }); diff --git a/test/scripts/config-reload-mutate-metadata.test.ts b/test/scripts/config-reload-mutate-metadata.test.ts index a23d6bfa435f..d636bde8bf2c 100644 --- a/test/scripts/config-reload-mutate-metadata.test.ts +++ b/test/scripts/config-reload-mutate-metadata.test.ts @@ -23,17 +23,15 @@ function runMutateMetadata(configPath: string) { } describe("config reload metadata mutator", () => { - it("updates the config reload gateway metadata knob without dropping existing config", () => { + it("updates config display metadata without dropping existing config", () => { const root = makeTempDir(tempDirs, "openclaw-config-reload-metadata-"); const configPath = path.join(root, "openclaw.json"); writeFileSync( configPath, `${JSON.stringify( { - gateway: { - channelHealthCheckMinutes: 1, - port: 18789, - }, + gateway: { port: 18789 }, + ui: { seamColor: "#ff4500" }, plugins: { entries: { demo: { enabled: true }, @@ -51,10 +49,8 @@ describe("config reload metadata mutator", () => { expect(result.status).toBe(0); expect(result.stderr).toBe(""); expect(JSON.parse(readFileSync(configPath, "utf8"))).toEqual({ - gateway: { - channelHealthCheckMinutes: 2, - port: 18789, - }, + gateway: { port: 18789 }, + ui: { seamColor: "#ff6600" }, plugins: { entries: { demo: { enabled: true }, diff --git a/test/test-env.ts b/test/test-env.ts index a944b41c792f..a428730b222c 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -373,10 +373,6 @@ function sanitizeLiveConfig(raw: string): string { }); } - if (parsed.diagnostics && typeof parsed.diagnostics === "object") { - delete parsed.diagnostics.memoryPressureSnapshot; - } - if (!isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_NORMALIZE_CONFIG)) { return `${JSON.stringify(parsed, null, 2)}\n`; } diff --git a/ui/src/pages/config/settings-search.ts b/ui/src/pages/config/settings-search.ts index 66ffc63a9c91..18fd255909c1 100644 --- a/ui/src/pages/config/settings-search.ts +++ b/ui/src/pages/config/settings-search.ts @@ -203,12 +203,7 @@ const WORKSPACE_SETTINGS_BLOCKS = { routeId: "worktrees", labelKey: "worktrees.title", hash: "", - searchKeys: [ - "worktrees.subtitle", - "worktrees.cleanupTitle", - "worktrees.cleanupMaxCount", - "worktrees.cleanupMaxSize", - ], + searchKeys: ["worktrees.subtitle"], aliases: "git checkout branch cleanup", }, } as const satisfies Record; diff --git a/ui/src/pages/config/view.browser.test.ts b/ui/src/pages/config/view.browser.test.ts index 152ec9a4426e..52e7c38ff29e 100644 --- a/ui/src/pages/config/view.browser.test.ts +++ b/ui/src/pages/config/view.browser.test.ts @@ -687,8 +687,8 @@ describe("config view", () => { auth: { type: "object", properties: { - authPermanentBackoffMinutes: { - type: "number", + order: { + type: "object", }, }, }, @@ -696,12 +696,12 @@ describe("config view", () => { }, formValue: { auth: { - authPermanentBackoffMinutes: 10, + order: {}, }, }, originalValue: { auth: { - authPermanentBackoffMinutes: 10, + order: {}, }, }, }); diff --git a/ui/src/pages/worktrees/worktrees-page.test.ts b/ui/src/pages/worktrees/worktrees-page.test.ts index e4b63a9b983a..c209a59d6f99 100644 --- a/ui/src/pages/worktrees/worktrees-page.test.ts +++ b/ui/src/pages/worktrees/worktrees-page.test.ts @@ -17,9 +17,6 @@ type WorktreesPageTestElement = HTMLElement & { createName: string; createBaseRef: string; createBranches: string[]; - cleanupLoaded: boolean; - cleanupMaxCount: number; - cleanupMaxSizeGb: number; updateComplete: Promise; requestUpdate: () => void; load: (options?: { preserveError?: boolean }) => Promise; @@ -27,8 +24,6 @@ type WorktreesPageTestElement = HTMLElement & { createWorktree: () => Promise; removeWorktree: (record: WorktreeRecord) => Promise; restore: (record: WorktreeRecord) => Promise; - setCleanupLimit: (key: "maxCount" | "maxTotalSizeGb", value: number) => void; - commitCleanupLimits: () => Promise; gc: () => Promise; }; @@ -110,42 +105,6 @@ function contextWithGateway(gateway: ApplicationContext["gateway"]): Application } as unknown as ApplicationContext; } -function runtimeConfigStub(cleanup?: { maxCount?: number; maxTotalSizeGb?: number }) { - const state = { - configSnapshot: { sourceConfig: cleanup ? { worktrees: { cleanup } } : {} }, - lastError: null as string | null, - }; - const listeners = new Set<(next: typeof state) => void>(); - return { - state, - ensureLoaded: vi.fn(async () => undefined), - refresh: vi.fn(async () => undefined), - patch: vi.fn(async () => true), - subscribe: (listener: (next: typeof state) => void) => { - listeners.add(listener); - return () => listeners.delete(listener); - }, - emit() { - for (const listener of listeners) { - listener(state); - } - }, - }; -} - -function contextWithConfig( - gateway: ApplicationContext["gateway"], - runtimeConfig: ReturnType, -): ApplicationContext { - return { - basePath: "", - gateway, - navigate: vi.fn(), - preload: vi.fn(async () => undefined), - runtimeConfig, - } as unknown as ApplicationContext; -} - afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); @@ -479,8 +438,6 @@ describe("WorktreesPage lifecycle", () => { ); await page.updateComplete; - // type="text" scopes to the create-draft inputs; the cleanup section's - // number inputs have their own disabled lifecycle. const draftInputs = Array.from( page.querySelectorAll('input.settings-input[type="text"]'), ); @@ -510,204 +467,6 @@ describe("WorktreesPage lifecycle", () => { expect(freshInputs.every((input) => !input.disabled)).toBe(true); }); - it("renders cleanup limits from config and disables controls until config loads", async () => { - const request = vi.fn(async () => ({ worktrees: [] })); - const withConfig = document.createElement( - "openclaw-worktrees-page", - ) as WorktreesPageTestElement; - withConfig.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - runtimeConfigStub({ maxCount: 25, maxTotalSizeGb: 50 }), - ); - document.body.append(withConfig); - await waitForFast(() => expect(withConfig.cleanupLoaded).toBe(true)); - await withConfig.updateComplete; - - expect(withConfig.cleanupMaxCount).toBe(25); - expect(withConfig.cleanupMaxSizeGb).toBe(50); - const inputs = Array.from( - withConfig.querySelectorAll('input.settings-input[type="number"]'), - ); - expect(inputs.map((input) => input.value)).toEqual(["25", "50"]); - expect(inputs.every((input) => !input.disabled)).toBe(true); - - // Without a runtimeConfig capability the section stays visible but inert. - const withoutConfig = document.createElement( - "openclaw-worktrees-page", - ) as WorktreesPageTestElement; - withoutConfig.context = contextWithGateway( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - ); - document.body.append(withoutConfig); - await withoutConfig.updateComplete; - const inertInputs = Array.from( - withoutConfig.querySelectorAll('input.settings-input[type="number"]'), - ); - expect(inertInputs).toHaveLength(2); - expect(inertInputs.every((input) => input.disabled)).toBe(true); - }); - - it("debounces stepper edits into one minimal config patch", async () => { - vi.useFakeTimers(); - try { - const request = vi.fn(async () => ({ worktrees: [] })); - const runtimeConfig = runtimeConfigStub({ maxCount: 25, maxTotalSizeGb: 50 }); - const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement; - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - runtimeConfig, - ); - document.body.append(page); - await page.updateComplete; - expect(page.cleanupLoaded).toBe(true); - - page.setCleanupLimit("maxCount", 26); - page.setCleanupLimit("maxCount", 27); - page.setCleanupLimit("maxTotalSizeGb", 49); - expect(page.cleanupMaxCount).toBe(27); - expect(page.cleanupMaxSizeGb).toBe(49); - expect(runtimeConfig.patch).not.toHaveBeenCalled(); - - await vi.advanceTimersByTimeAsync(2_100); - - expect(runtimeConfig.patch).toHaveBeenCalledOnce(); - expect(runtimeConfig.patch).toHaveBeenCalledWith({ - raw: { worktrees: { cleanup: { maxCount: 27, maxTotalSizeGb: 49 } } }, - note: "worktrees: update cleanup limits", - }); - expect(runtimeConfig.refresh).toHaveBeenCalledOnce(); - } finally { - vi.useRealTimers(); - } - }); - - it("flushes pending cleanup edits before Clean up now", async () => { - vi.useFakeTimers(); - try { - const calls: string[] = []; - const request = vi.fn(async (method: string) => { - calls.push(method); - return { worktrees: [] }; - }); - const runtimeConfig = runtimeConfigStub({ maxCount: 25 }); - runtimeConfig.patch = vi.fn(async () => { - calls.push("config.patch"); - return true; - }); - const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement; - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - runtimeConfig, - ); - document.body.append(page); - await vi.advanceTimersByTimeAsync(0); - expect(page.loading).toBe(false); - - page.setCleanupLimit("maxCount", 24); - await page.gc(); - - expect( - calls.filter((method) => method === "config.patch" || method === "worktrees.gc"), - ).toEqual(["config.patch", "worktrees.gc"]); - } finally { - vi.useRealTimers(); - } - }); - - it("aborts Clean up now when the pending limit commit fails", async () => { - vi.useFakeTimers(); - try { - const request = vi.fn(async () => ({ worktrees: [] })); - const runtimeConfig = runtimeConfigStub({ maxCount: 25 }); - runtimeConfig.patch = vi.fn(async () => false); - runtimeConfig.state.lastError = "save rejected"; - const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement; - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - runtimeConfig, - ); - document.body.append(page); - await vi.advanceTimersByTimeAsync(0); - - page.setCleanupLimit("maxCount", 30); - // The debounced save fails first; the draft must stay dirty so a later - // Clean up now retries the save instead of running with stale limits. - await vi.advanceTimersByTimeAsync(2_100); - expect(runtimeConfig.patch).toHaveBeenCalledTimes(1); - - await page.gc(); - - expect(runtimeConfig.patch).toHaveBeenCalledTimes(2); - expect(request).not.toHaveBeenCalledWith("worktrees.gc", {}); - expect(page.error).toBe("save rejected"); - expect(page.loading).toBe(false); - } finally { - vi.useRealTimers(); - } - }); - - it("drops a pending edit when the runtime-config source is replaced", async () => { - vi.useFakeTimers(); - try { - const request = vi.fn(async () => ({ worktrees: [] })); - const originalConfig = runtimeConfigStub({ maxCount: 25 }); - const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement; - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - originalConfig, - ); - document.body.append(page); - await vi.advanceTimersByTimeAsync(0); - - page.setCleanupLimit("maxCount", 30); - const replacementConfig = runtimeConfigStub({ maxCount: 25 }); - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - replacementConfig, - ); - page.requestUpdate(); - await vi.advanceTimersByTimeAsync(2_100); - - expect(originalConfig.patch).not.toHaveBeenCalled(); - expect(replacementConfig.patch).not.toHaveBeenCalled(); - } finally { - vi.useRealTimers(); - } - }); - - it("clamps cleanup edits to non-negative integers and surfaces patch failures", async () => { - vi.useFakeTimers(); - try { - const request = vi.fn(async () => ({ worktrees: [] })); - const runtimeConfig = runtimeConfigStub({ maxCount: 1 }); - runtimeConfig.patch = vi.fn(async () => false); - runtimeConfig.state.lastError = "config hash mismatch"; - const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement; - page.context = contextWithConfig( - gatewayWithClient({ request } as unknown as GatewayBrowserClient), - runtimeConfig, - ); - document.body.append(page); - await page.updateComplete; - - page.setCleanupLimit("maxTotalSizeGb", 0.5); - expect(page.cleanupMaxSizeGb).toBe(0.5); - page.setCleanupLimit("maxCount", 5.7); - expect(page.cleanupMaxCount).toBe(5); - page.setCleanupLimit("maxCount", -5); - expect(page.cleanupMaxCount).toBe(0); - - await vi.advanceTimersByTimeAsync(2_100); - expect(runtimeConfig.patch).toHaveBeenCalledWith({ - raw: { worktrees: { cleanup: { maxCount: 0, maxTotalSizeGb: 0.5 } } }, - note: "worktrees: update cleanup limits", - }); - expect(page.error).toBe("config hash mismatch"); - } finally { - vi.useRealTimers(); - } - }); - it("uses the current branch when a repository has no remote default", async () => { const request = vi.fn((method: string) => { if (method === "worktrees.branches") { diff --git a/ui/src/pages/worktrees/worktrees-page.ts b/ui/src/pages/worktrees/worktrees-page.ts index 36d94be68ae0..fb7a69435f21 100644 --- a/ui/src/pages/worktrees/worktrees-page.ts +++ b/ui/src/pages/worktrees/worktrees-page.ts @@ -16,7 +16,6 @@ import { } from "../../components/settings-ui.ts"; import { renderSettingsWorkspace } from "../../components/settings-workspace.ts"; import { t } from "../../i18n/index.ts"; -import { resolveEditableSnapshotConfig } from "../../lib/config/index.ts"; import { formatRelativeTimestamp } from "../../lib/format.ts"; import { searchForSession } from "../../lib/sessions/index.ts"; import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts"; @@ -40,36 +39,6 @@ function repoName(repoRoot: string): string { return repoRoot.split(/[\\/]/).findLast(Boolean) ?? repoRoot; } -type CleanupLimitKey = "maxCount" | "maxTotalSizeGb"; - -// Coalesces bursts of stepper clicks into one config.patch and keeps sustained -// editing far below the control-plane config-write quota (3 writes / 60 s). -const CLEANUP_COMMIT_DELAY_MS = 2_000; - -// The count is an integer, but the size limit accepts fractions (0.5 GB), so -// only maxCount gets floored; flooring the size would display 0.5 as the -// documented "disabled" value 0. -function normalizeCleanupLimit(key: CleanupLimitKey, value: number): number { - if (!Number.isFinite(value) || value <= 0) { - return 0; - } - return key === "maxCount" ? Math.floor(value) : value; -} - -function cleanupLimitFromConfig( - config: Record | null, - key: CleanupLimitKey, -): number { - const worktrees = config?.worktrees; - const cleanup = - worktrees && typeof worktrees === "object" - ? (worktrees as { cleanup?: unknown }).cleanup - : undefined; - const value = - cleanup && typeof cleanup === "object" ? (cleanup as Record)[key] : undefined; - return typeof value === "number" ? normalizeCleanupLimit(key, value) : 0; -} - class WorktreesPage extends OpenClawLightDomElement { @consume({ context: applicationContext, subscribe: true }) private context!: ApplicationContext; @@ -84,18 +53,6 @@ class WorktreesPage extends OpenClawLightDomElement { @state() private createBaseRef = ""; @state() private createBranches: string[] = []; @state() private creating = false; - @state() private cleanupLoaded = false; - @state() private cleanupMaxCount = 0; - @state() private cleanupMaxSizeGb = 0; - - // Debounced stepper commits: rapid clicks fold into one rate-limited config.patch. - private cleanupCommitTimer: ReturnType | null = null; - private pendingCleanupPatch: Partial> = {}; - // Pending edits stay bound to the capability they were made against so a - // replaced gateway context never receives another gateway's limits. - private pendingCleanupSource: ApplicationContext["runtimeConfig"] | null = null; - private cleanupCommitInFlight: Promise | null = null; - private client: GatewayBrowserClient | null = null; private gatewayConnected = false; private gatewaySource?: ApplicationContext["gateway"]; @@ -103,165 +60,31 @@ class WorktreesPage extends OpenClawLightDomElement { private loadGeneration = 0; private branchesGeneration = 0; private operationEpoch = 0; - private readonly subscriptions = new SubscriptionsController(this) - .effect( - () => this.context?.gateway, - (gateway) => { - const sourceChanged = this.hasBoundGateway && this.gatewaySource !== gateway; - this.gatewaySource = gateway; - this.hasBoundGateway = true; - this.applyGatewaySnapshot(gateway.snapshot, sourceChanged); - return gateway.subscribe((snapshot) => { - if (this.gatewaySource === gateway && this.context.gateway === gateway) { - this.applyGatewaySnapshot(snapshot); - } - }); - }, - ) - .effect( - () => this.context?.runtimeConfig, - (runtimeConfig) => { - // A replaced capability invalidates drafts made against the old one; - // controls stay inert until the new snapshot populates them. - this.resetCleanupDraft(); - void runtimeConfig.ensureLoaded(); - this.syncCleanupFromConfig(); - return runtimeConfig.subscribe(() => this.syncCleanupFromConfig()); - }, - ); - - private resetCleanupDraft() { - if (this.cleanupCommitTimer) { - clearTimeout(this.cleanupCommitTimer); - this.cleanupCommitTimer = null; - } - this.pendingCleanupPatch = {}; - this.pendingCleanupSource = null; - this.cleanupLoaded = false; - } + private readonly subscriptions = new SubscriptionsController(this).effect( + () => this.context?.gateway, + (gateway) => { + const sourceChanged = this.hasBoundGateway && this.gatewaySource !== gateway; + this.gatewaySource = gateway; + this.hasBoundGateway = true; + this.applyGatewaySnapshot(gateway.snapshot, sourceChanged); + return gateway.subscribe((snapshot) => { + if (this.gatewaySource === gateway && this.context.gateway === gateway) { + this.applyGatewaySnapshot(snapshot); + } + }); + }, + ); override disconnectedCallback() { this.subscriptions.clear(); this.invalidateLoad(); this.invalidateOperations(); - // Flush a pending edit so navigating away does not drop it. - void this.flushCleanupEdits(); this.gatewaySource = undefined; this.client = null; this.gatewayConnected = false; super.disconnectedCallback(); } - private syncCleanupFromConfig() { - // A pending local edit owns the draft values until its patch settles. - if (this.cleanupCommitTimer || Object.keys(this.pendingCleanupPatch).length > 0) { - return; - } - const runtimeConfig = this.context?.runtimeConfig; - if (!runtimeConfig) { - return; - } - const config = resolveEditableSnapshotConfig(runtimeConfig.state.configSnapshot); - if (!config) { - return; - } - this.cleanupLoaded = true; - this.cleanupMaxCount = cleanupLimitFromConfig(config, "maxCount"); - this.cleanupMaxSizeGb = cleanupLimitFromConfig(config, "maxTotalSizeGb"); - } - - private setCleanupLimit(key: CleanupLimitKey, rawValue: number) { - const value = normalizeCleanupLimit(key, rawValue); - if (key === "maxCount") { - this.cleanupMaxCount = value; - } else { - this.cleanupMaxSizeGb = value; - } - this.pendingCleanupPatch[key] = value; - this.pendingCleanupSource = this.context?.runtimeConfig ?? null; - if (this.cleanupCommitTimer) { - clearTimeout(this.cleanupCommitTimer); - } - this.cleanupCommitTimer = setTimeout(() => { - this.cleanupCommitTimer = null; - void this.commitCleanupLimits(); - }, CLEANUP_COMMIT_DELAY_MS); - } - - /** - * Cancels the debounce timer and commits pending cleanup edits now. A failed - * in-flight commit re-queues its draft, so the serialized retry below still - * reports false and callers refuse to act on limits that never saved. - */ - private async flushCleanupEdits(): Promise { - if (this.cleanupCommitTimer) { - clearTimeout(this.cleanupCommitTimer); - this.cleanupCommitTimer = null; - } - return await this.commitCleanupLimits(); - } - - private async commitCleanupLimits(): Promise { - // Serialize config writes: starting while another commit is in flight - // would reuse its stale base hash and could land limits out of order. - while (this.cleanupCommitInFlight) { - await this.cleanupCommitInFlight; - } - const patch = this.pendingCleanupPatch; - if (Object.keys(patch).length === 0) { - return true; - } - const source = this.pendingCleanupSource; - this.pendingCleanupPatch = {}; - this.pendingCleanupSource = null; - const runtimeConfig = this.context?.runtimeConfig; - if (!runtimeConfig || (source !== null && source !== runtimeConfig)) { - // Dropping an edit made against a replaced context beats writing one - // gateway's limits into another gateway's config. - return false; - } - // A failed save re-queues the draft (newer edits win per key) so a later - // flush retries instead of reporting the unsaved limits as committed. - const restoreDraft = () => { - if (this.context?.runtimeConfig !== runtimeConfig) { - // The capability was replaced while this write was in flight; its - // draft must not leak into the replacement gateway's config. - return; - } - this.pendingCleanupPatch = { ...patch, ...this.pendingCleanupPatch }; - this.pendingCleanupSource = this.pendingCleanupSource ?? source; - }; - const commit = (async () => { - try { - await runtimeConfig.ensureLoaded(); - const patched = await runtimeConfig.patch({ - raw: { worktrees: { cleanup: patch } }, - note: "worktrees: update cleanup limits", - }); - if (!patched) { - this.error = runtimeConfig.state.lastError ?? t("worktrees.cleanupSaveFailed"); - restoreDraft(); - return false; - } - await runtimeConfig.refresh(); - this.syncCleanupFromConfig(); - return true; - } catch (error) { - this.error = String(error); - restoreDraft(); - return false; - } - })(); - this.cleanupCommitInFlight = commit; - try { - return await commit; - } finally { - if (this.cleanupCommitInFlight === commit) { - this.cleanupCommitInFlight = null; - } - } - } - private applyGatewaySnapshot( snapshot: ApplicationContext["gateway"]["snapshot"], sourceChanged = false, @@ -432,18 +255,6 @@ class WorktreesPage extends OpenClawLightDomElement { } this.loading = true; this.error = null; - // A pending stepper edit must reach the config before gc reads it, - // otherwise Clean up now evicts against the previous limits. - const flushed = await this.flushCleanupEdits(); - if (!this.isOperationScopeCurrent(scope)) { - return; - } - if (!flushed) { - // The failed commit already surfaced its error; gc must not run - // against limits the operator just tried to change. - this.loading = false; - return; - } try { await scope.client.request("worktrees.gc", {}); } catch (error) { @@ -612,30 +423,6 @@ class WorktreesPage extends OpenClawLightDomElement { `; } - private renderCleanupRow(key: CleanupLimitKey, label: string, help: string, value: number) { - // Controls stay inert until the config snapshot populates the draft values, - // otherwise an early edit would commit 0 over the operator's real limits. - const disabled = !this.cleanupLoaded || !this.gatewayConnected; - return renderSettingsRow({ - title: label, - description: help, - control: html` - { - this.setCleanupLimit(key, Number((event.target as HTMLInputElement).value)); - }} - /> - `, - }); - } - private renderRecordRow(record: WorktreeRecord) { return renderSettingsRow({ title: record.name, @@ -681,23 +468,6 @@ class WorktreesPage extends OpenClawLightDomElement { { title: t("worktrees.title"), description: t("worktrees.subtitle"), actions }, rows, )} - ${renderSettingsSection( - { title: t("worktrees.cleanupTitle"), description: t("worktrees.cleanupSubtitle") }, - html` - ${this.renderCleanupRow( - "maxCount", - t("worktrees.cleanupMaxCount"), - t("worktrees.cleanupMaxCountHelp"), - this.cleanupMaxCount, - )} - ${this.renderCleanupRow( - "maxTotalSizeGb", - t("worktrees.cleanupMaxSize"), - t("worktrees.cleanupMaxSizeHelp"), - this.cleanupMaxSizeGb, - )} - `, - )} `, { wide: true }, );