Files
openclaw/docs/channels/group-messages.md
Peter Steinberger 82d1a03f25 refactor(agents): move implicit-main fallback into load-time roster injection (#112678)
* refactor(agents): require explicit roster defaults

* feat(onboard): create named first roster agent

* refactor(agents): remove runtime main fallbacks

* style(agents): apply roster refactor formatting

* refactor(agents): finish roster-only runtime sweep

* fix(doctor): migrate legacy main session sqlite

* fix(doctor): harden roster session migrations

* fix(onboard): commit first agent atomically

* fix(config): support empty-roster analysis

* fix(agents): preserve legacy main state during creation

* fix(setup): materialize baseline agent roster

* fix(agents): harden legacy default transfer recovery

* fix(agents): simplify roster-only legacy compatibility

* fix(agents): preserve staged first-agent entries

* fix(config): migrate persisted implicit-main rosters

* fix(config): preserve staged empty rosters

* fix(agents): finalize roster-only upgrade paths

* fix(sessions): close legacy main migration outcomes

* fix(config): migrate legacy roster markers at load

* fix(sessions): preserve roster upgrade history

* refactor(sessions): restore lean legacy main compatibility

* fix(setup): prepare first-agent credentials before publish

* fix(config): stabilize roster snapshot migration

* refactor(sessions): shrink legacy main compatibility

* fix(agents): restore roster compatibility fidelity

* fix(sessions): preserve divergent legacy history

* refactor(agents): narrow roster-only scope

* fix(config): isolate roster migration

* test(agents): align roster-only fixtures

* fix(agents): keep main agent undeletable

* fix(agents): harden roster migration invariants

* fix(agents): close setup and audit scope gaps

* fix(cron): scope session reaper throttles by agent

* fix(agents): preserve scoped owner precedence

* fix(config): preserve authored config ownership

* fix(setup): keep default workspace and roster in sync

* fix(setup): preserve default entry workspace on bare runs

* fix(agents): adapt roster rebase to keyed entries

* fix(agents): honor both roster representations

* fix(agents): route roster reads through shared helpers

* fix(config): preserve canonical roster writes

* fix(cron): resolve dynamic default for session reaper

* fix(agents): close dynamic default migration gaps

* fix(agents): align scoped session ownership

* fix(sessions): preserve legacy main directory casing

* fix(agents): align cron and legacy auth ownership

* fix(setup): provision the committed default workspace

* fix(cron): align scoped ownership and reaping

* fix(cron): treat blank agent ids as absent

* fix(cron): retain configured session-store owners

* fix(agents): repair roster-aware CI boundaries

* fix(cron): preserve scoped ownership resolution

* fix(agents): preserve rosterless maintenance paths

* fix(agents): propagate roster ownership through runtime boundaries

* fix(agents): preserve roster ownership across runtime paths

* fix(agents): harden roster diagnostics and legacy routing

* fix(agents): remove redundant diagnostic import

* test(agents): type CLI policy fixture explicitly

* fix(config): preserve canonical roster mutation identity

* fix(doctor): read canonical agent rosters consistently

* fix(config): resolve compound roster unsets safely

* fix(config): finalize main-session reconciliation

* fix(doctor): read canonical session state safely

* fix(sessions): preserve current visibility alias

* fix(config): track roster include provenance

* test(config): type roster provenance cases

* fix(config): refine roster include ownership

* fix(agents): preserve staged roster invariants

* test(config): align fixtures with explicit roster ownership

* test(node-host): preserve optional plan typing

* fix(config): preserve authored roster projections

* test(config): keep raw roster fixtures explicit

* test(config): normalize rosters at runtime fixtures

* fix(config): protect authored roster ownership

* fix(agents): require explicit session ownership

* fix(agents): enforce scoped roster ownership

* fix(sessions): merge fixed-store agent partitions

* fix(agents): harden roster ownership boundaries

* fix(config): reject ambiguous roster projections

* fix(sessions): preserve persisted store ownership

* fix(sessions): keep collision diagnostics additive

* fix(security): scan malformed roster workspaces

* test(config): align snapshot fixtures after rebase

* test(agents): use explicit roster fixtures

* fix(config): harden roster diagnostic boundaries

* fix(sessions): isolate fixed-store agent databases

* test(agents): type malformed default markers

* refactor(sessions): extract store collision resolution

* test(system-agent): split oversized setup coverage

* style(system-agent): format split setup suite

* fix(sessions): preserve promoted store ownership

* fix(sessions): derive scoped owner before target

* fix(sessions): preserve explicit sqlite ownership

* fix(agents): restore roster compatibility across CI

* fix(agents): enforce roster-owned runtime boundaries

* fix(agents): satisfy default lookup lint

* test(sessions): split known-owner coverage

* fix(state): satisfy path identity lint

* fix(agents): preserve malformed roster safety boundaries

* fix(agents): restore roster compatibility at runtime boundaries

* fix(config): satisfy roster boundary type checks

* fix(agents): preserve roster ownership across runtime probes

Setup inference probes now execute as the configured roster owner. Malformed agent-prefixed session rows are intentionally omitted by the fail-closed visibility contract rather than normalized by tests.

* fix(agents): satisfy session list owner lint

* fix(agents): preserve roster-owned runtime boundaries

Restore shared logical rows for exact SQLite session locators while keeping their physical database owner separate. The ownership regression test now constructs an explicit sole-owner database directly instead of relying on first-touch capture, matching the intentional shared-store contract.

* fix(sessions): preserve multiply owned exact stores

* fix(sessions): restore runtime owner boundaries

Keep incognito sentinels agent-owned, fold default-agent approvals into the global snapshot, and preserve the configless legacy-main CLI policy fallback. Also repair the existing CLI watchdog test lifecycle so the compact shard observes its timeout without an unawaited assertion or async timer stall; product behavior is unchanged by that test-only fix.

* test(ci): align owner-scoped fixtures

These assertions are unchanged. The fixtures now declare the intended non-default runner, expose the session-key constant imported by production status code, and select the main approvals bucket explicitly on Windows.

* fix(agents): close final roster ownership gaps
2026-07-24 22:38:09 -07:00

6.7 KiB

summary, read_when, title, sidebarTitle
summary read_when title sidebarTitle
WhatsApp group message handling — activation, allowlists, sessions, and context injection
Configuring WhatsApp groups specifically
Changing WhatsApp activation modes (`mention` vs `always`)
Tuning WhatsApp group session keys or pending-message context
WhatsApp group messages WhatsApp groups

For the cross-channel groups model (Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, Zalo), see Groups. This page covers the WhatsApp-specific behavior on top of that model: activation, group allowlists, per-group session keys, and pending-message context injection.

Goal: let OpenClaw sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.

`agents.entries.*.groupChat.mentionPatterns` is shared with the other channels' mention gating. For multi-agent setups, set it per agent, or use `messages.groupChat.mentionPatterns` as a global fallback. With neither set, patterns are derived from the agent identity name/emoji.

Behavior

  • Activation modes: mention (default) or always. mention requires a ping: a real WhatsApp @-mention (mentionedJids), a configured regex pattern, the bot's E.164 digits anywhere in the text, or a quoted reply to one of the bot's messages (except shared-number self-chat setups). always wakes the agent on every message, but the injected group prompt tells it to reply only when it adds value and to return the exact silent token NO_REPLY (case-insensitive) otherwise. Defaults come from config (channels.whatsapp.groups requireMention) and can be overridden per group via /activation.
  • Group allowlist: when channels.whatsapp.groups is set, only listed group JIDs are admitted (include "*" to allow all); messages from unlisted groups are dropped with a log hint.
  • Group policy: channels.whatsapp.groupPolicy controls whether group messages are accepted (open|disabled|allowlist). allowlist uses channels.whatsapp.groupAllowFrom (fallback: explicit channels.whatsapp.allowFrom). Default is allowlist (blocked until you add senders).
  • Per-group sessions: session keys look like agent:<agentId>:whatsapp:group:<jid> (non-default accounts append :thread:whatsapp-account-<accountId>), so directives such as /verbose on, /trace on, or /think high (sent as standalone messages) are scoped to that group; personal DM state is untouched.
  • Context injection: pending-only group messages (default 50) that did not trigger a run are prefixed under [Chat messages since your last reply - for context], with the triggering line under [Current message - respond to this]. The pending window is cleared after the run; messages already in the session are not re-injected.
  • Sender attribution: each group line carries the sender label inside the message envelope, e.g. [WhatsApp <groupJid> <timestamp>] Alice (+447700900123): text, and sender identity plus group subject/members ride along in the untrusted conversation-metadata block.
  • Ephemeral/view-once: wrappers are unwrapped before extracting text/mentions, so pings inside them still trigger.
  • Group system prompt: the first turn of a group session (and any turn after /activation changes the mode) injects activation guidance into the system prompt (Activation: trigger-only ... or Activation: always-on ..., plus "address the specific sender"). Persistent group-chat delivery guidance ("You are in a WhatsApp group chat...") is always included.

Config example (WhatsApp)

Make display-name pings work even when WhatsApp strips the visual @ from the text body:

{
  channels: {
    whatsapp: {
      groups: {
        "*": { requireMention: true },
      },
      historyLimit: 50, // pending group context window (default 50)
    },
  },
  agents: {
    entries: {
      main: {
        default: true,
        groupChat: {
          mentionPatterns: ["@?openclaw", "\\+?15555550123"],
        },
      },
    },
  },
}

Notes:

  • The regexes are case-insensitive and use the same safe-regex guardrails as other config regex surfaces; invalid patterns and unsafe nested repetition are ignored.
  • WhatsApp still sends canonical mentions via mentionedJids when someone taps the contact, so the number fallback is rarely needed but is a useful safety net.
  • The pending-context window resolves as channels.whatsapp.accounts.<id>.historyLimitchannels.whatsapp.historyLimitmessages.groupChat.historyLimit → 50.

Activation command (owner-only)

Use the group chat command:

  • /activation mention
  • /activation always

Only owner numbers (from channels.whatsapp.allowFrom, or the bot's own E.164 when unset) can change this; /activation from anyone else is ignored and stored as context only. Send /status as a standalone message in the group to see the current activation mode.

How to use

  1. Add your WhatsApp account (the one running OpenClaw) to the group.
  2. Say @openclaw ... (or include the number). Only allowlisted senders can trigger it unless you set groupPolicy: "open".
  3. The agent prompt includes the pending group context plus sender-labeled lines so it can address the right person.
  4. Session directives (/verbose on, /trace on, /think high, /new or /reset, /compact) apply only to that group's session; send them as standalone messages so they register. Your personal DM session stays independent.

Testing / verification

  • Manual smoke:
    • Send an @openclaw ping in the group and confirm a reply that references the sender name.
    • Send a second ping and verify the history block is included, then cleared on the next turn.
  • Check gateway logs (run with --verbose) for inbound web message entries showing from: <groupJid> and the sender-labeled body.

Known considerations

  • Heartbeats run in the agent's main session; group sessions never get heartbeat runs.
  • Echo suppression remembers the combined prompt (history + current message) per session so the bot's own delivered messages do not retrigger it; an identical repeated batch can be skipped as an echo.
  • Session store entries appear as agent:<agentId>:whatsapp:group:<jid> in the per-agent SQLite session store; a missing entry just means the group has not triggered a run yet.
  • Typing indicators follow agents.entries.*.typingMode / agents.defaults.typingMode. When visible replies are opted into message-tool-only mode, typing starts immediately by default so group members can see the agent working even if no automatic final reply is posted. Explicit typing-mode config still wins.