From 13a60aa93b02b30cdeb424c464d2aa119b81ff1f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 07:50:51 +0100 Subject: [PATCH] docs: document shared mention policy --- docs/channels/groups.md | 5 +- docs/channels/zalouser.md | 1 + docs/plugins/architecture.md | 3 +- docs/plugins/sdk-channel-plugins.md | 81 +++++++++++++++++++++++++++++ docs/plugins/sdk-overview.md | 2 +- docs/plugins/sdk-runtime.md | 40 ++++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) diff --git a/docs/channels/groups.md b/docs/channels/groups.md index 3999dfcbe51..655b0e5c764 100644 --- a/docs/channels/groups.md +++ b/docs/channels/groups.md @@ -227,7 +227,10 @@ Quick mental model (evaluation order for group messages): Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`. -Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams. +Replying to a bot message counts as an implicit mention when the channel +supports reply metadata. Quoting a bot message can also count as an implicit +mention on channels that expose quote metadata. Current built-in cases include +Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser. ```json5 { diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index 9ef4a99a186..aff1867a680 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -124,6 +124,7 @@ Example: - `channels.zalouser.groups..requireMention` controls whether group replies require a mention. - Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`). - This applies both to allowlisted groups and open group mode. +- Quoting a bot message counts as an implicit mention for group activation. - Authorized control commands (for example `/new`) can bypass mention gating. - When a group message is skipped because mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message. - Group history limit defaults to `messages.groupChat.historyLimit` (fallback `50`). You can override per account with `channels.zalouser.historyLimit`. diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 225c3671546..82923d97eaf 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -1120,7 +1120,8 @@ authoring plugins: `openclaw/plugin-sdk/secret-input`, and `openclaw/plugin-sdk/webhook-ingress` for shared setup/auth/reply/webhook wiring. `channel-inbound` is the shared home for debounce, mention matching, - envelope formatting, and inbound envelope context helpers. + inbound mention-policy helpers, envelope formatting, and inbound envelope + context helpers. `channel-setup` is the narrow optional-install setup seam. `setup-runtime` is the runtime-safe setup surface used by `setupEntry` / deferred startup, including the import-safe setup patch adapters. diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index 1bd6d5a7ff6..0bfdc7dd85d 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -152,6 +152,87 @@ surfaces: Auth-only channels can usually stop at the default path: core handles approvals and the plugin just exposes outbound/auth capabilities. Native approval channels such as Matrix, Slack, Telegram, and custom chat transports should use the shared native helpers instead of rolling their own approval lifecycle. +## Inbound mention policy + +Keep inbound mention handling split in two layers: + +- plugin-owned evidence gathering +- shared policy evaluation + +Use `openclaw/plugin-sdk/channel-inbound` for the shared layer. + +Good fit for plugin-local logic: + +- reply-to-bot detection +- quoted-bot detection +- thread-participation checks +- service/system-message exclusions +- platform-native caches needed to prove bot participation + +Good fit for the shared helper: + +- `requireMention` +- explicit mention result +- implicit mention allowlist +- command bypass +- final skip decision + +Preferred flow: + +1. Compute local mention facts. +2. Pass those facts into `resolveInboundMentionDecision({ facts, policy })`. +3. Use `decision.effectiveWasMentioned`, `decision.shouldBypassMention`, and `decision.shouldSkip` in your inbound gate. + +```typescript +import { + implicitMentionKindWhen, + matchesMentionWithExplicit, + resolveInboundMentionDecision, +} from "openclaw/plugin-sdk/channel-inbound"; + +const mentionMatch = matchesMentionWithExplicit(text, { + mentionRegexes, + mentionPatterns, +}); + +const facts = { + canDetectMention: true, + wasMentioned: mentionMatch.matched, + hasAnyMention: mentionMatch.hasExplicitMention, + implicitMentionKinds: [ + ...implicitMentionKindWhen("reply_to_bot", isReplyToBot), + ...implicitMentionKindWhen("quoted_bot", isQuoteOfBot), + ], +}; + +const decision = resolveInboundMentionDecision({ + facts, + policy: { + isGroup, + requireMention, + allowedImplicitMentionKinds: requireExplicitMention ? [] : ["reply_to_bot", "quoted_bot"], + allowTextCommands, + hasControlCommand, + commandAuthorized, + }, +}); + +if (decision.shouldSkip) return; +``` + +`api.runtime.channel.mentions` exposes the same shared mention helpers for +bundled channel plugins that already depend on runtime injection: + +- `buildMentionRegexes` +- `matchesMentionPatterns` +- `matchesMentionWithExplicit` +- `implicitMentionKindWhen` +- `resolveInboundMentionDecision` + +The older `resolveMentionGating*` helpers remain on +`openclaw/plugin-sdk/channel-inbound` as compatibility exports only. New code +should use `resolveInboundMentionDecision({ facts, policy })`. + ## Walkthrough diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 459bb068736..2af0276cc1f 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -108,7 +108,7 @@ explicitly promotes one as public. | `plugin-sdk/group-access` | Shared group-access decision helpers | | `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers | | `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers | - | `plugin-sdk/channel-inbound` | Debounce, mention matching, envelope helpers | + | `plugin-sdk/channel-inbound` | Inbound debounce, mention matching, mention-policy helpers, and envelope helpers | | `plugin-sdk/channel-send-result` | Reply result types | | `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` | | `plugin-sdk/channel-targets` | Target parsing/matching helpers | diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index 17a36cfcb25..e0a731976e4 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -330,6 +330,46 @@ api.runtime.tools.registerMemoryCli(/* ... */); Channel-specific runtime helpers (available when a channel plugin is loaded). +`api.runtime.channel.mentions` is the shared inbound mention-policy surface for +bundled channel plugins that use runtime injection: + +```typescript +const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, { + mentionRegexes, + mentionPatterns, +}); + +const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({ + facts: { + canDetectMention: true, + wasMentioned: mentionMatch.matched, + implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen( + "reply_to_bot", + isReplyToBot, + ), + }, + policy: { + isGroup, + requireMention, + allowTextCommands, + hasControlCommand, + commandAuthorized, + }, +}); +``` + +Available mention helpers: + +- `buildMentionRegexes` +- `matchesMentionPatterns` +- `matchesMentionWithExplicit` +- `implicitMentionKindWhen` +- `resolveInboundMentionDecision` + +`api.runtime.channel.mentions` intentionally does not expose the older +`resolveMentionGating*` compatibility helpers. Prefer the normalized +`{ facts, policy }` path. + ## Storing runtime references Use `createPluginRuntimeStore` to store the runtime reference for use outside