Matrix Integration
MindRoom uses the Matrix protocol for all agent communication. The integration is implemented in src/mindroom/matrix/.
Why Matrix?
- Federated - Connect to any Matrix homeserver
- Bridgeable - Bridge to Discord, Slack, Telegram, and more
- Open - Open standard and open-source implementations
- End-to-End Encryption - Secure communication with encrypted room support
Matrix Client
MindRoom uses matrix-nio for Matrix communication with SSL context handling and encryption key storage.
Environment Variables
| Variable | Default | Description |
|---|---|---|
MATRIX_HOMESERVER |
http://localhost:8008 |
Matrix homeserver URL |
MATRIX_SERVER_NAME |
(from homeserver) | Federation server name |
MATRIX_SSL_VERIFY |
true |
Set to false for dev/self-signed certs |
Streaming behavior is configured in config.yaml with defaults.enable_streaming (default: true).
Agent Users
Each agent gets its own Matrix user with the mindroom_ prefix:
Users are automatically created during orchestrator startup and credentials are persisted in mindroom_data/matrix_state.yaml.
Room Management
Agents can join existing rooms, create new rooms with AI-generated topics, respond to invites automatically, leave unconfigured rooms, and set room avatars.
Rooms are auto-created via _ensure_room_exists() (private) and ensure_all_rooms_exist() (public). DM rooms can be detected with async is_dm_room(client, room_id) -> bool.
Threading (MSC3440)
MindRoom emits thread replies following MSC3440, using m.relates_to with rel_type: m.thread.
For clients that send plain replies without thread metadata (m.in_reply_to but no rel_type: m.thread), MindRoom resolves the reply chain to the existing thread root and continues the same conversation.
Resolution Rules
When deriving context for a non-thread client reply, MindRoom:
- Traverses
m.in_reply_tobackwards until it finds a root, a known thread root, a cycle, or the traversal limit. - Uses cycle detection and a bounded traversal limit (
ReplyChainCaches.traversal_limit) to avoid runaway chains. - If the chain points to a real thread root, fetches thread history and merges chain history so plain replies are preserved in order.
- If no thread relation exists, treats the reply chain itself as the conversation context root.
- Falls back to the oldest successfully resolved event when traversal is interrupted by fetch failures or limits.
├── User: @assistant help with this code
│ ├── Assistant: I can help! Let me look at it...
│ ├── User: It should return a list
│ └── Assistant: Here's the updated version...
Use build_message_content() from message_builder.py to construct thread-aware messages, and EventInfo.from_event() to analyze event relations (threads, edits, replies, reactions).
Message Flow
Sync Loop
Each agent bot runs its own sync loop with 30-second long-polling timeout. Sync loops are wrapped with sync_forever_with_restart() for automatic restart on connection failures.
Events are processed in background tasks:
1. Sync receives event via long-polling
2. Event callback triggered (_on_message, _on_invite, etc.)
3. Background task created for async processing
4. Agent responds in thread
Streaming Responses
Agents stream responses by progressively editing messages. Streaming is enabled only when the requesting user is online (checked via should_use_streaming()), saving API calls for offline users. See Streaming Responses for the full feature documentation.
Tool call telemetry is emitted as plain inline markers and mirrored in io.mindroom.tool_trace metadata on the same message content.
Marker format:
Where N is 1-indexed per message and maps to io.mindroom.tool_trace.events[N-1].
Presence
Agents set their Matrix presence with status messages containing model and role information (e.g., "🤖 Model: anthropic/claude-sonnet-4-6 | 💼 Code assistant | 🔧 5 tools available").
Presence States: - online - Agent running and ready - unavailable - Agent idle but connected (treated as online for streaming) - offline - Agent stopped or disconnected
Typing Indicators
Agents show typing indicators while processing via typing_indicator() context manager. The indicator auto-refreshes at min(timeout/2, 15) seconds to remain visible during long operations.
Mentions
Mentions are parsed via format_message_with_mentions() which handles multiple formats:
- @calculator - Short agent name
- @mindroom_calculator - Full username
- @mindroom_calculator:localhost - Full Matrix ID
Returns content with m.mentions and formatted_body containing clickable links.
Large Messages
Messages exceeding the 64KB Matrix event limit are automatically handled by prepare_large_message():
- Messages > 55,000 bytes and edits > 27,000 bytes use a fallback event
- Full original Matrix message content is uploaded as a JSON sidecar (
message-content.json) - Preview text included in message body (maximum that fits)
- Custom metadata dict
io.mindroom.long_textcontainsversion: 2,encoding: "matrix_event_content_json", original and preview sizes, and a completeness flag - Preview event is compact (for example no inline
io.mindroom.tool_trace), while the sidecar preserves full content fidelity - Encrypted rooms: sidecar JSON is encrypted before upload (
message-content.json.enc)
Response Tracking
MindRoom prevents duplicate responses using a ResponseTracker that records which events have already been processed.
When a sync reconnection or retry delivers the same event twice, the tracker suppresses the duplicate so only one agent response is sent per triggering message.
Tracking state is persisted under mindroom_data/tracking/ and survives restarts.
Room Cleanup
On startup, MindRoom detects orphaned bot memberships left over from a previous configuration.
When an agent is removed from config.yaml, its Matrix bot account may still be a member of rooms it previously joined.
The cleanup process leaves those rooms safely without ejecting currently configured entities from their required rooms.
This runs automatically — no manual intervention is needed.
Identity Management
The MatrixID class handles Matrix user ID parsing and agent identification:
mid = MatrixID.parse("@mindroom_assistant:example.com")
mid.username # "mindroom_assistant"
mid.domain # "example.com"
mid.full_id # "@mindroom_assistant:example.com"
# Create from agent name
mid = MatrixID.from_agent("assistant", "example.com", runtime_paths)
# Extract agent name (returns "code" if configured, None otherwise)
agent_name = extract_agent_name("@mindroom_code:localhost", config, runtime_paths)
Root Space
MindRoom can create and maintain a root Matrix Space that groups all managed rooms.
When enabled, ensure_root_space() creates the Space on first boot (or resolves an existing one by alias), links all managed rooms as children, and sets the Space avatar from workspace or bundled assets.
The Space name is reconciled on each startup to match the configured value.
Configuration
Matrix settings are derived from config.yaml:
agents:
assistant:
rooms: [lobby, dev] # Room aliases (auto-created if needed)
teams:
research_team:
rooms: [research]
Room aliases are resolved to room IDs automatically. Full room IDs (starting with !) are also supported.
When a room doesn't exist, it's created with an AI-generated topic, power users are invited, and managed avatars are resolved from workspace overrides or bundled defaults if available.