Group mode — one shared agent for a whole group chat

Drop a Pouchy agent into a group chat (Telegram group, Slack channel, Matrix room, 飞书群, 钉钉群, …) and the whole room shares one brain: one memory, one conversation history, one persona. The agent reads everything, knows who said what, and speaks only when called — the "shared group assistant" model (coordinate a dinner plan, summarize the thread, run a skill for the group).

Without group mode a connector treats every sender as their own isolated 1:1 conversation (separate memory + history per person) — replies still land in the room, but the agent can never see the thread as a whole. That remains the default; group mode is opt-in per connector.

Enable it

Dashboard → Channels → New connector → "Group mode" checkbox (plus an optional comma-separated wake-word list), or via the Admin API:

curl -X POST https://pouchy.ai/v1/admin/channels \
  -H 'Authorization: Bearer pchy_admin_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "telegram",
    "agentId": "<agentId>",
    "config": {
      "groupMode": true,
      "wakeWords": ["@dinnerbot", "小助手"]
    },
    "secret": { "token": "<bot token>" }
  }'

The same shape works on the owner-auth surface (POST /v1/projects/{projectId}/channels).

Config knobs

Key Type Default Meaning
config.groupMode boolean false Key the instance on the ROOM instead of the sender. DMs on the same connector keep 1:1 semantics (a message without a room id is never grouped).
config.wakeWords string[] Case-insensitive containment match that counts as "the agent was called" (e.g. ["@bot", "小助手"]). Complements the transport's native mention signals.
config.groupRequireMention boolean true With the default ON, unaddressed chatter is recorded into the shared history without running the model — the agent sees the whole room but replies only when @mentioned, replied to, or a wake word matches. Set false to answer every message (noisy + every message is a billed turn).

How the agent experiences the room

  • Every user turn in the shared history carries a speaker label — Alice: let's do Saturday — so the model can address people by name and track positions across members.
  • The session carries a group appContext, so the system prompt tells the model it is in a multi-person room and its replies are visible to everyone.
  • Skills, the confirm boundary for sensitive actions, and long-term memory all work exactly as in 1:1 — the room accumulates group memory (decisions made, preferences mentioned) under its own namespace.

What counts as "addressed"

Transport Native signal Group id source
Telegram @mention entity, or a reply to a bot message chat.id when chat.type is group/supergroup
Slack app_mention event event.channel (non-im)
Matrix bot MXID appears in the body room_id (rooms are the native model)
Discord slash commands are always explicit guild channel_id
Teams <at> tag in text conversation.id for groupChat/channel
飞书 Feishu any mentions entry on the message chat_id when chat_type is group
钉钉 DingTalk group bots only receive @-messages → always addressed conversationId when conversationType is 2
LINE wake words only source.groupId / source.roomId

Wake words cover every transport, including the ones without a native mention signal.

Limits & billing

  • The room shares one inbound rate bucket (default 60/min) and each sender gets their own sub-bucket inside it, so one member flooding the room cannot starve everyone else.
  • A room meters as one instance / one MAU today. If group mode needs per-member pricing, that is a metering change — flag it before relying on the current behavior contractually.
  • Ambient (unaddressed) messages cost a bounded history write, never a model call.

Implementation pointers

Runtime: src/lib/server/platform/channels/runtime.ts (search "Group mode"); instance keying room:{transport}:{roomId}; adapter fields roomId / senderName / addressed on InboundMessage (channels/types.ts). End-to-end tests: channels/group-mode.test.ts.