Agent Orchestration
The MultiAgentOrchestrator (in src/mindroom/orchestrator.py) manages the lifecycle of all agents, teams, and the router.
Boot Sequence
main() entry
│
▼
┌──────────────────┐
│ Sync Provider │
│ Credentials │
│ (.env/bootstrap │
│ env → shared │
│ credentials) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Initialize() │
│ ─────────────────│
│ 1. Parse config │
│ (Pydantic) │
│ 2. Load plugins │
│ 3. Create "user" │
│ Matrix account│
│ (mindroom_user)│
│ 4. Prepare │
│ entity Matrix │
│ accounts │
│ 5. Create bots │
│ for entities │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Start() │
│ ─────────────────│
│ 1. try_start() │
│ each bot │
│ 2. Create sync │
│ tasks │
│ 3. Background │
│ room setup │
└────────┬─────────┘
│
▼
┌──────────────────────────────────────┐
│ Auxiliary Tasks (auto-restart) │
│ ─────────────────────────────────────│
│ • config watcher (file polling) │
│ • skills watcher (skill cache) │
│ • API server (if enabled) │
│ (each wrapped in │
│ _run_auxiliary_task_forever) │
└───────────────┬──────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Bot Sync Tasks (asyncio.gather) │
│ ─────────────────────────────────────│
│ • One sync loop per bot │
│ • sync_forever_with_restart() │
│ • Awaited until shutdown │
└──────────────────────────────────────┘
Key details:
- Entity order: Router first, then agents, then teams
- Room setup (
_setup_rooms_and_memberships): Resolve/create rooms and the root Space, join the router, reconcile managed policy once, then invite and join the remaining identities - Sync loops: Each bot runs
sync_forever_with_restart()with automatic retry;matrix_sync.mode: classicuses Classic/v3/sync, whileslidinguses MSC4186 Simplified Sliding Sync on a homeserver advertisingorg.matrix.simplified_msc3575 - Internal user identity:
mindroom_user.usernameis the account-creation request; runtime authorization uses the persisted actual Matrix ID
Room administration uses a fresh, pass-local full-state snapshot for each policy reconciliation instead of separate name, topic, power-level, encryption, and join-rule reads. Satisfied power-level policy uses the snapshot fast path; a required power-level write rereads current grants so intervening administrator changes are preserved. Root Space child links share one fresh Space snapshot rather than fetching every child separately. Managed-room invitations reuse joined and invited memberships from those snapshots; internal-user and configured-user invitations share one roster. The internal user logs in once and joins only rooms absent from its fresh joined-room inventory, falling back to idempotent join attempts if that inventory is unavailable. Alias resolution and directory visibility still require fresh reads, and the authoritative reply-membership refresh remains a separate startup barrier. No configuration hash or persisted state cache suppresses remote drift checks on the next startup or relevant config update. The duplicate full pass is removed, including its incidental retry of failed operations; transport retries remain with nio and returned administrative failures are retried on the next setup attempt.
Session Storage Recovery
Before opening an owned session database, session_storage_preflight.py checks any existing session table for the required Agno columns.
If required columns are missing, it renames the entire sessions/ directory to a unique sessions.incompatible-* sibling, including SQLite journals and sidecars, then recreates sessions/ with its original permissions.
The archive stays available for manual recovery, and logs report its path and the missing columns.
Only session storage is archived; learning, authored files, credentials, and Matrix encryption keys remain separate.
Permission failures, corruption, unexpected schema objects, and unsafe paths still raise errors rather than triggering an archive.
Compatible Agno 2 and mixed-schema session data remains readable through Agno's compatibility reads. MindRoom no longer schedules background conversion or clears legacy run blobs. Run upgrades while MindRoom is stopped so recovery cannot overlap active session writers.
Runtime Replacement Admission
Config changes are detected via polling (watch_paths() checks watched source-file mtimes every second and fires after one quiet scan).
MCP catalog changes use the same replacement admission path when the changed server has dependent agents or teams.
The MCP manager callback schedules an orchestrator-owned background task so the triggering tool call can return and release its admission slot before replacement draining begins.
- On a config change,
ConfigReloadLifecycle.request_reload()queues a debounced reload. - On an MCP catalog change, the orchestrator returns immediately when no configured entity references that server, while still clearing the worker validation snapshot cache. The dependent-entity check runs again under the config update lock immediately before replacement.
- Config reloads and MCP catalog replacements serialize behind one global admission owner; MCP replacements enter through
ConfigReloadLifecycle.apply_with_response_admission(). - Sampling the in-flight count and closing the shared
ResponseAdmissionGatehappen atomically, so a new response cannot race the decision to apply. The gate covers Matrix-driven response lifecycles, external-trigger delivery, call admission, and requester-driven call operations. Text and router planning, commands, edit regeneration, interactive selections, visible router voice echoes, calls, and external triggers perform their final reply-policy check after admission and retain the slot through their direct side effect or response-runner handoff. The OpenAI-compatible API inmindroom.api.openai_compatremains outside this gate because it does not use Matrix reply authorization. Config loading keeps response admission open; after current responses drain, the gate closes for diff planning and publication. Holding the gate while loading would block responses for validation work that cannot affect the live runtime. - While the gate is closed, a response waits before taking a lifecycle lock, incrementing the in-flight count, or publishing a placeholder. The gate is global and covers the whole apply window regardless of how narrow the plan turns out to be. When the apply finishes, responses owned by unchanged or replacement runtimes compete for admission normally.
- A runtime being replaced wakes its pre-admission waiters with
ResponseAdmissionRefusedError. The refusal leaves the admitted source pending in the event journal so the replacement runtime can replay it. The refusal path performs no Matrix I/O, so replacement shutdown cannot stall on an untimed send. Auto-resume messages received by replacement bots during the apply wait for the gate to reopen instead of being dropped. Before emitting a resume relay, history recovery requires a nonretired attempted outbox delivery binding the target response to the current principal and room membership. A send with no known response event remains the responsibility of existing outbox and pending-source recovery. - If responses never drain, either replacement flow stops deferring after 600 seconds and closes the gate over still-running responses. This bounded forced apply prevents a busy install from starving config or MCP replacement forever.
- For config reloads,
ConfigReloadLifecycle._update_config()loads and validates the new config while admission remains open, thenbuild_config_update_plan()computes targeted restarts and in-place reconciliations after the gate closes. - The orchestrator applies the resulting plan: changed entities are replaced, unchanged bots receive the new config, and room-only changes reconcile memberships in place without restarting receive loops. Call-enabled agents are conservatively replaced after any authored config change because active call tooling captures the full authored config snapshot.
- Removed entities prepare their response runtime for shutdown, reconcile approval work, and call
leave_rooms()while ingestion remains active; the orchestrator then cancels the receive loop and stops the bot. - New and restarted bots go through room setup.
- The gate reopens once the apply finishes, whether it succeeded, failed, or was cancelled, and deferred responses may then start.
Skills are watched separately via _watch_skills_task() with cache invalidation.
Orchestration Subpackage
The src/mindroom/orchestration/ subpackage contains helpers extracted from the monolithic orchestrator:
runtime.py— Sync loop helpers:sync_forever_with_restart()with exponential backoff capped at 60 seconds,cancel_task(), andcreate_logged_task()for safe asyncio task creation.config_lifecycle.py— Debounced config-reload and shared replacement-admission lifecycle:ConfigReloadLifecycleowns reload queueing, serialized global response draining for config and MCP replacements, and the load → diff → plan sequencing that dispatches config plans back to the orchestrator.config_updates.py— Config diffing and reload planning:build_config_update_plan()computes aConfigUpdatePlanby calling_identify_entities_to_restart(), which diffs old and new configs usingmodel_dump(exclude_none=True).plugin_watch.py— Plugin hot-reload watcher:watch_plugins_task()polls configured plugin roots, withPluginWatchStateowning the watcher baselines and dirty-state revision.rooms.py— Room invitation helpers:get_authorized_user_ids_to_invite()andget_root_space_user_ids_to_invite()compute which users should be invited to managed rooms and the root Matrix space.
Runtime Resolution
Agent and team materialization is handled by dedicated top-level modules (not inside the orchestration/ subpackage):
src/mindroom/runtime_resolution.py— ResolvesResolvedAgentRuntime(the full set of runtime parameters for one agent instance) includingResolvedKnowledgeBindingfor knowledge base attachment.src/mindroom/team_exact_members.py— ResolvesResolvedExactTeamMembersfor team materialization viamaterialize_exact_requested_team_members().src/mindroom/agent_policy.py— Resolves canonical execution policies and private-team eligibility derived from authored agent config.src/mindroom/model_loading.py— Ownsget_model_instance()and provider-specific model loader selection.src/mindroom/ai_runtime.py— Owns agent-run input copying and queued-notice hooks used during execution.src/mindroom/provider_media_fallback.py— Owns provider-boundary inline-media retry and process-local capability learning per model route.src/mindroom/agent_storage.py— Owns agent session and learning SQLite storage construction helpers.src/mindroom/agent_descriptions.py— Owns shared agent description rendering used by routing and delegation.src/mindroom/runtime_state.py— Shared runtime readiness state withset_runtime_starting(),set_runtime_ready(), andset_runtime_failed()used by health endpoints.
Subagent Ownership
run_subagent starts a separate child conversation; continue_subagent starts another turn in that same conversation.
The child uses the normal agent response envelope, so its history, tools, and model behavior follow the existing runtime.
Native Matrix approval pauses retain the parent wait and exact child run rather than keeping a Python call alive.
The runtime lives in the src/mindroom/delegation/ package, with explicit imports between its modules.
| Module | Owns |
|---|---|
custom_tools/delegate.py |
Agent-facing tool schemas and direct invocation |
ai.py |
run_delegated_child_response, supplied as a typed callback to the native driver |
delegation/execution.py |
Parent waits, approval gates, child approval projection, and parent continuation |
delegation/lifecycle.py |
Child preparation, attempt identity, outcome transitions, and publication to storage and audit |
delegation/recovery.py |
Abandoned-turn reconciliation and recursive cancellation from retained Agno runs |
delegation/sessions.py |
Scoped handle reads, atomic reservations, snapshots, and liveness locks |
delegation/audit.py / delegation/records.py |
Workspace audit projections, event logs, transcripts, and receipts |
delegation/state.py |
Serializable runtime state and the child-runner protocol |
delegation/hooks.py |
Persisted plugin hook phases across approval continuations |
delegation/storage.py |
Frozen storage bindings for retained runs |
Both direct and native invocation use the same child preparation and lifecycle owner. The native driver receives its response runner explicitly and does not construct the agent-facing toolkit. Handle reads do not recover or execute children; recovery runs above storage under a liveness lock. Audit snapshots do not settle child state or finish audit records; the lifecycle owner publishes terminal outcomes. Editable workspace receipts never grant continuation authority. A retained Agno run identifies the exact attempt; the lifecycle owner derives its outcome before publishing storage and audit projections. Tach dependency rules and isolated import tests enforce these directions.
See Agent Delegation for configuration, tool arguments, audit paths, and user-visible behavior.
Message Handling
Correctness-critical timeline callbacks cross durable journal admission before ordinary callbacks run, and background dispatch workers then process committed work without blocking the sync loop.
Inbound message flow:
matrix/durable_ingestion.pyconverts nio batches usingmatrix/journal_ingress.py, commits their application effects in one transaction, then acknowledges after ordered hooks.journal_dispatch.pyandpending_event_worker.pydispatch admitted or recovered work.turn_controller.pyruns ingress validation, normalization, conversation resolution, receipt ordering, and coalescing.text_ingress_dispatch.pyandturn_policy.pydecide whether to ignore, route, execute a command, or respond.response_runner.pyandresponse_turn.pyexecute the selected agent or team.delivery_gateway.pysends or edits the Matrix response andTurnStorerecords durable terminal truth.
Message edits: When a user edits a message that already received an agent response, the agent regenerates its response for the updated content.
The agent edits its own previous reply in place rather than sending a new message.
Edits from other agents are ignored, and the feature requires that the turn's anchor_event_id is recorded in the TurnStore.
_on_media_message: Handles media events (images, videos, files, and audio).
Downloads and decrypts media data, then processes it through the selected responder.
When no agent or team is mentioned, routing selects the appropriate agent or team, similar to text messages.
_on_reaction: Handles ReactionEvent for the interactive Q&A system (e.g., confirming or rejecting agent suggestions) and config confirmation workflows.
Routing (when no agent or team is mentioned): Router narrows candidates from room configuration or joined MindRoom entities, filters them by sender permissions, lets one remaining candidate answer directly, and uses suggest_responder_for_message() only when multiple candidates remain.
In threads where multiple non-agent users have posted, routing is skipped entirely — an explicit @mention is required.
Non-MindRoom bots listed in bot_accounts are excluded from this detection.
Concurrency
- Each bot runs its own sync loop via
sync_forever_with_restart() - Sync loop failures trigger automatic restart with capped exponential backoff (5s, 10s, 20s, 40s, then 60s maximum)
- Watchdog-driven restarts of stalled sync loops add 0–10s of random jitter on top of the backoff so a loop-wide stall does not restart every sync loop as one thundering herd
- An automatic receive-loop restart replaces only the sync task and its watchdog, so in-flight responses keep their original owner and finish across the restart
- The response runtime is drained and cancelled only when the bot itself stops: a config reload replacing the entity, entity removal, or process shutdown
- Each of those lifecycle events logs
restart_reason_categoryandresulting_action, somatrix_sync_transport_restartis distinguishable frommatrix_agent_response_runtime_shutdownin logs - Admitted callbacks are dispatched as background work and remain durably retryable until settled
TurnStore, backed by the durable handled-turn ledger, prevents duplicate repliesStopManagerhandles cancellation of in-progress responses
Graceful Shutdown
The entry-point shutdown helper cancels and settles startup before core teardown can release resources. Auxiliary watchers are cancelled after core teardown.
On orchestrator.stop():
- Mark the runtime stopped, signal runtime shutdown, unbind external triggers, and close approval transport/runtime state.
- Cancel config reload, drain MCP catalog and dispatch-recovery work, and cancel startup maintenance.
- Stop todo-poke and memory auto-flush workers plus knowledge watching and refresh scheduling.
- Cancel pending bot starts and stop the MCP manager.
- Quiesce ingestion while its pump can still admit captured input, then cancel receive loops.
- Stop all bots concurrently and finish retained response recovery proofs before releasing their clients.
- Wait for attachment cleanup and close the shared journal only once no response owner remains.
Each response keeps one ownership record through terminal cleanup and recovery-proof consumption. A timeout preserves the record and any in-flight proof, so shared resources remain available for deferred cleanup.