Group Chat — Multi-Agent Collaboration
Multiple AI agents (Claude, Codex, Gemini — or custom agents) work together on the same project directory. Each agent is a long-lived subprocess that communicates via MCP tools served by a dedicated MCP server on :7892. The GroupChatOrchestrator manages spawning, monitoring, and restarting agents. Messages flow through an in-memory GroupChatStore with per-agent asyncio.Queue inboxes.
User (Web UI / API) POST /api/group-chats/{id}/messages · GET /stream (SSE) Group Chat API web/api/group_chat.py GROUP CHAT ORCHESTRATOR — web/group_chat.py Agent Lifecycle _spawn_agent() / _kill_agent() Build system prompt from template Backend.get_group_launch_command() create_subprocess_shell() Start stdout reader + stderr drainer Write MCP configs per agent Per-agent restart locks Watchdog _watchdog() asyncio.Task Check every 10 seconds Process exit detection Heartbeat timeout check (poll_timeout + 120s margin) Auto-restart dead agents Broadcast AgentStatus events GroupChatStore web/group_chat_store.py In-memory message log (list[GroupMessage]) Per-agent asyncio.Queue inbox post() → persist to DB + push queues get_new_messages() → long-poll SSE broadcast to subscribers Reactions + read receipts Output Reader _read_agent_output() per agent Read JSONL from agent stdout backend.parse_entries() → events Broadcast TextDelta, ThinkingDelta Broadcast ToolInvocation events Update agent status (listening/responding) Extract + persist session_id, usage store.post(sender="user") Group Chat MCP Server :7892 web/group_mcp_server.py · FastMCP · BearerAuth · 127.0.0.1 get_new_messages (long-poll) · post_message · reply_to_thread · react_to_message delegates to store Claude Agent claude -p --append-system-prompt --stream-json Long-lived subprocess · Session resume via --resume Lead Software Engineer max_turns=200 · --dangerously-skip-permissions Codex Agent codex exec --json + AGENTS.md (system prompt) Long-lived subprocess · danger-full-access sandbox Senior Engineer (Implementation) skip-git-repo-check · approval_policy="never" Gemini Agent gemini -p --yolo + .gemini/system-group-*.md Long-lived subprocess · 1M context window Senior Engineer (Analysis) stdbuf -oL for line-buffered output get_new_messages() post_message() reply_to_thread() SHARED PROJECT DIRECTORY (same CWD) All agents read/write the same codebase · File changes visible to all · Git operations shared · MCP configs written here SSE → Browser group_message · agent_status · text_delta · reaction store.broadcast_raw()
Agent State Machine
idle listening responding dead spawn text/tool output get_new_messages() exit/timeout watchdog auto-restart (with context history + session resume) Initial state Waiting for messages Producing output Process exited

Agent Loop Pattern

Each agent runs an infinite MCP tool loop:

  1. Call get_new_messages(conversation_id, agent_type) — blocks up to 600s
  2. Read messages from all participants
  3. If Human sent a message → MUST respond
  4. If @mentioned → MUST respond
  5. Post reply via post_message() or reply_to_thread()
  6. Optionally react_to_message() (thumbs_up, fire, check, etc.)
  7. Call get_new_messages() again — never stop the loop

Message Routing

  • User message → pushed to ALL agent queues (push_to_sender=True)
  • Agent message → pushed to all OTHER agent queues (not sender)
  • @mention parsed but all agents see all messages
  • Thread replies: thread_id="thread-{parent_sequence}"
  • Reactions: emoji set {thumbs_up, thumbs_down, fire, check, x, thinking}
  • Read receipts: tracked per-agent in group_read_receipts
  • SSE broadcast: GroupMessagePosted, MessageReaction, MessageReadReceipt

Watchdog & Recovery

  • Health check every 10 seconds in asyncio.Task
  • Heartbeat timeout = poll_timeout + 120s (generous margin)
  • Process exit detected via process.returncode is not None
  • Dead agents auto-restarted with last 20 messages as context
  • Session IDs persisted to group_agent_sessions for resume
  • Per-agent asyncio.Lock prevents concurrent restart races
  • Active group chats auto-resume on server startup (is_active=1)

Dynamic Agent Configuration

  • agent_definitions table: global reusable agent profiles
  • CrewAI-style identity: name, role, goal, backstory
  • group_conversation_agents: per-conversation lineup (2-10)
  • System prompt template with placeholders: {agent_name}, {agent_strengths}, etc.
  • Each backend injects system prompt differently:
    Claude: --append-system-prompt
    Codex: AGENTS.md file
    Gemini: .gemini/system-group-*.md
  • Model override per-agent via AgentConfig.model

Output Reader Pipeline

Each agent has a dedicated _read_agent_output() asyncio.Task:

  1. Read line from process.stdout with timeout
  2. Extract session_id from init event (persist to DB)
  3. backend.parse_entries() → ParsedEntry list
  4. Update heartbeat timestamp on any output
  5. Track agent status: get_new_messages → "listening", text/tool → "responding"
  6. Broadcast TextDelta, ThinkingDelta, ToolInvocation events via store
  7. Persist tool_calls to DB for history
  8. Extract usage from "result" events → update group_agent_sessions

Files Involved

  • web/group_chat.py — GroupChatOrchestrator, AgentConfig, spawn/kill/restart
  • web/group_chat_store.py — GroupChatStore, message routing, SSE broadcast
  • web/group_mcp_server.py — FastMCP tools for agent communication
  • web/group_mcp_auth.py — BearerAuthMiddleware for :7892
  • web/api/group_chat.py — REST API routes for group chat
  • web/agent_definitions.py — CRUD for agent_definitions + per-conversation config
  • web/domain_events.py — GroupMessagePosted, AgentStatus, reactions, receipts
  • web/database.py — Schema, default prompt template, builtin agents
  • mcp_config.py — Write .mcp.json/.gemini/settings.json/.codex/config.toml