Draft
Conversation
Backend changes: - DB models: Company.icon_url, User.avatar_url, Conversation (conversation_type, company_id, parent_id, parent_message_id), Message.sender_user_id, ConversationParticipant table - Migrations in DB.py and run-local.py for new columns/tables - Group conversation CRUD endpoints (create, list by company, threads) - Thread creation/listing with message_count and last_message_at - @mention agent routing in chat completions (both main and MCP) - sender_user_id tracking on user messages with sender info in responses - Fixed group conversation access: UserCompany-based company membership fallback in get_conversation_name_by_id, get_conversation_id_by_name, and get_conversation() for group/dm/thread types - Fixed duplicate conversation creation: create_if_missing=False in Conversations.__init__ for group/thread creation endpoints - Company icon_url and User avatar_url endpoints - Pydantic models for group conversations, threads - WebSocket get_conversation_changes includes sender user info
- Add category column to Conversation model with migration
- Add UpdateChannelModel for PATCH endpoint
- Add PATCH /v1/conversation/{id}/channel endpoint for updating channel name/category
- Pass category param through create_group_conversation
- Include category field in group conversations response
- New MessageReaction DB model with message_id, user_id, emoji fields - Migration for message_reaction table with unique constraint - API endpoints: POST/GET/DELETE reactions with toggle behavior - Reactions included in conversation history data with user details - Pydantic models for reaction request/response
Add defense-in-depth guard to /v1/chat/completions and /v1/mcp/chat/completions endpoints. If the conversation is a DM with no agent participants, or a thread whose parent DM has no agent participants, return 400 instead of triggering an agent response. Also initialize conversation_id = None before the UUID check to prevent UnboundLocalError when conversation_name is '-'.
… ciphertext When decrypt_config_value failed (e.g. AGIXT_API_KEY changed), it returned the raw Fernet-encrypted string (gAAAAABp...) instead of empty string. This caused: 1. STRIPE_API_KEY to leak encrypted ciphertext to Stripe API, producing 500 errors with 'Invalid API Key provided: gAAAAABp...' 2. stripe_configured to evaluate as True (non-empty string), enabling the paywall even though Stripe isn't actually usable 3. New users created with is_active=False and no trial credits if their domain was already used Now returns empty string on failure (matching the original comment intention) and logs a warning about the encryption key mismatch.
The default limit of 100 could cause message loss on WebSocket reconnection when a conversation had more than 100 messages. The REST API still uses limit=100 for pagination, but the WebSocket needs all messages to maintain accurate state tracking in previous_message_ids.
The query used .order_by(timestamp.asc()).limit(100) which returned the OLDEST 100 messages. On channels with >100 messages, SWR would show yesterday's messages for 2-3 seconds until WebSocket connected and delivered the full history. Changed to .order_by(timestamp.desc()).limit().reversed() so the API always returns the most recent messages within the limit.
- Add total message count query to get_conversation() in Conversations.py - Return total/page/limit in conversation response dict - Update ConversationHistoryResponse model with Optional total/page/limit fields - v1 conversation endpoint now passes pagination metadata to response
Thread member lists were only showing the creator and agents because participants were only copied from the parent channel at thread creation time. Members who joined the channel later (or threads created before the inheritance code) would have incomplete participant lists. Modified get_participants() to detect thread conversations and dynamically sync missing parent channel participants into the thread. When a thread's participant list is fetched, any active parent channel members not yet in the thread are added as 'member' role participants. This is persisted to the DB so subsequent fetches don't need to re-sync. This means: - Existing threads with incomplete participants self-heal on first access - New channel members automatically appear in all channel threads - Thread-specific roles (owner) are preserved
…media
- Add /outputs/{agent_id}/{conversation_id}/thumb/{filename} endpoint that
generates JPEG thumbnails via ffmpeg, cached on disk next to originals
- Add _get_cache_headers() helper: media files get 24hr immutable cache,
other files retain no-cache behavior
- Thumbnails use scale=640:-2 at JPEG quality 5 (~75%) with fallback
for very short videos
- Replaces client-side canvas capture which failed silently due to CORS
- Skip recording*.webm files early (audio-only, no video stream) - Check content_type for audio/* before attempting ffmpeg - Return 404 instead of 500 when thumbnail generation fails - Log ffmpeg stderr for debugging failed extractions
last_read_at was returned as naive UTC str() (e.g. '2026-02-13 19:10:37') while message timestamps used ISO 8601 with timezone offset (e.g. '2026-02-13T14:10:37-05:00'). String comparison always evaluated messages as 'newer' because 'T' > ' ' in ASCII. - Pass last_read_at and joined_at through convert_time() in get_participants() so they match message timestamp format (ISO 8601 with user timezone) - Frontend now uses Date-based numeric comparison as an additional safety net
…read Two changes to cut DM/channel loading time: 1. WebSocket initial_data: read client-sent ?limit= query param instead of hardcoding limit=1000. Clamped to 1-200, defaults to 50. Processing 1000 messages through timezone conversion, sender lookups, and reaction assembly was the primary cause of 5+ second DM load times. 2. Notification mark-read (UPDATE Message SET notify=False): moved AFTER the message query instead of before it. The UPDATE+COMMIT was blocking the read path — users had to wait for the write to complete before seeing any messages. Now messages are returned first, then notifications are marked read.
When conversation_id is available (from the v1 endpoint), use the same access-check pattern as get_conversation: owner → company member → participant. This replaces the fragile name-based lookup that silently failed for shared DM conversations where the current user isn't the conversation owner. The name-based fallback is preserved for the legacy /api/ endpoint.
…propagation
- Add 'locked' column to Conversation model with migration
- Add PUT /v1/conversation/{id}/lock endpoint for lock/unlock
- Update can_speak() to block non-admin/owner messages in locked threads
- Add locked field to get_threads() and get_conversations_with_detail() responses
- Add ThreadResponse.locked field to Models.py
- Parse <@userid> mentions and [uid:userId] reply targets in messages
- Send targeted 'mention' and 'reply' WebSocket notifications to mentioned/replied users
- Avoid double-notifying users who are both mentioned and replied to
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add Collaborative Chats
Summary
Adds full group chat backend infrastructure to AGiXT: channels, threads, participants, reactions, pinning, presence, @mention routing, message search, and comprehensive database migrations. Transforms the conversation system from single-user ownership to multi-user participation with role-based access control.
New Database Models & Schema
New Tables
MessageReaction— Emoji reactions on messages (message_id,user_id,emoji,created_at). Unique index on(message_id, user_id, emoji)to prevent duplicates.ConversationParticipant— Tracks users and agents in group conversations. Fields:conversation_id,user_id,agent_id,participant_type(user/agent),role(owner/admin/member/observer),joined_at,last_read_at,status(active/left/removed).New Columns on Existing Tables
icon_url(server avatar)avatar_url,last_seen(presence heartbeat),status_text,status_mode(online/away/dnd/invisible)conversation_type(private/group/dm/thread),company_id(FK → Company),parent_id(FK → self, for threads),parent_message_id(FK → Message, thread origin),category(channel grouping),invite_only,description(channel topic)sender_user_id(FK → User),pinned,pinned_at,pinned_by(FK → User)Migrations (6 total)
migrate_group_chat_tables()— Adds all group chat columns + createsconversation_participanttablemigrate_message_reaction_table()— Createsmessage_reactiontable with indexesmigrate_message_pinning()— Addspinned,pinned_at,pinned_byto Messagemigrate_performance_indexes()— Composite indexes onmessage(conversation_id, timestamp),message(conversation_id, notify),conversation(user_id),conversation(name, user_id),conversation(company_id),conversation_participant(conversation_id, status),user_preferences(user_id, pref_key)migrate_extract_data_urls_from_messages()— One-time extraction of inline base64 data URLs from existing messages into workspace files (prevents 10MB+ base64 video blobs in DB/DOM)migrate_backfill_channel_participants()— Backfills missingConversationParticipantrecords for company members added before channels existedNew API Endpoints
Group Chat Endpoints
POST/v1/conversation/groupGET/v1/company/{company_id}/conversationsGET/v1/conversation/{id}/participantsPOST/v1/conversation/{id}/participantsPATCH/v1/conversation/{id}/participants/{pid}DELETE/v1/conversation/{id}/participants/{pid}POST/v1/conversation/{id}/leavePOST/v1/conversation/{id}/readlast_read_at)GET/v1/conversation/{id}/threadsPOST/v1/conversation/{id}/threadsPATCH/v1/conversation/{id}/channelMessage Feature Endpoints
POST/v1/conversation/{id}/message/{mid}/reactionsGET/v1/conversation/{id}/message/{mid}/reactionsDELETE/v1/conversation/{id}/message/{mid}/reactions/{emoji}PUT/v1/conversation/{id}/message/{mid}/pinGET/v1/conversation/{id}/pinsPOST/v1/conversations/searchAuth & User Endpoints
POST/v1/user/presenceGET/v1/user/statusGET/v1/companies/{company_id}/membersNew API Models
Request Models
CreateGroupConversationModel— name, company_id, type, agents, parent_id, parent_message_id, category, invite_onlyAddParticipantModel— user_id, agent_id, participant_type, roleUpdateParticipantRoleModel— roleUpdateChannelModel— category, name, descriptionAddReactionModel— emojiResponse Models
ReactionResponse,MessageReactionsResponseParticipantResponse,GroupConversationResponse,ThreadResponse,ThreadListResponse,GroupConversationListResponseUserResponse(+avatar_url),CompanyResponse(+icon_url),UserInfo(+username),UpdateCompanyInput(+icon_url)Core Logic Changes
Conversations.py (+1,753 lines)
New methods:
create_group_conversation()— Creates group/dm/thread conversations, auto-adds company members as participants (unless invite-only), adds creator as ownercan_speak()— Permission check: observer-role participants cannot send messagesadd_participant(),remove_participant(),get_participants(),update_participant_role()update_last_read()— Updateslast_read_atfor unread trackingget_group_conversations_for_company()— Gets all group channels a user participates inget_threads(),get_thread_count()— Thread listing for channelstoggle_pin_message(),get_pinned_messages()— Message pinningsearch_messages()— Full-text search across accessible conversations with type/company filtersextract_data_urls_to_workspace()— Extracts base64 data URLs from message content, saves as files, replaces with/outputs/URLsKey behavior changes:
get_conversation_id_by_name,get_conversation,log_interaction, etc.) now check company membership and participant records, so users can access group conversations they didn't createnotifyflag tolast_read_at-based tracking viaConversationParticipantget_conversations()— Includes DMs where user is a participant (not just owner), excludes group/thread types, showsdisplay_namefor DMs, addsconversation_typeandparent_idget_conversation()— Pre-fetches sender user info and reactions in batch (eliminates N+1 queries), single timezone lookup instead of per-messagelog_interaction()— Acceptssender_user_id, auto-extracts base64 data URLs before storagedelete_conversation()— Cascading deletion through all related tables including reactions, participants, shares, and child threadsWebSocket System
broadcasted_message_idsto per-connection tracking via_connection_broadcasted_ids. Prevents race conditions between clients.broadcast_typing_event()broadcasts to all connections except sender. WebSocket handles incoming{"type": "typing"}messages.notify_conversation_participants_message_added()notifies ALL participants via user-level WebSocket._transcribe_channel_audio()auto-transcribes audio file attachments and updates messages.@Mention Agent Routing (Completions.py +225 lines)
parse_agent_mentions()— Parses@AgentNameand@"Agent Name"from messages/v1/chat/completionsand MCP chat completion endpointsprompt.modelto route to mentioned agentAuth & User Management (MagicalAuth.py +245 lines)
add_user_to_company_channels()— Auto-adds users to all non-invite-only group channels when joining a companyupdate_presence()— Heartbeat endpoint updatinglast_seen,status_text,status_modeget_user_status()— Returns current presence infoicon_urland useravatar_url,last_seen,status_textincluded in all response serializationsFile Serving Changes (app.py)
serve_filechecksConversationParticipantin addition to conversation ownership, so group chat members can access shared files.webmfiles with "recording" in the filename served asaudio/webminstead ofvideo/webm(voice recordings are audio-only)Bug Fixes
sender_user_idnot being passed through conversation endpoints.webmMIME type causing video player errors on voice recordings