Chat Groups
Agents can create focused group chats for coordinating on shared work — a middle ground between 1-to-1 messages and broadcasts to everyone.
Why Groups?
Direct messages (AGENT_MESSAGE) are 1-to-1. Broadcasts (BROADCAST) go to everyone. Groups let a subset of agents — say, three developers working on related features — discuss and coordinate without routing through the lead or spamming the whole team.
Creating and Using Groups
Any agent can create groups. The lead is auto-included for visibility. Groups support both explicit member IDs and role-based membership.
Commands
Group creation (any agent):
⟦⟦ CREATE_GROUP {"name": "config-team", "members": ["a1b2c3d4", "e5f6a7b8"]} ⟧⟧Creates a named group. Members can be specified by short agent ID (8-char prefix), full UUID, role ID, or role name. The lead is automatically added. Responds with a confirmation including the group name and resolved member list.
Role-based membership:
⟦⟦ CREATE_GROUP {"name": "frontend-team", "roles": ["developer", "designer"]} ⟧⟧Auto-adds all active agents with matching roles. Terminated/completed agents are excluded via isTerminalStatus() filter. Can be combined with explicit members.
⟦⟦ ADD_TO_GROUP {"group": "config-team", "members": ["c9d0e1f2"]} ⟧⟧Adds members to an existing group. The new member receives the group's recent message history (last 20 messages) so they have context.
⟦⟦ REMOVE_FROM_GROUP {"group": "config-team", "members": ["agent-id-2"]} ⟧⟧Removes members. The lead cannot be removed.
Any group member:
⟦⟦ GROUP_MESSAGE {"group": "config-team", "content": "I found a pattern we should all follow..."} ⟧⟧Sends a message to all other group members. The sender sees a delivery confirmation. Each recipient receives the message with the sender's role and ID.
Any agent — discover groups:
⟦⟦ QUERY_GROUPS ⟧⟧Lists all groups the agent belongs to, with member names/roles, message count, and last message preview (first 100 chars). Also aliased as LIST_GROUPS.
Message Format (Delivery to Recipients)
When an agent receives a group message:
[Group "config-team" — Developer (abc12345)]: I found a pattern we should all follow...When a new member is added:
[System] You've been added to group "config-team". Members: Developer (abc12345), Architect (def67890), Code Reviewer (ghi11111).Data Model
Server: ChatGroup (in-memory, backed by MessageBus)
interface ChatGroup {
name: string; // unique group name (kebab-case)
leadId: string; // lead who created the group
memberIds: Set<string>; // agent IDs (always includes leadId)
createdAt: string; // ISO timestamp
archived: boolean; // true when all members have terminated
}
interface GroupMessage {
id: string; // unique message ID
group: string; // group name
from: string; // sender agent ID
fromRole: string; // sender role name
content: string; // message text
timestamp: string; // ISO timestamp
}Database Table (persistent across restarts)
CREATE TABLE IF NOT EXISTS chat_groups (
name TEXT NOT NULL,
lead_id TEXT NOT NULL,
archived INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (name, lead_id)
);
CREATE TABLE IF NOT EXISTS chat_group_members (
group_name TEXT NOT NULL,
lead_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
added_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (group_name, lead_id, agent_id)
);
CREATE TABLE IF NOT EXISTS chat_group_messages (
id TEXT PRIMARY KEY,
group_name TEXT NOT NULL,
lead_id TEXT NOT NULL,
from_agent_id TEXT NOT NULL,
from_role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_group_messages_group ON chat_group_messages(group_name, lead_id);Server Architecture
New File: packages/server/src/comms/ChatGroupRegistry.ts
export class ChatGroupRegistry extends EventEmitter {
constructor(db: Database) { ... }
create(leadId: string, name: string, memberIds: string[]): ChatGroup
addMembers(leadId: string, name: string, memberIds: string[]): void
removeMembers(leadId: string, name: string, memberIds: string[]): void
archiveGroup(leadId: string, name: string): void
sendMessage(group: string, leadId: string, fromId: string, fromRole: string, content: string): GroupMessage
getGroups(leadId: string): ChatGroup[] // excludes archived
getGroupsForAgent(agentId: string): ChatGroup[] // excludes archived
getMessages(group: string, leadId: string, limit?: number): GroupMessage[]
getMembers(group: string, leadId: string): string[]
// Events emitted:
// 'group:created' — { group: ChatGroup }
// 'group:message' — { message: GroupMessage, recipientIds: string[] }
// 'group:member_added' — { group: string, agentId: string }
// 'group:member_removed' — { group: string, agentId: string }
// 'group:archived' — { group: string, leadId: string }
}AgentManager Integration
Command regex patterns in the handler modules use ⟦⟦ ⟧⟧ delimiters:
const CREATE_GROUP_REGEX = /⟦⟦\s*CREATE_GROUP\s*(\{.*?\})\s*⟧⟧/s;
const ADD_TO_GROUP_REGEX = /⟦⟦\s*ADD_TO_GROUP\s*(\{.*?\})\s*⟧⟧/s;
const REMOVE_FROM_GROUP_REGEX = /⟦⟦\s*REMOVE_FROM_GROUP\s*(\{.*?\})\s*⟧⟧/s;
const GROUP_MESSAGE_REGEX = /⟦⟦\s*GROUP_MESSAGE\s*(\{.*?\})\s*⟧⟧/s;
const LIST_GROUPS_REGEX = /⟦⟦\s*LIST_GROUPS\s*⟧⟧/s;New handlers:
detectCreateGroup(agent, data)— lead-only, creates group, sends confirmationdetectAddToGroup(agent, data)— lead-only, adds members, sends history to new memberdetectRemoveFromGroup(agent, data)— lead-only, removes membersdetectGroupMessage(agent, data)— any member, delivers to all other membersdetectListGroups(agent, data)— returns groups the agent belongs to
Agent Context Updates
In buildContextManifest(), if the agent belongs to groups, show them:
== YOUR GROUPS ==
- "config-team" (3 members: Developer, Architect, Code Reviewer)
- "testing" (2 members: Developer, Critical Reviewer)
Send messages: ⟦⟦ GROUP_MESSAGE {"group": "config-team", "content": "..."} ⟧⟧In the lead's prompt, add to AVAILABLE COMMANDS:
Create a chat group for agents working on related tasks:
`⟦⟦ CREATE_GROUP {"name": "config-team", "members": ["a1b2c3d4", "e5f6a7b8"]} ⟧⟧`
Send a message to a group:
`⟦⟦ GROUP_MESSAGE {"group": "config-team", "content": "Use factory pattern for services"} ⟧⟧`WebSocket Events (UI)
| Event | Payload | Description |
|---|---|---|
group:created | { group, leadId } | New group created |
group:message | { message, groupName, leadId } | Message sent in a group |
group:reaction | { leadId, groupName, messageId, emoji, agentId, action } | Reaction added/removed |
group:member_added | { group, agentId, leadId } | Member added to group |
group:member_removed | { group, agentId, leadId } | Member removed from group |
group:archived | { group, leadId } | Group archived (all members terminated) |
Auto-Group-Creation for Parallel Delegations
When the lead delegates tasks to multiple agents, the system automatically creates coordination groups:
- After each delegation,
maybeAutoCreateGroup()checks all active delegations from the same lead - It extracts the first significant keyword (>3 characters) from each task description
- When 3+ active delegations share a keyword, it creates a
{keyword}-teamgroup - All matching agents + the lead are added to the group
- A system message is sent: "Auto-created coordination group for parallel {keyword} work"
The creation is idempotent — if the group already exists, new agents are simply added. This reduces the lead's coordination overhead for parallel work.
Auto-Archive Lifecycle
Groups are automatically archived when they are no longer active:
- When an agent is terminated, the system checks all groups the agent belongs to
- For each group, if all remaining members (excluding the lead) are in terminal status (completed/failed/terminated), the group is archived
- Archived groups are excluded from
QUERY_GROUPSresults - Message history is preserved and remains queryable via the API
- The
archivedcolumn is stored as an INTEGER (0/1) in SQLite
Unread Badges
The frontend tracks unread messages per group:
- Each group chat tab maintains a
lastSeentimestamp (persisted tolocalStorage) - Unread count = messages with timestamp >
lastSeen - A blue badge appears on the Groups sidebar item showing total unread count
- Badge shows
99+for overflow - Visiting a group resets its
lastSeento now
Frontend: Group Messages Panel
In the LeadDashboard, add a "Groups" tab/section alongside the existing Activity, Comms, and Reports panels. This panel shows:
- Group list — all groups under the current lead, with member counts
- Group chat view — click a group to see its message history
- Messages shown chronologically with sender role/ID and timestamp
- Color-coded by sender role (reuse role colors from RoleRegistry)
- Auto-scrolls to latest message
- Group creation — lead can create groups from the UI (not just via commands)
API Endpoints
GET /api/lead/:id/groups — list groups for a lead
GET /api/lead/:id/groups/:name — get group details + members
GET /api/lead/:id/groups/:name/messages — get group message history (with pagination)
POST /api/lead/:id/groups — create a group { name, memberIds }
POST /api/lead/:id/groups/:name/members — add members { memberIds }
DELETE /api/lead/:id/groups/:name/members/:agentId — remove a memberDesign Decisions
Groups are scoped to a lead — each lead has its own namespace of groups. This avoids conflicts when multiple leads run simultaneously.
Lead auto-included — the lead always sees all group messages for coordination visibility. The lead cannot be removed from a group.
Persistence — groups and messages are stored in SQLite so they survive server restarts. In-flight agents can re-discover their groups via
QUERY_GROUPS.History on join — when a new member is added, they receive the last 20 messages so they have context. This mirrors how real chat tools work.
No external dependencies — extends the existing
MessageBusEventEmitter pattern. At current scale (5-20 agents), in-process messaging is sufficient. If we ever need 100+ agents, consider NATS or Redis pub/sub.Sub-agents can message groups — not just the lead. This enables peer coordination (e.g., two developers discussing a shared interface) without lead involvement.
Auto-group-creation — When 3+ agents are delegated tasks sharing a keyword, a coordination group is auto-created. This reduces lead overhead and ensures agents working on the same feature can communicate directly.
Auto-archive lifecycle — When all non-lead members of a group reach terminal status, the group is automatically archived. This keeps
QUERY_GROUPSclean without losing history.Unread badges — The frontend tracks per-group
lastSeentimestamps to show unread counts. This ensures users notice new group messages even when focused on another view.
Example Usage
Lead: I'll create a team for the config work.
<!-- CREATE_GROUP {"name": "config-team", "members": ["abc12345", "def67890"]} -->
[System] Group "config-team" created with 3 members (you + Developer abc12345 + Architect def67890).
Lead: Let the team know about the constraint.
<!-- GROUP_MESSAGE {"group": "config-team", "content": "Important: _configs.py has breaking changes in progress. Coordinate before editing."} -->
[System] Message delivered to 2 group members.
Developer abc12345 (in their context):
[Group "config-team" — Project Lead (lead1234)]: Important: _configs.py has breaking changes in progress. Coordinate before editing.
Developer abc12345 responds:
<!-- GROUP_MESSAGE {"group": "config-team", "content": "Understood. I'll wait for Architect to finish the RoPEConfig extraction before I touch _configs.py."} -->
Architect def67890 (in their context):
[Group "config-team" — Developer (abc12345)]: Understood. I'll wait for Architect to finish the RoPEConfig extraction before I touch _configs.py.Emoji Reactions
Group chat messages support emoji reactions. Agents and users can react to messages to signal agreement, acknowledgment, or sentiment.
REACT Command
⟦⟦ REACT {"group": "config-team", "messageId": "msg-123", "emoji": "👍"} ⟧⟧| Field | Required | Description |
|---|---|---|
group | ✅ | Group name |
messageId | ✅ | Target message ID |
emoji | ✅ | Single emoji character |
Toggle behavior: Reacting with the same emoji a second time removes it.
REST API
POST /api/lead/:id/groups/:name/messages/:messageId/reactions { emoji }
DELETE /api/lead/:id/groups/:name/messages/:messageId/reactions/:emojiWebSocket Event
When a reaction changes, a group:reaction event is broadcast:
{
"type": "group:reaction",
"leadId": "lead-123",
"groupName": "config-team",
"messageId": "msg-123",
"emoji": "👍",
"agentId": "abc12345",
"action": "add"
}The action field is "add" or "remove".
UI
Reactions appear as badges below each message. Clicking a reaction badge toggles it (adds if not reacted, removes if already reacted). A + button opens a picker with common emoji: 👍 👎 🎉 ❤️ 🤔 👀.
Auto-Creation for Parallel Work
When the lead delegates the same feature to 3+ agents, groups are automatically created based on keyword extraction from task descriptions.
How it works:
- After each delegation,
maybeAutoCreateGroup()scans all active delegations from the same lead - Extracts the first significant keyword (>3 chars, excluding stop words) from each task
- If 3+ agents share a keyword, a
{keyword}-teamgroup is created - Stop words include:
the,and,implement,create,build,fix,add,review,update,check,test,run,verify,ensure,handle,process,manage - Only newly added members receive notification messages (dedup guard)
Example: If the lead delegates "implement timeline filtering", "implement timeline brush", and "implement timeline keyboard nav" to three devs, a timeline-team group is auto-created.
Auto-Archive (Lifecycle Cleanup)
Groups are automatically archived when all members reach terminal status (completed, failed, or terminated).
- Archived groups are excluded from
QUERY_GROUPSresults - Message history is preserved in the database for audit
- The
archivedcolumn onchat_groupstable tracks this state AgentManagertriggers archive checks after each agent termination