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.