agentleFS
mindroom-aimindroom-ai/mindroom/skills/mindroom-docs/references/llms-full.txt

mindroom / references

llms.txt354 starsChanged todayApache-2.0View on GitHub

AI agents that know you and your work, in a chat app anyone can use. Open source, any model, self-host or hosted. :lucide-rocket: Get started{ .md-button .md-button--primary } :lucide-play: See it in action{ .md-button } :lucide-cloud: Try hosted MindRoom{ .md-button } :lucide-arrow-right-left: Coming from OpenClaw?{ .md-button } === "Personal" Agents connect to 100+ tools, including Gmail, Google Drive, Slack, Jira, Confluence, GitHub, and any MCP server (Tools, MCP Servers), and search know

  • Reads credentials
  • Installs packages
  • Sends data out

We split this llms-full.txt into the rules it gives an agent

73 rules. About 0 are common, 28 are distinctive, and 45 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈28

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 45

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep your calendar, reminders, and to-do lists in order, by voice from your phone.

    line 41
  2. Distinctive:

    Keep notes, a journal, and memories you can still find months later.

    line 43
  3. Distinctive:

    Follow the topics you care about, with a digest that only arrives when there is something new.

    line 45
  4. Distinctive:

    Look after your homelab and smart home: check services, read logs, change configs, and control devices through Home Assistant, with …

    line 46
  5. Distinctive:

    Write status updates from what actually happened in chat and the tracker, not from what people remembered to report.

    line 53
  6. Very specific:

    Extensible: add your own tools, skills, and event hooks as plugins, which reload while MindRoom keeps running (Plugins, Hooks).

    line 128
  7. Very specific:

    Run uvx mindroom run and pair it with your MindRoom Chat account.

    line 167
  8. Very specific:

    Run MindRoom with Docker or Kubernetes when you are ready to share it.

    line 183
  9. Very specific:

    Every video on this page is a recording of MindRoom Chat talking to a MindRoom backend.

    line 191
  10. Very specific:

    When no config exists and MindRoom runs in a terminal, it asks a few setup questions:

    line 499
  11. Very specific:

    After approval, MindRoom prints the approving account and asks Is this your account?

    line 513
  12. Very specific:

    After pairing, first-run setup asks whether to run MindRoom in the background and start it at login, as a systemd user service on Linux or …

    line 519
  13. Very specific:

    Check it with mindroom service status, and run mindroom service restart after editing ~/.mindroom/.env.

    line 521
  14. Very specific:

    Use config init to review the files before starting, to choose a preset without prompts, or to use a self-hosted homeserver:

    line 552
  15. Very specific:

    Use this when you want everything local: the MindRoom dashboard, a Matrix homeserver, and the MindRoom Chat client in one stack.

    line 567
  16. Very specific:

    Use this when you want to give a MindRoom agent full freedom over its own virtual machine while you, from the host, control precisely what …

    line 590
  17. Very specific:

    Use this if you already have a Matrix homeserver and want to run MindRoom directly.

    line 608
  18. Very specific:

    Put credentials in a .env next to config.yaml:

    line 676
  19. Distinctive:

    If registration fails, check the selected credentials and the homeserver's registration policy.

    line 707
  20. Very specific:

    Local agents: run MindRoom on this Mac and connect to your Matrix chat account.

    line 741
  21. Distinctive:

    Use either role, or both; starting one does not start the other.

    line 744
  22. Distinctive:

    Use Open in Browser to use your browser session or a sign-in provider that requires a browser.

    line 767
  23. Very specific:

    When the installed runtime is a different release, for example after an app update, it shows Update needed and Update MindRoom.

    line 776
  24. Distinctive:

    Check that the code above Chat matches the page and that the signed-in account is yours, then click Approve.

    line 782
  25. Very specific:

    Use Open Config Folder to add AI provider credentials to the adjacent .env or configure a local model in config.yaml.

    line 785
  26. Distinctive:

    Files found: only means configuration exists.

    line 787
  27. Distinctive:

    If the service starts before your account is connected, the section shows Waiting for your chat account until you choose Connect Account …

    line 793
  28. Distinctive:

    No separate inference application or model-provider API key is required.

    line 802
  29. Very specific:

    After five idle minutes, model memory is released.

    line 822
  30. Distinctive:

    Use Stop Inference to stop local inference and Start Inference to start it again.

    line 827
  31. Distinctive:

    Run the Matrix server separately from this app.

    line 841
  32. Very specific:

    After changing MINDROOM_URL or MINDROOM_API_KEY, select Reload in Dashboard.

    line 849
  33. Distinctive:

    Check the homeserver, Matrix account, controller fingerprint, requester, and agent before signing in, because the setup data chooses which …

    line 860
  34. Distinctive:

    If the app closes before this step, request fresh setup data and repeat Connect.

    line 864
  35. Distinctive:

    If you saved applications, allow Accessibility and Screen Recording in Permissions with Request or Open Settings, restart the app if macOS …

    line 869
  36. Distinctive:

    Start needs at least one saved application, folder, or shell choice.

    line 873
  37. Very specific:

    Stop the bridge in the interface that started it before starting it in the other.

    line 905
  38. Distinctive:

    If you replaced the signed release with a local build, reinstall the signed release or remove the old permission entry and approve the …

    line 911
  39. Distinctive:

    If the dashboard does not open, start the service and check its logs for missing provider credentials or startup errors.

    line 948
  40. Very specific:

    Use brew uninstall --zap --cask mindroom to also remove app preferences and logs.

    line 956
  41. Very specific:

    When the homeserver advertises a handoff page, the app instead sends the Access token in a cf-access-token header, which Access accepts.

    line 978
  42. Distinctive:

    When the Access session ends, the bar at the top asks for that sign-in again.

    line 981
  43. Very specific:

    Add "push": false to turn push off: the app then registers no pusher for the homeserver and hides the push setting.

    line 994
  44. Very specific:

    Write the origin into the config rather than taking it from the request.

    line 1005
  45. Very specific:

    If the same origin serves the MindRoom Chat web app, exclude /native from its service worker's navigation fallback …

    line 1008
  46. Very specific:

    Every OPTIONS request then reaches the origin without an Access session, so make sure no route answers OPTIONS with private content.

    line 1012
  47. Distinctive:

    Leave the application's binding cookie off, because the app sends only the token.

    line 1013
  48. Very specific:

    If /_matrix also passes through this nginx, the check refuses Matrix requests that encode / in a path parameter, such as some state keys, …

    line 1024
  49. Very specific:

    Run a homeserver that allows the cf-access-token header in CORS, such as the MindRoom fork of Tuwunel (v1.9.4-mindroom.18 or later).

    line 1025
  50. Very specific:

    Before moving an existing homeserver behind Access, delete its pushers for the app (app ID chat.mindroom.app) that point at the public push …

    line 1027
  51. Very specific:

    Standalone: run mindroom run and open http://localhost:8765.

    line 1044
  52. Very specific:

    When running from a source checkout, MindRoom builds the dashboard assets on first start if Bun is available.

    line 1045
  53. Very specific:

    When MINDROOM_API_KEY is set, the dashboard first asks for that key, and the login page shows which .env file holds it.

    line 1046
  54. Distinctive:

    When the router, an agent, or a team uses a model whose provider has no API key, every dashboard page shows a Connect your AI provider …

    line 1053
  55. Distinctive:

    If you connect Anthropic or OpenAI while your models still use OpenRouter, the dialog lists the affected models so you can switch them on …

    line 1058
  56. Very specific:

    When an agent cannot reply because no provider key is set, it answers in chat with a pointer to this setup step, including the dashboard …

    line 1059
  57. Very specific:

    More settings: - every other agent field, such as participation, mid_turn, accept_invites, access, memory_search, thread_exports, and …

    line 1075
  58. Very specific:

    Add and edit models with provider, model ID, host URL, and advanced settings, and set provider API keys.

    line 1107
  59. Very specific:

    Create, edit, and delete knowledge bases, including description, mode (semantic search or files-only), path, watch, and Git repository, …

    line 1128
  60. Distinctive:

    Search tools, filter by category or status (Available, Unconfigured, Configured), and choose Shared deployment credentials or an agent with …

    line 1154
  61. Distinctive:

    If another dashboard tab or a server-side edit changed the configuration, saving shows

    line 1184
  62. Very specific:

    If config.yaml fails validation when the dashboard loads, it opens a raw YAML recovery editor so you can fix and save the whole file.

    line 1189
  63. Distinctive:

    When the configuration uses include tags, the dashboard shows a banner and structured saves fail, so edit the included source files …

    line 1193
  64. Very specific:

    If the source files cannot be read well enough to tell whether they use !include, structured saves return HTTP 422.

    line 1196
  65. Very specific:

    Add agent_name to target an agent's credentials in its saved worker scope.

    line 1229
  66. Very specific:

    Add an agent to config.yaml, starting from this example:

    line 1328
  67. Very specific:

    If you switch the agent to mem0 and still want MEMORY.md preloaded, add it to context_files.

    line 1386
  68. Distinctive:

    Use knowledge bases only for non-memory documents that should be searchable as external knowledge.

    line 1387
  69. Very specific:

    If MEMORY.md or another workspace file is sensitive, keep the agent in private rooms only, or split it into a private and a public agent …

    line 1402
  70. Very specific:

    If MINDROOM_CONFIG_PATH is set, MindRoom uses that path, even when no file exists there.

    line 1424
  71. Very specific:

    Every section rejects keys MindRoom does not define, except free-form maps such as a plugin's settings, memory.llm.config, and …

    line 1435
  72. Very specific:

    All top-level sections are optional.

    line 1448
  73. Very specific:

    Keep models.default configured, because room topic generation and automatic thread summaries fall back to it even when agents, teams, and …

    line 1467

≈ “Common” is an estimate: at least 1 in 5 of the 3,225 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
llms-full.txt
# MindRoom

> AI agents that know you and your work, in a chat app anyone can use. Open source, any model, self-host or hosted.

# MindRoom Docs

<div class="mr-hero" markdown>

<img src="assets/logo.svg" alt="" width="128" height="128" />

# MindRoom

<p class="mr-tagline">AI agents that know you and your work, in a chat app anyone can use.</p>

<p class="mr-lede">
MindRoom gives you a personal AI agent for everything from family trips to your homelab, and gives teams shared agents connected to their email, chat, calendar, documents, tickets, and code.
They find what you need, write what you ask for, run recurring jobs on their own, and learn from every conversation, so they get better the more they are used.
It is open source, and you choose each agent's model: a local one that keeps your private life at home, or a frontier one for hard problems.
You can run all of it yourself: the chat app, the server, and the AI backend.
</p>

[:lucide-rocket: Get started](https://docs.mindroom.chat/getting-started/){ .md-button .md-button--primary }
[:lucide-play: See it in action](https://docs.mindroom.chat/showcase/){ .md-button }
[:lucide-cloud: Try hosted MindRoom](https://mindroom.chat/#hosted){ .md-button }
[:lucide-arrow-right-left: Coming from OpenClaw?](https://docs.mindroom.chat/openclaw/){ .md-button }

</div>

<div class="mr-video">
<video controls playsinline preload="metadata" aria-label="Tour of MindRoom: approvals, a team thread, code, documents, and the browser" style="width: 100%" poster="https://github.com/user-attachments/assets/578109b5-9129-412b-b5ab-8bdb10da2e22" data-poster-light="https://github.com/user-attachments/assets/578109b5-9129-412b-b5ab-8bdb10da2e22" data-poster-dark="https://github.com/user-attachments/assets/e1ff7e10-bf9a-46a8-bf18-6f142130246a">
  <source src="https://github.com/user-attachments/assets/9c68a218-da26-47a5-a46f-7f0d4a2607fb" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/dfeece3b-ff67-4412-83fb-b02b6c531814" type="video/mp4">
</video>
</div>

## What People Use It For

=== "Personal"

    - Plan a family trip: flights, places to stay, a day-by-day itinerary, and a packing list.
    - Keep your calendar, reminders, and to-do lists in order, by voice from your phone.
    - Sort your personal email and draft replies, approving each one before it goes out.
    - Keep notes, a journal, and memories you can still find months later.
    - Track your budget, workouts, or anything else in files the agent keeps up to date.
    - Follow the topics you care about, with a digest that only arrives when there is something new.
    - Look after your homelab and smart home: check services, read logs, change configs, and control devices through Home Assistant, with approval for the actions you choose.
    - Build quick tools and scripts in the agent's own workspace.

=== "Work"

    - Find anything across email, chat, documents, tickets, and code, and see where each answer came from.
    - Get a morning briefing, or a summary of your week before every one-on-one.
    - Write status updates from what actually happened in chat and the tracker, not from what people remembered to report.
    - Turn a question into a report or an interactive presentation built from your own documents.
    - Triage the inbox, prepare for meetings, and draft emails, then approve each one before it is sent.
    - Investigate a technical question across specifications, code, and records, with every claim traced to its source.
    - Join a new team and ask its agent how the work fits together and whom to ask.

## Why MindRoom

<div class="grid cards mr-why" markdown>

-   ### :lucide-plug-zap:{ .lg } Agents that know you and your work

    ---

    Agents connect to 100+ tools, including Gmail, Google Drive, Slack, Jira, Confluence, GitHub, and any MCP server ([Tools](https://docs.mindroom.chat/tools/), [MCP Servers](https://docs.mindroom.chat/mcp/)), and search knowledge bases built from your folders and Git repositories ([Knowledge Bases](https://docs.mindroom.chat/knowledge/)).
    Each person can get a private room with their own agent ([Personal Rooms](https://docs.mindroom.chat/personal-rooms/)), each team can share agents that answer only its members ([Access Control](https://docs.mindroom.chat/authorization/)), and agents can work as a team or hand tasks to each other ([Teams](https://docs.mindroom.chat/configuration/teams/), [Agent Orchestration](https://docs.mindroom.chat/tools/agent-orchestration/)).

-   ### :lucide-lock:{ .lg } Private where it matters

    ---

    You choose the model for each agent.
    A private agent can run on a local model through Ollama or llama.cpp, so you can share your most personal data with it, while an agent that writes code uses a frontier model from Anthropic, OpenAI, or Google ([Models](https://docs.mindroom.chat/configuration/models/)).
    Pair a local model with local memory and your own server, and nothing that agent sees leaves your home ([Memory](https://docs.mindroom.chat/memory/), [Deployment](https://docs.mindroom.chat/deployment/)).
    Each agent keeps its own memory, tools, and access, so what you tell a private agent stays out of shared ones, and rooms can be end-to-end encrypted ([End-to-End Encryption](https://docs.mindroom.chat/matrix/#end-to-end-encryption)).

-   ### :lucide-brain:{ .lg } They remember, and keep improving

    ---

    Agents keep what is worth remembering from every conversation without being asked, and look it up again when it becomes relevant ([Memory](https://docs.mindroom.chat/memory/)).
    A shared agent gets better as more people use it: what it learns while helping one person helps the next.
    With skill learning on, an agent also turns work it repeats into reusable skills, following the self-improvement loop of Hermes Agent ([Automatic Skill Learning](https://docs.mindroom.chat/skills/#automatic-skill-learning)).
    With a knowledge graph on, it builds a map of the people, projects, and documents its conversations and tools reveal, with when each fact held and where it came from, and can explore its sources on a schedule to fill it in ([Knowledge Graph](https://docs.mindroom.chat/knowledge-graph/)).
    You can teach it directly too: tell it a preference or a correction, and it is remembered.

-   ### :lucide-calendar-clock:{ .lg } They work while you don't

    ---

    Schedule recurring jobs in plain language, such as a daily briefing or a weekly report, including silent checks that post only when something changed ([Scheduling](https://docs.mindroom.chat/scheduling/)).
    Send a voice message from your phone ([Voice Messages](https://docs.mindroom.chat/voice/)), or call an agent and talk while it hands longer tasks to background agents ([Voice Calls](https://docs.mindroom.chat/voice-calls/)).

-   ### :lucide-shield-check:{ .lg } Safe to give real access

    ---

    - Sending or changing something can wait for your one-tap approval, which shows exactly what will be sent and to whom ([Tool Approval](https://docs.mindroom.chat/tool-approval/)).
    - Code can run in isolated workers ([Workers & Sandboxing](https://docs.mindroom.chat/deployment/sandbox-proxy/)), and agents can be kept off the open internet except for sites a person approves ([Approved Egress](https://docs.mindroom.chat/deployment/approved-egress/)).
    - Each agent answers only the people and rooms you allow ([Access Control](https://docs.mindroom.chat/authorization/)).
    - It all runs on [Matrix](https://docs.mindroom.chat/matrix/), an open messaging standard, with optional end-to-end encryption, that governments chose for their own secure messaging: France's Tchap, the German armed forces' BwMessenger, Germany's national healthcare messenger TI-Messenger, and a messenger NATO is testing all run on it.

    The [Security Model](https://docs.mindroom.chat/architecture/security-posture/) documents exactly which boundary protects what.

-   ### :lucide-messages-square:{ .lg } A chat app built for agents

    ---

    MindRoom builds its own client, MindRoom Chat, on the [web](https://chat.mindroom.chat), on [iPhone and iPad](https://docs.mindroom.chat/ios/), on the [Mac](https://docs.mindroom.chat/installation/macos-app/), and in beta on Android, along with the server and the AI backend.
    So agents are not limited to what Slack or WhatsApp can display:

    - Replies stream in with every tool call shown, and you can stop them at any time ([Streaming Responses](https://docs.mindroom.chat/streaming/)).
    - Approval cards and clickable questions sit right in the conversation ([Interactive Questions](https://docs.mindroom.chat/interactive/)).
    - An agent can work in its own browser that you watch live and take over, for example to log in ([Worker Computer](https://docs.mindroom.chat/tools/worker-computer/#watch-take-control-and-resume)).
    - An agent running on a server across the world can still use your laptop, or any computer you pair: the apps and folders you allow and the shell commands you approve, over end-to-end encrypted Matrix with no open ports ([Desktop Bridge](https://docs.mindroom.chat/tools/desktop/)).
    - An agent can open an interactive page next to the conversation, such as a dashboard, a presentation, or a form you answer in place ([Interactive Canvases](https://docs.mindroom.chat/canvases/)).
    - You can switch the model for a thread or a room from the chat ([Model Overrides in Chat](https://docs.mindroom.chat/configuration/models/#model-overrides-in-chat)).

-   ### :lucide-key-round:{ .lg } Yours to own

    ---

    - **Open source:** MindRoom is Apache 2.0 licensed, so you can run the whole stack on your own hardware, or use the [hosted Matrix server](https://docs.mindroom.chat/deployment/hosted-matrix/) and run only the backend yourself.
    - **Any model:** pick a model per agent, room, or thread, and switch the day a better one comes out ([Models](https://docs.mindroom.chat/configuration/models/)).
    - **Federated like email:** separate MindRoom deployments, such as groups with different data rules, can still share rooms where that is allowed.
    - **Extensible:** add your own tools, skills, and event hooks as plugins, which reload while MindRoom keeps running ([Plugins](https://docs.mindroom.chat/plugins/), [Hooks](https://docs.mindroom.chat/hooks/)).

-   ### :lucide-globe:{ .lg } Works where you already are

    ---

    Matrix bridges bring the same agents, with the same memory, to Slack, Telegram, WhatsApp, Discord, and email ([Bridges](https://docs.mindroom.chat/deployment/bridges/)).
    Agents also answer any OpenAI-compatible client ([OpenAI-Compatible API](https://docs.mindroom.chat/openai-api/)), and tools like Claude Code and Codex can use them through the [MCP Gateway](https://docs.mindroom.chat/deployment/mcp-gateway/).

</div>

## How MindRoom Compares

AI apps such as ChatGPT, Claude, and Gemini can connect to your email, calendar, and documents, but each one ties those connections to one company.
Connect your inbox to ChatGPT, and OpenAI's servers process it for OpenAI's models only; using Claude or Gemini as well means connecting everything again, handing your data to another company, and teaching it about you from scratch.
In MindRoom you connect once: your accounts, memory, and documents live in MindRoom, which can run on your own machine, and every agent uses them with the model that fits.
A cloud model sees only what its agent sends it, and an agent on a local model, with local memory and your own server, keeps everything at home.

Agents such as OpenClaw and Hermes Agent plug into existing messaging apps through one adapter per app.
That lets them reach many apps, but each app decides what the agent can show.
MindRoom takes the opposite approach.

| | Agents inside other messaging apps | MindRoom |
|---|---|---|
| Who they serve | Usually one person's assistant | Personal agents for everyone and shared agents for teams, in rooms where people and agents work together, with access control |
| What agents can show | Whatever each app allows, mostly text and simple buttons | Its own client: approval cards, live tool traces, voice calls, a browser you can take over, and interactive pages |
| How they connect | One adapter per messaging app | One open, encrypted protocol, plus bridges for reach |
| Where messages go | Through each messaging platform | On servers you can run yourself, where rooms can be end-to-end encrypted |

Coming from OpenClaw? MindRoom reads OpenClaw workspace files and skills; see [Import from OpenClaw](https://docs.mindroom.chat/openclaw/).

## Get Started

<div class="grid cards" markdown>

-   ### :lucide-download:{ .lg } 1. Install & First Run

    ---

    Run `uvx mindroom run` and pair it with your MindRoom Chat account.

    [:lucide-arrow-right: Install & First Run](https://docs.mindroom.chat/getting-started/)

-   ### :lucide-sliders-horizontal:{ .lg } 2. Configuration

    ---

    Define agents, models, and teams in `config.yaml` or the [Dashboard](https://docs.mindroom.chat/dashboard/); most changes apply without a restart.

    [:lucide-arrow-right: Configuration](https://docs.mindroom.chat/configuration/)

-   ### :lucide-server:{ .lg } 3. Deployment

    ---

    Run MindRoom with Docker or Kubernetes when you are ready to share it.

    [:lucide-arrow-right: Deployment](https://docs.mindroom.chat/deployment/)

</div>

# Showcase

Every video on this page is a recording of MindRoom Chat talking to a MindRoom backend.
The model responses and external services are scripted so each recording is repeatable; the rooms, threads, approvals, streaming, and tool traces are MindRoom's own.
Each video comes in a light and a dark recording to match your color scheme.

## Films

=== "Film · 50 s"

    Approvals, a team thread, code, documents, and the browser at work, then lights, trips, and dinners at home.

    <video class="only-light" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/1e6ecd17-024a-497d-8c12-041dbeaf5438" aria-label="MindRoom at work and at home" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/fcc38c9c-61ba-4db4-b026-4f947c931381" type="video/mp4">
    </video>
    <video class="only-dark" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/320881ba-a36d-4c00-949d-f4f56dc3d15c" aria-label="MindRoom at work and at home" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/67ca35c1-e66c-40c7-8295-740d57ceec92" type="video/mp4">
    </video>

=== "Short · 29 s"

    The same story with two moments at work and two at home, as in the README.

    <video class="only-light" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/ed99f419-4afc-4b58-a634-24b814fd99bc" aria-label="The short version" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/ccba1454-3bf5-4f4c-aae4-856536df916f" type="video/mp4">
    </video>
    <video class="only-dark" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/46971661-b88d-46c4-89ba-d87f569f636e" aria-label="The short version" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/ff3c9970-1ed5-4c98-a68d-cedcd9713017" type="video/mp4">
    </video>

=== "Work tour · 82 s"

    Thirteen moments at work, one after another, in an earlier full-screen style.

    <video class="only-light" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/578109b5-9129-412b-b5ab-8bdb10da2e22" aria-label="Narrated tour" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/dfeece3b-ff67-4412-83fb-b02b6c531814" type="video/mp4">
    </video>
    <video class="only-dark" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/e1ff7e10-bf9a-46a8-bf18-6f142130246a" aria-label="Narrated tour" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/9c68a218-da26-47a5-a46f-7f0d4a2607fb" type="video/mp4">
    </video>

## Canvases

Agents can show a page they made beside the conversation and act on what you pick in it.
See [Interactive Canvases](https://docs.mindroom.chat/canvases/).

=== "Overview · 27 s"

    A narrated film of all four: a scheduling grid, a data fit, a slide deck, and a trip planner.

    <video class="only-light" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/5941fd9d-90d2-4a52-b0e9-abbff53354b4" aria-label="Canvas film: four agent canvases beside the conversation" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/3a2e29e3-90a0-4cf7-a3cb-c0e41765695a" type="video/mp4">
    </video>
    <video class="only-dark" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/dc3e3352-7cef-45ce-95be-3edad3bc7fe5" aria-label="Canvas film: four agent canvases beside the conversation" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/a704564b-be05-45cd-9c74-2b5de4f5c875" type="video/mp4">
    </video>

=== "Scheduling"

    A week grid of four people's availability; the pick goes back with **Send**, and the agent books the meeting after approval.

    <video controls playsinline preload="metadata" aria-label="An agent shows a week grid; the user picks a slot and sends it, and the agent books it" style="width: 100%" poster="https://github.com/user-attachments/assets/21d0d8ad-a7c4-4937-8fac-3d178b047011" data-poster-light="https://github.com/user-attachments/assets/21d0d8ad-a7c4-4937-8fac-3d178b047011" data-poster-dark="https://github.com/user-attachments/assets/1247f0ca-1c73-4759-bfa7-1b06fb8c2635">
      <source src="https://github.com/user-attachments/assets/2ae227fd-8edb-4aa2-af62-f1ef3c99a530" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/3926d694-04cf-4f17-ae4b-70f00dde705e" type="video/mp4">
    </video>

=== "Data"

    An interactive fit drawn in inline SVG with the theme variables; leaving out an outlier refits the curve live.

    <video controls playsinline preload="metadata" aria-label="An agent shows a data fit; the user leaves out an outlier and sends back the fit" style="width: 100%" poster="https://github.com/user-attachments/assets/795ebae7-99dc-4660-bf7c-2754fdc5916d" data-poster-light="https://github.com/user-attachments/assets/795ebae7-99dc-4660-bf7c-2754fdc5916d" data-poster-dark="https://github.com/user-attachments/assets/61718584-7b95-4bac-87f3-2e71b3cb6ab4">
      <source src="https://github.com/user-attachments/assets/7385503a-982b-4811-9146-0a3a0378084d" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/36d79992-fc82-47e2-a012-6bc2c07b4956" type="video/mp4">
    </video>

=== "Slides"

    A deck the agent writes to a workspace file and shows by `path`; after a change request it rewrites the file and the open deck updates in place.

    <video controls playsinline preload="metadata" aria-label="An agent shows a slide deck from its workspace and updates it in place after a change request" style="width: 100%" poster="https://github.com/user-attachments/assets/0a8304cd-77bb-481a-8654-c735d91f5fd4" data-poster-light="https://github.com/user-attachments/assets/0a8304cd-77bb-481a-8654-c735d91f5fd4" data-poster-dark="https://github.com/user-attachments/assets/1e2819a7-8c5a-4095-8749-a7a3610dddc9">
      <source src="https://github.com/user-attachments/assets/c033ea6b-fc57-4a66-a235-be716d33286c" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/6219e504-dcf2-40a7-a919-fc5b372c13d6" type="video/mp4">
    </video>

=== "Trip planner"

    One canvas, two steps: pick a stay, then plan the days against a live budget, updated in place with `canvas_event_id`.

    <video controls playsinline preload="metadata" aria-label="One canvas, two steps: pick a stay, then plan the days against a live budget" style="width: 100%" poster="https://github.com/user-attachments/assets/fbe6bf8f-f1b4-4195-ad37-df3a2dfdbe80" data-poster-light="https://github.com/user-attachments/assets/fbe6bf8f-f1b4-4195-ad37-df3a2dfdbe80" data-poster-dark="https://github.com/user-attachments/assets/c71df2bd-54c9-4946-a8b1-54e191db5c3b">
      <source src="https://github.com/user-attachments/assets/341cd5bd-7fb1-441f-8a6a-470b701359d6" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/26870d8e-5303-4cfd-bb10-9fa66c1d6632" type="video/mp4">
    </video>

## At work

### Ask, approve, done

An agent finds two requests in the inbox, picks a free slot, and pauses for approval before it books the meeting.
See [Tool Approval](https://docs.mindroom.chat/tool-approval/#tool-approval).

<video controls playsinline preload="metadata" aria-label="Ask, approve, done" style="width: 100%" poster="https://github.com/user-attachments/assets/72632152-a056-40de-961e-2435119812d6" data-poster-light="https://github.com/user-attachments/assets/72632152-a056-40de-961e-2435119812d6" data-poster-dark="https://github.com/user-attachments/assets/44197f0e-da33-4076-b859-ac4e2ca39513">
  <source src="https://github.com/user-attachments/assets/502962b4-4dc6-4819-ae5a-eef572a41422" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/cd2e754b-dbb7-4af7-a050-c29514ab712e" type="video/mp4">
</video>

### One thread, the whole team

Three colleagues work with one agent in a shared thread, and everyone sees each answer stream in live.
See [Streaming Responses](https://docs.mindroom.chat/streaming/).

<video controls playsinline preload="metadata" aria-label="One thread, the whole team" style="width: 100%" poster="https://github.com/user-attachments/assets/dc935612-af9a-4556-a502-5c1ffaf3810a" data-poster-light="https://github.com/user-attachments/assets/dc935612-af9a-4556-a502-5c1ffaf3810a" data-poster-dark="https://github.com/user-attachments/assets/d3312f2a-0315-4109-83d6-3470bb2a4247">
  <source src="https://github.com/user-attachments/assets/0545ae90-c825-496d-81ad-45d3cc069822" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/71036d2f-4ccd-4cdd-99c2-518b915f2ba0" type="video/mp4">
</video>

### Bring the team in

New colleagues are invited to a room, and the agent tells them what is left before the deadline.
See [Authorization](https://docs.mindroom.chat/authorization/).

<video controls playsinline preload="metadata" aria-label="Bring the team in" style="width: 100%" poster="https://github.com/user-attachments/assets/e9d474d9-2d94-4c2a-be48-35c50edc1fd9" data-poster-light="https://github.com/user-attachments/assets/e9d474d9-2d94-4c2a-be48-35c50edc1fd9" data-poster-dark="https://github.com/user-attachments/assets/f6949b71-2746-4d4e-84ed-3a7ccb676d57">
  <source src="https://github.com/user-attachments/assets/20b4d748-efd8-4423-a23e-f8153d3cf845" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/d25d59e5-02a4-4fd0-86ef-108cf1f24a6e" type="video/mp4">
</video>

### Tag two agents, get a team

Mentioning two agents in one message forms a team that researches and answers together.
See [Team Configuration](https://docs.mindroom.chat/configuration/teams/).

<video controls playsinline preload="metadata" aria-label="Tag two agents, get a team" style="width: 100%" poster="https://github.com/user-attachments/assets/a892a233-6585-4156-932f-8addb1409879" data-poster-light="https://github.com/user-attachments/assets/a892a233-6585-4156-932f-8addb1409879" data-poster-dark="https://github.com/user-attachments/assets/613f4f1e-cae4-4e82-a082-282fbbf2d73b">
  <source src="https://github.com/user-attachments/assets/fc66ec35-de5f-4a4e-9494-5bb1f361973a" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/0fc87c04-06ed-4508-bc4f-f461720218dc" type="video/mp4">
</video>

### From alert to all-clear

One agent traces failing signups to a deploy, rolls it back with approval, and hands the customer update to another agent.
See [Agent conversations](https://docs.mindroom.chat/tools/matrix-message/#agent-conversations).

<video controls playsinline preload="metadata" aria-label="From alert to all-clear" style="width: 100%" poster="https://github.com/user-attachments/assets/f0841f4c-b329-4f6e-beb8-db97385ca747" data-poster-light="https://github.com/user-attachments/assets/f0841f4c-b329-4f6e-beb8-db97385ca747" data-poster-dark="https://github.com/user-attachments/assets/4f646502-aa3d-45e0-9252-caeb82cff64b">
  <source src="https://github.com/user-attachments/assets/f52dc877-6750-41a7-b7df-682a28cbdbfc" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/dfae0111-bd08-465d-a316-b37fc3b8324f" type="video/mp4">
</video>

### Connect once, then just ask

A user connects an account from the conversation, and the agent answers from it.
See [OAuth Integration Framework](https://docs.mindroom.chat/oauth-framework/).

<video controls playsinline preload="metadata" aria-label="Connect once, then just ask" style="width: 100%" poster="https://github.com/user-attachments/assets/474025d7-38f7-4695-a838-d75c8404efe0" data-poster-light="https://github.com/user-attachments/assets/474025d7-38f7-4695-a838-d75c8404efe0" data-poster-dark="https://github.com/user-attachments/assets/b54e0d21-18fb-4c85-ad0a-4b8052f7b379">
  <source src="https://github.com/user-attachments/assets/3d033cc5-268c-4467-a69e-3badddc337a6" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/9ccc5590-0d48-4ddd-8272-28d7d485c21f" type="video/mp4">
</video>

### What the policy actually says

The agent answers a travel question from Confluence and Drive, with its sources.
See [Atlassian Cloud](https://docs.mindroom.chat/tools/atlassian/).

<video controls playsinline preload="metadata" aria-label="What the policy actually says" style="width: 100%" poster="https://github.com/user-attachments/assets/59921dee-7062-4327-80eb-b53c6e2cd392" data-poster-light="https://github.com/user-attachments/assets/59921dee-7062-4327-80eb-b53c6e2cd392" data-poster-dark="https://github.com/user-attachments/assets/b74ba2fc-af0b-471e-8779-cff61de0c698">
  <source src="https://github.com/user-attachments/assets/1e0337e0-30d6-400f-88df-8374cd728158" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/5a12e5b4-f8cd-4e88-9837-80e2f7678036" type="video/mp4">
</video>

### Answers you can trust

The agent compares two Drive documents and flags a stale number before a drawing goes out.
See [Google Drive](https://docs.mindroom.chat/tools/data-and-databases/#google_drive).

<video controls playsinline preload="metadata" aria-label="Answers you can trust" style="width: 100%" poster="https://github.com/user-attachments/assets/38ae5852-8273-4cd5-b05e-27e36c5ff157" data-poster-light="https://github.com/user-attachments/assets/38ae5852-8273-4cd5-b05e-27e36c5ff157" data-poster-dark="https://github.com/user-attachments/assets/08b198a5-3134-4b36-b688-4f431e87babf">
  <source src="https://github.com/user-attachments/assets/e5d0d6c6-f406-425a-a22d-570f0f4f45d5" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/ba7d6fcc-83e9-4a88-be2e-30ce9c8754e5" type="video/mp4">
</video>

### Two policies, one draft

The agent merges two travel policies into one draft and asks a multiple-choice question where they disagree.
See [Interactive Q&A](https://docs.mindroom.chat/interactive/).

<video controls playsinline preload="metadata" aria-label="Two policies, one draft" style="width: 100%" poster="https://github.com/user-attachments/assets/2a7ab600-0934-4b53-a47b-ec8fcd80201d" data-poster-light="https://github.com/user-attachments/assets/2a7ab600-0934-4b53-a47b-ec8fcd80201d" data-poster-dark="https://github.com/user-attachments/assets/4ee1ad16-4d36-4aca-932d-49b8447b5239">
  <source src="https://github.com/user-attachments/assets/35bb8221-3405-4efb-b236-304a58f898e8" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/35bfe7b2-feab-484f-b5e8-22b26ac92df8" type="video/mp4">
</video>

### From numbers to a board deck

The agent gathers the quarter's numbers and the launch status and builds a five-slide board deck.
See [Execution & Coding](https://docs.mindroom.chat/tools/execution-and-coding/).

<video controls playsinline preload="metadata" aria-label="From numbers to a board deck" style="width: 100%" poster="https://github.com/user-attachments/assets/48281371-ccfa-4ba6-b3e8-a11664f6738d" data-poster-light="https://github.com/user-attachments/assets/48281371-ccfa-4ba6-b3e8-a11664f6738d" data-poster-dark="https://github.com/user-attachments/assets/8bf0c216-daad-4205-afb4-917ced0a7690">
  <source src="https://github.com/user-attachments/assets/defe591b-71c2-46c0-8eef-93422aa82439" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/e455ba1d-cdcf-47a2-9bd9-5ce0d2d0b0ef" type="video/mp4">
</video>

### Real analysis, real numbers

The agent writes and runs an analysis script on its own computer and posts the fitted result with a plot.
See [Execution & Coding](https://docs.mindroom.chat/tools/execution-and-coding/).

<video controls playsinline preload="metadata" aria-label="Real analysis, real numbers" style="width: 100%" poster="https://github.com/user-attachments/assets/524efcc4-9b4c-4ee1-85c3-bca8dc13e1f8" data-poster-light="https://github.com/user-attachments/assets/524efcc4-9b4c-4ee1-85c3-bca8dc13e1f8" data-poster-dark="https://github.com/user-attachments/assets/e62497d7-3133-4abe-afc2-ec3db2889552">
  <source src="https://github.com/user-attachments/assets/36d029f7-b67d-46c7-ae38-f76517dcd354" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/1e73d587-9a0e-44c3-a66d-8b6631cdbf61" type="video/mp4">
</video>

### It drives, you take the wheel

The agent reorders supplies in its browser while the user watches, takes control, and hands it back.
See [Worker Computer](https://docs.mindroom.chat/tools/worker-computer/#watch-take-control-and-resume).

<video controls playsinline preload="metadata" aria-label="It drives, you take the wheel" style="width: 100%" poster="https://github.com/user-attachments/assets/f22ef271-53a8-4000-950c-28b2344de13b" data-poster-light="https://github.com/user-attachments/assets/f22ef271-53a8-4000-950c-28b2344de13b" data-poster-dark="https://github.com/user-attachments/assets/be8be64a-6b82-453f-b294-c888bc8bcda0">
  <source src="https://github.com/user-attachments/assets/5fc87c49-f21b-4a23-8eeb-d66becd2289c" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/db2b13b5-35c0-4b15-bbc9-93dec148f666" type="video/mp4">
</video>

### Your morning, already handled

A scheduled task posts a morning brief.
See [Scheduling](https://docs.mindroom.chat/scheduling/).

<video controls playsinline preload="metadata" aria-label="Your morning, already handled" style="width: 100%" poster="https://github.com/user-attachments/assets/ada165aa-24ad-407f-9831-30928126d091" data-poster-light="https://github.com/user-attachments/assets/ada165aa-24ad-407f-9831-30928126d091" data-poster-dark="https://github.com/user-attachments/assets/01a8d67f-f1ed-4c6a-b2df-a1681149722e">
  <source src="https://github.com/user-attachments/assets/99f7e664-131d-4831-9c7e-64a2d95b2b6b" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/67fd537c-d810-4a3c-b5a5-d8e307708d56" type="video/mp4">
</video>

### Nothing falls through the cracks

A check asked for in conversation becomes a weekly scheduled report.
See [Scheduler tool](https://docs.mindroom.chat/scheduling/#scheduler).

<video controls playsinline preload="metadata" aria-label="Nothing falls through the cracks" style="width: 100%" poster="https://github.com/user-attachments/assets/7221a1d4-2429-4591-8b6f-aca242cc152b" data-poster-light="https://github.com/user-attachments/assets/7221a1d4-2429-4591-8b6f-aca242cc152b" data-poster-dark="https://github.com/user-attachments/assets/70ad19a8-6728-4344-9e6d-0258c43b5173">
  <source src="https://github.com/user-attachments/assets/3377307e-268f-4ae0-a0e6-061dd16c1627" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/4423fd02-b6c9-4cf5-bf10-ae50149ebd2e" type="video/mp4">
</video>

### Say it, and it's handled

A voice message between meetings becomes a reminder.
See [Voice Messages](https://docs.mindroom.chat/voice/).

<video controls playsinline preload="metadata" aria-label="Say it, and it's handled" style="width: 100%" poster="https://github.com/user-attachments/assets/5b7b4f52-ea23-415c-b2d5-50b3fbbb7bc1" data-poster-light="https://github.com/user-attachments/assets/5b7b4f52-ea23-415c-b2d5-50b3fbbb7bc1" data-poster-dark="https://github.com/user-attachments/assets/3fb56349-ad0f-435d-be4e-6c95860e7b9a">
  <source src="https://github.com/user-attachments/assets/4c49c8f1-2b87-45a1-949a-b165d0e18c1a" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/f91d0d7f-b8b0-4039-9fda-c7a9caed7534" type="video/mp4">
</video>

## At home

### Lock up on the way out

A voice note from a phone locks up, turns off the lights, and lowers the heating.
See [Home Assistant](https://docs.mindroom.chat/tools/location-commerce-and-home/#homeassistant).

<video controls playsinline preload="metadata" aria-label="Lock up on the way out" style="width: 100%" poster="https://github.com/user-attachments/assets/534252f8-c599-4a1c-9aef-f1131ef9dfaf" data-poster-light="https://github.com/user-attachments/assets/534252f8-c599-4a1c-9aef-f1131ef9dfaf" data-poster-dark="https://github.com/user-attachments/assets/6729cad6-62ae-4ea4-92f4-6a3e422e7c36">
  <source src="https://github.com/user-attachments/assets/bf32f178-1c18-4643-a2d6-1450e306cbe0" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/30f0c744-ecaf-4fbe-9c95-08576425ab3f" type="video/mp4">
</video>

### A weekend away, planned together

Two people plan a weekend trip, and the agent checks the calendar, offers options, and books the one they pick.
See [Google Calendar](https://docs.mindroom.chat/tools/calendar-and-scheduling/#google_calendar).

<video controls playsinline preload="metadata" aria-label="A weekend away, planned together" style="width: 100%" poster="https://github.com/user-attachments/assets/6021fb0e-84fc-48a0-803e-410f4904b84a" data-poster-light="https://github.com/user-attachments/assets/6021fb0e-84fc-48a0-803e-410f4904b84a" data-poster-dark="https://github.com/user-attachments/assets/d1ced210-a86c-49e8-abac-640bf7116686">
  <source src="https://github.com/user-attachments/assets/6f3e7958-0733-4141-b667-a798d94fcaee" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/108f8f22-6752-414e-93dd-38154715981c" type="video/mp4">
</video>

### Dinners that fit everyone

The agent remembers each person's diet and plans the week's dinners.
See [Memory System](https://docs.mindroom.chat/memory/).

<video controls playsinline preload="metadata" aria-label="Dinners that fit everyone" style="width: 100%" poster="https://github.com/user-attachments/assets/9046ecb6-3be5-440d-b06e-5917d58d2f59" data-poster-light="https://github.com/user-attachments/assets/9046ecb6-3be5-440d-b06e-5917d58d2f59" data-poster-dark="https://github.com/user-attachments/assets/00d4a4fc-58c3-4968-855a-61089cf19e0b">
  <source src="https://github.com/user-attachments/assets/92f62053-ff1e-4982-80c2-bbbac3c5c757" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/f1d8e8ce-8160-46bf-811d-2a3e71cde673" type="video/mp4">
</video>

# Getting Started

This page installs MindRoom and gets a first agent answering in chat.
Pick one setup:

| Setup | Use when |
| --- | --- |
| [Your computer + MindRoom Chat](#recommended-your-computer-mindroom-chat) (recommended) | You want the simplest start: MindRoom runs on your computer, and Matrix and the chat app are hosted at `mindroom.chat` |
| [macOS app](https://docs.mindroom.chat/installation/macos-app/) | You want a native app that runs your agents on an Apple silicon Mac, including one-click local models |
| [Full stack Docker Compose](#full-stack-docker-compose) | You want everything on your own machine: dashboard, Matrix homeserver, and chat app |
| [Agent-managed NixOS container](#advanced-agent-managed-nixos-container) | You want an agent to manage its own persistent machine while the host controls what it can see |
| [Manual install](#manual-install-with-your-own-matrix-homeserver) | You already run a Matrix homeserver |
| [Hosted MindRoom](https://mindroom.chat/#hosted) | You want nothing to install, and MindRoom runs your agents for you |




## Recommended: your computer + MindRoom Chat

MindRoom runs on your computer, while the Matrix homeserver is hosted at `mindroom.chat` and the chat app at `chat.mindroom.chat`.

Before you start:

- Install [uv](https://docs.astral.sh/uv/getting-started/installation/), which installs Python when needed.
- Have a model ready: an API key for a provider such as Anthropic, OpenAI, or OpenRouter; a subscription login such as Codex; or a local model through Ollama or llama.cpp (see [Supported Providers](https://docs.mindroom.chat/configuration/models/#supported-providers)).
- Sign in at [chat.mindroom.chat](https://chat.mindroom.chat); the first sign-in creates the MindRoom Chat account that approves the pairing.

### 1. Run MindRoom

```bash
uvx mindroom run
```

When no config exists and MindRoom runs in a terminal, it asks a few setup questions:

1. Pick a model provider from the menu (default OpenAI).
2. For Anthropic, OpenAI, or OpenRouter, paste the API key; input is hidden.
   Press Enter to skip, then connect the provider later through the [dashboard](https://docs.mindroom.chat/dashboard/#connect-your-ai-provider), or add the key to `~/.mindroom/.env` and restart `mindroom run`.
   MindRoom does not ask when the key is already set in your environment.
   Other presets print their remaining step instead, such as `codex login`, `ollama pull`, or the Azure, Bedrock, or Vertex AI settings to add to `~/.mindroom/.env` before restarting.

MindRoom then writes `~/.mindroom/config.yaml` and `~/.mindroom/.env` with hosted Matrix defaults (`MATRIX_HOMESERVER=https://mindroom.chat`) and a starter `Mind` agent in a `Personal` room.

### 2. Approve pairing

MindRoom prints a pairing link and QR code.
Open the link or scan the QR code with your MindRoom Chat account to approve, or enter the displayed code in MindRoom Chat → Settings → Local MindRoom.
After approval, MindRoom prints the approving account and asks `Is this your account?` before saving anything.
Pair codes expire after 10 minutes, and `mindroom run` prints a new one when that happens.
See [connect](https://docs.mindroom.chat/deployment/hosted-matrix/#connect) for pairing details, the credentials it saves, and how to revoke a pairing.

### 3. Choose how MindRoom keeps running

After pairing, first-run setup asks whether to run MindRoom in the background and start it at login, as a systemd user service on Linux or a launchd agent on macOS.
Pressing Enter installs and starts the service, the same as `mindroom service install`, and returns to your shell.
Check it with `mindroom service status`, and run `mindroom service restart` after editing `~/.mindroom/.env`.
Answering `n` runs MindRoom and the dashboard in the terminal instead, which also happens when the service cannot be installed.
See [`mindroom service`](https://docs.mindroom.chat/cli/#service) for when the question is skipped.

### Verify

- **Chat:** open `https://chat.mindroom.chat` and send `hello` in the `Personal` room; `Mind` is its only responder, so it answers without a mention.
- **Dashboard:** open `http://localhost:8765` to configure agents, models, and tools.
  It asks for the `MINDROOM_API_KEY` that setup generated in `~/.mindroom/.env`, because `mindroom run` serves the dashboard on every network interface by default.
- **Preflight check:** `uvx mindroom doctor` checks config, API keys, Matrix connectivity, pairing, and storage in one pass; see [doctor](https://docs.mindroom.chat/cli/#doctor).

See [Hosted Matrix + Local Backend](https://docs.mindroom.chat/deployment/hosted-matrix/) for what runs where and the credential and trust model.
To run worker-routed execution tools such as `coding`, `docker`, `file`, `python`, and `shell` in dedicated Docker workers on the same machine, see [Sandbox Proxy Isolation](https://docs.mindroom.chat/deployment/sandbox-proxy/).



### Set up from a script or service

Without a terminal (services, Docker, the macOS app), a missing config is an error that points to `mindroom config init`, unless you pass `--provider`.
Flags answer the setup questions up front, which lets a script or coding agent set up MindRoom:

```bash
OPENAI_API_KEY=sk-... uvx mindroom run --provider openai --service
```

`--provider` picks the preset and the API key comes from the environment.
`--service` installs the login service after pairing, and `--no-service` skips the question.
MindRoom prints the pairing link and code for a person to approve, and prints the approving account instead of asking about it.

### Create the config first with `config init`

Use `config init` to review the files before starting, to choose a preset without prompts, or to use a self-hosted homeserver:

```bash
uvx mindroom config init --provider anthropic
$EDITOR ~/.mindroom/.env
uvx mindroom run
```

The generated `.env` lists the credentials the chosen preset needs; set them before running.
See [`config init`](https://docs.mindroom.chat/cli/#config-init) for every option and the extra steps for the `codex`, `kimi`, `ollama`, and `llama.cpp` presets, and [Model Environment Variables](https://docs.mindroom.chat/configuration/models/#environment-variables) for each provider's credentials.



## Full stack Docker Compose

Use this when you want everything local: the MindRoom dashboard, a Matrix homeserver, and the MindRoom Chat client in one stack.
It requires Docker, Docker Compose, and Python 3 for the quickstart script.

```bash
git clone https://github.com/mindroom-ai/mindroom-stack
cd mindroom-stack
cp .env.example .env
$EDITOR .env  # set ANTHROPIC_API_KEY for the default stack config
./scripts/quickstart.py
```

The quickstart validates provider configuration, starts the stack, waits until it is ready, and explains common port and startup failures.
The default stack config uses Anthropic and requires `ANTHROPIC_API_KEY`.
To use another provider, edit `config/config.yaml` and supply its credentials before starting; see the [stack model configuration guide](https://github.com/mindroom-ai/mindroom-stack#configure-models).

The stack serves the MindRoom dashboard at `http://localhost:8765`, the MindRoom Chat client at `http://localhost:8080`, and Matrix at `http://localhost:8008`, using the published `mindroom`, `mindroom-chat`, and `mindroom-tuwunel` images.
To reach it from another device, set `CLIENT_HOMESERVER_URL=http://<host-ip>:8008` in `.env` before starting.
See the [stack README](https://github.com/mindroom-ai/mindroom-stack) for running `docker compose` directly, changing host ports, and pinning images.



## Advanced: agent-managed NixOS container

Use this when you want to give a MindRoom agent full freedom over its own virtual machine while you, from the host, control precisely what it can see.
The [mindroom-ai/lxc-nixos](https://github.com/mindroom-ai/lxc-nixos) flake provisions an Incus LXC system container running NixOS with MindRoom, the Tuwunel Matrix homeserver, MindRoom Chat, and Caddy, plus Docker and `ragenix`-based secrets.
Because the whole machine is declared in the flake, the agent can rebuild and manage the persistent system it runs on without touching the host, unlike the mostly stateless Docker Compose stack.
It is harder to set up by hand, but a coding agent such as Codex or Claude Code can follow the repo's `AGENTS.md` runbook directly.

It requires a Linux host running [Incus](https://linuxcontainers.org/incus/docs/main/installing/):

```bash
git clone https://github.com/mindroom-ai/lxc-nixos.git
cd lxc-nixos
incus launch images:nixos/unstable mindroom -c security.nesting=true
incus config device add mindroom repo disk source="$PWD" path=/mnt/repo shift=true
```

Then follow the repo's `AGENTS.md` runbook for operator SSH keys, secrets bootstrap, and the `nixos-rebuild switch` deployment.

## Manual install with your own Matrix homeserver

Use this if you already have a Matrix homeserver and want to run MindRoom directly.

Prerequisites:

- Python 3.13–3.14.
- A Matrix homeserver where MindRoom can create agent accounts (see [Matrix account provisioning](#matrix-account-provisioning)).
- Credentials for at least one AI provider.

### Installation

=== "uv (recommended)"

    ```bash
    uv tool install mindroom
    ```

=== "pip"

    ```bash
    pip install mindroom
    ```

=== "From source"

    ```bash
    git clone https://github.com/mindroom-ai/mindroom
    cd mindroom
    uv sync
    source .venv/bin/activate
    ```

    Install Node.js 24 and [Bun](https://bun.sh/) to build missing dashboard assets on first run, or set `MINDROOM_FRONTEND_DIST` to a prebuilt dashboard directory.
    Without assets and Bun, the API is available but the dashboard is not.

### Configuration

Generate a starter with `mindroom config init --matrix-server self-hosted`, or write `config.yaml` yourself.
A generated config contains `__MINDROOM_OWNER_USER_ID_FROM_PAIRING__` placeholders; replace each with your quoted Matrix user ID, such as `"@alice:matrix.example.com"`, because only hosted pairing fills them in.
See [Configuration](https://docs.mindroom.chat/configuration/#configuration-file) for where MindRoom looks for `config.yaml`.

A minimal hand-written `config.yaml`:

```yaml
agents:
  assistant:
    display_name: Assistant
    role: A helpful AI assistant that can answer questions
    model: default
    rooms: [lobby]

models:
  default:
    provider: openai
    id: gpt-6-astra

defaults:
  tools: [scheduler]
  markdown: true

administrators:
  - "@alice:matrix.example.com"
room_defaults:
  invite_users:
    - "@alice:matrix.example.com"

timezone: America/Los_Angeles
```

Put credentials in a `.env` next to `config.yaml`:

```bash
MATRIX_HOMESERVER=https://matrix.example.com

# Recommended when the homeserver supports token-gated registration.
# MATRIX_REGISTRATION_TOKEN=your-registration-token

# Optional: for self-signed certificates (development)
# MATRIX_SSL_VERIFY=false

# Optional: when server_name differs from the homeserver hostname
# MATRIX_SERVER_NAME=example.com

# AI provider API keys
OPENAI_API_KEY=your_openai_key
# ANTHROPIC_API_KEY=your_anthropic_key
# OPENROUTER_API_KEY=your_openrouter_key

# Dashboard API key; generate one with `openssl rand -hex 32`.
# `mindroom run` listens on every network interface by default, so keep this key set;
# leave it empty (MINDROOM_API_KEY=) only together with `--api-host 127.0.0.1`.
MINDROOM_API_KEY=replace-with-a-long-random-secret
```

See [Environment Variables](https://docs.mindroom.chat/configuration/#environment-variables) for every setting.

### Matrix account provisioning

MindRoom creates a Matrix account for each agent, team, and the router.
Configure one way for it to do so: hosted provisioning, `MATRIX_REGISTRATION_TOKEN`, `MATRIX_REGISTRATION_SHARED_SECRET`, an [application service](https://docs.mindroom.chat/matrix/#agent-users), or intentionally open registration.
If registration fails, check the selected credentials and the homeserver's registration policy.

### Optional: local Synapse and MindRoom Chat with Docker

On Linux or macOS, `local-stack-setup` starts Synapse and a MindRoom Chat container from the core MindRoom repository's `local/matrix` Compose files and writes the matching Matrix settings to the `.env` next to your active config:

```bash
mindroom local-stack-setup --synapse-dir /path/to/mindroom/local/matrix
```

From a source checkout, run `uv run mindroom local-stack-setup --synapse-dir local/matrix`.
With a custom config path, export `MINDROOM_CONFIG_PATH` first so it updates the matching `.env`.
See [local-stack-setup](https://docs.mindroom.chat/cli/#local-stack-setup) for its options.

### Run MindRoom

```bash
mindroom run
```

MindRoom connects to the homeserver, creates Matrix users for each agent, creates and joins the configured rooms, and starts answering messages.
See [`mindroom run`](https://docs.mindroom.chat/cli/#run) for options such as `--config` and `--storage-path`.

## Next Steps

- Learn about [agent configuration](https://docs.mindroom.chat/configuration/agents/)
- Learn about [OpenClaw workspace import](https://docs.mindroom.chat/openclaw/) if you want file-based memory/context patterns
- Explore [available tools](https://docs.mindroom.chat/tools/)
- Set up [teams for multi-agent collaboration](https://docs.mindroom.chat/configuration/teams/)

# macOS App

The MindRoom macOS app is a native window with a menu bar companion for two independent roles:

- **Local agents** run MindRoom on this Mac and connect to your Matrix chat account.
- **Computer access** lets a paired agent running elsewhere observe or control selected applications, read selected folders, and request shell commands that you approve on this Mac.

Use either role, or both; starting one does not start the other.

## Requirements

- macOS 14 Sonoma or later on an Apple silicon Mac; Intel Macs are not supported.
- Network access to install the MindRoom runtime and reach your Matrix server.
- For local agents, a model provider credential, local model, or supported provider login.
- For computer access, a Desktop-enabled MindRoom agent; application access also needs the macOS permissions in the [Desktop guide](https://docs.mindroom.chat/tools/desktop/).

## Install

```bash
brew install --cask mindroom-ai/tap/mindroom
```

Open **MindRoom** from Applications or Spotlight.
The window has **Overview**, **Chat**, **Dashboard**, **Local agents**, **Computer access**, and **Settings** sections, and Overview shows the status of each role.

## Chat in the App

**Chat** opens your chat client inside the app.
Sign in once; the app remembers that session separately from your browser, and switching sections keeps your conversation and draft.
The default chat website is `https://chat.mindroom.chat`; for a self-hosted client, enter an HTTPS URL or a loopback HTTP URL in **Settings** > **Chat website** and select **Save Chat Website**.
Use **Open in Browser** to use your browser session or a sign-in provider that requires a browser.
**Reload** returns to the configured chat website, or reloads the approval page while you connect local agents.

## Set Up Local Agents

**Local agents** has four numbered steps, each with its own status.
The app opens at the first step that needs attention: **Install** when the runtime needs installing or updating, **Check** when configuration exists without a service, and **Start** when a service is installed.

1. **Install** finds an existing `mindroom` executable, including a terminal installation, or installs the runtime release that matches the app version with **Install MindRoom**.
   When the installed runtime is a different release, for example after an app update, it shows **Update needed** and **Update MindRoom**.
   Configuring, pairing, checking, and installing the service wait until the runtime matches; starting, stopping, and restarting an installed service stay available.
   Installing the runtime does not install the background service.
2. **Configure** uses the installed service's saved configuration, or `~/.mindroom/config.yaml` for a new service, and shows the path.
   **Prepare Configuration** creates missing files and keeps existing configuration and credentials.
   Under **Connect or reconnect your chat account**, **Connect Account** opens the approval page in the **Chat** section.
   Check that the code above Chat matches the page and that the signed-in account is yours, then click **Approve**; **Cancel** stops waiting without saving credentials.
   An unapproved code expires after 10 minutes; choose **Connect Account** again for a new one.
   Pairing a Mac that is already connected asks first, because it creates a new connection and a new agent namespace while existing agents keep working.
   Use **Open Config Folder** to add AI provider credentials to the adjacent `.env` or configure a local model in `config.yaml`.
3. **Check** (**Check Setup**) runs `mindroom doctor` on the displayed configuration and the service's storage path, covering configuration, providers, Matrix connectivity, and storage.
   **Files found** only means configuration exists; **Passed last check** means a check in this app session had no failures or warnings.
   Warnings show **Needs attention** with a summary, and **Command output** has details.
   Editing `config.yaml` or `.env` clears the result; rerun the check after editing included files or changing external services.
4. **Start** offers **Install and Start Agents** when the launchd service is missing, **Start Agents** when it is stopped, and stop, restart, chat, and dashboard controls when it runs.
   A check is recommended but not required.
   An installed service always uses its saved configuration path, including a custom path chosen in the terminal.
   If the service starts before your account is connected, the section shows **Waiting for your chat account** until you choose **Connect Account** and approve.

**Refresh Status** rereads the runtime, configuration, and service state, including changes made in a terminal.
These controls manage only this Mac's launchd service.
**Service running** means the process is running; use Chat or the dashboard to confirm agents are ready.

### Local AI Models

In **Configure**, choose a model under **Run AI on this Mac** and click **Set Up Local Model**.
No separate inference application or model-provider API key is required.
Setup downloads the model, displays download progress, and makes it the default for your agents.
Choose **Cancel Setup** to stop a download; retry setup to resume it.
The initial download requires internet; after setup, the model runs locally without internet.
Matrix chat and hosted pairing still require their configured network connection.

The suggested models are conservative starting points based on total unified memory:

| Unified memory | Suggested model | Approximate model download |
| --- | --- | --- |
| 8–15 GB | Qwen 3.5 2B | 1.5 GB |
| 16–23 GB | Qwen 3.5 4B | 2.8 GB |
| 24–31 GB | Qwen 3.5 9B | 5.7 GB |
| 32 GB or more | Qwen 3.8 27B | 17.6 GB |

We recommend Qwen 3.8 27B on Macs with at least 32 GB unified memory, leaving room for macOS, MindRoom, and the model's working memory.

Local inference currently supports text and tool calls.
Other apps and longer conversations consume additional memory; choose a smaller model when memory is tight.
Local models support an 8,192-token context, with one model active at a time.
After five idle minutes, model memory is released; the next request takes longer while the model loads again.
Agents and the router using `default` follow the new selection; explicitly selected models and existing credentials are kept.
Setup saves the original YAML as `config.yaml.before-local-model` and rewrites the active YAML without its comments.
For configurations using `!include`, edit the model's source file manually instead of using automatic setup.

Use **Stop Inference** to stop local inference and **Start Inference** to start it again.
Agents configured to use it cannot answer while it is stopped.
The service starts at login and continues after quitting the app.
Downloaded models are stored in `~/Library/Application Support/MindRoom/inference/models`.

### Where Do My Agents Run?

Your agents run on this Mac.
The hosted `mindroom.chat` Matrix server only relays their chat messages, and signing in to `chat.mindroom.chat` creates your hosted Matrix account without moving agents to the cloud.
For pairing details, see [Hosted Matrix](https://docs.mindroom.chat/deployment/hosted-matrix/).

### Your Own Matrix Server

Under **Use your own Matrix server**, choose **Prepare Self-Hosted Configuration**, then edit `config.yaml` and `.env` for your server and model provider.
Run the Matrix server separately from this app.

### Open the Dashboard

While the service runs, **Open Dashboard** opens the local dashboard in the app for agent and model configuration.
The app signs in with `MINDROOM_API_KEY` from `~/.mindroom/.env` and uses `MINDROOM_URL` from the same file, defaulting to `http://127.0.0.1:8765`.
`MINDROOM_URL` must be an HTTP loopback URL (`localhost`, `127.0.0.1`, or `[::1]`) with an explicit port; otherwise the app reports `Local dashboard address is invalid. Check MINDROOM_URL and retry.`
The key is sent only to this Mac's MindRoom service, so if the dashboard cannot connect while the service is starting, select **Reload**.
After changing `MINDROOM_URL` or `MINDROOM_API_KEY`, select **Reload** in Dashboard.
The dashboard session is separate from Chat and ends when the app quits.

## Computer Access

Computer access uses the Desktop Helper bundled with the app and does not need the local-agent runtime or service.
The **Computer access** section has a summary and four steps: **Connect**, **Access**, **Permissions**, and **Start**.
The summary shows **Not connected**, **Connection saved · Access off**, or **Connected**, and always names the next required action.

1. In a private chat with your Desktop-enabled agent, send `!desktop setup` and copy its JSON setup data.
2. In **Connect**, paste it into **Setup data** and select **Import Setup**.
3. Check the homeserver, Matrix account, controller fingerprint, requester, and agent before signing in, because the setup data chooses which server receives your sign-in.
   The app reuses a matching saved Matrix login; otherwise choose **Sign In with Browser** or the password option.
4. Confirm the values again and select **Save and Connect**.
5. Copy the displayed confirmation command into the same agent chat, and after the agent confirms pairing, select **I’ve Confirmed in Chat**.
   If the app closes before this step, request fresh setup data and repeat **Connect**; the saved login is kept.
6. In **Access**, choose **Applications**, **Read-only folders**, or **Shell commands**, and save each choice.
   For applications, search and check the ones to allow, then select **Save App Access**; search accepts partial and reordered words, such as `chr goo` for Google Chrome.
   The list shows running apps and apps in standard folders; use **Add App…** to choose an application from another location.
   For folders and shell commands, see [Folders and Shell Commands](#folders-and-shell-commands).
7. If you saved applications, allow Accessibility and Screen Recording in **Permissions** with **Request** or **Open Settings**, restart the app if macOS requires it, and select **Check Again**.
   Without saved applications, **Permissions** shows **Not needed**.
8. Select **Start Observe Only**, or **Start Access** when folders or shell commands are saved.

Start needs at least one saved application, folder, or shell choice.
**Save App Access** and **Save Folder and Shell Access** each update their own choices without changing the other.
For homeservers behind Cloudflare Access, install `cloudflared`; see [Homeservers Behind an Identity-Aware Proxy](https://docs.mindroom.chat/tools/desktop/#homeservers-behind-an-identity-aware-proxy).

### Control Applications

Applications start observe-only.
**Grant Control…** shows the identities, allowed applications, and duration for confirmation, and control ends when that lease expires; it is never renewed at launch or restart.
**Revoke Now** removes input authority while observation continues, and **Stop Access** ends the session.
Controlling a terminal, a scripting or automation app, the primary screen, or a browser or Matrix client signed in as you gives the agent your own authority; see [Security Model](https://docs.mindroom.chat/tools/desktop/#security-model).
Optional browser settings and redacted diagnostics are in expandable sections; see the [Desktop guide](https://docs.mindroom.chat/tools/desktop/#use-the-signed-in-browser-profile) for browser-extension setup.

### Folders and Shell Commands

In **Access**, **Read-only folders** lists the folders agents may list and read; **Add Folder…** adds one and **Remove** drops one.
**Shell commands** has one switch, **Allow shell command requests**.
Save both with **Save Folder and Shell Access**; while access runs, **Stop and Save…** stops it first after confirmation.
Approved shell commands run with your full macOS account access; read [Shell Commands](https://docs.mindroom.chat/tools/desktop/#shell-commands) before enabling them.

While access runs with shell requests allowed, each request appears, unless auto-approval is active, on an approval card in the window and in a separate approval window that does not take keyboard focus.
Closing that window or choosing **Later** leaves the request waiting until it expires.
**Approve Once** and **Approve & Allow…** become available one second after a request appears.
Choose **Reject**, **Approve Once**, or **Approve & Allow…** with **5 Minutes**, **15 Minutes**, **60 Minutes**, or **Until I Stop**; without a waiting request, **Allow Without Asking…** offers the same durations.
Both auto-approval choices ask for confirmation and list the agents and requesters they cover; see [Local Approval](https://docs.mindroom.chat/tools/desktop/#local-approval) for what auto-approval allows.
While a command waits, every window section shows a **Review Command** bar and the menu bar shows **Command waiting** with **Review Command…**, which opens the card but never approves.
**Revoke Shell Access** ends auto-approval, rejects a waiting request, and stops running commands.
**Background commands** lists commands still running after their first reply, with a **Kill** button for each.

### Use the Terminal with the App

The app and `mindroom desktop setup` share the same saved setup in `~/.mindroom`; an open app picks up terminal setup while access is stopped.
You can save folders and shell access with `mindroom desktop access` and start from either the app or `mindroom desktop run`.
Stop the bridge in the interface that started it before starting it in the other.
A terminal `--config` or `--storage-path` override creates a separate setup that the app does not use.

### Fix Computer Access Problems

- **Permission shows Not allowed yet although System Settings shows MindRoom enabled**: quit and reopen MindRoom.
  If you replaced the signed release with a local build, reinstall the signed release or remove the old permission entry and approve the current copy, then select **Check Again**.
- **Access fails to start**: **View Details** shows the full error, recovery advice, and redacted diagnostics to copy for support.
- **Connection or TLS certificate errors**: your saved setup is kept, so check the network or update the app and select **Start Access** again; you do not need to sign in or pair again.

See the [Desktop guide](https://docs.mindroom.chat/tools/desktop/) for pairing recovery and device rotation.

## Window, Menu Bar, and Login

Closing the window keeps the app and its background work running in the menu bar; reopen it with **Open MindRoom…** or by opening the app again.
The menu shows local-agent and computer-access status as shortcuts to their sections, plus chat, dashboard, start and stop, **Revoke Computer Control** while a control lease is active, **Revoke Shell Access** while shell auto-approval or a command is active, settings, and quit actions.

**Settings** > **Open menu bar app at login** launches the menu app at login.
Local agents start at login through their own launchd service, while computer access always needs an explicit start in the app.

**Quit MindRoom** stops computer access but leaves the local-agent service running; use **Stop Local Agents** to stop it.
Let a running runtime action finish before quitting.

## Updates

**Settings** separates app updates from runtime updates:

- **Check App Updates…** updates the app, including the bundled Desktop Helper.
- **Update Local Runtime** installs the runtime release that matches the app version; `uv tool upgrade mindroom` does not move the runtime off that version.
- **Apply Runtime to Service…** reinstalls the background service on the updated runtime and starts or restarts local agents after confirmation.

Homebrew users can also update the app with:

```bash
brew update
brew upgrade --cask mindroom
```

## Logs and Troubleshooting

**Open Logs Folder** opens `~/Library/Logs/mindroom`, and **Open Config Folder** opens the folder holding the local agents' configuration, `~/.mindroom` by default, which the CLI shares.
Failed local-agent actions show their output in the window with a copy action.
For local model download or loading errors, check `inference.log` in the logs folder.
If the dashboard does not open, start the service and check its logs for missing provider credentials or startup errors.

## Uninstall

```bash
brew uninstall --cask mindroom
```

Use `brew uninstall --zap --cask mindroom` to also remove app preferences and logs.
Neither removes `~/.mindroom`, which holds configuration, credentials, and agent data, or the runtime; remove the runtime with `uv tool uninstall mindroom`.

# iOS App

MindRoom AI is the MindRoom Chat client for iPhone, iPad, and Apple silicon Macs, available on the App Store.
It requires iOS or iPadOS 16.4 or later, or macOS 13.3 or later on a Mac with an M1 chip or newer.

<p>
  <a class="md-button md-button--primary" href="https://apps.apple.com/us/app/mindroom-ai/id6760272172">Download on the App Store</a>
</p>

## Sign In

The app is a Matrix client, so sign in with the Matrix homeserver and account you normally use.
For hosted MindRoom, the homeserver is `https://mindroom.chat`.

To pair a local MindRoom runtime with that account, follow [Getting Started](https://docs.mindroom.chat/getting-started/#2-approve-pairing); on a computer, approve the pairing at `https://chat.mindroom.chat`.

## Homeservers Behind Cloudflare Access

The app cannot hold the [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/access-controls/) cookie, so a homeserver behind Access redirects the app's requests to the Access login and sign-in fails.
When the homeserver advertises a handoff page, the app instead sends the Access token in a `cf-access-token` header, which Access accepts.

With the steps below in place, the sign-in screen asks for the organization's sign-in first, then shows the usual sign-in options.
When the Access session ends, the bar at the top asks for that sign-in again.
The organization's sign-in, and SSO sign-in to the homeserver, run in a private browser session, because Safari's cross-site tracking protection breaks the Access login in one that shares Safari's cookies.
So with SSO the organization's full login comes up twice when adding the account (the organization's sign-in, then the SSO one), and it comes up once each time the Access session ends.
An Access session revoked before it expires shows as a lost connection until the token's expiry time.
Calls need `/.well-known/matrix/client`, which these steps leave behind Access, so calls do not work on iOS for such a homeserver.
Push notifications for such a homeserver carry no message content and read "New message".
The public push gateway still receives the event and room IDs, the unread count and the device's push token, and each push tells it that the homeserver is active; the advert can turn push off instead.

Set up the homeserver (examples use `chat.example.com`):

1. Publish the advert at `https://chat.example.com/.well-known/mindroom/cloudflare-access`, outside Access, as JSON with `Access-Control-Allow-Origin: *`:
   `{"handoff_url": "https://chat.example.com/native/cloudflare-access"}`.
   The handoff URL must use HTTPS on the homeserver's own origin.
   Add `"push": false` to turn push off: the app then registers no pusher for the homeserver and hides the push setting.
   Signed-in accounts pick up a change of the advert only when they sign out and in again.
2. Serve the handoff page behind Access. It redirects to the app with the homeserver's own origin and the value of the `CF_Authorization` cookie:

    ```nginx
    location = /native/cloudflare-access {
      add_header Cache-Control "no-store" always;
      return 302 "mindroom://cloudflare-access#origin=https://chat.example.com&token=$cookie_CF_Authorization";
    }
    ```

   Write the origin into the config rather than taking it from the request; the app refuses a token whose callback names no origin or another one.
   Without `origin` the app's Access sign-in fails with "Cloudflare Access sign-in returned no valid token."; earlier app releases ignore it, so add it before users update the app.

   If the same origin serves the MindRoom Chat web app, exclude `/native` from its service worker's navigation fallback (`serviceWorker.navigationFallbackExcludePaths` in the client chart, or `window.__SERVICE_WORKER_NAVIGATION_FALLBACK_EXCLUDE_PATHS__`), or a Safari that has opened the web app answers the handoff with its own page.
   The page also answers when opened in Safari, where iOS hands the token to whichever app handles `mindroom://`.
   That token gets past Access for every host of the Access application until the session ends, so give the homeserver its own Access application.
3. Turn on "Bypass OPTIONS requests to origin" (`options_preflight_bypass`) on the Access application, because browsers send CORS preflights without credentials.
   Every `OPTIONS` request then reaches the origin without an Access session, so make sure no route answers `OPTIONS` with private content; nginx `return` answers any method.
   Leave the application's binding cookie off, because the app sends only the token.
4. Let the advert path, `/_matrix/client/versions`, `/_matrix/client/v1/media` and `/_matrix/media/v3/upload` bypass Access.
   The app checks `/versions` before the organization's sign-in, images and uploads cannot carry the header, and the homeserver refuses the media paths without a Matrix access token.
   A bypass covers everything below its path, so answer 404 for anything below the advert path.
   Cloudflare also matches a bypass before nginx decodes `%2F` and resolves `..`, so `/.well-known/mindroom/cloudflare-access/..%2F..%2Fsecret` would reach `/secret` without Access.
   Reject such requests in the nginx `server` block, before any `location`; the check looks only at the path, so encoded URLs in query strings still pass:

    ```nginx
    if ($request_uri ~* "^[^?#]*(?:%2f|%5c|%2e|/\.\.?(?:[/?#]|$))") { return 404; }
    ```

   If `/_matrix` also passes through this nginx, the check refuses Matrix requests that encode `/` in a path parameter, such as some state keys, so send `/_matrix` to the homeserver without this nginx where you can.
5. Run a homeserver that allows the `cf-access-token` header in CORS, such as the MindRoom fork of Tuwunel (`v1.9.4-mindroom.18` or later).

Before moving an existing homeserver behind Access, delete its pushers for the app (app ID `chat.mindroom.app`) that point at the public push gateway, for example with Tuwunel's `!admin query pusher get-pushers <user>` and `!admin query pusher remove-pusher <user> <pushkey>`.
Those pushers send full message content, and an account signed in before the move cannot remove them, because its sign-out cannot pass Access.
Such accounts need to sign out and in again.
To take a homeserver off Access later, keep answering the advert path with a 404 and `Access-Control-Allow-Origin: *`, and have iOS accounts sign out and in again; until then the app keeps waiting for an Access sign-in that can no longer finish.

## Reporting Issues

Report problems through the [support page](https://docs.mindroom.chat/support/#what-to-include-in-a-support-request), which lists the device, app, and homeserver details to include.

# Web Dashboard

The web dashboard configures agents, teams, rooms, models, credentials, and integrations without editing YAML by hand.
Each editor keeps a local draft and writes `config.yaml` when you save.
This page covers opening the dashboard, what each tab does, saving and conflict errors, and the dashboard REST API.

## Accessing the Dashboard

**Standalone:** run `mindroom run` and open `http://localhost:8765`.
When running from a source checkout, MindRoom builds the dashboard assets on first start if Bun is available.
When `MINDROOM_API_KEY` is set, the dashboard first asks for that key, and the login page shows which `.env` file holds it.
See [Authorization](https://docs.mindroom.chat/authorization/#dashboard-configuration) for who can open the dashboard and [Environment Variables](https://docs.mindroom.chat/configuration/#dashboard-and-api) for the related settings.

**SaaS platform:** open `https://<instance-id>.mindroom.chat`.

## Connect Your AI Provider

When the router, an agent, or a team uses a model whose provider has no API key, every dashboard page shows a **Connect your AI provider** banner.
Keys in `.env`, keys saved per model on the **Models** tab, and provider keys saved under their environment variable name (for example `OPENROUTER_API_KEY`) all count as set.
Select **Connect provider**, choose OpenRouter, Anthropic, or OpenAI, and paste a key.
MindRoom checks the key with the provider without using credits and saves it as the `openrouter`, `anthropic`, or `openai` credential only if the provider accepts it.
OpenRouter is recommended for the hosted default setup, where one key covers chat, memory, and voice.
If you connect Anthropic or OpenAI while your models still use OpenRouter, the dialog lists the affected models so you can switch them on the **Models** tab.
When an agent cannot reply because no provider key is set, it answers in chat with a pointer to this setup step, including the dashboard address when `MINDROOM_PUBLIC_URL` is set.

## Dashboard Tabs

### Dashboard (Home)

The home tab shows today's activity, upcoming schedules, and shortcuts to room and agent editors.
Select **Browse workspace** to search and filter agents, rooms, and teams, inspect an item and open its editor, or **Export summary**.
The exported JSON summary lists counts, agents, rooms, teams, and model configurations; it is not a complete `config.yaml` backup.

### Agents

- **Display name**, **Role description**, **Model**, **Instructions**, and **Rooms**
- **Memory backend** - inherit the global backend or override it per agent (`mem0`, `file`, or `none`)
- **Tools** - configured tools (green badge) and tools that need no setup; unchecking a tool discards its settings
- **Deferred tools** - open a checked tool's settings and enable **Defer until searched** (`defer`) or **Send on every request** (`initial`, which requires `defer`); presets and control-plane tools such as `dynamic_tools` do not offer it (see [Dynamic Tools](https://docs.mindroom.chat/tools/dynamic-tools/))
- **More settings** - every other agent field, such as `participation`, `mid_turn`, `accept_invites`, `access`, `memory_search`, `thread_exports`, and `room_thread_modes`
- **Personal agents** - a read-only list of the agents generated from [`personal_agents`](https://docs.mindroom.chat/personal-agents/) templates, with each owner, Matrix ID, template, and status; edit the templates under Settings → Personal rooms

### Teams

- **Display name** and **Team purpose**
- **Collaboration mode** - Coordinate or Collaborate (see [Teams](https://docs.mindroom.chat/configuration/teams/))
- **Team model** - optional model override
- **Team members** and **Team rooms**

### Rooms

- **Room Admins** - the panel at the top of the tab edits `room_defaults.admins`, the default Matrix room-admin list; add or remove user IDs and click **Save**
- **Display name** and **Description**
- **Room model** - optional model override
- **Agents in room**
- **More settings** - per-room `join_policy`, `listed`, `encrypted`, `invite_users`, and `admins` overrides (see [Rooms](https://docs.mindroom.chat/rooms/))

### Schedules

Lists scheduled tasks across rooms with room, status, schedule type, delivery mode, and next run time.
A room whose schedules cannot be read, for example because the router is not in it, is named above the list with the reason, and the home tab counts it, instead of showing as empty.
You can edit a task's timing, description, and visible or silent delivery, or cancel it.
See [Scheduling](https://docs.mindroom.chat/scheduling/).

### External Rooms

The **External** tab lists, per agent, rooms the agent has joined that are not in the configuration.
Select rooms and use **Leave rooms** to remove the agents, or open a room in your Matrix client.

### Models

Add and edit models with provider, model ID, host URL, and advanced settings, and set provider API keys.
A row's **Save** only updates the draft; click **Save All Changes** to write model changes to `config.yaml`.
The add-model dropdown does not have a preset for every supported provider, but the dashboard keeps any provider already in the configuration.
A model's **More settings** section appears below the table while its row is being edited, and **Cancel** on the row also reverts More settings changes made since the last save.
See [Models](https://docs.mindroom.chat/configuration/models/) for providers and fields.

### Usage

See [Usage Tracking](https://docs.mindroom.chat/usage/#dashboard-usage-tab).

### Budgets

See [Budgets](https://docs.mindroom.chat/budgets/#dashboard-budgets-page).

### Memory

Sets the global memory backend, embedder, file backend, and auto-flush settings; per-agent backend overrides are on the **Agents** tab.
See [Memory](https://docs.mindroom.chat/memory/#ui-configuration) for the fields.

### Knowledge

- Create, edit, and delete knowledge bases, including `description`, `mode` (semantic search or files-only), `path`, `watch`, and Git repository, branch, filtering, credentials service, and sync options
- Upload and remove files for non-Git bases
- Reindex or sync a base on demand
- See index status (`file_count` and `indexed_count`)

Git-backed bases hide upload and delete controls; change files in the repository, then sync.
The file list shows only files that pass the base's `include_patterns` and `exclude_patterns`.
See [Knowledge Bases](https://docs.mindroom.chat/knowledge/) for configuration and sync behavior; assign bases to agents on the **Agents** tab.

### Credentials

- List, create, edit (as raw JSON), test, and delete credential services, such as `github_private` or `model:sonnet`
- Reference a stored service from config, for example `knowledge_bases.<id>.git.credentials_service`

A new service named after a provider key environment variable, such as `ANTHROPIC_API_KEY`, is saved as the canonical provider service (`anthropic`) that the Models tab and runtime read.
An existing service with an environment-variable name keeps receiving writes, because config may reference it by that exact name.
See [Credentials from the Environment](https://docs.mindroom.chat/oauth-framework/#credentials-from-the-environment) for credentials read from `.env`, such as `GITHUB_TOKEN`.

### Voice

Enables voice messages and configures speech-to-text (OpenAI or a self-hosted OpenAI-compatible service), the transcript-cleanup model, and [voice calls](https://docs.mindroom.chat/voice-calls/#calls-configuration) with named call profiles and per-agent assignments.
See [Voice Messages](https://docs.mindroom.chat/voice/).

### Tools

The **Tools** tab connects external services that tools need, including OAuth flows such as Google, Spotify, and Home Assistant.
Search tools, filter by category or status (**Available**, **Unconfigured**, **Configured**), and choose **Shared deployment credentials** or an agent with a worker scope to configure that scope (see [Dashboard credentials and worker scopes](https://docs.mindroom.chat/deployment/sandbox-proxy/#dashboard-credentials-and-worker-scopes)).

### Skills

Lists installed skills with origin and edit status, shows `SKILL.md` content, and creates, edits, and deletes user skills.
See [Skills](https://docs.mindroom.chat/skills/#installing-and-managing-skills).

### Settings

The **Settings** tab edits every top-level configuration section without a dedicated tab, grouped into Responses, History and context, Tools and workers, Router, Personal rooms, Room defaults, Tool approval, Prompts, MCP servers, Plugins, Access and identity, Matrix and runtime, and Diagnostics.
Any option no tab claims appears under **Other**.
**Tools and workers** also edits per-tool overrides for the default tools every agent inherits; removing a tool from the default tools discards its overrides.

## Editing and Saving

### More Settings and Schema Forms

The Agents, Teams, Rooms, Memory, Knowledge, Voice, and Models tabs show a collapsible **More settings** section with the fields their main editor does not cover.
These sections and the Settings tab are generated from the running backend's configuration schema.
Each field shows its description and default, references such as model or agent names are pickers, and credential fields are masked.
Resetting a field removes it from `config.yaml` so the default applies again, and optional blocks are added or removed with their **Configure** checkbox.
Validation errors from a save appear beside the affected field, and Settings sections with errors are marked **Needs attention**.

### Save Status

The header shows **Synced**, **Syncing...**, **Sync Error**, or **Disconnected** (lost connection to the backend).
The dashboard has dark and light themes and works on mobile.

### Save Conflicts

If another dashboard tab or a server-side edit changed the configuration, saving shows `Configuration changed elsewhere. Your draft has not been saved. Copy any changes you want to keep, then refresh this page and reapply them.`
Saving stays blocked until you refresh, even if you edit the draft.

### Invalid Configuration

If `config.yaml` fails validation when the dashboard loads, it opens a raw YAML recovery editor so you can fix and save the whole file.

### Configurations Split With `!include`

When the configuration uses [include tags](https://docs.mindroom.chat/configuration/#splitting-the-configuration-into-multiple-files), the dashboard shows a banner and structured saves fail, so edit the included source files directly.
The raw recovery editor still edits the top-level file's text.
Through the API, structured saves return HTTP 409 with `{"code": "config_composed_from_includes", "message": "configuration uses !include; edit the source files instead"}`, unlike the plain-text 409 for a stale write, which can be retried.
If the source files cannot be read well enough to tell whether they use `!include`, structured saves return HTTP 422; repair the files or edit the raw YAML.
`POST /api/config/load` reports include use in the `x-mindroom-config-uses-includes` response header, and `GET /api/config/raw` in its `uses_includes` field.

## API Endpoints

The dashboard uses the backend REST API under `/api/`, with the same authentication as the dashboard.

### Configuration API

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/config/load` | Fetch current configuration |
| PUT | `/api/config/save` | Save full configuration |
| GET | `/api/config/raw` | Fetch raw `config.yaml` text |
| PUT | `/api/config/raw` | Replace raw `config.yaml` text |
| GET | `/api/config/schema` | Configuration JSON schema used by Settings and More settings |
| GET | `/api/config/personal-agents` | List generated personal agents |
| POST | `/api/config/agent-policies` | Derived agent policies for a draft config |

### Credentials API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/credentials/list` | List services with credentials |
| GET | `/api/credentials/{service}/status` | Get credential status |
| GET | `/api/credentials/{service}` | Get credentials for editing |
| POST | `/api/credentials/{service}` | Set credentials |
| POST | `/api/credentials/{service}/api-key` | Set API key |
| GET | `/api/credentials/{service}/api-key` | Get masked API key |
| POST | `/api/credentials/{service}/test` | Check stored credentials exist |
| DELETE | `/api/credentials/{service}` | Delete credentials |
| POST | `/api/credentials/{service}/copy-from/{source_service}` | Copy credentials from another service |

Add `agent_name` to target an agent's credentials in its saved worker scope.
The optional `execution_scope` requires `agent_name` and must match that agent's saved scope; save the configuration first after changing the scope.
Agent-scoped requests return HTTP 403 unless the requester may manage that agent's credentials; see [Platform and credential authority](https://docs.mindroom.chat/authorization/#platform-and-credential-authority).

### Knowledge API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/knowledge/bases` | List knowledge bases |
| GET | `/api/knowledge/bases/{base_id}/files` | List files in a base |
| POST | `/api/knowledge/bases/{base_id}/upload` | Upload files to a non-Git base |
| DELETE | `/api/knowledge/bases/{base_id}/files/{path}` | Delete a file from a non-Git base |
| GET | `/api/knowledge/bases/{base_id}/status` | Get indexing status |
| POST | `/api/knowledge/bases/{base_id}/reindex` | Reindex a base, or sync a Git-backed base |

Upload and delete return HTTP 409 for Git-backed bases (see [Sync Behavior](https://docs.mindroom.chat/knowledge/#sync-behavior)).

### Skills API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/skills` | List installed skills |
| GET | `/api/skills/{skill_name}` | Get skill content, origin, and edit status |
| POST | `/api/skills` | Create a user skill |
| PUT | `/api/skills/{skill_name}` | Update a user skill |
| DELETE | `/api/skills/{skill_name}` | Delete a user skill |

### Schedules API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/schedules` | List scheduled tasks, optionally filtered by room, including each task's `silent` flag; `unreadable_rooms` names each room whose tasks could not be read, with the reason |
| PUT | `/api/schedules/{task_id}` | Edit a scheduled task, including `silent` |
| DELETE | `/api/schedules/{task_id}` | Cancel a scheduled task |

### Workers API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/workers` | List sandbox workers |
| POST | `/api/workers/cleanup` | Clean up idle sandbox workers |

### Tools and Matrix API

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/tools` | List available tools |
| GET | `/api/matrix/agents/rooms` | All agents' room memberships |
| GET | `/api/matrix/agents/{id}/rooms` | One agent's rooms |
| POST | `/api/matrix/rooms/leave` | Leave one room |
| POST | `/api/matrix/rooms/leave-bulk` | Leave multiple rooms |

For usage reports and health endpoints, see [Usage Report API](https://docs.mindroom.chat/usage/#usage-report-api) and [Health & Readiness](https://docs.mindroom.chat/deployment/operational-log-events/#health-readiness).

# OpenClaw Workspace Import

This page covers moving an OpenClaw workspace into MindRoom: where to put the files, a starting agent config, the `openclaw_compat` tool preset, and what does not carry over.
MindRoom reuses OpenClaw's file-based workspace (`SOUL.md`, `AGENTS.md`, `USER.md`, `MEMORY.md`, daily notes, and skills); it is not a clone of the OpenClaw gateway.

## What carries over

Supported:

- File-based identity and instruction documents, loaded through `context_files`
- File-first memory (`MEMORY.md` plus `memory/` notes) through MindRoom's `file` memory backend
- OpenClaw skills, including `metadata.openclaw` eligibility gating
- A matching tool bundle through the [`openclaw_compat`](#openclaw_compat) preset, including Matrix messaging with `matrix_message`

Not included:

- The OpenClaw gateway control plane
- Device nodes and canvas platform tools
- OpenClaw tool aliases such as `exec`, `process`, `web_search`, `web_fetch`, `tts`, and `image`; use MindRoom's native tools instead
- The heartbeat runtime; schedule heartbeats with the `scheduler` tool instead (see [Scheduling](https://docs.mindroom.chat/scheduling/))

## Import a workspace

1. Copy or sync your OpenClaw files into the agent's workspace, `agents/<agent>/workspace/` in the storage directory (`mindroom_data/` by default):

    ```text
    mindroom_data/
    └── agents/
        └── openclaw/
            └── workspace/
                ├── SOUL.md
                ├── AGENTS.md
                ├── USER.md
                ├── IDENTITY.md
                ├── MEMORY.md
                ├── TOOLS.md
                ├── HEARTBEAT.md
                ├── memory/
                │   ├── YYYY-MM-DD.md
                │   └── topic-notes.md
                └── skills/
                    └── transcribe/
                        └── SKILL.md
    ```

2. Add an agent to `config.yaml`, starting from this example:

    ```yaml
    agents:
      openclaw:
        display_name: OpenClawAgent
        role: OpenClaw-style personal assistant with persistent file-based identity and memory.
        model: default
        rooms: [personal]
        include_default_tools: false
        memory_backend: file

        instructions:
          - You wake up fresh each session with no memory of previous conversations. Your context files are already loaded into your system prompt.
          - Important long-term context is persisted by the configured MindRoom memory backend. If something must be preserved exactly, write/update the relevant file directly.
          - MEMORY.md is curated long-term memory; daily files are short-lived notes and logs.
          - Ask before external/public actions and destructive operations.
          - Before answering prior-history questions, search memory files first with `search_memories`.

        context_files:
          - SOUL.md
          - AGENTS.md
          - USER.md
          - IDENTITY.md
          - TOOLS.md
          - HEARTBEAT.md

        tools:
          - openclaw_compat
          - memory
          - python

    memory:
      search:
        mode: semantic
      auto_flush:
        enabled: true
    ```

`context_files`, file memory, and `search_memories` all read this one workspace.
`context_files` paths are relative to it; see [File-Based Context Loading](https://docs.mindroom.chat/configuration/agents/#file-based-context-loading).

## [`openclaw_compat`]

`openclaw_compat` is a preset that belongs in an agent's `tools:` list and expands to `shell`, `coding`, `duckduckgo`, `website`, `browser`, `scheduler`, and `matrix_message`.
`matrix_message` also enables `attachments` and `matrix_room`, so the agent gets all nine tools.
The preset has no callable functions of its own, and listing it next to one of its member tools does not create duplicates.

The preset has no configuration fields and cannot be deferred.
Setting `defer` or `initial` on it fails with `'openclaw_compat' is a preset and cannot be deferred; defer/initial are only valid on individual tools.`
To configure or lazy-load a member tool, list that tool individually, and if you need only one or two of the tools, list them instead of the preset.

## Memory

OpenClaw-style agents use the same memory backends as every other agent; see [Memory System](https://docs.mindroom.chat/memory/) for backends, file layout, semantic search, and auto-flush.
The recommended setup is `memory_backend: file` with `memory.auto_flush.enabled: true`, as in the example above.

With the `file` backend, MindRoom loads `MEMORY.md` into every reply's prompt automatically, so leave it out of `context_files`.
If you switch the agent to `mem0` and still want `MEMORY.md` preloaded, add it to `context_files`.
Use [knowledge bases](https://docs.mindroom.chat/knowledge/) only for non-memory documents that should be searchable as external knowledge.

## Context and history

Preloaded `context_files` are capped by `defaults.max_preload_chars` (default `50000`); see [File-Based Context Loading](https://docs.mindroom.chat/configuration/agents/#file-based-context-loading) for what gets dropped when a workspace exceeds it.
How much conversation history each reply sees is set by `num_history_runs`, `num_history_messages`, and `compaction`; see [History & Compaction](https://docs.mindroom.chat/configuration/history/).

## Room conversations instead of threads

MindRoom replies in Matrix threads by default, while OpenClaw keeps one continuous conversation per room.
To match OpenClaw, especially on mobile or through bridges (Telegram, Signal, WhatsApp), set `thread_mode: room` on the agent; see [Threads, Replies & Participation](https://docs.mindroom.chat/configuration/threads/).

## Privacy

`context_files` and file memory load in every room the agent is in.
If `MEMORY.md` or another workspace file is sensitive, keep the agent in private rooms only, or split it into a private and a public agent and leave the sensitive files out of the public agent's workspace.

## Skills

Skills in `agents/<agent>/workspace/skills/<name>/` load for that agent without being listed in `skills:`, so copying the OpenClaw workspace's `skills/` folder keeps them portable.
To share a skill across agents, install it under `~/.mindroom/skills/<name>/` and add its name to each agent's `skills:` allowlist:

```bash
mkdir -p ~/.mindroom/skills
cp -r /path/to/openclaw-workspace/skills/transcribe ~/.mindroom/skills/
```

Set any environment variables the skill's `SKILL.md` requires, such as `WHISPER_URL`.
See [Skill locations and precedence](https://docs.mindroom.chat/skills/#skill-locations-and-precedence) and [`metadata.openclaw` eligibility gating](https://docs.mindroom.chat/skills/#eligibility-gating-openclaw-metadata) (`os`, `requires`, `always`).

# Configuration

MindRoom is configured through one `config.yaml` file, optionally split across included files, plus environment variables.
This page covers where the file lives, a minimal working configuration, which page owns each top-level section, splitting the file, built-in prompt overrides, and the environment variable reference.

## Configuration File

If `MINDROOM_CONFIG_PATH` is set, MindRoom uses that path, even when no file exists there.
Otherwise, it uses `./config.yaml` in the current working directory if present, then `~/.mindroom/config.yaml`.

Data storage (`mindroom_data/`) is placed next to the config file by default.

Validate a file with:

```bash
mindroom config validate --path /path/to/config.yaml
```

Every section rejects keys MindRoom does not define, except free-form maps such as a plugin's `settings`, `memory.llm.config`, and `voice.stt.extra_kwargs`, so a misspelled key fails validation and startup with an error that names its location, for example `memory → embedder → config → base_url: Unknown key 'base_url': remove it or correct its spelling`.

Edits to `config.yaml` apply by hot reload without restarting MindRoom, except [event journal](https://docs.mindroom.chat/deployment/storage/#event-journal) changes and turning [background tool jobs](https://docs.mindroom.chat/tools/agent-orchestration/#background-jobs) on or off or changing their excluded toolkits, which need a restart.
Edits to an agent's or team's own settings, such as `instructions`, `model`, `tools`, or a team's `mode`, apply from its next reply without restarting it.
Changing `display_name` or `accept_invites`, an agent's `private` settings, whether an agent has the `desktop` tool, or which MCP tools it has restarts that agent or team.
Removing an agent or team makes it leave its rooms, except direct messages, when the reload applies.
An edit saved while MindRoom is still starting applies once startup finishes.
An edit that restarts no agent, team, or router and changes no access, room, or authorization setting, such as a model definition, knowledge base, or agent instruction change, applies within seconds even while agents are replying.
Other edits, including ones that restart an agent or team, an agent's or team's `access` or `rooms`, `administrators`, `authorization`, `room_defaults`, `rooms`, or `router.access`, first wait for active responses to finish, for at most 600 seconds.
To confirm that a reload finished, use [`mindroom config check-applied`](https://docs.mindroom.chat/deployment/config-bundles/#config-fingerprint-and-config-check-applied).

## Minimal Configuration

All top-level sections are optional; configure at least one agent for conversational replies.
This configuration runs one agent in a `lobby` room, with `ANTHROPIC_API_KEY` set in the environment or in the `.env` next to `config.yaml`:

```yaml
models:
  default:
    provider: anthropic
    id: claude-sonnet-5-5

agents:
  assistant:
    display_name: Assistant
    role: A helpful AI assistant
    model: default
    rooms: [lobby]

timezone: America/Los_Angeles
```

Keep `models.default` configured, because room topic generation and automatic thread summaries fall back to it even when agents, teams, and the router select other models.

## Top-Level Sections

| Section | Purpose | Documented in |
| --- | --- | --- |
| `agents` | Individual AI agents | [Agents](https://docs.mindroom.chat/configuration/agents/) |
| `models` | Model providers and IDs | [Models](https://docs.mindroom.chat/configuration/models/) |
| `teams` | Multi-agent collaboration | [Teams](https://docs.mindroom.chat/configuration/teams/) |
| `router` | Message routing | [Router](https://docs.mindroom.chat/configuration/router/) |
| `defaults` | Fallback values for agent settings and global-only behavior | [Agents — Defaults](https://docs.mindroom.chat/configuration/agents/#defaults) |
| `memory` | Memory backends and embedder | [Memory](https://docs.mindroom.chat/memory/) |
| `knowledge_bases` | File-backed knowledge bases | [Knowledge Bases](https://docs.mindroom.chat/knowledge/) |
| `knowledge_reranker` | Optional reranker for knowledge search results | [Reranking](https://docs.mindroom.chat/knowledge/#reranking) |
| `knowledge_graph` | Graph of facts built from agents' conversations | [Knowledge Graph](https://docs.mindroom.chat/knowledge-graph/) |
| `mcp_servers` | External Model Context Protocol servers | [MCP](https://docs.mindroom.chat/mcp/) |
| `plugins` | Plugin loading | [Plugins](https://docs.mindroom.chat/plugins/) |
| `voice` | Speech-to-text for voice messages | [Voice](https://docs.mindroom.chat/voice/) |
| `calls` | Agents joining Element Call voice calls | [Voice Calls](https://docs.mindroom.chat/voice-calls/#calls-configuration) |
| `administrators`, `authorization`, `bot_accounts` | Platform administrators, command access, bridge aliases, and bridge bots | [Authorization](https://docs.mindroom.chat/authorization/) |
| `room_defaults`, `rooms` | Managed room policy, invitations, and room admins | [Rooms & Spaces](https://docs.mindroom.chat/rooms/#room-policy) |
| `room_models` | Map of managed room name to model name, the authored model default for a room | [Models](https://docs.mindroom.chat/configuration/models/#room_model) (`!room_model`) |
| `room_thread_summary_models` | Map of room alias or Matrix room ID to automatic thread summary model | [Matrix & Attachments](https://docs.mindroom.chat/tools/matrix-and-attachments/#automatic-thread-summaries) |
| `matrix_space` | Root Matrix Space for managed rooms | [Matrix Space](https://docs.mindroom.chat/rooms/#matrix-space) |
| `timezone`, `scheduler_catch_up_grace_seconds` | Default timezone for dates, times, and schedules (default `UTC`; agents can override it) and missed-run catch-up | [Scheduling](https://docs.mindroom.chat/scheduling/#timezone) |
| `external_trigger_policy` | Inbound external triggers | [External Triggers](https://docs.mindroom.chat/external-triggers/) |
| `slack` | How Slack workspace members map to Matrix users | [Slack](https://docs.mindroom.chat/tools/slack/#slack-configuration) |
| `tool_approval` | Human approval for tool calls | [Tool Approval](https://docs.mindroom.chat/tool-approval/#tool-approval) |
| `egress_broker` | Agent Keys: services whose API keys and connected accounts are added to worker requests without the worker seeing them | [Agent Keys](https://docs.mindroom.chat/deployment/egress-broker/) |
| `background_tool_jobs` | Experimental background execution of tool calls, off by default | [Background Jobs](https://docs.mindroom.chat/tools/agent-orchestration/#background-jobs) |
| `personal_rooms` | One private room per onboarded user | [Personal Rooms](https://docs.mindroom.chat/personal-rooms/#personal-rooms) |
| `personal_agents` | Templates that give each onboarded user their own agent | [Personal Agents](https://docs.mindroom.chat/personal-agents/#personal-agents) |
| `budgets` | Default monthly spending cap per user and the fallback model | [Budgets](https://docs.mindroom.chat/budgets/) |
| `prompts` | Built-in prompt overrides | [Built-In Prompt Overrides](#built-in-prompt-overrides) |
| `matrix_sync` | Matrix sync transport and limits | [Matrix Sync](https://docs.mindroom.chat/matrix/#matrix-sync) |
| `event_journal` | Storage for the Matrix event journal | [Event Journal](https://docs.mindroom.chat/deployment/storage/#event-journal) |
| `debug` | Provider request logging | [Debug Logging](https://docs.mindroom.chat/deployment/operational-log-events/#debug-logging) |
| `mindroom_user` | Internal MindRoom user account | [Internal User](https://docs.mindroom.chat/matrix/#internal-user) |

Tools that stay out of the model's context until the agent searches for them are configured as described in [Dynamic Tools](https://docs.mindroom.chat/tools/dynamic-tools/), and skills in [Skills](https://docs.mindroom.chat/skills/).

## Splitting the Configuration Into Multiple Files

Large configs can be split across multiple files with Home-Assistant-style include tags.

| Tag | Result |
|-----|--------|
| `!include rel/path.yaml` | The parsed content of that YAML file, with nested include tags resolved recursively |
| `!expand rel/path.yaml` | Splices the file's YAML list into the surrounding list at this item |
| `!include_text rel/path.md` | The file's raw text as a string (one trailing newline stripped), for long prompt or instruction blocks |
| `!include_dir_list rel/dir` | A list with one item per YAML file in the directory |
| `!include_dir_named rel/dir` | A mapping of filename-without-extension to each file's parsed content |
| `!include_dir_merge_list rel/dir` | The concatenation of the lists contained in each file |
| `!include_dir_merge_named rel/dir` | The merge of the mappings contained in each file |

A realistic split keeps each agent in its own file, shared lists in `_`-prefixed snippets, and long prompts in Markdown:

```yaml
# config.yaml
agents: !include_dir_merge_named agents/
models: !include models.yaml
```

```yaml
# agents/researcher.yaml
researcher:
  display_name: Researcher
  role: Deep research and analysis
  model: default
  tools:
    - !expand _shared/web_tools.yaml
    - calculator
  instructions:
    - !include_text ../prompts/researcher.md
```

```yaml
# agents/_shared/web_tools.yaml
- duckduckgo
- website
- browser
```

Here `tools` resolves to `[duckduckgo, website, browser, calculator]`.

- Relative paths resolve against the directory of the file containing the tag.
- Absolute paths are rejected, and every include must stay inside the top-level config file's directory after symlinks and `..` are resolved.
- Explicit include paths may not contain hidden components starting with `.`, so `!include_text .env` fails.
- Directory includes recurse into subdirectories, take only `.yaml`/`.yml` files in lexicographic order of their relative path, and skip files and directories whose name starts with `.` or `_`.
- YAML anchors cannot cross an include boundary, so share values through `_`-prefixed snippet files.
- `!expand` is only valid as a list item, may appear several times in one list, and splices one level while keeping order and duplicates.
  The expanded file must be a YAML list; `[]` adds nothing, while an empty file, `null`, a scalar, or a mapping is an error.
  An ordinary `- !include tools.yaml` inserts the included value as one item, and `tools: !include tools.yaml` replaces the whole list.
- An empty included file resolves to `null` under `!include` and contributes nothing to directory includes.
- Include cycles, missing files, duplicate keys across `!include_dir_merge_named` files, and duplicate filename stems under `!include_dir_named` are errors that name the file and line of the failing tag.

Editing any file in the include tree triggers the same hot reload as editing `config.yaml`, and a multi-file update such as `git pull` applies as one reload.
Adding a file to an included directory or deleting an included file does not trigger a reload by itself; save `config.yaml` again (for example with `touch config.yaml`) or restart MindRoom to apply it.
After a failed reload, fixing any file the failed attempt read triggers a retry.

When migrating an existing monolith, diff the fully merged config before and after the split:

```bash
mindroom config resolve > before.yaml
# split config.yaml into include files
mindroom config resolve > after.yaml
diff before.yaml after.yaml
```

When the configuration uses include tags, structured saves from the dashboard and the self-config tools are rejected, so edit the source files instead; the raw config editor still edits the top-level file's text (see [Dashboard](https://docs.mindroom.chat/dashboard/#configurations-split-with-include)).
`MINDROOM_CONFIG_TEMPLATE` copies only the single template file, so a template that uses includes needs its included files shipped next to the config path.

## Built-In Prompt Overrides

Use the optional root `prompts` block to replace MindRoom's built-in prompts, keyed by the prompt's uppercase name:

```yaml
prompts:
  HIDDEN_TOOL_CALLS_PROMPT: |
    Tool calls are hidden from users.
    Answer directly without narrating internal tool use.
  ROUTER_AGENT_SELECTION_PROMPT_TEMPLATE: |
    Decide which agent or team should respond to this message.

    Available agents and teams:

    {agents_info}

    User message: {message}

    Return only the agent or team name.
```

An unknown key fails config validation with an error that lists every allowed prompt name.
Template prompts accept only their own `{field_name}` placeholders, and an unsupported placeholder fails validation with an error that lists the allowed ones.
Placeholders are exact `{field_name}` replacements, not Jinja or Python `str.format`, so `{message.text}`, `{message!r}`, and `{message:.2f}` are rejected.
Escape literal braces as `{{` and `}}` in overrides that use placeholders.
Changed overrides apply through hot reload.
Changing a prompt that agents build in when they start, such as `HIDDEN_TOOL_CALLS_PROMPT` or `AGENT_IDENTITY_CONTEXT_TEMPLATE`, restarts every agent, team, and the router; other prompt changes apply without restarts.

## Environment Variables

Set variables in the process environment or in the `.env` file next to `config.yaml`; the process environment wins.
`MINDROOM_CONFIG_PATH`, `LOG_LEVEL`, `MINDROOM_LOGGER_LEVELS`, `MINDROOM_LOG_FORMAT`, `NO_COLOR`, and [`MINDROOM_TIMING`](https://docs.mindroom.chat/deployment/operational-log-events/#debug-logging) are read only from the process environment, so the `.env` file does not set them.
Container `env_file`/`--env-file` injection sets process variables and therefore works.

### Core

| Variable | Description | Default |
|----------|-------------|---------|
| `MINDROOM_CONFIG_PATH` | Path to `config.yaml` | `./config.yaml`, then `~/.mindroom/config.yaml` |
| `MINDROOM_STORAGE_PATH` | Data storage directory | `mindroom_data/` next to config |
| `MINDROOM_CONTROL_STATE_PATH` | Separate root for the primary's [control state](https://docs.mindroom.chat/deployment/storage/#data-persistence); relative paths resolve from the config directory | `control_state/` under `MINDROOM_STORAGE_PATH` |
| `MINDROOM_SESSION_STORAGE_PATH` | Separate root for agent and team session databases; relative paths resolve from the config directory, and other state stays under `MINDROOM_STORAGE_PATH` (see [Storage](https://docs.mindroom.chat/deployment/storage/)) | `MINDROOM_STORAGE_PATH` |
| `MINDROOM_CONFIG_TEMPLATE` | File copied to the config path when `config.yaml` does not exist, used by Docker images to seed config | _(unset)_ |
| `MINDROOM_CREDENTIALS_ENCRYPTION_KEY` | Key for encrypted-at-rest credential files (see [Credential Storage Encryption](https://docs.mindroom.chat/oauth-framework/#credential-storage-encryption)) | _(unset)_ |
| `LOG_LEVEL` | Logging level for `mindroom run` (`DEBUG`, `INFO`, `WARNING`, `ERROR`); `mindroom run --log-level` takes precedence | `INFO` |
| `MINDROOM_LOGGER_LEVELS` | Comma- or semicolon-separated logger level overrides, for example `mindroom:DEBUG,httpx:WARNING,nio:WARNING`; MindRoom logger names are module paths, so `mindroom.scheduling:DEBUG` covers one package and `mindroom.coalescing.ingress_lanes:DEBUG` one module | _(unset)_ |
| `MINDROOM_LOG_FORMAT` | `text` for readable logs or `json` for structured logs | `text` |
| `NO_COLOR` | Any nonempty value disables console colors and styled tracebacks; installed background services set this to `1` | _(unset)_ |

### Matrix

| Variable | Description | Default |
|----------|-------------|---------|
| `MATRIX_HOMESERVER` | Matrix homeserver URL; use the default URL for hosted pairing or your own URL for self-hosting | `https://mindroom.chat` |
| `MATRIX_SERVER_NAME` | Server name for federation | _(derived from homeserver)_ |
| `MATRIX_SSL_VERIFY` | Set to `false` for a dev or self-signed homeserver; it covers `MATRIX_HOMESERVER` and its MatrixRTC voice-call requests, never the provisioning service or other homeservers (see [TLS Trust](https://docs.mindroom.chat/matrix/#tls-trust)) | `true` |
| `MATRIX_REGISTRATION_TOKEN` | Registration token for creating managed agent accounts on a self-hosted homeserver | _(unset)_ |
| `MATRIX_REGISTRATION_SHARED_SECRET`, `MATRIX_REGISTRATION_SHARED_SECRET_FILE` | Synapse shared registration secret, or a file containing it, for creating managed agent accounts without pairing | _(unset)_ |
| `MATRIX_MANAGED_ACCOUNT_AUTH` | `password` or `appservice` authentication for accounts MindRoom creates and operates (see [Agent Users](https://docs.mindroom.chat/matrix/#agent-users)) | `password` |
| `MATRIX_APPSERVICE_TOKEN`, `MATRIX_APPSERVICE_TOKEN_FILE` | Application-service token, or a file containing it, used when `MATRIX_MANAGED_ACCOUNT_AUTH=appservice` | _(unset)_ |
| `MINDROOM_MATRIX_HOMESERVER_STARTUP_TIMEOUT_SECONDS` | Seconds to wait at startup for the homeserver to respond (`0` waits indefinitely); a certificate verification failure stops startup immediately | _(wait indefinitely)_ |
| `MINDROOM_MATRIX_SYNC_STARTUP_TIMEOUT_SECONDS` | Positive seconds allowed for the first Matrix sync response | `600` |
| `MINDROOM_MATRIX_INGESTION_GRACE_SECONDS` | Positive seconds the sync watchdog and `/api/health` keep waiting while event catch-up is still making progress; set it above the deployment's observed healthy catch-up time | `600` |

Hosted pairing variables written by `mindroom connect` are described in [Hosted Matrix](https://docs.mindroom.chat/deployment/hosted-matrix/#connect), and desktop setup variables in [Desktop](https://docs.mindroom.chat/tools/desktop/).

### Model Providers

Provider API keys such as `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`, their `_FILE` variants, and Vertex AI and Bedrock settings are listed in [Models — Environment Variables](https://docs.mindroom.chat/configuration/models/#environment-variables), and `codex login` and `CODEX_HOME` in [Codex Models with ChatGPT Login](https://docs.mindroom.chat/configuration/models/#codex-models-with-chatgpt-login).
Provider keys from the environment or `.env` also supply shared credentials, as described in [Credentials from the Environment](https://docs.mindroom.chat/oauth-framework/#credentials-from-the-environment).

| Variable | Description |
|----------|-------------|
| `OPENAI_BASE_URL` | Base URL for `provider: openai` models, such as a local inference server; a model's `extra_kwargs.base_url` overrides it, and `extra_kwargs.client_params.base_url` overrides both |

### Dashboard and API

| Variable | Description | Default |
|----------|-------------|---------|
| `MINDROOM_API_KEY` | API key for dashboard/API requests; `mindroom config init` and first-run `mindroom run` add a generated key to `.env` when it has none; unset or empty means open access | _(none)_ |
| `MINDROOM_PORT` | Port in the default OAuth callback URL, the default [Connections token audience](https://docs.mindroom.chat/connections-portal/#token-binding), and the address MindRoom Chat finds when `MINDROOM_PUBLIC_URL` is unset, also read by deployment tooling; it does not set the API server's bind port, which is `mindroom run --api-port`, and when unset it follows that port | the `--api-port` port, `8765` by default |
| `MINDROOM_DASHBOARD_URL` | Origin, HTTPS or HTTP on a local address, where administrators open the dashboard when another app owns `/` on `MINDROOM_PUBLIC_URL`; prerequisites and effects are in [Dashboard On Its Own Host](https://docs.mindroom.chat/deployment/trusted-upstream-auth/#dashboard-on-its-own-host) | _(unset: the dashboard is at `/` on the public origin)_ |
| `MINDROOM_DASHBOARD_ALLOWED_HOSTS` | Comma-separated extra host names that unauthenticated dashboard or `/v1` requests may address (see [Authorization](https://docs.mindroom.chat/authorization/#unauthenticated-dashboard-host-names)) | _(none)_ |
| `MINDROOM_DASHBOARD_CORS_ALLOWED_ORIGINS` | Comma-separated origins allowed credentialed dashboard CORS responses; cookie and trusted-upstream mutations still require the app's own origin, or `MINDROOM_DASHBOARD_URL` on that host | `http://localhost:3003`, `http://localhost:5173`, `http://127.0.0.1:3003`, `http://127.0.0.1:5173`, or the hosted public origins when `MINDROOM_PUBLIC_URL` is set |
| `MINDROOM_DASHBOARD_CORS_ALLOW_ALL_ORIGINS` | `true` allows every dashboard API origin while disabling credentialed CORS responses; without dashboard authentication, origins outside `MINDROOM_DASHBOARD_ALLOWED_HOSTS` are still refused | _(unset)_ |
| `MINDROOM_ENABLE_API_DOCS` | `false` hides the generated API docs at `/docs`, `/redoc`, and `/openapi.json` | `true` (`false` on hosted platform instances) |
| `OPENAI_COMPAT_API_KEYS` | Comma-separated API keys for `/v1/*` requests (see [OpenAI-Compatible API](https://docs.mindroom.chat/openai-api/)) | _(none; `/v1` is locked without this or the flag below)_ |
| `OPENAI_COMPAT_ALLOW_UNAUTHENTICATED` | `true` allows unauthenticated `/v1/*` access for local development only | _(unset)_ |
| `OPENAI_COMPAT_API_KEY_REQUESTERS` | JSON object mapping `/v1` API keys to Matrix user IDs (see [Requester identities](https://docs.mindroom.chat/openai-api/#requester-identities-and-delegation)) | _(none)_ |
| `MINDROOM_AUTO_BUILD_FRONTEND` | Set to `0` to skip the automatic dashboard frontend build | _(enabled)_ |

### Workers and Background Scripts

| Variable | Description | Default |
|----------|-------------|---------|
| `MINDROOM_NAMESPACE` | Installation namespace for Matrix identity isolation (4–32 lowercase alphanumeric chars) | _(none)_ |
| `MINDROOM_NO_AUTO_INSTALL_TOOLS` | `1`/`true`/`yes` disables automatic installation of missing optional dependencies, such as tool and voice-call extras | _(unset; auto-install enabled)_ |
| `MINDROOM_WORKER_BACKEND` | Worker backend for tool execution (`static_runner`, `docker`, or `kubernetes`) | `static_runner` |
| `MINDROOM_SANDBOX_PROXY_URL` | Sandbox proxy endpoint URL (static runner) | _(none)_ |
| `MINDROOM_SANDBOX_PROXY_TOKEN` | Auth token for the sandbox proxy | _(none)_ |
| `MINDROOM_EGRESS_BROKER_PORT` | Port of the [Agent Keys](https://docs.mindroom.chat/deployment/egress-broker/#environment-variables) egress broker; setting it turns Agent Keys on, and that page lists the other `MINDROOM_EGRESS_BROKER_*` variables | _(unset; off)_ |
| `MINDROOM_SCRIPT_GATEWAY_URL` | Worker-reachable background-script gateway base URL, including `/api/script-gateway` | _(none)_ |
| `MINDROOM_SCRIPT_GATEWAY_PORT` | Port for a second listener that serves only the script gateway and Agent CLI routes | _(none)_ |
| `MINDROOM_SCRIPT_GATEWAY_ISOLATED` | `true` attests that the Kubernetes script-gateway listener exposes only the gateway and Agent CLI routes | `false` |
| `MINDROOM_KUBERNETES_DEFAULT_SCRIPT_RESOURCE_PROFILE` | Default Kubernetes background-script profile (`small`, `standard`, or `large`) when `start_script` omits `resource_profile` | `small` |
| `MINDROOM_KUBERNETES_SCRIPT_RESOURCE_PROFILES_JSON` | JSON object defining CPU and memory requests and limits for the `small`, `standard`, and `large` profiles | Built-in profiles |
| `MINDROOM_SCRIPT_RETENTION_SECONDS` | Positive seconds to retain finished background-script runs, tool-call receipts, and approval records | `2592000` (30 days) |

See [Background Scripts](https://docs.mindroom.chat/tools/background-scripts/#worker-and-network-requirements) for when each script gateway variable is required, and [Sandbox Proxy](https://docs.mindroom.chat/deployment/sandbox-proxy/#environment-variable-reference) for the other `MINDROOM_SANDBOX_*` and Kubernetes worker variables.
Credential seed variables are described in [Credential Seeds](https://docs.mindroom.chat/oauth-framework/#credential-seeds).

# Agent Configuration

This page covers the `agents:` section of `config.yaml`, where each agent is one AI actor with its own model, tools, rooms, and instructions.
It also covers requester-private agents, files preloaded into an agent's prompt, naming rules, and the `defaults:` section that every agent inherits from.

## Basic Agent

```yaml
agents:
  assistant:
    display_name: Assistant
    role: A helpful AI assistant
    model: sonnet
    rooms: [lobby]
```

The key (`assistant`) is the agent's name, `model` names an entry under `models:`, and `rooms` lists the rooms the agent joins.

A coding agent that runs its code tools in a worker, preloads workspace notes, and replies as plain room messages in a bridged room:

```yaml
agents:
  developer:
    display_name: Developer
    role: Generate code, manage files, execute shell commands
    model: sonnet
    tools:
      - file
      - github
      - shell:
          extra_env_passthrough: "DAWARICH_*"
    worker_tools: [shell, file]
    instructions:
      - Always read files before modifying them
    context_files: [AGENTS.md, USER.md]
    rooms: [lobby, dev, bridge_telegram]
    room_thread_modes:
      bridge_telegram: room
    access:
      members_of_rooms: [dev]
    delegate_to: [research]
```

To keep `defaults.tools` away from one agent:

```yaml
agents:
  researcher:
    display_name: Researcher
    role: Focus on deep research
    include_default_tools: false
    tools: [duckduckgo]
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `display_name` | string | *required* | Name shown in Matrix |
| `role` | string | `""` | System prompt describing the agent's purpose and expertise |
| `model` | string | `"default"` | Key of an entry under `models:` |
| `cheap_model` | string or null | `null` | Cheaper model key this agent may switch to; used by `cold_cache`. Inherits `defaults.cheap_model`. See [Cold-Cache Model](https://docs.mindroom.chat/configuration/models/#cold-cache-model) |
| `tools` | list | `[]` | Tool names or single-key dicts with per-agent overrides; see [Tools](https://docs.mindroom.chat/tools/) and [Per-Agent Tool Configuration](https://docs.mindroom.chat/tools/#per-agent-tool-configuration). The agent gets these plus `defaults.tools`, without duplicates |
| `include_default_tools` | bool | `true` | Set `false` to leave out `defaults.tools` and its overrides for this agent |
| `skills` | list | `[]` | Skill names the agent can use; see [Skills](https://docs.mindroom.chat/skills/) |
| `skill_learning` | object | disabled | [Automatic skill learning](https://docs.mindroom.chat/skills/#settings), separate from `learning` |
| `instructions` | list | `[]` | Extra lines appended to the system prompt after the role |
| `minimal_instructions` | list | `[]` | Guidance included on every minimal-mode request; see [Prompt and `minimal_instructions`](https://docs.mindroom.chat/tools/agent-cli/#prompt-and-minimal_instructions) |
| `rooms` | list | `[]` | Room keys, aliases, or Matrix room IDs to join; missing managed rooms are created (see [Rooms](https://docs.mindroom.chat/rooms/)) |
| `accept_invites` | bool or list | `true` | `true` accepts every room invitation, `false` or `[]` accepts none, and a list accepts only inviters matching an exact or wildcard Matrix user ID. Rooms joined this way are kept across restarts. Joining a room never grants its members access; see [Authorization](https://docs.mindroom.chat/authorization/) |
| `access` | object | `null` | Who may converse with the agent: `current_room_members`, `members_of_rooms`, and `users`. When omitted, members of the agent's own managed `rooms` have access. See [Responder access](https://docs.mindroom.chat/authorization/#responder-access) |
| `credential_managers` | list | `[]` | Concrete Matrix user IDs (no wildcards) allowed to manage this agent's credentials and shared OAuth connections; grants no conversation access. See [OAuth](https://docs.mindroom.chat/oauth-framework/) |
| `mcp_gateway_shared_credentials` | bool | `false` | Let users who may use this agent call its tools through the [MCP gateway](https://docs.mindroom.chat/deployment/mcp-gateway/#agent-eligibility) even though its worker scope is `shared` or unset. Every caller then runs them with the agent's shared credentials and worker, without its instructions. Agents with `private` or `worker_scope: user` or `user_agent` are available without it |
| `participation` | object | `null` | Opt into adaptive replies in existing multi-human threads; see [Adaptive Participation](https://docs.mindroom.chat/configuration/threads/#adaptive-participation) |
| `mid_turn` | object | `null` | Judge whether messages queued during a response can wait until it finishes; see [Mid-Turn Coalescing](https://docs.mindroom.chat/configuration/threads/#mid-turn-coalescing) |
| `cold_cache` | object | `null` | Move an idle thread to `cheap_model` for good when a judge accepts it; see [Cold-Cache Model](https://docs.mindroom.chat/configuration/models/#cold-cache-model) |
| `thread_mode` | string | `"thread"` | `thread` replies in Matrix threads; `room` sends plain room messages with one continuous conversation per room, which suits bridges (Telegram, Signal, WhatsApp) and mobile clients |
| `room_thread_modes` | map | `{}` | Per-room `thread` or `room` overrides keyed by room key, alias, or Matrix room ID; see [Thread Mode Resolution](https://docs.mindroom.chat/configuration/threads/#thread-mode-resolution) |
| `markdown` | bool | `null` | Tell the agent in its system prompt that its replies are shown as Markdown; chat replies are rendered as Markdown either way. Team leaders follow `defaults.markdown`, and voice calls leave the line out |
| `knowledge_graph` | bool | `null` | Build a [knowledge graph](https://docs.mindroom.chat/knowledge-graph/) from this agent's conversations and give it the `knowledge_graph` tool |
| `knowledge_graph_edits` | bool | `null` | Let the agent correct its knowledge graph: delete wrong facts and entities, merge duplicate entities, and rewrite summaries; needs `knowledge_graph`. See [Letting the Agent Fix Its Graph](https://docs.mindroom.chat/knowledge-graph/#letting-the-agent-fix-its-graph) |
| `timezone` | string | `null` | IANA timezone for this agent's dates, times, and schedules, such as `Europe/Amsterdam`; inherits the top-level `timezone`. See [Timezone](https://docs.mindroom.chat/scheduling/#timezone) |
| `memory_backend` | string | `null` | `mem0`, `file`, or `none`, overriding `memory.backend`; `none` disables memory. See [Memory](https://docs.mindroom.chat/memory/) |
| `memory_search` | object | `null` | File-memory search override (`mode`, `include`, `include_entrypoint`) when the backend is `file`; omitted fields inherit `memory.search`. See [Searching file memory](https://docs.mindroom.chat/memory/#searching-file-memory) |
| `automations` | list | `null` | Automations such as `prompt_curation`, `dreaming`, or one a plugin provides; `null` inherits the `defaults.automations` entries the agent can run and `[]` turns them off. See [Automations](https://docs.mindroom.chat/scheduling/#automations) |
| `knowledge_bases` | list | `[]` | Keys under top-level `knowledge_bases`, each at most once; see [Knowledge Bases](https://docs.mindroom.chat/knowledge/) |
| `context_files` | list | `[]` | Workspace files preloaded into the prompt; see [File-Based Context Loading](#file-based-context-loading) |
| `private` | object | `null` | Give each requester a separate copy of the agent's state; see [Private Instances](#private-instances) |
| `num_history_runs`, `num_history_messages`, `max_tool_calls_from_history` | | | See [History Settings](https://docs.mindroom.chat/configuration/history/#history-settings) |
| `compaction` | object | `defaults.compaction` | Per-agent compaction overrides; `enabled: false` turns automatic compaction off. See [Agent Compaction Settings](https://docs.mindroom.chat/configuration/history/#agent-compaction-settings) |
| `max_tool_calls_per_turn` | int, >= 1 | `null` | Tool calls one turn may execute. Further calls return a tool error, and after this many plus two model requests the turn ends with the text produced so far, which also stops loops of unknown or malformed tool calls |
| `created_agents_per_owner` | int, >= 0 | `0` | How many agents each person may create through this agent with `create_agent`; `0` turns the tool off. It applies to this agent only and is not inherited from `defaults`. See [Created Agents and Self-Updates](https://docs.mindroom.chat/personal-agents/#created-agents-and-self-updates) |
| `show_tool_calls` | bool | `null` | Show tool-call markers and trace metadata in Matrix messages; see [Tool Calls During Streaming](https://docs.mindroom.chat/streaming/#tool-calls-during-streaming) |
| `show_reasoning` | bool | `null` | Show the model's reasoning on streamed replies to everyone in the room; see [Reasoning](https://docs.mindroom.chat/streaming/#reasoning) |
| `worker_tools` | list | `null` | Tools to run in an isolated worker instead of the primary process; `[]` runs everything in the primary process. When unset here and in `defaults`, the deployment's [execution mode](https://docs.mindroom.chat/deployment/sandbox-proxy/#execution-modes) decides. See [Worker Routing](https://docs.mindroom.chat/deployment/sandbox-proxy/#worker-routing) |
| `worker_scope` | string | `null` | How worker runtimes are shared: `shared` (one per agent), `user` (one per user, across agents), or `user_agent` (one per user and agent). Not allowed together with `private`. See [Worker scopes](https://docs.mindroom.chat/deployment/sandbox-proxy/#worker-scopes) |
| `file_access` | string | `null` | `workspace` limits path-taking tools such as `file`, `coding`, `attachments`, and `matrix_message` to the agent workspace and its attachments, and lets `file` and `coding` also use the agent's [shared directories](https://docs.mindroom.chat/tools/execution-and-coding/#shared_directories); `unrestricted` allows any path the tool's process can reach. `shell`, `python`, and other code-execution tools are never confined; isolate them with `worker_tools`. See [File access](https://docs.mindroom.chat/architecture/security-posture/#file-access) |
| `allow_self_config` | bool | `null` | Give the agent a tool to read and change its own entry under `agents:`; see [`self_config`](https://docs.mindroom.chat/tools/agent-orchestration/#self_config) |
| `delegate_to` | list | `[]` | Agents this agent may run as subagents, including itself only when listed; see [Agent Delegation](https://docs.mindroom.chat/tools/agent-orchestration/#agent-delegation) |
| `thread_exports` | bool or object | `null` | Keep YAML exports of the agent's threads under `<workspace>/thread_exports/`; `true` uses the defaults. See [Thread Exports](https://docs.mindroom.chat/thread-exports/#thread-exports) |

Unset fields that appear in the inheritance table under [Defaults](#defaults) take the `defaults` value; a per-agent value overrides it.
`memory_backend` and `memory_search` inherit from the top-level `memory:` section instead.

## Private Instances

Use `private` when one shared agent definition should give each requester their own workspace, file memory, and knowledge index.
The YAML stays shared, while each requester's files live in their own private root.

```yaml
knowledge_bases:
  company_docs:
    path: ./company_docs
    watch: false

agents:
  mind:
    display_name: Mind
    role: A persistent personal AI companion
    model: sonnet
    tools: [file, shell]
    worker_tools: [file, shell]
    memory_backend: file
    private:
      per: user
      root: mind_data
      template_dir: ./mind_template
      context_files: [SOUL.md, USER.md, MEMORY.md]
      knowledge:
        path: memory
        watch: false
    knowledge_bases: [company_docs]
```

Here every user gets their own `mind_data/` root, seeded from `./mind_template/` (for example `SOUL.md`, `USER.md`, `MEMORY.md`, and a `memory/` folder), while `company_docs` stays shared by everyone.
Private roots live at `<storage>/private_instances/<requester scope>/<agent>/<private.root>/`, not next to `config.yaml`, and dedicated workers mount only that root.
A private agent is still one Matrix account, so use [personal agents](https://docs.mindroom.chat/personal-agents/) when each person needs an agent of their own that others can mention or add to a team.

How a private agent behaves:

- The template directory is copied into each new private root, and template files added later are copied in without overwriting requester edits.
- The private root is created even without a template.
- `private.context_files` load from the private root; agent-level `context_files` still load from the shared agent workspace.
- With `memory_backend: file`, the private root is the requester's file-memory root; with any other backend, private files exist but are not file memory, and `none` turns memory off.
- Nothing is enabled implicitly: set `memory_backend`, `private.context_files`, and `private.knowledge` explicitly for whatever the template provides; the file names are up to you.
- Because `private.per` sets the agent's worker scope, configuring both `private` and `worker_scope` fails with `Private agents derive their execution scope from private.per; configure private or worker_scope, not both`.
- Private agents cannot be configured as team members, and a shared team member cannot reach one through `delegate_to`.
  Tagging a private agent together with other agents in a message still forms an ad hoc team for that requester.
- A private agent runs only for a known requester; otherwise it fails with `Private agent '<name>' requires an active execution identity to resolve requester-local state`.
- It never runs for MindRoom's own router, agent, and team accounts or the internal `mindroom_user` account, for example when a shared agent's automation mentions it or assigns it a todo; such a run fails with `Private agent '<name>' requires a human or bot-account requester to resolve requester-local state`.
  A delegation to it from such a turn is refused with `Cannot delegate to '<name>': a private agent runs only for a person or bot account.`

### Private Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `private.per` | `user` or `user_agent` | *required* | Requester boundary that gets its own private instance; it also sets the agent's [worker scope](https://docs.mindroom.chat/deployment/sandbox-proxy/#worker-scopes) |
| `private.root` | string | `<agent_name>_data` | Relative directory name of the private root; cannot be absolute, contain `..`, or start with `sessions`, `learning`, `chroma`, `mem0`, `knowledge_db`, `memory_files`, `calls`, `agent_modes.json`, `agent_modes.lock`, `browser`, `browser-profiles`, or `.sessions-recovery.lock` |
| `private.template_dir` | string | `null` | Directory copied into each private root; relative paths resolve from `config.yaml`, absolute paths are allowed, and a missing directory is a config error (`Agent '<name>' has invalid private.template_dir`) |
| `private.context_files` | list | `null` | Private-root-relative files preloaded into the prompt; cannot escape the private root |
| `private.knowledge` | object | `null` | Per-requester knowledge index built from the private root; see [Private Agent Knowledge](https://docs.mindroom.chat/knowledge/#private-agent-knowledge). Omit it or set `enabled: false` for no index |
| `private.knowledge.enabled` | bool | `true` | Whether to index private knowledge |
| `private.knowledge.description` | string | `""` | What the private knowledge contains, shown to the agent in the `search_knowledge_base` tool description |
| `private.knowledge.path` | string | *required* when enabled | Knowledge directory relative to the private root (`.` allowed) |
| `private.knowledge.watch` | bool | `true` | Refresh the index in the background on access; when `false`, external edits need an explicit refresh |
| `private.knowledge.chunk_size` | int | `5000` | Maximum characters per indexed chunk (min: 128) |
| `private.knowledge.chunk_overlap` | int | `0` | Overlapping characters between adjacent chunks; must be smaller than `chunk_size` |
| `private.knowledge.git` | object | `null` | Git sync, with the same schema as `knowledge_bases.<id>.git`; needs a dedicated `path` subtree, see [Private Agent Knowledge](https://docs.mindroom.chat/knowledge/#private-agent-knowledge) |

## File-Based Context Loading

`context_files` inlines files into the agent's prompt under a `Personality Context` section, without a knowledge base.

- Paths are relative to the agent's workspace, `agents/<name>/workspace/` in the storage directory; absolute paths and `..` are rejected.
  With `memory_backend: file`, the same workspace holds the agent's file memory.
- Files load in list order, each headed by its resolved path so the agent knows where to edit it.
- Missing files are skipped with a warning in the logs.
- Edits take effect on the next reply without a restart.

`defaults.max_preload_chars` (default `50000`) caps the section.
When it is exceeded, MindRoom drops whole files from the start of the list first and then trims the last remaining file from its end.
Each affected file keeps its path and a marker with the omitted character count, so the agent can open the file for the rest.
A cap too small to hold the headings and markers fails with `max_preload_chars=<n> cannot fit required context headings and omission markers`.

## Naming Rules

Agent and team keys may contain only letters, digits, and underscores (`^[a-zA-Z0-9_]+$`).
The same key cannot appear under both `agents:` and `teams:`.
`router`, `user`, and `_shared` are reserved.

## Defaults

The `defaults` section sets values every agent inherits unless it sets its own, plus global-only settings that cannot be overridden per agent.

```yaml
defaults:
  tools: [scheduler]
  max_tool_calls_per_turn: 200
  worker_tools: [shell, file, python]
  enable_streaming: true
  max_preload_chars: 50000
```

These defaults apply to each agent that leaves the same field unset:

| Field | Default |
|-------|---------|
| `markdown` | `true` |
| `cheap_model` | `null`; ignored for an agent whose `model` is the same. See [Cold-Cache Model](https://docs.mindroom.chat/configuration/models/#cold-cache-model) |
| `knowledge_graph` | `false` |
| `knowledge_graph_edits` | `true` |
| `num_history_runs`, `num_history_messages`, `max_tool_calls_from_history` | `null` (no limit) |
| `compaction` | enabled; see [Agent Compaction Settings](https://docs.mindroom.chat/configuration/history/#agent-compaction-settings) |
| `max_tool_calls_per_turn` | `1000` |
| `show_tool_calls` | `true` |
| `show_reasoning` | `true` |
| `worker_tools` | `null` (the deployment's execution mode decides) |
| `worker_scope` | `null` |
| `file_access` | `workspace` |
| `allow_self_config` | `false` |
| `automations` | `[]`; private agents inherit them only as `personal_rooms.agent`, and agents without file memory skip the built-ins. See [Automations](https://docs.mindroom.chat/scheduling/#automations) |

`defaults.tools` (default `[scheduler]`) is added to every agent with `include_default_tools: true`; set it to `[]` to add nothing.
It accepts the same per-tool overrides as `agents.<name>.tools`.

These settings are global-only:

| Field | Default | Description |
|-------|---------|-------------|
| `enable_streaming` | `true` | Show replies as progressive message edits; see [Streaming](https://docs.mindroom.chat/streaming/#configuration) |
| `streaming` | see [Streaming](https://docs.mindroom.chat/streaming/#configuration) | Timing settings for progressive edits |
| `large_message_strategy` | `sidecar` | Oversized replies as a preview with the full text attached (`sidecar`) or as several complete messages (`split`); see [Large Messages](https://docs.mindroom.chat/streaming/#large-messages) |
| `coalescing.debounce_ms` | `1000` | Milliseconds (>= 0) to wait after media for more attachments or a trailing caption before replying; text replies immediately |
| `show_stop_button` | `true` | Add a 🛑 reaction while an agent responds; see [Stop Button](https://docs.mindroom.chat/chat-commands/#stop-button) |
| `max_consecutive_agent_replies` | `50` | Consecutive agent or team messages (>= 1) before agents stop waking each other; see [Agents mentioning other agents](https://docs.mindroom.chat/authorization/#agents-mentioning-other-agents) |
| `max_preload_chars` | `50000` | Cap (>= 1) on preloaded context files; see [File-Based Context Loading](#file-based-context-loading) |
| `tool_output_auto_save_threshold_bytes` | `51200` | Larger tool outputs are saved to the workspace; see [Workspace and tool output files](https://docs.mindroom.chat/tools/execution-and-coding/#workspace-and-tool-output-files) |
| `worker_grantable_credentials` | `null` | Shared credential services available inside isolated workers (`null` grants none); Google OAuth client and token services, `google_vertex_adc`, and `slack_app` cannot be granted. See [Credential leases](https://docs.mindroom.chat/deployment/sandbox-proxy/#credential-leases) |
| `thread_summary_model`, `thread_summary_temperature`, `thread_summary_first_threshold`, `thread_summary_subsequent_interval` | `null`, `0.2`, `1`, `10` | Automatic thread summaries; see [Automatic Thread Summaries](https://docs.mindroom.chat/tools/matrix-and-attachments/#automatic-thread-summaries) |

# Model Configuration

The `models` section names the AI providers and model IDs that agents, teams, and the router use.
This page covers model fields, provider credentials, provider-specific setup, request tuning, and switching models from chat.
The same model can have different IDs on different providers; check the [OpenAI](https://developers.openai.com/api/docs/models), [Claude](https://platform.claude.com/docs/en/models/overview), [Gemini](https://ai.google.dev/gemini-api/docs/models), and [OpenRouter](https://openrouter.ai/api/v1/models) catalogs.

## Supported Providers

- `anthropic` - Claude models (Anthropic)
- `bedrock_claude` - Anthropic Claude models on Amazon Bedrock
- `azure` - OpenAI models through Azure OpenAI deployments
- `openai` - GPT models and OpenAI-compatible endpoints
- `codex` or `openai_codex` - OpenAI models available through a local Codex CLI ChatGPT login
- `kimi` or `kimi_code` - Kimi models available through a local Kimi Code CLI login
- `google` or `gemini` - Google Gemini models
- `vertexai_claude` - Anthropic Claude models on Google Vertex AI
- `ollama` - Local models via Ollama
- `llama_cpp` - Local models through an OpenAI-compatible llama.cpp server
- `groq` - Groq-hosted models (fast inference)
- `openrouter` - OpenRouter-hosted models (access to many providers)
- `cerebras` - Cerebras-hosted models
- `deepseek` - DeepSeek models
- `zai` - Z.ai GLM models
- `synthetic` - Built-in Lorem Ipsum model for local conversations and load generation

Letter case, surrounding spaces, and `-` versus `_` do not matter, so `Llama-CPP` means `llama_cpp`.
A `provider` value that does not match one of these fails config load with an error that names the model and lists these values, even when no agent uses that model.
Config load and `mindroom config validate` also fail, naming each reference, when an agent, a team, or the router (including the implicit `default` an entity without a `model` uses), a `room_models` or `room_thread_summary_models` entry, `defaults.thread_summary_model`, or `voice.intelligence.model` while voice is enabled names a model that `models` does not define.

To generate a starter config for one provider, use `mindroom config init --provider <preset>` (see [Getting Started](https://docs.mindroom.chat/getting-started/)).
The [macOS app's local model setup](https://docs.mindroom.chat/installation/macos-app/#local-ai-models) downloads and runs models with one click, with hardware recommendations including Qwen3.8 27B when memory permits.

## Model Config Fields

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `provider` | Yes | - | The AI provider (see supported providers above) |
| `id` | Yes | - | Model ID specific to the provider; for Azure OpenAI, the deployment name |
| `display_name` | No | `null` | Friendly model name shown in clients |
| `icon` | No | `null` | Image path relative to the config file's directory, or a Matrix `mxc://` URI |
| `api` | No | `null` | `responses` or `chat_completions`; valid only with `provider: openai` (see [OpenAI API Models](#openai-api-models)); unset selects automatically |
| `host` | No | `null` | Host URL for self-hosted models (e.g., Ollama) |
| `api_key` | No | `null` | Model-specific API key used instead of the provider's shared key; equivalent to `extra_kwargs.api_key` |
| `extra_kwargs` | No | `null` | Additional provider-specific parameters (see [Extra Kwargs](#extra-kwargs)) |
| `context_window` | No | `null` | Provider context window in tokens, at least 1; budgets history replay and compaction (see [Context Window](https://docs.mindroom.chat/configuration/history/#context-window)); a `compaction.model` or `compaction.fallback_model` needs its own value |
| `pricing` | No | `null` | Prices in USD per million tokens for [Budgets](https://docs.mindroom.chat/budgets/) and [reply costs](https://docs.mindroom.chat/usage/#reply-cost): `input` and `output` (both required), plus `cache_read` and `cache_write`, which default to the `input` price; set `cache_write` for Claude models, whose cache writes cost more than input |
| `stream_idle_timeout_seconds` | No | `null` | Seconds a streamed request may go without a provider event before it counts as stalled, at least 0; `0` disables the limit (see [Stalled Streams](#stalled-streams)) |

The mapping key, such as `default` below, is the stable name agents, teams, the router, and commands use.
It may contain only letters, digits, `:`, `_`, and `-`, so a key such as `gpt-6.1` fails config load; name it `gpt-6-1` instead and set `display_name` to show the dotted name.
`display_name` and `icon` only change how clients show the model and are never sent to the provider:

```yaml
models:
  default:
    provider: openai
    id: gpt-6-astra
    display_name: Quick helper
    icon: icons/helper.png
```

A local `icon` must be a PNG, JPEG, WebP, or GIF image of at most 1 MiB inside the config file's directory; absolute paths fail validation.
A missing, unreadable, unsupported, or out-of-directory image falls back to the client's provider icon and does not prevent startup.
External image URLs are not fetched.
Blank `display_name` or `icon` values count as unset.

## Configuration Examples

```yaml
models:
  # Anthropic Claude
  sonnet:
    provider: anthropic
    id: claude-sonnet-5-5
    context_window: 1000000

  haiku:
    provider: anthropic
    id: claude-haiku-5-5
    context_window: 200000

  # OpenAI
  gpt:
    provider: openai
    id: gpt-6-astra
    context_window: 1050000

  # Google Gemini (both 'google' and 'gemini' work as provider names)
  gemini:
    provider: google
    id: gemini-3.8-flash
    context_window: 1048576

  # Anthropic Claude on Vertex AI
  vertex_claude:
    provider: vertexai_claude
    id: claude-sonnet-5-5
    extra_kwargs:
      project_id: your-gcp-project
      region: global

  # Local via Ollama (see Ollama Context Size)
  local:
    provider: ollama
    id: qwen3.8:27b
    context_window: 32768
    host: http://localhost:11434
    extra_kwargs:
      options:
        num_ctx: 32768

  # OpenRouter (access to many model providers)
  openrouter:
    provider: openrouter
    id: anthropic/claude-sonnet-5.5

  # Groq (fast inference; Qwen3.8 is a preview model)
  groq:
    provider: groq
    id: qwen/qwen3.8-27b
    context_window: 131042

  # Cerebras (65,536 tokens on the free tier; paid plans support 131,072)
  cerebras:
    provider: cerebras
    id: qwen-3.8-27b
    context_window: 65536

  # DeepSeek
  deepseek:
    provider: deepseek
    id: deepseek-v4-pro
    context_window: 1048576

  # Z.ai GLM Coding Plan (OpenAI Chat Completions endpoint)
  glm:
    provider: zai
    id: glm-5.3
    context_window: 1048576
    stream_idle_timeout_seconds: 300  # hosted service behind a custom endpoint; see Stalled Streams
    extra_kwargs:
      base_url: https://api.z.ai/api/coding/paas/v4

  # Custom OpenAI-compatible endpoint (e.g., vLLM, llama.cpp server)
  custom:
    provider: openai
    id: my-model
    extra_kwargs:
      base_url: http://localhost:8080/v1
```

Models of every provider except `google`, `ollama`, and `synthetic` verify an HTTPS endpoint's certificate against the operating system's trust store, as do downloads of media that tools return as URLs, OpenAI embeddings, avatar generation, and the OpenAI speech requests of voice calls and outgoing voice messages, so a private CA installed in that store is trusted.
Setting `SSL_CERT_FILE` or `SSL_CERT_DIR` in the process environment replaces that store, so the file or directory must hold the public CAs too, for example the system CA bundle with the private CA appended.
An `SSL_CERT_DIR` must be a hashed directory, as `c_rehash` or `openssl rehash` creates.

Azure, Bedrock, Codex, and Kimi examples are in their own sections below.
Claude Fable 5.1 uses `claude-fable-5-1` on Anthropic, `anthropic.claude-fable-5-1` on Bedrock, and `anthropic/claude-fable-5.1` on OpenRouter.
Claude Opus 5.5 and Sonnet 5.5 use `claude-opus-5-5` and `claude-sonnet-5-5` on Anthropic and Vertex AI, `anthropic.claude-opus-5-5` and `anthropic.claude-sonnet-5-5` on Bedrock, and `anthropic/claude-opus-5.5` and `anthropic/claude-sonnet-5.5` on OpenRouter.
For the direct DeepSeek API, use `deepseek-flash` for V4.1 Flash or `deepseek-v4-pro` for Pro; the older `deepseek-v4-flash` name is a [temporary compatibility route to V4.1 Flash](https://api-docs.deepseek.com/updates/#date-2026-09-10).
OpenRouter serves the current V4.1 Flash release as `deepseek/deepseek-v4.1-flash-1017`.
[GLM 5.3](https://docs.z.ai/guides/llm/glm-5.3) is available through the GLM Coding Plan endpoint shown above.
The [Qwen3.8-27B model card](https://huggingface.co/Qwen/Qwen3.8-27B) and each hosting provider document their own context limits.

### Claude 5.5 and Fable 5.1 Limits

Fable 5.1, Opus 5.5, and Sonnet 5.5 follow the same [tool-choice rules](https://platform.claude.com/docs/en/models/opus-5-5/migration-guide): `auto` and `none` are allowed, while forcing `any` or a named tool returns an error.
They reject `thinking: {type: disabled}` and manual `budget_tokens`; lower effort with `extra_kwargs.output_config: {effort: low}` instead, or use `extra_kwargs.thinking: {type: between_tools}` on [Sonnet 5.5](https://platform.claude.com/docs/en/models/sonnet-5-5/whats-new-sonnet-5-5#turn-off-up-front-thinking).
With adaptive thinking, the notes these models write between tool calls are hidden, so responses show tool traces without that narration; Sonnet 5.5's `between_tools` setting returns those notes as text.
Fable 5.1, Opus 5.5, Sonnet 5.5, and Haiku 5.5 reuse their reasoning from an earlier turn only while everything sent before it is unchanged, so after a change they answer without it instead of failing, most often because the date or other content of the system prompt has changed, or a compaction or replay limit rewrote earlier turns.
With Sonnet 5.5's `between_tools` or on `bedrock_claude`, earlier turns' reasoning is never sent, while the current turn's reasoning between tool calls always is; Haiku 5.5 with `thinking: {type: disabled}` writes no reasoning to send.
A reply that resumes after a tool approval or a delegation keeps its reasoning, unless its history was trimmed to fit the context window or another reply or a compaction ran in the conversation while it waited; then it answers without that reasoning, and with `between_tools` or on `bedrock_claude`, an account created on or after August 31, 2026 can get a 400 error ``Invalid `signature` in `thinking` block`` on that resume.

## Environment Variables

Provider credentials come from environment variables, stored credentials, CLI logins, or a model-specific key.
A model uses the key saved for it in the dashboard's **Models** editor first, then `api_key` or `extra_kwargs.api_key` from its config, then the provider's shared key from the environment or credential store.
A key in `extra_kwargs.client_params.api_key` reaches the provider's API client over all of these, so that model never needs the shared key.
Setting both `api_key` and `extra_kwargs.api_key` is a validation error, and a blank key counts as unset.
A model that authenticates another way (`auth_token` for `anthropic`, `azure_ad_token` or `azure_ad_token_provider` for `azure`, or Vertex AI for `google` and `gemini`) never receives the shared key.
`codex`, `kimi`, `bedrock_claude`, `vertexai_claude`, and `synthetic` models authenticate without an API key and ignore a configured one.

Shared API keys are read from these variables:

```bash
ANTHROPIC_API_KEY=sk-ant-...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
# Optional, only when overriding the default Azure OpenAI API version:
# AZURE_OPENAI_API_VERSION=2024-10-21
# Optional Azure deployment for the client:
# AZURE_OPENAI_DEPLOYMENT=...
OPENAI_API_KEY=sk-...
GOOGLE_API_KEY=...
GROQ_API_KEY=...
OPENROUTER_API_KEY=...
CEREBRAS_API_KEY=...
DEEPSEEK_API_KEY=...
ZAI_API_KEY=...
```

For Amazon Bedrock Claude, use standard AWS credential resolution:

```bash
AWS_REGION=us-east-1          # or AWS_DEFAULT_REGION; either one exported in the process environment wins over the .env
# Optional static credentials:
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=...
# Optional local profile instead:
AWS_PROFILE=...
```

Without static credentials or a profile, Bedrock uses the runtime IAM role.

For Ollama, you can also set:

```bash
OLLAMA_HOST=http://localhost:11434
# Optional key for a server that requires one, sent by models that configure no api_key:
# OLLAMA_API_KEY=...
```

For Vertex AI Claude, set these instead of an API key:

```bash
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project
CLOUD_ML_REGION=global
# Optional custom Vertex AI endpoint:
# ANTHROPIC_VERTEX_BASE_URL=https://...
```

For `claude-sonnet-5-5`, use the [global endpoint or a supported multi-region endpoint](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai#global-multi-region-and-regional-endpoints), with `region` / `CLOUD_ML_REGION` set to `global`, `us`, or `eu`.
Authenticate with `gcloud auth application-default login` or set `GOOGLE_APPLICATION_CREDENTIALS` to a service account key file.

For Gemini on Vertex AI (`provider: google` with `extra_kwargs.vertexai: true`), authenticate the same way and set `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`, or pass `extra_kwargs.project_id` and `extra_kwargs.location`.
Setting `GOOGLE_GENAI_USE_VERTEXAI=true` moves every `google` and `gemini` model to Vertex AI unless the model chooses its route itself: `extra_kwargs.client_params.enterprise`, then `extra_kwargs.client_params.vertexai`, then `extra_kwargs.vertexai`, set to `true` or `false`, wins over the variable.
`GOOGLE_GENAI_USE_ENTERPRISE` selects nothing.

`OPENAI_BASE_URL` is described in [Configuration — Model Providers](https://docs.mindroom.chat/configuration/#model-providers).

### File-based Secrets

For container environments (Kubernetes, Docker Swarm), supported API-key and secret variables can also use a `_FILE` suffix:

```bash
# Instead of setting the key directly:
ANTHROPIC_API_KEY=sk-ant-...

# Point to a file containing the key:
ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic-api-key
```

This works for every model provider key and every endpoint, project, region, Azure, and CLI login home variable MindRoom reads for model providers, every variable a tool's settings read, `TYPESAFE_API_KEY`, and the variables a skill's `requires.env` names (e.g., `OPENAI_API_KEY_FILE`, `GOOGLE_API_KEY_FILE`, `TAVILY_API_KEY_FILE`).
The plain variable wins when the process environment or the `.env` beside `config.yaml` sets it, and a blank value or an unedited starter-template placeholder counts as unset, so an exported blank variable does not hide the `.env` value.

## OpenAI API Models

Set `api: responses` or `api: chat_completions` on an `openai` model to choose the API independently of its model ID or endpoint.
When `api` is unset, GPT 5.4 and newer on the first-party OpenAI endpoint use Responses, and every other route, including all custom OpenAI-compatible endpoints, uses Chat Completions.
The endpoint must support the selected API.

```yaml
models:
  astra:
    provider: openai
    id: gpt-6-astra
    api: responses
    extra_kwargs:
      base_url: http://localhost:4000/v1
      reasoning_effort: high
```

[GPT-6 Astra, GPT-6.1 Sol, and GPT-6.1 Terra require Responses for function calling](https://developers.openai.com/api/docs/guides/latest-model), and GPT-6 Luna supports function calling in Chat Completions only with `reasoning_effort: none`.
Through a proxy or custom alias, set `api: responses` for GPT-6 models, or tool calls on Astra, Sol, Terra, and Luna fail at their default reasoning effort.
Set `extra_kwargs.reasoning_effort` or `extra_kwargs.reasoning` only when you want to customize reasoning.
A reply shows the model's reasoning while it thinks only when the model requests a reasoning summary, with `extra_kwargs.reasoning_summary: auto` or `extra_kwargs.reasoning: {summary: auto}`.
An unverified OpenAI organization then gets `Your organization must be verified to generate reasoning summaries`; verify the organization or remove the setting.
Selecting Responses keeps any `extra_kwargs.store: false` setting.
How `api` affects tool search is described in [Native Server-Side Tool Search](https://docs.mindroom.chat/tools/dynamic-tools/#native-server-side-tool-search).

## OrcaRouter via the OpenAI-Compatible Endpoint

[OrcaRouter](https://docs.orcarouter.ai/introduction) works through the `openai` provider with its endpoint and an OrcaRouter API key:

```yaml
models:
  orcarouter:
    provider: openai
    id: orcarouter/auto
    api: chat_completions
    stream_idle_timeout_seconds: 300  # hosted service behind a custom endpoint; see Stalled Streams
    extra_kwargs:
      base_url: https://api.orcarouter.ai/v1
      api_key: your-orcarouter-api-key
```

`orcarouter/auto` lets OrcaRouter choose a model per request; replace it with an ID from its [catalog](https://docs.orcarouter.ai/getting-started/models) to pin one.
Set an agent's `model: orcarouter`, or select it for a thread with `!model orcarouter`.
To keep the key out of YAML, omit `extra_kwargs.api_key` and save the key in this model's **API Key** field in the dashboard's **Models** editor, so OrcaRouter and direct OpenAI models use separate keys.
`ORCAROUTER_API_KEY` is not read automatically.

## Codex Models with ChatGPT Login

Use `provider: codex` to call models through an authenticated local Codex CLI session instead of the billed OpenAI API.
Run `codex login` first so `~/.codex/auth.json` contains ChatGPT OAuth tokens; MindRoom refreshes the access token as needed.
Model access and usage limits depend on the logged-in ChatGPT account, so check the [current Codex model catalog](https://developers.openai.com/codex/models).

| Model | Model ID | Best fit |
|-------|----------|----------|
| GPT-6.1 Sol | `gpt-6.1-sol` | Near-Astra reasoning for long-running work; the starter default |
| GPT-6 Astra | `gpt-6-astra` | The hardest end-to-end reasoning and agentic work |
| GPT-6.1 Terra | `gpt-6.1-terra` | Fast everyday coding, writing, screenshots, and computer use |
| GPT-6 Luna | `gpt-6-luna` | Efficient, high-volume work such as summaries and routing |

Older or preview slugs work when the account exposes them, the form `openai-codex/gpt-6.1-sol` is accepted, and the legacy `gpt-5.6` alias maps to `gpt-5.6-sol`.
If Codex state lives outside `~/.codex`, set `CODEX_HOME` in the `.env` beside `config.yaml` or the process environment, or `extra_kwargs.codex_home`; `~` prefixes are expanded.
`mindroom config init --provider codex` generates this config (see [CLI](https://docs.mindroom.chat/cli/)):

```yaml
models:
  default:
    provider: codex
    id: gpt-6.1-sol
    display_name: Sol
    context_window: 258000
    extra_kwargs:
      reasoning_effort: medium
  astra:
    provider: codex
    id: gpt-6-astra
    display_name: Astra
    context_window: 258000
    extra_kwargs:
      reasoning_effort: medium
  luna:
    provider: codex
    id: gpt-6-luna
    display_name: Luna
    context_window: 258000
    extra_kwargs:
      reasoning_effort: low

router:
  model: luna

defaults:
  thread_summary_model: luna
```

Use `258000` as the context window: it fits the Codex window of every model above, which is smaller than the OpenAI API window.
Set reasoning effort with `extra_kwargs.reasoning_effort`; GPT-6.1 Sol, GPT-6 Astra, GPT-6.1 Terra, and GPT-6 Luna accept `low`, `medium`, `high`, `xhigh`, and `max`.
Codex models ask for a reasoning summary (`auto`, as the Codex CLI does) unless `extra_kwargs.reasoning_summary` or `extra_kwargs.reasoning.summary` sets another, so a reply shows the model's reasoning while it thinks.
Codex clients also offer Ultra for Sol, Astra, and Terra, but Ultra's Codex-managed subagents are not available through MindRoom.
The provider supports text and image input with text output; transcription, text-to-speech, and realtime speech are not supported.
The adapter depends on the Codex CLI login format and backend, so a Codex CLI update can require a MindRoom update; use `provider: openai` for the public API contract and API billing.

## Kimi Models with Kimi Code Login

Use `provider: kimi` to call Kimi models through an authenticated local Kimi Code CLI session (Kimi Code subscription) instead of the billed Moonshot API.
Run `kimi` and `/login` first so `~/.kimi-code/credentials/kimi-code.json` contains OAuth tokens; MindRoom refreshes the access token as needed and calls `https://api.kimi.com/coding/v1`.

| Model | Model ID | Best fit |
|-------|----------|----------|
| Kimi K3.1 Pro | `k3.1-pro` | Flagship reasoning, long-horizon coding, and agent work with up to a 1M-token context; the starter default |
| Kimi K3.1 Pro Highspeed | `k3.1-pro-highspeed` | K3.1 Pro at about 2-3x the speed and twice the quota use, on Pro (legacy Allegretto) and higher |
| Kimi K3 | `k3` | The previous flagship generation with up to a 1M-token context |
| Kimi K3 256k | `k3-256k` | The same K3 generation with a 256k-token context |
| Kimi for Coding | `kimi-for-coding` | Coding-tuned tier exposed by the Kimi Code CLI |
| Kimi for Coding Highspeed | `kimi-for-coding-highspeed` | Faster coding tier exposed by the Kimi Code CLI |

[Kimi Code context limits depend on the subscription plan](https://www.kimi.com/code/docs/en/kimi-code/models.html): `k3.1-pro` and `k3` support 1,048,576 tokens on Pro (legacy Allegretto) and higher, while Plus (legacy Moderato) is limited to 262,144.
The starter config and this example require Pro/Allegretto or higher; on Plus/Moderato, and for `k3-256k` on any plan, set `context_window: 262144`.

```yaml
models:
  default:
    provider: kimi
    id: k3.1-pro
    context_window: 1048576
```

The form `kimi-code/k3.1-pro` is accepted.
If Kimi Code state lives outside `~/.kimi-code`, set `KIMI_CODE_HOME` in the `.env` beside `config.yaml` or the process environment, or `extra_kwargs.kimi_home`; `~` prefixes are expanded.
Kimi K3.1 Pro and K3 reason before replying by default, so responses include reasoning tokens even for short answers.
Setting `extra_kwargs.reasoning_effort` to `none` makes K3.1 Pro requests fail with HTTP 400, while K3 then answers with K2.8 Preview without reasoning.
The adapter depends on the Kimi Code CLI login format and backend, so a Kimi Code update can require a MindRoom update.

## OpenRouter Provider Routing

OpenRouter routes each request to one of several upstream providers serving the model, and upstream quality varies.
Control routing with OpenRouter [provider preferences](https://openrouter.ai/docs/features/provider-routing) in `extra_kwargs.extra_body`:

```yaml
models:
  deepseek:
    provider: openrouter
    id: deepseek/deepseek-v4.1-flash-1017
    context_window: 1048576
    extra_kwargs:
      extra_body:
        provider:
          sort: price                     # cheapest available endpoint
          # order: [fireworks, together]  # or pin specific upstreams, in order
          # allow_fallbacks: false        # fail instead of using unlisted upstreams
```

Provider slugs are in the `tag` field of `https://openrouter.ai/api/v1/models/<model-id>/endpoints`.
Account-level OpenRouter settings (ignored providers, data-policy filters) still apply, so pinning an upstream your account excludes fails with `No endpoints found`.
Verify a new pin with a direct API request and check the `provider` field in the response.
MindRoom's `LLM usage` log line names the upstream that served each OpenRouter response in `served_provider`.

## Azure OpenAI

Use `provider: azure` for models deployed through Azure OpenAI, with `id` set to your deployment name rather than the upstream model name.
Set the Azure variables listed in [Environment Variables](#environment-variables) in the config-adjacent `.env` file or the exported environment.

```yaml
models:
  default:
    provider: azure
    id: your-azure-openai-deployment
```

Deployment limits vary, so the starter config leaves `context_window` unset; set it to your deployment's limit when you know it.

## Amazon Bedrock Claude

Use `provider: bedrock_claude` to call Anthropic Claude through Amazon Bedrock, with `id` set to a Bedrock model ID or inference profile ID enabled in your AWS account and region.
The `aws_bedrock` optional extra installs automatically on first use unless `MINDROOM_NO_AUTO_INSTALL_TOOLS=1` is set; otherwise install `mindroom[aws_bedrock]`.
AWS settings come from the config-adjacent `.env` file, the exported environment, a local AWS profile, or the runtime IAM role.

```yaml
models:
  default:
    provider: bedrock_claude
    id: anthropic.claude-opus-5-5
    context_window: 1000000
```

Bedrock lists Fable 5.1 and Sonnet 5 as open access, while Opus 5.5 and Sonnet 5.5 access can depend on the AWS account and region.
The Bedrock starter config defaults to Opus 5.5 with commented alternatives, so confirm access, or switch to its `fable` alternative or `id: anthropic.claude-sonnet-5`.

## Ollama Context Size

Unless `extra_kwargs.options.num_ctx` is set, MindRoom sends Ollama no context size, so Ollama runs each model with its own default, which depends on the Ollama version and server and is often only a few thousand tokens.
When a conversation is longer, Ollama drops its oldest messages to fit, without an error, so the model answers without seeing them.
Set `extra_kwargs.options.num_ctx` to the context size Ollama should use, and `context_window` to the same value so MindRoom fits history into it (see [Context Window](https://docs.mindroom.chat/configuration/history/#context-window)), as the `local` model in [Configuration Examples](#configuration-examples) does.
A larger `num_ctx` needs more memory on the Ollama server.

## Built-In Synthetic Model

Use `provider: synthetic` to run normal MindRoom conversations without an API key or model server, for example for load testing.
The model streams a repeatable random amount of Lorem Ipsum at a fixed rate, and when the agent has the `shell` tool it occasionally calls `run_shell_command` with `echo hi` before continuing.
The `extra_kwargs` values below are the defaults:

```yaml
models:
  synthetic:
    provider: synthetic
    id: lorem-ipsum
    extra_kwargs:
      seed: 1
      min_response_chars: 320
      max_response_chars: 960
      chunk_chars: 40
      chars_per_second: 80
      tool_call_probability: 0.2

agents:
  load_test:
    display_name: Load Test
    role: Generate synthetic traffic.
    model: synthetic
    tools: [shell]
    rooms: [lobby]
```

Tag `@load_test` in Lobby to receive a streamed synthetic reply.
Set `tool_call_probability` to `1` to call the shell tool on every turn or `0` to disable tool calls.
Set `chars_per_second` to `0` to stream without pacing delays for maximum throughput.
Changing `seed` changes the response length, split point, and tool-call choice, which stay the same for a given conversation history.

## Extra Kwargs

`extra_kwargs` sets provider-specific options for the model's requests and API client.
Each provider accepts only the keys in the table below.
A model with any other key, including a misspelled key or a retired Agno model setting such as `retries`, fails config validation with an `Unsupported extra_kwargs` error that names the model and the unknown keys and lists the accepted ones; remove or correct those keys.
`max_retries` is not accepted, since MindRoom retries failed requests itself (see [Retries](#retries)); a model that sets it fails config validation with an error that says to remove it.
`role_map` is not accepted either: Chat Completions requests send the system prompt as `developer` to OpenAI's own API and as `system` to every other endpoint, including an OpenAI-compatible server such as vLLM or llama.cpp set as `provider: openai` with its `base_url`, so remove it.

- `base_url`, a string, sends requests to a custom endpoint, such as an OpenAI-compatible server.
- `api_key` is the same as the model's `api_key` field.
- `request_params` holds request body fields that MindRoom merges over its own.
- `extra_body` adds JSON body fields, for example for [OpenRouter provider routing](#openrouter-provider-routing).
- `client_params`, a mapping, passes options to the provider's API client; its `base_url`, when set, must be a string.

| Provider | Accepted keys |
|----------|---------------|
| `openai` with Chat Completions, `zai`, `llama_cpp` | `api_key`, `base_url`, `client_params`, `default_headers`, `default_query`, `extra_body`, `extra_headers`, `extra_query`, `frequency_penalty`, `logit_bias`, `logprobs`, `max_completion_tokens`, `max_tokens`, `metadata`, `organization`, `presence_penalty`, `reasoning_effort`, `request_params`, `seed`, `service_tier`, `stop`, `store`, `strict_output`, `temperature`, `timeout`, `top_logprobs`, `top_p`, `user`, `verbosity` |
| `azure` | The Chat Completions keys, plus `api_version`, `azure_endpoint`, `azure_deployment`, and `azure_ad_token` |
| `openrouter` | The Chat Completions keys, plus `models`, `cache_system_prompt`, and `extended_cache_time` |
| `deepseek` | The Chat Completions keys, plus `use_thinking` |
| `kimi` | The Chat Completions keys, plus `kimi_home` and `prompt_cache_key` |
| `groq` | `api_key`, `base_url`, `client_params`, `default_headers`, `default_query`, `extra_headers`, `extra_query`, `frequency_penalty`, `logit_bias`, `logprobs`, `max_tokens`, `presence_penalty`, `request_params`, `seed`, `stop`, `temperature`, `timeout`, `top_logprobs`, `top_p`, `user` |
| `cerebras` | `api_key`, `base_url`, `client_params`, `default_headers`, `default_query`, `extra_body`, `extra_headers`, `extra_query`, `max_completion_tokens`, `parallel_tool_calls`, `repetition_penalty`, `request_params`, `strict_output`, `temperature`, `timeout`, `top_k`, `top_p` |
| `openai` with Responses | `api_key`, `base_url`, `cache_system_prompt`, `client_params`, `default_headers`, `default_query`, `extra_body`, `extra_headers`, `extra_query`, `include`, `max_output_tokens`, `max_tool_calls`, `metadata`, `organization`, `parallel_tool_calls`, `reasoning`, `reasoning_effort`, `reasoning_summary`, `request_params`, `service_tier`, `store`, `strict_output`, `temperature`, `timeout`, `top_p`, `truncation`, `user`, `verbosity` |
| `codex` | The Responses keys without `max_output_tokens` and `temperature`, which the Codex endpoint rejects, plus `codex_home`, `default_instructions`, `prompt_cache_key`, and `session_id` |
| `anthropic` | `api_key`, `auth_token`, `betas`, `cache_system_prompt`, `client_params`, `context_management`, `default_headers`, `extended_cache_time`, `max_tokens`, `output_config`, `request_params`, `stop_sequences`, `temperature`, `thinking`, `timeout`, `top_k`, `top_p` |
| `vertexai_claude` | The `anthropic` keys without `auth_token`, plus `base_url`, `project_id`, and `region` |
| `bedrock_claude` | The `anthropic` keys without `auth_token`, plus `aws_access_key`, `aws_profile`, `aws_region`, `aws_secret_key`, and `aws_session_token` |
| `google`, `gemini` | `api_key`, `cached_content`, `client_params`, `frequency_penalty`, `generation_config`, `generative_model_kwargs`, `include_thoughts`, `location`, `logprobs`, `max_output_tokens`, `presence_penalty`, `project_id`, `safety_settings`, `search`, `seed`, `stop_sequences`, `temperature`, `thinking_budget`, `thinking_level`, `timeout`, `top_k`, `top_p`, `vertexai` |
| `ollama` | `api_key`, `client_params`, `format`, `keep_alive`, `options`, `request_params`, `think`, `timeout` |
| `synthetic` | `chars_per_second`, `chunk_chars`, `max_response_chars`, `min_response_chars`, `seed`, `tool_call_probability` (see [Built-In Synthetic Model](#built-in-synthetic-model)) |

On `deepseek`, `use_thinking`, a boolean that defaults to `true`, runs requests in DeepSeek's thinking mode, where the model reasons before it answers; set it to `false` to answer without thinking.
An `ollama` model's `think` turns a thinking model's reasoning on (`true`) or off (`false`), or sets its effort (`low`, `medium`, or `high`) on models that take a level; unset leaves the choice to Ollama.
A `google` or `gemini` model's `search: true` grounds its answers with Google Search, and search with tools needs a Gemini 3 or later model name on the Gemini API, such as `gemini-3.8-flash`: there the agent keeps its tools beside search, while other models, aliases such as `gemini-flash-latest`, and every Vertex AI model use search alone, in place of the agent's tools.

When a response reaches the model's output limit (`max_tokens` or the route's equivalent), its text is kept, but none of its tool calls run: the agent is asked to split the work into smaller tool calls, and a third such response ends the reply with an error.
A Claude model whose `extra_kwargs` sets no `max_tokens` can write up to its maximum output on `anthropic`, 128,000 tokens for Claude 5 models, except Claude Sonnet 4.5 and models older than Claude 4.5, which write up to 8,192 tokens, as every model does on `bedrock_claude` and `vertexai_claude`, whose quotas count the requested output when a request starts; set `max_tokens` to choose another limit.

Some models reject sampling controls:

- Claude Opus 4.7, Opus 4.8, Mythos Preview, and every Claude 5 and 5.x model reject non-default `temperature`, `top_p`, and `top_k`, so MindRoom omits them on Anthropic, Bedrock, and Vertex requests.
- MindRoom also omits those controls for direct Gemini 3.8 Flash, Gemini 3.6 Flash, and Gemini 3.5 Flash-Lite requests.
- [GPT-6 Astra and GPT-6.1 Sol do not support `temperature`, `top_p`, or `top_logprobs`](https://developers.openai.com/api/docs/guides/latest-model#update-api-and-model-parameters), and GPT-6.1 Terra and GPT-6 Luna reject them unless reasoning effort is `none`; leave them out of these models' `extra_kwargs`.

## Prompt Caching

An agent's system prompt has a stable part (identity, role, instructions, workspace context files, tool instructions, skills, and knowledge bases) and a turn part (the current date, file memory, and system enrichment); the stable part stays the same across turns and conversations, so providers that cache prompts can reuse it.
Cache reads and writes appear in usage metrics as cached and cache-write input tokens.

The `anthropic`, `bedrock_claude`, and `vertexai_claude` providers enable prompt caching with a one-hour lifetime by default.
The `openrouter` provider caches the same way for model IDs that route to Anthropic, such as `anthropic/claude-sonnet-5.5` and `~anthropic/claude-sonnet-latest`, using OpenRouter [`cache_control` breakpoints](https://openrouter.ai/docs/guides/best-practices/prompt-caching); other OpenRouter model families cache implicitly.
Set `extra_kwargs.cache_system_prompt: false` to turn off MindRoom's Claude cache breakpoints, or `extra_kwargs.extended_cache_time: false` to use the five-minute lifetime.

The `openai` Responses API and `codex` providers rely on provider-managed caching; on the public OpenAI API, GPT-5.6 and later also get an explicit cache breakpoint after the stable part of the system prompt.
See [OpenAI's prompt caching guide](https://developers.openai.com/api/docs/guides/prompt-caching) for eligibility and accounting.
Set `extra_kwargs.cache_system_prompt: false` on a Responses model to send no explicit breakpoint.
Chat Completions providers other than OpenRouter Claude routes keep their usual request format.

Kimi Code caches repeated request prefixes automatically and reports them as `cached_tokens`.
For `codex` and `kimi`, conversations share a `prompt_cache_key` per agent and requester within one MindRoom storage path, account, and channel, while different agents and requesters stay separate.
Set `extra_kwargs.prompt_cache_key` to choose your own cache group, for example to separate deployments that share a storage path and provider account, or set it to `null` to send no key.
A shared key still needs identical prefixes and tools, and provider routing can produce cache misses.

## Cold-Cache Model

Claude providers read a cached prompt for a small fraction of the normal input price and charge more than the normal price to write a new cache; the exact rates vary by model and provider.
Switching models mid-thread therefore usually costs money, because the new model starts without a cache.
Once the agent model's cache for a thread has expired, the next turn pays for a full write either way, and a cheaper model can continue a simple conversation for a fraction of the cost.
Set `agents.<name>.cheap_model` to the agent's cheaper alternative and `agents.<name>.cold_cache` to let a judge switch to it:

```yaml
agents:
  assistant:
    model: opus
    cheap_model: haiku
    cold_cache:
      judgment:
        provider: openai_decisions
        threshold: 0.8
```

Set `defaults.cheap_model` to give every agent the same cheap model; an agent's own `cheap_model` wins, and an agent whose `model` is the default cheap model has none.
`cold_cache: {}` enables the switch with the default judge, which asks `cheap_model` itself whether it can answer.
`cold_cache` requires a cheap model, and the cheap model needs `context_window` set on both models or neither; when set, it must be at least the agent model's.

| `cold_cache` field | Type | Default | Description |
|-------|------|---------|-------------|
| `idle_seconds` | integer | `3600` | Seconds since the agent's last reply in a thread after which its prompt cache counts as cold |
| `judgment` | object or null | `null` | LLM model alias, TypeSafe, or OpenAI Decisions backend; see [Judgment Backends](https://docs.mindroom.chat/configuration/threads/#judgment-backends). Unset asks `cheap_model` itself |

A thread counts as cold once the agent's last reply in it, timed from that reply's final edit, is older than `idle_seconds`.
The default of one hour matches the cache lifetime MindRoom sets for Claude; set it to your provider's, for example `300` with `extra_kwargs.extended_cache_time: false`.
A thread the agent has not replied in yet keeps the agent model, so no thread is decided by its first request.

On a cold turn, the judge sees the thread and the new request, and decides whether the cheap model can continue the whole conversation as well as the agent model; the question names both by their configured model `id`.
Tool calls in the thread reach it as `<tool>` tags with the short input and output previews the chat shows.
A simple request in a thread about coding, planning, or other demanding work counts as a no.
Long threads reach the judge as their newest messages that fit its 16 KB request, with long messages shortened to their start and end.
Only an accepting decision switches; a decline, abstention, timeout, or missing credential keeps the agent model.
Warm turns make no judgment call.

An accepted switch is saved as the agent's thread override, like `!model`, so the thread stays on the cheap model without further judgments, even after the cheap model's own cache expires.
`!model` shows it, and `!model reset` or another `!model` choice ends it.
`!model reset` returns the thread to automatic selection, so a later idle turn can switch it again; choose the agent's model with `!model` to keep it.
Removing `cold_cache` stops new switches, and after a `cheap_model` change new switches use the new model; threads that already switched stay on their saved model until their override is reset.

The switch never happens when a scheduled model, a `!model` or `!room_model` override (even one naming the agent's own model), or a `room_models` entry that selects a different model applies, or when the request carries images, audio, video, or files, or regenerates or resumes an earlier answer.
Earlier attachments reach the judge only as placeholders, so pick a cheap model that supports the same media as the agent model.
Agents in `thread_mode: room` keep their model, because a room has no thread to switch.
Teams and the router keep their models.
Each decision logs `Cold cache model selected` with a `reason`: `no_earlier_reply`, `warm`, `history_unavailable`, `request_incomplete`, `judgment_unavailable`, or `judged`.

## Retries

MindRoom retries a model request that fails with a rate limit, an overload, a server error (any 5xx status, a CDN's origin timeout included), a request timeout, a conflict (409), or a dropped connection up to four times, as long as nothing was streamed yet and no provider-hosted tool had started.
Before each retry it waits as long as the provider's `Retry-After` or Azure's `retry-after-ms` header asks, when that is at most a minute, and otherwise a delay that doubles with each retry.
SDK retry settings in `client_params`, such as `max_retries` or Gemini's `http_options.retry_options`, have no effect; the Gemini SDK still resends on its own after a dropped connection and during a file upload's transfer.
A request that still fails ends the turn with "⏱️ Rate limited. Please wait a moment and try again." for a rate limit, or "⚠️ Model provider temporarily unavailable after automatic retries. Please try again shortly." otherwise.
A request larger than the account's tokens-per-minute limit, which OpenAI rejects as "Request too large", is not retried, since only a smaller request can pass; it ends the turn with "⚠️ This request is larger than the account's tokens-per-minute limit. Start a new thread or compact the conversation, then try again."
A request that OpenAI, Anthropic, Gemini, or Moonshot refuses because the account has no credit left or reached its monthly spend or usage limit, that Z.ai refuses because the account has no balance or its plan or package expired, or that any provider answers with HTTP `402 Payment Required` is not retried either, and the turn ends with an error that shows the provider's message, such as "⚠️ Error: You exceeded your current quota, please check your plan and billing details."

## Stalled Streams

A provider sometimes accepts a streamed request and then sends nothing more, which would leave the agent busy and queue every follow-up message behind it.
MindRoom ends a streamed request when the provider sends no event for `stream_idle_timeout_seconds`.
If nothing was streamed yet, MindRoom sends the request once more, unless a provider-hosted tool such as web search or code execution had started, since resending would run that tool again.
A second silent attempt, or a stall after text appeared or such a tool started, ends the turn with "⚠️ Model provider temporarily unavailable after automatic retries. Please try again shortly."
The limit measures the gap between provider events, so long replies and slow tool calls are unaffected.
It applies to every model request, also when `defaults.enable_streaming` is off, except compaction summaries, which their own [`timeout_seconds`](https://docs.mindroom.chat/configuration/history/#agent-compaction-settings) bounds.

By default the limit is 300 seconds for hosted providers on their built-in endpoint.
Local servers can stay silent for minutes while a request waits in a queue or a model loads, so `ollama`, `llama_cpp`, and any model with a configured endpoint get no limit unless you set one.
A configured endpoint is a `base_url` anywhere in `extra_kwargs` (such as `extra_kwargs.base_url` or `extra_kwargs.client_params.base_url`) or one read from `OPENAI_BASE_URL`, `GROQ_BASE_URL`, `CEREBRAS_BASE_URL`, `ANTHROPIC_BASE_URL`, or `ANTHROPIC_VERTEX_BASE_URL`.
Set the limit explicitly for a hosted service behind a custom endpoint, such as OrcaRouter or the Z.ai coding endpoint.
Endpoint variables that only a provider SDK reads, such as `GOOGLE_GEMINI_BASE_URL`, are not detected; set `0` when one points at a local server.
For a local model, pick a value above its slowest queue wait plus model load; set `0` to turn the limit off for a hosted model:

```yaml
models:
  local:
    provider: openai
    id: qwen3-coder
    extra_kwargs:
      base_url: http://localhost:8080/v1
    stream_idle_timeout_seconds: 1800  # longest wait for a queued request plus model load
  deep_thinker:
    provider: openrouter
    id: openai/gpt-6-astra
    stream_idle_timeout_seconds: 0  # never cut off long silent reasoning
```

## Hot Reload

After a successful config reload, changes to a model definition, including its provider options, model ID, and `context_window`, apply from the next agent, team, or router response without restarting the bot.
Agents with voice calls enabled can still restart, because their call runtime keeps its configuration between responses.

## Model Overrides in Chat

Each agent, team, and the router normally answers with its own configured `model` (`router.model` for the router).
Users can change that per thread or per room, and the most specific choice wins:

1. An explicit model for one run, such as a delegated child run.
2. The thread override set with `!model`, the `thread_model` tool, or a client's model picker.
3. The runtime room default set with `!room_model`.
4. The authored room default in `room_models`.
5. The entity's configured model.

Thread and room overrides name models from the `models:` section and survive restarts.
A Matrix client's model picker lists the configured models and changes the thread override like `!model`.
The picker works in encrypted and unencrypted rooms when the router and at least one agent you may address are joined; teams alone do not qualify.
It shows each model's key, display name, provider, model ID, and icon, never API keys, hosts, local paths, or `extra_kwargs`.
Catalogs with more than 256 models or over 64 KiB are not offered to the picker; `!model` still works.
The client protocol is described in [Model Selection Protocol](https://docs.mindroom.chat/architecture/matrix/#model-selection-protocol).

### `!model`

Show or switch the model that the agents, teams, and router you may address use in the current thread.

```
!model
!model list
!model opus
!model reset
```

`!model` and `!model list` show each thread override with the entities it applies to, plus the available model names.
The override applies from the next message in the thread, and only to the entities whose `access` admits the user who set it in this room; every other entity keeps its own thread override or room-level model.
During a configured team's turn, the team's thread override also applies to its member agents, because the team's `access` reaches them.
`!model reset` removes the override only for the entities you may address.
Other threads and rooms are unaffected.

### `!room_model`

Show or switch the default model for every agent, team, and the router in the current room.

```
!room_model
!room_model list
!room_model opus
!room_model reset
```

`!room_model` and `!room_model list` show the current runtime override and available model names to authorized room members.
Setting and resetting require a Matrix room admin; other users get `❌ Room admin only.`
`!room_model opus` stores a room override without modifying `config.yaml`, keyed by Matrix room ID, so it survives restarts and room-alias changes.
The new default applies to later turns; a turn in progress or waiting for approval keeps the model it started with.
`!room_model reset` removes the runtime override so `room_models` or each entity's configured model applies again.

To set an authored room default in `config.yaml`, map managed room names to model names with `room_models` (default `{}`):

```yaml
room_models:
  lobby: haiku
  dev: opus
```

### [`thread_model`]

The `thread_model` tool lets an agent list models and show, switch, or reset the current thread's model override, like `!model`.
It has no tool-specific configuration.

```yaml
agents:
  assistant:
    tools:
      - thread_model
```

- `list_models()` returns every configured model name with its provider and model ID and works outside a thread.
- `get_thread_model()` returns an `overrides` map from each entity with an active thread override to its model, plus the available model names; an override naming a model since removed from `models` is ignored and reported under `stale_overrides`.
- `switch_thread_model(model_name, when)` accepts a configured model name and rejects unknown names with the available list.
  `when` is `next-turn` (default), which keeps the current response on its model, or `after-toolcall`, which continues the current response with the new model after the tool call.
- `reset_thread_model()` removes the thread override so room-level selection applies again.

All functions except `list_models` require an active thread and return an error outside one.
Overrides set by the tool follow the same entity scope and team rules as [`!model`](#model) and are stored per thread root and entity in `mindroom_data/tracking/thread_models.json`.

# Team Configuration

A team lets several agents answer one request together, either as a named team under `teams:` that has its own Matrix account, or as an ad hoc team formed when a message involves several agents.

<video controls playsinline preload="metadata" aria-label="Mentioning two agents forms a team that answers together" style="width: 100%" poster="https://github.com/user-attachments/assets/a892a233-6585-4156-932f-8addb1409879" data-poster-light="https://github.com/user-attachments/assets/a892a233-6585-4156-932f-8addb1409879" data-poster-dark="https://github.com/user-attachments/assets/613f4f1e-cae4-4e82-a082-282fbbf2d73b">
  <source src="https://github.com/user-attachments/assets/fc66ec35-de5f-4a4e-9494-5bb1f361973a" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/0fc87c04-06ed-4508-bc4f-f461720218dc" type="video/mp4">
</video>

## Team Modes

| Mode | What happens | Use when | Example |
|------|--------------|----------|---------|
| `coordinate` (default) | The team model splits the task into subtasks, assigns each to the member whose role fits, runs them in sequence or in parallel depending on their dependencies, and synthesizes the results | Members need to do different parts of the work | "Get weather and news": one agent fetches weather, another fetches news |
| `collaborate` | Every member works on the same task independently, and the team model synthesizes their answers | You want several perspectives on one question | "What do you think about X?": every agent answers, then the views are combined |

A team reply opens with a `🤝 Team Response (<members>)` header, shows each member's contribution under its name, and ends with a **Team Consensus** section; without a consensus it notes that only the individual responses are shown.
A member that fails, including one that cannot start because of a configuration error such as invalid model settings, fails only the work handed to it: the team model reads why and still answers, and the reply shows the reason under the member's name.

## Named Teams

```yaml
teams:
  dev_team:
    display_name: Dev Team
    role: Development team for building features
    agents: [architect, coder, reviewer]
    mode: coordinate
    rooms: [dev]
    model: sonnet

  research_team:
    display_name: Research Team
    role: Research team for comprehensive analysis
    agents: [researcher, analyst, writer]
    mode: collaborate
    num_history_runs: 8
    compaction:
      enabled: true
      threshold_percent: 0.8
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `display_name` | string | *required* | Name shown in Matrix |
| `role` | string | *required* | Description of the team's purpose, which the team model reads beside each member agent's own `role` when it chooses whom to delegate to |
| `agents` | list | *required* | Names of agents under `agents:` that form the team; at least one, no duplicates |
| `mode` | string | `coordinate` | `coordinate` or `collaborate`; any other value fails config validation |
| `rooms` | list | `[]` | Room keys, aliases, or Matrix room IDs the team joins and responds in |
| `accept_invites` | bool or list | `true` | `true` accepts every room invitation, `false` or `[]` accepts none, and a list accepts only inviters matching an exact or wildcard Matrix user ID; this is separate from the members' own settings. Joining a room never grants its members access; see [Invitations](https://docs.mindroom.chat/authorization/#invitations) |
| `access` | object | `null` | Who may converse with the team: `current_room_members`, `members_of_rooms`, and `users`. When omitted, members of the team's own managed `rooms` have access. See [Responder access](https://docs.mindroom.chat/authorization/#responder-access) |
| `model` | string | `"default"` | Key under `models:` used for coordination and synthesis |
| `max_tool_calls_per_turn` | int, >= 1 | `defaults.max_tool_calls_per_turn` | Tool calls the team may execute in one turn, delegations to members included; each member keeps its own budget, and the end-of-budget behavior matches [agents](https://docs.mindroom.chat/configuration/agents/#configuration-options) |
| `num_history_runs`, `num_history_messages`, `max_tool_calls_from_history`, `compaction` | | | History replay and compaction for the team's shared history; see [Team History and Compaction](https://docs.mindroom.chat/configuration/history/#team-history-and-compaction) |

Team keys follow the agent [naming rules](https://docs.mindroom.chat/configuration/agents/#naming-rules), and a key cannot be used under both `agents:` and `teams:`.
A field a team entry does not have, such as an agent-only field placed under a team, fails config validation with an error that names it; move it to the right entry or remove it.
Private agents, and agents that can reach a private agent through `delegate_to`, cannot be team members; see [Private Instances](https://docs.mindroom.chat/configuration/agents/#private-instances).

## Ad Hoc Teams

MindRoom forms a team without any `teams:` entry when:

1. **Several agents are tagged in one message**, for example `@code @research analyze this`.
2. **An untagged follow-up in a thread** where several agents were mentioned earlier or several agents have replied.
3. **An untagged message in the main timeline of a DM room** that contains several agents.

Tagging exactly one agent gets a reply from that agent alone.
Threads where two or more people have posted never form a team from earlier thread context; tag the agents again in the current message (see [Multi-Human Thread Protection](https://docs.mindroom.chat/configuration/threads/#multi-human-thread-protection)).

If any tagged agent cannot take part, for example because it is not in the room or not available to you, MindRoom replies with the reason instead of forming a partial team, such as `Team request includes agent 'code' that is not available to you in this room.`
For teams formed from thread context or a DM room, unavailable agents are left out, and if only one remains it answers alone.

The `default` model picks the mode for each ad hoc team from the request: `coordinate` when the agents have different subtasks, `collaborate` for opinions or brainstorming, and `collaborate` when the choice fails.
Ad hoc teams take their history and compaction settings from `defaults`; see [Team History and Compaction](https://docs.mindroom.chat/configuration/history/#team-history-and-compaction).

# Router Configuration

The router is a built-in MindRoom account that is always present, cannot be disabled, and joins every room with configured agents or teams.
It picks which agent or team answers a message that mentions no one, accepts room invitations, and sends welcome messages.
Configure it under `router:` to choose its routing model, who may invite it into rooms, who may interact with it, and optional System One or OpenAI Decisions responder selection.

See also [Threads, Replies & Participation](https://docs.mindroom.chat/configuration/threads/), [Access Control](https://docs.mindroom.chat/authorization/), and [Logs & Monitoring](https://docs.mindroom.chat/deployment/operational-log-events/).

## Configuration

```yaml
router:
  model: haiku          # Must exist under models
  accept_invites: true
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `model` | string | `"default"` | Model alias used for routing decisions |
| `judgment` | object or null | `null` | Optional System One or OpenAI Decisions responder selection before LLM routing; see [Responder Selection Judgments](#responder-selection-judgments) |
| `access` | object or null | `null` | Who may interact with the router; see [Access Control](https://docs.mindroom.chat/authorization/) |
| `accept_invites` | bool or list[string] | `true` | `true` accepts every room invitation, `false` or `[]` accepts none, and a list accepts only inviters matching an exact or wildcard Matrix user ID |

Point `model` at a cheap, fast alias to keep routing inexpensive.
Room and thread model overrides also apply to routing.

### Invitations and access

Inviter patterns are matched after [identity alias](https://docs.mindroom.chat/authorization/#requester-identity-and-private-state) resolution and use the same case-sensitive wildcard rules as `access.users`.
Rooms joined through an invitation are kept across restarts and survive room cleanup.
Accepting an invitation only joins the room; every interaction afterwards still requires the router's `access` policy.
Router `access` defaults to `current_room_members: true`, which still applies when you set only `access.users`; set `current_room_members: false` to remove it.

## How Routing Works

When a message mentions no agent or team, MindRoom first finds the responders the sender may address in that room.

- In configured rooms, candidates are the agents and teams that list the room in `rooms`.
- In ad-hoc rooms the router joined by invitation, candidates are the MindRoom agents and teams currently joined to the room.
- Either way, candidates the sender is not allowed to address are removed.

Then:

1. If the thread requires explicit targeting, the router stays silent; eligible individual agents already in the thread may still use opt-in [adaptive participation](https://docs.mindroom.chat/configuration/threads/#adaptive-participation).
2. If exactly one eligible responder remains, it answers directly without an AI routing call.
3. If several remain, the router reads the message and up to three previous thread messages (each cut to 100 characters) and picks the best match from the candidates' roles, tools, and instructions.
4. The router posts a message mentioning the chosen agent or team (for example "@code could you help with this?"), and that entity answers in the thread.

If routing fails, for example because of a model error or an invalid choice, the router replies "Please try mentioning an agent or team directly with @ or rephrase your request."
Mentioning an agent or team with `@name` always bypasses routing, but the entity still answers only if it is in the room, listed for that room in its `rooms` when the room is configured, and allowed by its `access`.
An entity invited into a configured room that does not list it posts a note when it joins, and repeats that note instead of answering when it alone is mentioned there: the room's agents are managed in the MindRoom configuration, so talk to the entity in a new room it is invited to.
A request that mentions several entities gets the same explanation when every entity it cannot use is such an invited, unlisted entity.
The note speaks only to people the entity's `access` admits and names the room's agents they can ask instead.
See [Multi-Human Thread Protection](https://docs.mindroom.chat/configuration/threads/#multi-human-thread-protection) for when threads require explicit tags.

### Mentioning the router

The router is not a conversational agent.
A message that mentions only the router gets a short rules-of-engagement reply instead of an answer.
Mention a specific agent or team to get an answer, or several agents for an ad-hoc collaboration.

## Responder Selection Judgments

Set `router.judgment` to let System One (TypeSafe) or the OpenAI Decisions API choose among the eligible agents and teams before ordinary LLM routing.
It is off by default, even when the backend's API key is set.
Explicit mentions, thread participation rules, access control, and the single-candidate shortcut work the same way with or without it.

Set `TYPESAFE_API_KEY` in the process environment or the `.env` next to `config.yaml`, then enable it:

```yaml
router:
  model: default
  judgment:
    provider: typesafe
    threshold: 0.8
    timeout_seconds: 1.5
```

For the OpenAI Decisions API, set `provider: openai_decisions`; it uses the OpenAI API key described in [Judgment Backends](https://docs.mindroom.chat/configuration/threads/#judgment-backends).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `provider` | string | Required | `typesafe` or `openai_decisions` |
| `threshold` | float | `0.8` | From `0` to `1`; both the chosen option's probability and the backend's confidence must reach it |
| `timeout_seconds` | float | `1.5` | Positive deadline of at most `30` seconds |

Unknown fields are rejected.

The backend receives each candidate's role, tools, delegation targets, and brief instructions, the current message, and the last three visible messages with senders replaced by aliases.
Tool calls in those messages appear as `<tool>` tags with the same short input and output previews the chat shows.
It does not receive system prompts, full tool output, attachment contents, or the rest of the conversation.
It runs only with 2 to 253 candidates and a request of at most 16,000 bytes.
Long recent messages are shortened to their start and end, and the oldest drop out first; the current message is never shortened, so a long current message or input that looks like a secret uses ordinary routing instead.

| Outcome | Result |
|---------|--------|
| A candidate is chosen | The router mentions that agent or team as usual |
| `no_fit` | The router asks the user to mention a responder or rephrase; OpenAI-compatible [`auto`](https://docs.mindroom.chat/openai-api/#auto-routing) requests get HTTP 400 with code `no_suitable_responder` |
| `multiple` | Ordinary LLM routing picks one responder; several agents are never launched |
| Tie, low confidence, abstention, missing key, full judgment limit, timeout, or error | Ordinary LLM routing with `router.model` |

The router's prompt overrides apply only to LLM routing, not to the judgment backend.
Router judgments share the process-wide limit and logging described in [Judgment Backends](https://docs.mindroom.chat/configuration/threads/#judgment-backends), with at most one router judgment at a time.

## Welcome Messages

After accepting an invitation into a room with no message history, the router posts a welcome if the inviter has router access, listing the agents and teams the inviter may address, how to mention them, and a quick command reference.
At startup, empty configured rooms get a welcome listing their configured responders.
Send [`!hi`](https://docs.mindroom.chat/chat-commands/#hi) to see the welcome again, limited to responders visible to you.

The router's other duties are documented on their own pages: [command handling](https://docs.mindroom.chat/chat-commands/#command-handling), [room management](https://docs.mindroom.chat/rooms/#room-management), [voice message processing](https://docs.mindroom.chat/voice/#voice-message-processing), [configuration confirmations](https://docs.mindroom.chat/chat-commands/#configuration-confirmations), and [running scheduled tasks](https://docs.mindroom.chat/scheduling/#scheduled-task-restoration).

# Threads, Replies & Participation

This page covers how agents group replies into threads or one room conversation, when agents answer in threads with several people, how agents can opt into judging untagged messages, what happens to messages sent while an agent is still working, and how edits and restarts affect replies.
See also [Routing & Responder Selection](https://docs.mindroom.chat/configuration/router/), [Access Control](https://docs.mindroom.chat/authorization/), and [Logs & Monitoring](https://docs.mindroom.chat/deployment/operational-log-events/).

## Thread Mode Resolution

Each agent's `thread_mode` and `room_thread_modes` fields (see [Agent Configuration](https://docs.mindroom.chat/configuration/agents/)) choose between Matrix threads and one continuous conversation per room.
A room override set with [`!thread_mode`](#thread_mode) takes precedence for every agent, team, and the router in that room.
Without one, an agent uses the `room_thread_modes` entry matching the current room by Matrix room ID, room key, or alias, and otherwise its `thread_mode`.

A team uses its members' mode for the room when all members agree, and `thread` when they differ.
The router uses the mode shared by all agents relevant to the room, including agents that join through `teams.<name>.rooms`, and `thread` when they differ.

### `!thread_mode`

Show or switch how future agent replies are grouped in the current Matrix room.

```
!thread_mode
!thread_mode show
!thread_mode room
!thread_mode thread
!thread_mode reset
```

`!thread_mode` and `!thread_mode show` (also `list` or `status`) show the current room override.
`!thread_mode room` uses one continuous conversation for the whole room.
Room-mode conversations are not included in [thread exports](https://docs.mindroom.chat/thread-exports/).
`!thread_mode thread` uses Matrix threads for separate conversations in this room.
`!thread_mode reset` (also `clear`) removes the room override so agents use their configured `thread_mode` and `room_thread_modes` again.
Only Matrix room admins can set or reset the override; others get `Room admin only.`
The override is stored in MindRoom's runtime state under `mindroom_data/tracking`, not in `config.yaml`, so it works when config is static or read-only.

## Threading (MSC3440)

Agents reply in [MSC3440](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3440-threading-via-relations.md) threads.
In `thread` mode, an agent answering a room-level message starts a new thread on it.
Clients and bridges that send plain replies instead of thread replies still work: a plain reply whose reply chain reaches a thread joins that thread, and one that never reaches a thread stays room-level.
Edits, reactions, and redactions belong to the same thread as the message they target.

## Multi-Human Thread Protection

When two or more people have posted in a thread, agents answer only when explicitly mentioned, so they do not interrupt a human conversation.
The router stays silent in such threads, and they never automatically form ad hoc teams.
Agents that already replied there can opt into [Adaptive Participation](#adaptive-participation) to judge untagged messages.

The rules are:

1. **Mentioned eligible agents or teams respond**: an explicit `@agent` or `@team` bypasses AI routing, but room configuration and reply permissions still apply.
2. **Non-thread messages**: a single eligible agent or team can auto-respond, regardless of how many humans are present.
3. **Threads with one human**: normal auto-response behavior applies, so the agent or team continues the conversation.
4. **Threads with two or more humans**: explicit targeting is required, except for agents using adaptive participation.
5. **Mentioning another joined room participant**: if a message tags only joined users who are neither agents, teams, nor configured bot accounts, agents and teams stay silent.

MindRoom reads a message's mentions from `m.mentions.user_ids`, or when that is empty from HTML mention pills in `formatted_body`, or otherwise from plain `@name` text in the body, so bridged messages without mention metadata still count.
Mentions of users who are not in the room or only invited do not suppress normal responses.
Mentioning a configured agent that has not joined the room still counts as an explicit mention for the other agents.

A thread too long to read at once, more than 2,000 messages or 16 MiB of stored content, is not continued from an untagged message when the room has more than one possible responder, unless an agent is still answering there; mention the agent or team to continue it.

### Bot accounts

Any Matrix user that is not a MindRoom agent or team counts as a human for these rules, including bridge bots (Telegram, Slack, etc.) and other non-MindRoom bots.
A bridge bot relaying messages into a thread therefore looks like a second human and makes agents require mentions.
List such accounts in the top-level [`bot_accounts`](https://docs.mindroom.chat/authorization/#bot-accounts) field so their messages and mentions do not count toward multi-human detection.

## Adaptive Participation

Set `agents.<name>.participation` to let an agent that already replied in a multi-human thread decide whether to answer an untagged message or stay silent.
`participation: {}` enables it with the defaults below; omitting it or setting it to `null` keeps the mention requirement.
The setting applies in every room where the agent may reply, including ad hoc rooms, and has no per-room override.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `debounce_seconds` | number | `3.0` | Quiet window before deciding, from `0` to `30` seconds; a burst of messages from one sender is judged as one turn |
| `instructions` | string | `""` | Additional guidance for deciding whether the agent should participate |
| `decline_reaction` | string or null | `null` | Reaction to a deliberate decline, such as `"👀"`; nonblank and at most 64 characters; `null` keeps declines invisible |
| `judgment` | object or null | `null` | Separate [judgment backend](#judgment-backends); `null` lets the agent's reply model decide |

```yaml
agents:
  assistant:
    display_name: Assistant
    participation:
      debounce_seconds: 3.0
      instructions: "Reply when you can help; leave human conversation uninterrupted."
      decline_reaction: "👀"
```

An agent judges a message only when all of these hold:

- The message is text in a thread with at least two human participants, including the sender.
- The message mentions neither an agent nor another human; mentioned messages follow the normal rules.
- This agent has already replied in the thread and may still reply to the sender.

Configured human aliases count as one participant, while agents, the internal service account, and `bot_accounts` do not count as humans.
Agents merely present in the room never join: with 50 agents in a room and two already in the thread, only those two can judge, and only if each opted in.
Each eligible agent decides separately.
Commands and scheduled work never use adaptive participation.

An agent that declines sends no reply; with `decline_reaction` set, it reacts once to the latest message of the turn.
Choose the emoji with care, because 👍 can read as agreement with the message.
A failed check never reacts and the agent stays quiet.

Without `judgment`, the agent's own reply model decides with tools disabled.
Gemini explicit context caches, OpenAI Chat search-only requests, OpenRouter automatic web search, and Groq Compound systems cannot decide this way, so agents using them stay quiet unless a separate judgment backend approves.
After a judgment backend approves, the agent replies normally with its own model, including those provider modes.

### Judgment Backends

`participation.judgment` and `mid_turn.judgment` select a separate decision model, while the agent's configured model still writes the reply.

| Field | Backend | Default | Description |
|-------|---------|---------|-------------|
| `provider` | all | Required | `llm` for a configured model alias, `typesafe` for System One, or `openai_decisions` for the OpenAI Decisions API |
| `model` | `llm` | Required | Existing alias under `models`, which can be cheaper than the reply model |
| `threshold` | `typesafe`, `openai_decisions` | `0.8` | Minimum probability from `0` to `1`; rejected for `llm` |
| `credentials_service` | `openai_decisions` | `null` | Credential service holding the OpenAI API key, instead of the OpenAI model credential |
| `timeout_seconds` | all | `5` for `llm`, otherwise `1.5` | Positive deadline of at most `30` seconds; for participation it starts after `debounce_seconds` |

Unknown fields are rejected.

```yaml
agents:
  assistant:
    display_name: Assistant
    participation:
      instructions: "Reply when you can help; leave human conversation uninterrupted."
      judgment:
        provider: llm
        model: fast
        timeout_seconds: 5
```

To use System One instead, set `TYPESAFE_API_KEY` in the process environment or config-adjacent `.env` and change the judgment:

```yaml
      judgment:
        provider: typesafe
        threshold: 0.8
        timeout_seconds: 1.5
```

To use the OpenAI Decisions API with `gpt-6-luna`, set `provider: openai_decisions` instead.
It uses the same OpenAI API key as OpenAI models, from `OPENAI_API_KEY` or the dashboard, and always calls `api.openai.com`, regardless of any model `base_url`.
When OpenAI models go through a proxy, that credential holds the proxy's key, which `api.openai.com` rejects; set `credentials_service` to a service holding a real OpenAI key instead.
A named service that has no key skips the backend; it never falls back to the OpenAI model credential.
No environment variable fills a custom service name, so add its `api_key` in the dashboard or through [credential seeds](https://docs.mindroom.chat/oauth-framework/#credential-seeds).

The LLM backend uses the alias's normal provider credentials and receives no tools, agent system prompt, or agent memory.
A model alias whose provider adds native tools that cannot be disabled is refused.
For participation, a TypeSafe or OpenAI Decisions probability at or above `threshold` approves; the default `0.8` has not been calibrated on representative conversations.
All backends get the same question and context, but their judgments can differ.

At most eight judgments run at once across the process, including [router judgments](https://docs.mindroom.chat/configuration/router/#responder-selection-judgments), and one per agent; a judgment that finds the limit full is not queued and is treated as failed.
Judgment outcome logs record backend, model, decision, latency, token usage, input size, and failure category, plus probability and threshold for TypeSafe and OpenAI Decisions, without request text or credentials.

#### Participation Context and Fallback

A participation judgment sends the agent's guidance and up to eight recent user and assistant messages, with Matrix identities replaced by aliases.
System prompts, memory, tool definitions and results, and media are not sent, but message text can still contain private information.
When the context contains media, attachment references, detected secrets, or more than 16 KB of text, the backend is skipped and the reply model decides instead.
Missing credentials, a full limit, a timeout, a provider error, malformed output, or an abstention also fall back to the reply model's decision, and the agent stays quiet if that fails too.

## Mid-Turn Coalescing

When another human message arrives while an agent is responding, MindRoom normally sends the agent a wrap-up notice after its current tool batch, asking it to stop making new tool calls and summarize its progress.
A "thanks" or "is this still running?" can therefore cut a long task short.
Set `agents.<name>.mid_turn` to let a judge decide whether the queued messages can wait until the task finishes.
The setting applies in every room where the agent may reply and has no per-room override; teams do not inherit it from their members.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `judgment` | object | Required | LLM model alias, TypeSafe, or OpenAI Decisions backend; see [Judgment Backends](#judgment-backends) |
| `instructions` | string | `""` | Extra guidance for the continue-or-wrap-up decision |
| `defer_reaction` | string or null | `null` | Reaction such as `"👀"` on a queued message that can wait |

```yaml
agents:
  helper:
    display_name: Helper
    mid_turn:
      instructions: Continue for acknowledgements; wrap up for corrections or changed requirements.
      defer_reaction: "👀"
      judgment:
        provider: llm
        model: fast  # An existing alias under models
        timeout_seconds: 5
```

The judge lets the task continue for acknowledgements, thanks, "continue", "do not interrupt", and unrelated requests, which wait for a later turn.
It asks for a wrap-up when a message stops, pauses, or corrects the task, changes a relevant requirement, or asks to switch tasks now, even alongside praise.
When the judge is unsure, times out, or fails, the agent gets the wrap-up notice.
With TypeSafe or OpenAI Decisions, the task continues only when the probability that no interruption is needed is at least `threshold` (default `0.8`).

The judge sees the active request, recent earlier messages in the thread, up to eight queued messages, the reply text the agent had published when each message arrived, and your `instructions`.
Long earlier messages are shortened and the oldest drop out first, so the request fits the judge's 16 KB limit.
Earlier files and images appear only as their file name and any caption, such as `[file: report.pdf]`.
Tool calls appear as `<tool name="..." input="..." output="..."/>` tags built from the previews the chat shows in its tool details: up to 1,200 characters of input and 500 of output per call, with recognized secrets already removed.
Attachment contents, full tool output, system prompts, and memory are never sent, but message text and tool previews can still contain private information.

The agent gets the wrap-up notice without a judgment when:

- the thread's history could not be read completely;
- the active request or a queued message has an attachment or is not typed text, such as a scheduled task or a voice message;
- the active request and queued messages alone exceed 16 KB, or more than eight messages are queued;
- anything the judge would see looks like a credential;
- the agent replies in `thread_mode: room`.

Each skip logs `Mid-turn judgment skipped` with a `reason` such as `history_unavailable` or `essential_input_too_large`, and judge timeouts and errors log `Mid-turn continuation evaluated` with a `failure`.

The check runs between completed tool batches and never interrupts a running tool.
With background tool jobs enabled, the judge also decides whether a queued message moves a waiting tool call to the background; see [When a call becomes a job](https://docs.mindroom.chat/tools/agent-orchestration/#when-a-call-becomes-a-job).
Queued messages are handled after the active response finishes either way.
A reply waiting for a [tool approval](https://docs.mindroom.chat/tool-approval/) counts as responding, so messages queue behind it until the approval ends.
A decision to continue covers only the messages the judge saw, so a later message gets its own decision, and a wrap-up notice once sent is not taken back.
The wrap-up notice asks the agent to hand off; it does not cancel tools, abort the response, or show the agent the queued text, and stopping a response and tool approval work as usual.
With `defer_reaction` set, each message that can wait gets that reaction once.

## Message Edits

When a user edits the latest message of a conversation, the agent regenerates its reply to that message for the updated content and edits the reply in place.
If that reply is still streaming, the edit stops it first and the regenerated answer replaces it.
A text message edited while it still waits to be gathered with the messages sent around it is answered as edited; an edit made after that, before the agent starts sending its reply, leaves the original text as the one answered.
Once someone other than an agent writes again in the conversation, an edit of an earlier message changes no reply.
While a reply waits for a tool approval, an edit of its message cancels that approval and regenerates the reply.
Edits by agents never trigger regeneration, and an agent's message edited while it still waits is answered with its original text.
When another agent's reply finishes with a mention of this agent, it reaches this agent as a new message.

# History & Compaction

This page covers how much earlier conversation an agent or team sees on each reply, how a model's `context_window` keeps that history within the provider limit, and how compaction summarizes old history so long conversations keep working.
Use it to tune replay depth, set up or disable compaction, or explain why an agent forgot earlier context or posted a `Compacting history...` notice.

## History Settings

Set these fields in `defaults`, on an agent, or on a team (see [Team History and Compaction](#team-history-and-compaction)).

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `num_history_runs` | int, >= 1 | `null` | Number of prior runs to replay as history (`null` = all). Mutually exclusive with `num_history_messages` |
| `num_history_messages` | int, >= 1 | `null` | Maximum messages replayed from history (`null` = no limit). Mutually exclusive with `num_history_runs` |
| `max_tool_calls_from_history` | int, >= 0 | `null` | Maximum tool-call messages replayed from history. `null` inherits `defaults.max_tool_calls_from_history`; no limit when that is also `null` |
| `compaction` | object | `defaults.compaction` | Compaction overrides; see [Agent Compaction Settings](#agent-compaction-settings) |

An agent or team that leaves both `num_history_runs` and `num_history_messages` at `null` inherits both from `defaults`; setting either one replaces both defaults.
History replay is unlimited only when the inherited values are also `null`.
Each turn's context, such as retrieved memories, `persist=False` enrichment, and the note telling the agent how a tool approval was decided, replays with that turn until compaction, except under `num_history_messages`, which counts and replays only conversation messages.
Text compaction summaries leave that context out, while a [native compaction](#native-compaction) summary can include it, and so can a later text summary that starts from a Claude checkpoint.
Tool results replay unchanged.

## Context Window

Set `context_window` on each model to the provider's actual limit (see [Models](https://docs.mindroom.chat/configuration/models/)).
When the active model has a known `context_window`, MindRoom fits replayed history to it before each reply, reducing raw replay, replaying only the saved summary, or skipping saved history for that run when needed.
Your configured `num_history_runs` stays unchanged; the reduction applies only to the run that needs it.
Without a known `context_window` (and without `compaction.replay_window_tokens`), history is not fitted and automatic text compaction is unavailable.

With compaction enabled, MindRoom also checks every model request of a reply against a known `context_window`, including the requests after tool results, and compacts when one would not fit; see [Compaction during a reply](#compaction-during-a-reply).

```yaml
models:
  default:
    provider: anthropic
    id: claude-sonnet-5-5
    context_window: 1000000  # 1M tokens

defaults:
  compaction:
    replay_window_tokens: 200000  # Keep replayed history around a smaller operational window
```

## Agent Compaction Settings

Compaction is enabled by default through `defaults.compaction`.
It summarizes older history so later replies keep useful context instead of losing it to replay fitting.
Supported OpenAI and Claude routes use [native compaction](#native-compaction) inside ordinary requests; every other route uses portable text compaction.
Text compaction runs before a reply when the request, including the system prompt and tools, exceeds the compaction threshold on a route without native compaction, when history exceeds the hard replay budget, or when [requested manually](#manual-compaction), and [during a reply](#compaction-during-a-reply) when its next model request would not fit.
While it runs, the agent posts `Compacting history...` and edits that notice with the result.
If compaction fails, the notice reads `Compaction failed; continuing without compaction.` with the reason; before a reply, the reply continues with fitted history.

Set these fields under `defaults.compaction`, an agent's `compaction`, or a team's `compaction`:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | `false` disables automatic compaction for the scope, both before and during a reply |
| `threshold_tokens` | int, >= 1 | `null` | Compaction trigger in tokens, counted over the whole request including the system prompt and tools: the native provider trigger on native routes, and the point where text compaction runs on every other route, unless the system prompt, tools, and saved summary already exceed it. Mutually exclusive with `threshold_percent` |
| `threshold_percent` | float, > 0 and < 1 | `null` | The same trigger as a fraction of the effective replay window. When neither threshold is set, the trigger is 80% of that window |
| `replay_window_tokens` | int, >= 1 | `null` | Cap replayed history and compaction planning below the model's real `context_window` without lowering the provider request limit. The smaller of the two applies; when the model window is unknown, this value alone sets the replay window |
| `reserve_tokens` | int, >= 0 | `16384` | Headroom kept free for the current prompt, tool definitions, and output; set it at least to the model's maximum output tokens, including any thinking budget |
| `model` | string | `null` | Model config name used to write summaries; `null` uses the active model. Must define its own `context_window` |
| `fallback_model` | string | `null` | Different model config retried once when the summary model refuses for safeguards; after success it writes the remaining summaries for that compaction. Must define its own `context_window` |
| `timeout_seconds` | float, > 0 | `600` | Maximum seconds for each summary request; a shorter provider timeout still applies |

An agent or team `compaction` block overrides `defaults.compaction` field by field, and setting a field to `null` clears the inherited value.
Setting either threshold in an override replaces an inherited value of the other.
A `compaction` block that sets any field enables compaction for that scope, even when `defaults.compaction` disables it, unless it sets `enabled` to `false` or `null`.
A block that only clears `model` or `fallback_model` with `null` keeps the inherited enabled state.
Set `defaults.compaction: null` or `enabled: false` to turn compaction off everywhere that does not override it.

`model` and `fallback_model` must name entries under `models:` that define `context_window`; otherwise config validation fails with `Compaction model references unknown models: ...` or `Explicit compaction model references require a model with context_window: ...`.
A `fallback_model` that resolves to the same provider and model ID as the summary model is ignored.

Text compaction needs more than 2,000 tokens of summary input after `reserve_tokens` and prompt overhead are subtracted from the summary model's `context_window`.
With the default `reserve_tokens`, a summary model with a context window of roughly 10,000 tokens or less cannot compact; lower `reserve_tokens` to make it work.

### Compaction during a reply

A long reply, for example a tool loop that keeps reading files, can outgrow the context window before it finishes.
Before each model request, MindRoom checks whether the request would exceed the replay window minus `reserve_tokens`; if it would, it compacts first and the reply continues.
After that compaction the model sees its system prompt, the user's current message, the current turn's context, and a summary that records the progress made so far; earlier conversation and the reply's own earlier tool calls and results are replaced by that summary.
The tool calls still appear in the reply.
If the summary and the current message together exceed the model's `context_window` minus `reserve_tokens`, the reply fails with `Saved conversation summary exceeds the available history budget` (see [Compacted history and redaction](#compacted-history-and-redaction)).
If the summary model fails, the reply continues without compacting and may then fail with a provider error.
A reply that restarts after such a compaction, for example after a crash, continues from the summary, so it knows which tool calls already ran.
With compaction disabled, or with a summary model too small to compact, nothing bounds the requests of a reply, and one that outgrows the model's window fails with a provider error.

A compaction leaves the system prompt and tools unchanged, so provider prompt caching keeps working after it.
Agents, teams, subagents, workflow participants, and minimal-mode agents all compact the same way.
Team members, workflow participants, and scheduled tasks with a `history_limit` of `0` compact only their own work, using their own compaction settings, and post no notice.
Subagents and replies resuming after a tool approval compact the conversation like any other reply but post no notice.

### Compacted history and redaction

Text compaction keeps every run in the conversation database and replays a merged summary in place of the older ones.
Compaction deletes no stored history; only Matrix redaction, regenerating an edited message, and conversation deletion remove runs.
Redacting a message, including one the agent never answered, removes the first stored run that read, answered, or wrote it, and every later run, from an agent's or team's history before that agent or team next replies in the conversation (for a private agent, its next reply to that requester).
Until then the text stays in local storage; a reply that continues after a tool approval, and a voice-call reply, do not remove it first.
Redacting a Matrix event that a compacted run consumed rolls compaction back to just before that run, removing that run and everything after it, instead of clearing the whole conversation.
Editing a message whose reply compacted history during the reply rolls that compaction back too, so the regenerated reply never sees a summary of the reply it replaces.

If a reply fails with `Saved conversation summary exceeds the available history budget`, the saved summary no longer fits beside the current prompt.
This can happen after switching to a model with a smaller context window or when system instructions and the current message leave too little room.
Choose a model with a larger context window or shorten the prompt; MindRoom never silently drops or truncates the saved summary.

### Native compaction

MindRoom supports automatic [OpenAI Responses compaction](https://developers.openai.com/api/docs/guides/compaction) and [Claude compaction](https://platform.claude.com/docs/en/build-with-claude/compaction).

- **OpenAI**: GPT-5.3 Codex, GPT-5.4, and GPT-6 model families on the official Responses endpoint or the Codex login backend.
  An explicit `store: true`, background mode, an alternate OpenAI endpoint, or a custom context-management request keeps the model on text compaction.
- **Claude**: direct Anthropic and Vertex AI Claude on Sonnet 4.6 and later, Opus 4.6 and later, Fable, and Mythos models.
  This is a provider beta and needs a trigger of at least 50,000 tokens.
  An authored `compact_20260112` context-management policy keeps its own trigger and instructions, and `pause_after_compaction: true` is rejected because MindRoom needs the provider to continue its response.
- Gemini, Chat Completions, and all other providers use text compaction.

Native compaction also requires compaction to be enabled, replay of all history (no `num_history_runs` or `num_history_messages`), no `max_tool_calls_from_history`, and a trigger above the current prompt size and below the hard request limit.
An explicit `compaction.model`, a [scheduled task](https://docs.mindroom.chat/scheduling/) with a `history_limit`, or a request already over the hard budget uses text compaction instead.
Native checkpoints survive restarts and are tied to their provider, model, and endpoint; after a model switch, the conversation is rebuilt from its full stored history.
When that history then needs text compaction, it starts from the latest Claude checkpoint's summary instead of summarizing everything that checkpoint covers again, as long as the summary and turns before it are still the ones its request replayed and leave room for its turn; OpenAI checkpoints are encrypted, so they are skipped.
If a reply's request still would not fit, for example after a very large tool result, native compaction stays off for the rest of that reply and it [compacts as text](#compaction-during-a-reply).
A completed response that carries a provider checkpoint logs a `Native compaction checkpoint` event with the trigger (`threshold_tokens`, `null` for an authored policy) and the checkpoint's conservative size estimate (`checkpoint_tokens`, from byte length for OpenAI's encrypted checkpoints).
The `Compaction summary chunk` events and the `compaction_*` fields of `Dispatch pipeline timing` cover text compaction only.

### Manual compaction

Add the `compact_context` tool to an agent's `tools` to let it request compaction itself.
The request runs before the next reply in the same conversation and always uses text compaction.
It requires a summary model with a `context_window` and the same 2,000-token summary input budget; otherwise the tool returns `Error: Compaction is unavailable for this scope because ...` with the reason.

## Team History and Compaction

Named teams accept `num_history_runs`, `num_history_messages`, `max_tool_calls_from_history`, and `compaction`, with the same meaning, limits, and mutual exclusion as for agents.
The team's shared history uses the team's own settings, never a member agent's, and the team model's `context_window` sets its replay budget.

[Ad hoc teams](https://docs.mindroom.chat/configuration/teams/#ad-hoc-teams) have no `teams:` entry, so their history and compaction settings come from `defaults`, not from any participating agent.
Team members keep no history of their own.

# Memory System

Memory lets agents remember facts, preferences, and context across conversations.
This page covers choosing a memory backend, configuring embeddings and extraction, file-based memory, and the `memory` tool.

| Backend | What it does | Use it when |
|---|---|---|
| `mem0` (default) | Vector memory: relevant memories are retrieved before each reply and new ones are extracted automatically after each turn | You want hands-off semantic memory |
| `file` | Markdown files (`MEMORY.md` plus dated notes in `memory/`) that are the source of truth | You want memory you can read and edit, or an OpenClaw-style workspace |
| `none` | No built-in memory | The agent should keep no memories |

<video controls playsinline preload="metadata" aria-label="The agent remembers each person's diet and plans the week's dinners" style="width: 100%" poster="https://github.com/user-attachments/assets/9046ecb6-3be5-440d-b06e-5917d58d2f59" data-poster-light="https://github.com/user-attachments/assets/9046ecb6-3be5-440d-b06e-5917d58d2f59" data-poster-dark="https://github.com/user-attachments/assets/00d4a4fc-58c3-4968-855a-61089cf19e0b">
  <source src="https://github.com/user-attachments/assets/92f62053-ff1e-4982-80c2-bbbac3c5c757" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/f1d8e8ce-8160-46bf-811d-2a3e71cde673" type="video/mp4">
</video>

## Choosing a Backend

Set the global backend with `memory.backend` (`mem0`, `file`, or `none`; default `mem0`), and override it for one agent with `agents.<name>.memory_backend`.
`memory: none` is shorthand for `memory: {backend: none}`.

```yaml
memory:
  backend: mem0

agents:
  coder:
    display_name: Coder
    role: Write and review code
    memory_backend: file
  scratch:
    display_name: Scratch
    role: Stateless scratchpad agent
    memory_backend: none
```

With `none`, the agent gets no memories in its prompt, stores none after its turns, and does not receive the `memory` tool.
Credential-like values are removed on a best-effort basis before conversation text reaches memory automatically, through `mem0`, auto-flush, a team's `MEMORY.md`, or a voice-call transcript sent to `mem0`; the transcript file itself is kept as spoken.
What an agent writes itself, with the `memory` tool or in a [dreaming](#dreaming) run, is stored as written.
Conversation history is separate and stays in the session; see [History & Compaction](https://docs.mindroom.chat/configuration/history/).
Memories retrieved for a reply [stay in that conversation's history](https://docs.mindroom.chat/configuration/history/#history-settings), so its later replies can still see them after the memory is deleted.
OpenClaw-compatible agents use this same backend selection.

## Memory Scopes

| Scope | ID | Contents |
|---|---|---|
| Agent | `agent_<name>` | The agent's preferences and durable user context |
| Team | `team_<agent1>+<agent2>+...` (member names sorted) | Shared memory from team conversations |

Each team fact is kept once: a team fact one member changes or deletes with the `memory` tool changes for every member that reads it.
An agent's memory reads cover its own scope and the scopes of the configured teams it belongs to, searched through the agent's own backend.
A team's replies are not given recalled memories; the team scope is filled from the conversations the team answers, and its members recall it in their own replies.

Team memory picks one backend from the members' effective backends:

- All members use `file`: the team uses file memory.
- All members use `none`: team memory is disabled.
- Any other combination, including `file` plus `none`, uses Mem0 with the configured `memory.embedder` and `memory.llm`.

A member's `none` setting disables only its own memory, so team conversations it takes part in can still be remembered in the team scope.
A `file` member of a team that uses Mem0 does not see that team's Mem0 memories in its own conversations.

## Backend: `mem0`

Before each reply, Mem0 retrieves memories relevant to the message and adds them to the prompt.
After each turn, the [memory LLM](#memory-llm) extracts durable facts into a Chroma vector store under the agent's storage directory, or, for a team's turn, once into the one Mem0 store under `team_memory/` that holds every team's memories, with each team's records kept apart.
The `mem0` and `chromadb` extras install on first use unless `MINDROOM_NO_AUTO_INSTALL_TOOLS=1` is set; otherwise install `mindroom[mem0,chromadb]`.

### Embedder

`memory.embedder` sets the embedding model used by Mem0, semantic file-memory search, and [knowledge bases](https://docs.mindroom.chat/knowledge/#embedder-configuration).
Mem0 supports the `openai` (default), `ollama`, `huggingface`, and `sentence_transformers` providers; semantic file memory and knowledge bases support the [narrower set](https://docs.mindroom.chat/knowledge/#embedder-configuration).

| Field | Default | Description |
|---|---|---|
| `provider` | `openai` | Embedding provider |
| `config.model` | `text-embedding-3-small` | Embedding model name |
| `config.host` | `null` | Base URL for OpenAI-compatible endpoints, or the Ollama host; for `ollama`, a configured `OLLAMA_HOST` takes precedence over `host`, and without either the host is `http://localhost:11434` |
| `config.dimensions` | `null` | Embedding dimension override (>= 1) for OpenAI-compatible providers |
| `config.api_key` | `null` | Explicit API key for the `openai` provider |
| `config.credentials_service` | `null` | Credential service that supplies the `openai` provider's key |
| `config.query_prefix` | `null` | Text the `openai` provider prepends to search queries |
| `config.document_prefix` | `null` | Text the `openai` provider prepends to indexed documents and stored memories |

Only the `openai` provider reads `api_key`, `credentials_service`, `query_prefix`, and `document_prefix`, and only `openai` and `ollama` read `host`; config load rejects a nonblank value in these fields for any other provider.
Config load also rejects `dimensions` for `ollama` and `sentence_transformers`, which always return the model's full-size vectors.

```yaml
memory:
  backend: mem0
  embedder:
    provider: openai
    config:
      model: text-embedding-3-small
      credentials_service: embedder # Optional strict credential binding
      dimensions: null              # Optional, for example 256
```

Fully local embedders:

```yaml
memory:
  embedder:
    provider: sentence_transformers
    config:
      model: sentence-transformers/all-MiniLM-L6-v2
```

```yaml
memory:
  embedder:
    provider: ollama
    config:
      model: nomic-embed-text
      host: http://localhost:11434
```

MindRoom installs the optional `sentence_transformers` extra the first time the `sentence_transformers` or `huggingface` embedder is used.

Some models expect a prompt in front of each text, different for queries and documents.
With a local `sentence_transformers` model, knowledge, file-memory and knowledge-graph searches get the model's own prompts automatically, while Mem0 embeds without them.
For the same model behind an OpenAI-compatible server such as llama.cpp, set the prompts as prefixes; the `openai` provider applies them everywhere, Mem0 included.
For EmbeddingGemma 2:

```yaml
memory:
  embedder:
    provider: openai
    config:
      model: embeddinggemma-2
      host: http://localhost:8080/v1
      dimensions: 768
      query_prefix: "task: search result | query: "
      document_prefix: "title: none | text: "
```

Knowledge-graph texts all get the query prefix, because Graphiti embeds names, facts and searches through the same calls.
Changing the embedder provider, model, host, dimensions, or prefixes switches Mem0 to a separate collection, so memories stored with the previous embedder are no longer retrieved.

The `openai` embedder takes its API key from the first of these that is set:

1. `memory.embedder.config.api_key`.
2. The service named by `memory.embedder.config.credentials_service`; when it has no key, MindRoom does not fall back to another key.
3. When no service is named, the dedicated `embedder` credential (seeded from `EMBEDDER_API_KEY` or `EMBEDDER_API_KEY_FILE`), then the shared `openai` provider key.

Use `credentials_service` when the embedding endpoint needs a different key than chat, voice, or other OpenAI-compatible models.
With no key at all, keyless local OpenAI-compatible endpoints still work, while a keyed endpoint fails with an authentication error.

An Ollama server that requires a key gets `OLLAMA_API_KEY`.
For knowledge bases, the knowledge graph, and semantic file-memory search, MindRoom reads it from the process environment, then the `.env` beside `config.yaml`.
Mem0's own Ollama clients, for its embedder and an `ollama` memory LLM, read it only from the process environment.
When the embedder rejects its credential, semantic memory search reports the cause in `memory` tool output and in the agent's prompt instead of returning empty results, and `/api/health` includes an `embedder` block until a request succeeds again.

### Memory LLM

`memory.llm` sets the model Mem0 uses to extract memories.
Supported providers are `ollama`, `openai`, `openrouter`, and `anthropic`.
When `memory.llm` is omitted, MindRoom uses `gemma4` on the Ollama server set by `OLLAMA_HOST` (default `http://localhost:11434`).
With `provider: ollama`, `config.host` names the Ollama server only when no `OLLAMA_HOST` is configured, unlike a model's `host`; without either, extraction uses `http://localhost:11434`, and other providers ignore `host`.
With `provider: openai`, use `gpt-6-luna`; other GPT-5 and GPT-6 models that support Chat Completions also work.
For GPT-5 and GPT-6 models, with `provider: openai` or as `openai/<model>` with `provider: openrouter`, Mem0 sends no `temperature`, `top_p`, or `max_tokens` from `config` unless `config.is_reasoning_model` is `false`; set `config.reasoning_effort`, for example `low`, to choose their reasoning effort.

```yaml
memory:
  llm:
    provider: ollama
    config:
      model: gemma4
```

OpenRouter-only example, where both the memory LLM and the embeddings use the `openrouter` key:

```yaml
memory:
  embedder:
    provider: openai
    config:
      model: openai/text-embedding-3-small
      host: https://openrouter.ai/api/v1
      credentials_service: openrouter
      dimensions: 1536
  llm:
    provider: openrouter
    config:
      model: openai/gpt-6-luna
```

The LLM uses `memory.llm.config.api_key` when set and not blank, and otherwise the provider's credential (for example `openrouter`, or a dashboard credential stored as `OPENROUTER_API_KEY`).
When `OPENROUTER_API_KEY` is set in the process environment, the `openai` and `openrouter` memory LLMs send that key to OpenRouter, overriding `memory.llm.config.api_key`.

## Backend: `file`

File memory keeps `MEMORY.md` and dated notes under `memory/` in the agent's workspace, and those files are the source of truth.

```yaml
memory:
  backend: file
  file:
    max_entrypoint_lines: 200
    max_entrypoint_tokens: 50000
  search:
    mode: keyword
    include:
      - memory/**/*.md
    include_entrypoint: false
  auto_flush:
    enabled: true
```

For a single agent, the file backend writes memory only when the agent uses the [`memory` tool](#memory) or edits the files, or when [auto-flush](#file-auto-flush-worker) is enabled; it does not extract memories after every turn like `mem0`.
A team whose members all use `file` appends each message it answers to the team's `MEMORY.md`, even without auto-flush.
Before each reply, MindRoom inlines `MEMORY.md` into the prompt under a header with its path, so the agent does not need to re-read it, and adds memories that match the message.
An ongoing conversation's inlined copy can miss entries added to `MEMORY.md` since it last loaded the file, for example from another conversation, until an hour passes without replies, its history is compacted, or MindRoom restarts; edited or deleted lines apply from its next reply, new conversations see every change right away, and the agent can read the file directly.
`memory.file.max_entrypoint_lines` (default `200`, minimum `1`) caps the inlined lines, and `memory.file.max_entrypoint_tokens` (default `50000`, minimum `1`) caps their estimated size at characters / 4.
Because `add_memory` appends, a file over either cap keeps the lines before its first `- [id=...]` entry and as many of the newest entries as fit, and withholds the older ones; a file without entries, or whose lines before the first entry already reach a cap, keeps its first lines.
Only whole lines are inlined, never part of a line.
When either cap truncates, a marker reports the included and total line counts, both caps, and the path to read for the rest.

### File layout

Agent file memory lives in the agent workspace, `agents/<agent>/workspace/` in the storage directory:

- `MEMORY.md`, where `add_memory` writes
- `memory/YYYY-MM-DD.md`, where auto-flush writes

Team file memory lives once per team, in `team_memory/team_<sorted_members>/MEMORY.md` in the storage directory.
A team whose directory name would pass 255 characters gets a shortened one ending in a hash.

Memory files must be regular files; symlinks, FIFOs, and device nodes are ignored when reading and rejected when writing.
Each file is read up to 1 MiB, cut at its last complete line, and updating a truncated or non-UTF-8 file fails so the rest of it is preserved.

### Searching file memory

`memory.search` controls how `search_memories` and per-turn retrieval search file memory, and `agents.<name>.memory_search` overrides it per agent, with omitted fields inherited.

| Field | Default | Description |
|---|---|---|
| `mode` | `keyword` | `keyword` or `semantic` |
| `include` | `[memory/**/*.md]` | Glob patterns relative to the file-memory root that semantic search indexes; must be non-empty |
| `include_entrypoint` | `false` | Also index `MEMORY.md` for semantic search |

In `keyword` mode, `search_memories` matches structured entries with IDs in `MEMORY.md`, plus entries and plain-text lines in `memory/**/*.md`.
Ordinary prose in `MEMORY.md` reaches the agent through the prompt preload, not keyword search; to reach lines beyond the preload cap, read the file directly or use semantic search with `include_entrypoint: true`.

In `semantic` mode, MindRoom builds a vector index of the `include` files with `memory.embedder` on first use and stores it under `<storage-root>/knowledge_db/` (see [Knowledge storage](https://docs.mindroom.chat/knowledge/#storage)).
Until the index is ready, or when embeddings fail, search falls back to keyword results.
Writes through the `memory` tool, auto-flush, and an applied [dreaming](https://docs.mindroom.chat/scheduling/#dreaming) proposal refresh the index, but a ready index does not notice direct file edits until a later such write refreshes it.
Semantic mode covers the agent's own memory; team file memory is always keyword searched.

```yaml
agents:
  coder:
    display_name: Coder
    role: Write and review code
    memory_backend: file
    memory_search:
      mode: semantic
```

Once its semantic index is ready, an agent's file memory also appears as a read-only source in `search_knowledge_base`.
That source is semantic-only, without keyword fallback, team memory, or memory IDs, so use `search_memories` when you need those.

### Per-requester file memory

To give each requester their own file memory from one agent definition, combine `memory_backend: file` with [`private`](https://docs.mindroom.chat/configuration/agents/#private-instances); the requester's private root then becomes the file-memory root.

## File Auto-Flush Worker

Auto-flush periodically turns the new turns of file-backed agents' conversations into durable memories.
It applies only to agents whose effective backend is `file`, including each requester's private instance.

1. Every `flush_interval_seconds`, a background worker looks for conversations with new finished turns that have been quiet for `idle_seconds`.
2. The agent's own model, or `extractor.model` when set, reads the new messages, plus existing memories to avoid duplicates, and writes durable facts; a longer backlog is read in order over later passes.
3. When the model answers with the `no_reply_token`, nothing is written.
4. Results are appended to `memory/YYYY-MM-DD.md`, and a retry never appends the same result twice.

Compacted turns and voice-call turns are included.
Not auto-flushed: turns that [automations](https://docs.mindroom.chat/scheduling/#automations) start, tasks another agent delegated, team conversations, conversations through the OpenAI-compatible API, and context that hooks attach to messages, such as location.
A message redacted before its turn is flushed stays out of memory, and so do the turn that read it and every later turn its redaction removes from [history](https://docs.mindroom.chat/configuration/history/#compacted-history-and-redaction).
A turn that waits for tool approval is flushed once it finishes.
Enabling auto-flush starts with the turns after that; earlier conversation is not flushed, and turning it off and on again flushes the turns in between.
An agent switched to `file` memory later starts with its turns from about one `flush_interval_seconds` before auto-flush first sees it, and its private instances, including new ones, keep all their turns since then.

```yaml
memory:
  backend: file
  auto_flush:
    enabled: true
    flush_interval_seconds: 300
    idle_seconds: 120
    batch:
      max_sessions_per_cycle: 10
    extractor:
      max_messages_per_flush: 20
```

| Field | Default | Description |
|---|---|---|
| `enabled` | `false` | Turn on auto-flush |
| `flush_interval_seconds` | `300` (min 5) | Time between worker passes |
| `idle_seconds` | `120` (min 0) | How long a conversation must be quiet before its new turns are flushed |
| `stale_ttl_seconds` | `86400` (min 60) | Give up on turns whose flush has kept failing for this long |
| `retry_cooldown_seconds` | `30` (min 1) | Wait before retrying a failed flush |
| `max_retry_cooldown_seconds` | `300` (min 1) | Upper bound for the growing retry wait |
| `batch.max_sessions_per_cycle` | `10` (min 1) | Conversations flushed per pass, across all agents |
| `batch.max_sessions_per_agent_per_cycle` | `3` (min 1) | Conversations flushed per pass for one agent or private instance |
| `extractor.model` | none | A key of `models` that runs extraction instead of the agent's own model; set a fast model when extractions time out |
| `extractor.no_reply_token` | `NO_REPLY` | Reply meaning nothing should be saved |
| `extractor.max_messages_per_flush` | `20` (min 1) | New messages read per extraction; a longer backlog is read over later passes |
| `extractor.max_chars_per_flush` | `12000` (min 1) | Message characters read per extraction |
| `extractor.max_extraction_seconds` | `30` (min 1) | Timeout for one extraction, retried in a later pass |
| `extractor.include_memory_context.memory_snippets` | `5` (min 0) | Existing memories shown to the model to avoid duplicates |
| `extractor.include_memory_context.snippet_max_chars` | `400` (min 1) | Characters per existing memory shown |

The retired `max_dirty_age_seconds` and `max_cross_session_reprioritize` settings, which older dashboards saved under `auto_flush`, pass validation but have no effect, and the next dashboard save removes them.

## Prompt Curation

Agents append to `MEMORY.md` and their context files more often than they condense them, so the prompt they send on every turn keeps growing.
Enable the [`prompt_curation`](https://docs.mindroom.chat/scheduling/#prompt_curation) automation to have MindRoom check their size daily and, once they pass a trigger or the preload has to withhold part of `MEMORY.md`, ask the agent in a visible thread to condense them gradually, fold corrections into the entries they correct, and move detail into searchable `memory/` files, then measure the result and ask the agent to re-check a cut outside the bounds.

```yaml
agents:
  mind:
    memory_backend: file
    automations: [prompt_curation]
```

## Dreaming

Facts in `memory/` go stale when a later conversation corrects them.
Enable the [`dreaming`](https://docs.mindroom.chat/scheduling/#dreaming) automation to have the agent reconcile `memory/` nightly with new conversations and daily notes, through a proposal that a second run reviews before MindRoom applies it.

```yaml
memory:
  auto_flush:
    enabled: true           # writes the daily notes dreaming reviews
agents:
  mind:
    memory_backend: file
    thread_exports: true    # also reviews threaded conversations
    automations: [dreaming]
```

## [`memory`]

The `memory` tool lets an agent deliberately remember, look up, correct, or forget something, alongside automatic memory.
It has no tool-specific configuration; it always uses the agent's effective backend and the `memory.search` settings, and agents with `memory_backend: none` do not receive it.

```yaml
agents:
  assistant:
    tools: [memory]
```

```python
add_memory("The user prefers terse release notes.")
search_memories("release notes", limit=3)   # default limit 5
list_memories(limit=20)                     # default limit 50
get_memory("abc123")
update_memory("abc123", "The user prefers terse release notes with dates.")
delete_memory("abc123")
```

The tool reaches every agent and team scope visible to the agent, as described in [Memory Scopes](#memory-scopes).
Search and list results include an ID: Mem0 memory IDs and file-backed `file:<path>:<line>` IDs work with `get_memory`, `update_memory`, and `delete_memory`.
Semantic file-memory matches use read-only `semantic:<source_file>:<rank>` locators that these functions do not accept.
After storing, `add_memory` lists up to three similar existing memories with their IDs, so the agent can rewrite an entry the new memory corrects; file memory finds them by keyword even in `semantic` mode, because semantic matches cannot be edited.
Result metadata includes `search_mode` (`keyword` or `semantic`), showing which search produced each result.
Failures are returned as readable error messages rather than raised into the conversation.

## UI Configuration

The Dashboard **Memory** page edits the `memory` section: backend, embedder provider, model, credential service, and host, file settings (`max_entrypoint_lines`, `max_entrypoint_tokens`), search settings, and all auto-flush settings.
The remaining fields, such as `llm`, are under **More settings**.
Save from the Memory page to write the changes to `config.yaml`.

# Knowledge Bases

Knowledge bases give agents access to your own documents.
Point a knowledge base at a folder or Git repository and assign it to agents.
In `semantic` mode (the default), MindRoom indexes the files with an embedder into a vector index, and assigned agents get a `search_knowledge_base` tool.
In `files` mode, MindRoom skips embeddings and agents inspect the files directly with their file and shell tools.

## Quick Start

Add a knowledge base and assign it to an agent:

```yaml
knowledge_bases:
  docs:
    description: Product documentation, support notes, and internal operating procedures
    mode: semantic
    path: ./knowledge_docs
    watch: false
    chunk_size: 5000
    chunk_overlap: 0

agents:
  assistant:
    display_name: Assistant
    role: A helpful assistant with access to our docs
    knowledge_bases: [docs]
```

Place files in `./knowledge_docs/`, then trigger a reindex from the dashboard or API, or set `watch: true` so MindRoom picks up file changes automatically.
Knowledge base IDs are the keys under `knowledge_bases`.
An ID must be a non-empty single path component such as `docs` or `company_docs`; `.`, `..`, and names containing `/`, `\`, or line breaks are rejected.

### Files-Only Knowledge Base

Use `mode: files` when agents should search, grep, list, and read the source files directly without paying for embeddings.

```yaml
knowledge_bases:
  source_docs:
    description: Source documents the agent should inspect directly
    mode: files
    path: ${MINDROOM_STORAGE_PATH}/agents/assistant/workspace/source_docs

agents:
  assistant:
    display_name: Assistant
    role: A helpful assistant with direct file access to source docs
    tools: [file, shell]
    knowledge_bases: [source_docs]
```

A files-mode base has no vector index, does not use the embedder, and does not provide `search_knowledge_base`.
The agent's workspace is `${MINDROOM_STORAGE_PATH}/agents/<agent>/workspace` (private agents use their private root).
When the base's path is inside that workspace and the folder exists, MindRoom exposes it as `knowledge/<base_id>` and tells the agent to use file tools on it.
Bases outside the workspace are not linked, and with the default [`file_access: workspace`](https://docs.mindroom.chat/configuration/agents/#configuration-options) the agent's file tools cannot reach them.

## Configuration

```yaml
knowledge_bases:
  my_docs:
    description: Product documentation, support notes, and internal operating procedures
    mode: semantic                    # "semantic" builds a vector search index; "files" skips embeddings
    path: ./knowledge_docs/my_docs   # Folder containing documents
    watch: false                      # Direct external edits require reindex; dashboard/API changes still refresh
    require_content_before_publish: false
    chunk_size: 5000
    chunk_overlap: 0
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `description` | string | `""` | What the base contains; shown to agents in the `search_knowledge_base` tool description and in files-mode instructions so they know when to use it |
| `mode` | `semantic` or `files` | `semantic` | `semantic` builds an embedding-backed search index; `files` skips embeddings and exposes the files to agents' file tools |
| `path` | string | `./knowledge_docs` | Folder path, relative to the config file directory or absolute |
| `watch` | bool | `true` | Watch the local folder and refresh in the background when files change. When `false`, direct external edits require an explicit reindex unless `poll_interval_seconds` is set; dashboard/API uploads and deletes still trigger a refresh |
| `poll_interval_seconds` | int | `null` | Schedule a background refresh of the local folder this often (minimum `5`), for folders that another host writes to; see [Folders Written by Another Host](#folders-written-by-another-host). Unset means no polling. Semantic mode only, so `mode: files` bases reject it. Git-backed bases use `git.poll_interval_seconds` instead, and setting both is rejected |
| `require_content_before_publish` | bool | `false` | Keep a new semantic index in the initializing state until at least one source file exists, instead of publishing an empty index |
| `chunk_size` | int | `5000` | Maximum characters per chunk for plain-text and Markdown files (minimum `128`); other formats such as JSON and PDF use their reader's own chunking; semantic mode only |
| `chunk_overlap` | int | `0` | Overlap characters between adjacent plain-text and Markdown chunks; must be smaller than `chunk_size`; semantic mode only |
| `chunk_context_model` | string | `null` | Model (a key under `models`) that writes a short context for each chunk of a multi-chunk file before it is indexed; see [Contextual Chunk Headers](#contextual-chunk-headers); semantic mode only |
| `include_patterns` | list | `[]` | Root-anchored glob patterns a file must match to be included |
| `exclude_patterns` | list | `[]` | Root-anchored glob patterns removed after include filtering |
| `include_extensions` | list | `null` | Exact set of extensions to index instead of the default text-like set (semantic mode only) |
| `extra_extensions` | list | `[]` | Extensions to index in addition to the default set (semantic mode only) |
| `exclude_extensions` | list | `[]` | Extensions to exclude, applied last (semantic mode only) |
| `skip_hidden` | bool | `true` | Skip files and folders whose names start with `.`, such as temp files from editors that write in place. Git-backed bases use `git.skip_hidden` instead |
| `git` | object | `null` | Sync the folder from a Git repository; see [Git-Backed Knowledge Bases](#git-backed-knowledge-bases) |

Set `skip_hidden: false` if the folder intentionally contains dot-prefixed files that should be indexed.
Use a smaller `chunk_size` when your embedding server has lower token or batch limits; a chunk too long for the embedding model fails the file that holds it, and with it the refresh, for example with `embedder request failed (HTTP 400)` from OpenAI, or `(HTTP 500)` from a llama.cpp server whose physical batch size is smaller than the chunk.

### Keeping Indexes Fresh

Chat never waits for indexing or Git sync.
Agents search the last successfully published index, and a missing, stale, or failed base schedules a background refresh while the current request continues.
A successful refresh replaces the published index; a failed refresh keeps the previous one and shows the error in the base's status.

A refresh reuses the stored vectors of unchanged files and only embeds files that were added or edited.
A semantic base keeps the index it last replaced on disk beside the live one, so the next refresh rewrites only the files that changed instead of copying the whole index; between refreshes the base therefore stores two indexes.
Rewriting leaves free space inside an index file, which later writes reuse; once a quarter of a kept index is free space, for example after many files were deleted, the next refresh copies the live index instead, so neither file stays much larger than its contents.
Changing `chunk_size`, `chunk_overlap`, `chunk_context_model`, the embedder, or any file filter re-embeds the whole base on the next refresh.
An explicit reindex from the dashboard or API rebuilds every vector, which is how you replace an index you no longer trust.
A reindex requested while a refresh of the base runs starts once that refresh finishes.
Closing the dashboard or dropping the API request does not stop a reindex once it is requested.
An interrupted or failed semantic build, including a reindex, continues where it stopped on the next refresh instead of starting over.
Temporary embedding failures (timeouts, connection errors, and HTTP 408, 409, 429, and any other 5xx status) are retried, while permanent failures such as authentication errors, or a 429 that says the OpenAI account has no credit left or reached its monthly spend or usage limit, stop the refresh until you fix the cause.
A batch of chunks larger than the account's tokens-per-minute limit is embedded again one chunk per request.

Knowledge base configuration hot-reloads.
After a `chunk_size`, `chunk_overlap`, or `chunk_context_model` change, agents keep searching the previous index until the rebuild succeeds.
After a change to `path`, `mode`, the embedder, the Git source, or any file filter, the base is not searchable until its new index is built.

### Folders Written by Another Host

Writes from another host, such as a job writing to an NFS share, produce no filesystem events.
With `watch: true`, MindRoom notices them only when an agent searches the base, at most every five minutes, and that search still uses the previous index.
With `watch: false`, MindRoom never notices them.
Set `poll_interval_seconds` on the base to refresh on a timer instead.

```yaml
knowledge_bases:
  synced_docs:
    path: /mnt/shared/synced_docs
    poll_interval_seconds: 300
```

The first poll runs one interval after startup and after each config reload.
Each poll reads every file to detect changes, so choose an interval suited to the folder size.
Polling works with `watch` on or off, and private agent knowledge is not polled.

### Indexing Performance

| Environment variable | Default | Description |
|----------------------|---------|-------------|
| `MINDROOM_KNOWLEDGE_FILE_INDEX_CONCURRENCY` | `4` | Files indexed concurrently within one semantic refresh, from `1` through `128`; it also caps the batched embedding requests a refresh keeps in flight, at most 4; invalid values are rejected at startup |
| `MINDROOM_KNOWLEDGE_REFRESH_CONCURRENCY` | `1` | Knowledge base refreshes that run at the same time, file-memory index refreshes included; must be at least `1` |
| `MINDROOM_KNOWLEDGE_REFRESH_SUBPROCESS_TIMEOUT_SECONDS` | `3600` | Maximum seconds for one background refresh; must be a finite positive number. Raise it for very large corpora |

Local `sentence_transformers` embedding indexes one file at a time regardless of `MINDROOM_KNOWLEDGE_FILE_INDEX_CONCURRENCY`.

### File Type Filtering

In semantic mode, MindRoom indexes a default set of text-like extensions covering Markdown, plain text, source code, and structured text formats such as `.json`, `.yaml`, and `.csv`.

```yaml
knowledge_bases:
  reports:
    path: ./knowledge_docs/reports
    extra_extensions: [".pdf", ".docx"]  # Default text-like set plus PDF and Word
    exclude_extensions: [".csv"]         # Drop CSV files from the default set
```

- `extra_extensions` adds extensions on top of the default set.
- `include_extensions` replaces the default set entirely, and `extra_extensions` still adds on top of it.
- `exclude_extensions` removes extensions last and wins over both.

The allowed set depends only on this configuration, never on the embedder or installed packages.
Formats such as `.pdf`, `.docx`, or `.pptx` need their reader package (for example `pypdf`, `python-docx`, or `python-pptx`) installed in the MindRoom environment.
When a reader package is missing, the refresh indexes the other files, logs a warning naming the file and the missing package, and reports an indexing error instead of publishing an index that silently lacks those files.
Extension filtering does not apply in `files` mode.
Semantic indexing skips symlinked files and folders inside a base, so copy linked documents into the base instead.
Files larger than 64 MiB are left out of every knowledge base with a logged warning, and the next refresh removes any vectors they already had.

A `.json` file is split into one searchable document per top-level JSON value.
If the file is not valid JSON, its raw text is indexed like plain text and a warning names the file and the line and column of the parse error; fix that error to get per-value splitting back.

### Multiple Knowledge Bases

Define several knowledge bases and assign each agent the ones it needs:

```yaml
knowledge_bases:
  engineering:
    path: ./knowledge_docs/engineering
  product:
    path: ./knowledge_docs/product
  legal:
    path: ./knowledge_docs/legal
    chunk_size: 1000
    chunk_overlap: 100

agents:
  developer:
    display_name: Developer
    role: Engineering assistant
    knowledge_bases: [engineering]

  pm:
    display_name: Product Manager
    role: Product planning assistant
    knowledge_bases: [product, engineering]

  compliance:
    display_name: Compliance
    role: Legal and compliance reviewer
    knowledge_bases: [legal]
```

When an agent searches several semantic bases, results are interleaved so no single base dominates the top results.
Knowledge base paths must not be nested inside one another.
Several bases may use exactly the same path to expose different filtered views of one folder, as long as their Git settings (`repo_url`, `branch`, `credentials_service`, and `lfs`) match or none of them uses Git; see [File Filtering with Patterns](#file-filtering-with-patterns).

### Private Agent Knowledge

Use `agents.<name>.private.knowledge` when each requester of a [private agent](https://docs.mindroom.chat/configuration/agents/#private-instances) should have their own knowledge index built from their private root.

```yaml
knowledge_bases:
  company_docs:
    description: Shared company policies, project notes, and operating procedures
    path: ./company_docs
    watch: false

agents:
  mind:
    display_name: Mind
    role: A persistent personal AI companion
    model: sonnet
    private:
      per: user
      root: mind_data
      template_dir: ./mind_template
      knowledge:
        description: Requester-private notes, preferences, and working memory for this agent
        path: memory
        watch: false
    knowledge_bases: [company_docs]
```

Each requester's private knowledge path becomes `<their private root>/memory`, and their index is never shared with another requester.
The fields are listed under [Private Fields](https://docs.mindroom.chat/configuration/agents/#private-fields).
`private.knowledge.path` is required unless `private.knowledge.enabled` is `false`; it must be relative to the private root (`.` allowed) and cannot be absolute or escape with `..`.
Private knowledge has no background file watcher: with `watch: true` or `git`, it refreshes when the agent uses it, and with `watch: false` and no `git`, external edits are not picked up automatically.
An agent can combine private knowledge with shared top-level `knowledge_bases`, which stay the mechanism for documents shared across agents or users.
Private knowledge is available in normal agent conversations, not through the OpenAI-compatible `/v1` API.
With `private.knowledge.git`, use a dedicated subtree such as `kb_repo`; MindRoom rejects `.` and any path that overlaps private file memory (`memory/`, `MEMORY.md`) or content copied from `template_dir`.

## Search

`search_knowledge_base` ranks a base's chunks two ways and merges the two rankings.
One compares the query's embedding with every chunk's embedding, exactly rather than approximately.
The other scores the query's words with BM25 over a keyword index that matches English word forms, so `leads` finds `lead`.
Reciprocal rank fusion merges them, so a chunk that names the exact command, setting, or error message in the query ranks high even when its embedding is not the closest.
Each result still reports its cosine distance as `distance`.
A base whose folder sandboxed workers can write, or that contains such a folder, such as private knowledge or a base inside an agent workspace or shared directory, can hold 256 MiB of vectors, about 43,000 chunks with the default `text-embedding-3-small` embedder; past that its searches fail until files are removed and the base is refreshed.

### Reranking

A reranker reads the query together with each candidate chunk and reorders them.
A strong reranker improves on the merged ranking, but a small one can make it worse: on MindRoom's own documentation with the default 5000-character chunks, the right page came first for 79% of questions without a reranker, 86% with `BAAI/bge-reranker-v2-m3`, and 92% with Vertex AI's `semantic-ranker-default@latest`, while models limited to 512 tokens, such as `cross-encoder/ms-marco-MiniLM-L6-v2` and `BAAI/bge-reranker-base`, ranked worse than no reranker.
Strong rerankers need a GPU or a hosted API to answer quickly: Vertex AI ranked 20 candidates in about 0.3 seconds and 400 in 0.8, while `bge-reranker-v2-m3` took about 30 seconds per search on a 32-core CPU.

```yaml
knowledge_reranker:
  provider: cohere                 # or sentence_transformers, vertex_ai
  model: rerank-v3.5
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `provider` | `cohere`, `sentence_transformers`, or `vertex_ai` | required | `cohere` calls a rerank API in Cohere's request format; `sentence_transformers` runs a cross-encoder such as `BAAI/bge-reranker-v2-m3` inside MindRoom, installing the `sentence-transformers` extra on first use; `vertex_ai` calls Google's Vertex AI ranking API |
| `model` | string | required | Rerank model name, such as `semantic-ranker-default@latest` for `vertex_ai` |
| `url` | string | `https://api.cohere.com/v2/rerank` | Rerank endpoint for `cohere`; Jina (`https://api.jina.ai/v1/rerank`), vLLM, and Infinity accept the same format |
| `api_key` | string | `null` | API key for `cohere` |
| `credentials_service` | string | `cohere` | Credential service that holds the `cohere` API key when `api_key` is unset |

`vertex_ai` signs in with the credential file `GOOGLE_APPLICATION_CREDENTIALS` names, or else Google Application Default Credentials, and ranks in `GOOGLE_CLOUD_PROJECT`, or else the project those credentials name, such as the cluster's project on GKE.
Both variables may be set in MindRoom's `.env`, as for Gemini on Vertex AI.
That project needs the `discoveryengine.googleapis.com` API, and the identity needs `roles/discoveryengine.viewer`, the smallest predefined role that may rank.
The ranking API reads the first 1024 tokens of each chunk with its default models, 512 with older ones; MindRoom sends candidates in concurrent requests of 100, the unit Google bills as one query.

The reranker sees the top 20 chunks of every base one search covers and returns the best of them across bases.
It applies to `search_knowledge_base` only; automatic memory search keeps the merged ranking, so it adds no latency to replies.
If the reranker fails, the search returns its results in the merged order and logs a warning.

### Contextual Chunk Headers

A chunk taken out of a long document often loses what it is about, such as which product or section it belongs to.
With `chunk_context_model`, a model reads each chunk of a multi-chunk file together with the document around it and writes a sentence or two that places the chunk in that document.

```yaml
models:
  fast:
    provider: openai
    id: gpt-6-luna

knowledge_bases:
  docs:
    path: ./knowledge_docs
    chunk_context_model: fast
```

The context is stored in front of its chunk, so it is embedded, keyword-indexed, and shown to agents with the chunk.
It costs one model call per chunk of every added or changed multi-chunk file; single-chunk files are indexed as they are, and unchanged files keep their stored chunks.
Turning `chunk_context_model` on, or changing it, rebuilds the base, so every multi-chunk file gets new contexts once.
A file whose context call fails is not indexed in that refresh and is retried on the next one.
Usage is recorded under `knowledge_chunk_context`.

## Git-Backed Knowledge Bases

A knowledge base can sync its folder from a Git repository.
MindRoom refreshes a shared Git-backed base every `poll_interval_seconds`, starting one interval after startup, and agents keep using the last published index while a refresh runs.

```yaml
knowledge_bases:
  pipefunc_docs:
    path: ./knowledge_docs/pipefunc
    chunk_size: 1200
    chunk_overlap: 120
    git:
      repo_url: https://github.com/pipefunc/pipefunc
      branch: main
      poll_interval_seconds: 300
      lfs: false
      skip_hidden: true
      include_patterns:
        - "docs/**"
```

### Git Configuration Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `repo_url` | string | *required* | Repository URL to clone and fetch; see the accepted forms below |
| `branch` | string | `main` | Branch to track |
| `poll_interval_seconds` | int | `300` | Interval between background Git refreshes (minimum `5`) |
| `credentials_service` | string | `null` | Credential service for private repositories; see [Private Repository Authentication](#private-repository-authentication) |
| `lfs` | bool | `false` | Download Git LFS files after each sync. Requires `git-lfs` on the machine running MindRoom; bundled container images include it |
| `sync_timeout_seconds` | int | `3600` | Abort a single Git command after this many seconds (minimum `5`) |
| `skip_hidden` | bool | `true` | Skip files and folders whose names start with `.` |
| `include_patterns` | list | `[]` | Root-anchored glob patterns to include |
| `exclude_patterns` | list | `[]` | Root-anchored glob patterns to exclude |

#### Accepted `repo_url` forms

| Form | Example |
|------|---------|
| URL with a host | `https://github.com/org/repo.git`, `ssh://git@host/org/repo.git` |
| scp-style SSH | `git@github.com:org/repo.git`, `github.com:org/repo.git` |
| Absolute local path | `/srv/repos/repo.git` |
| `file:` URL | `file:///srv/repos/repo.git` |

Network forms must have a resolvable host.
MindRoom refuses, with an error naming the reason, relative and home-relative paths (`./repo.git`, `../repo.git`, `~/repo.git`, `repo.git`), and URLs with an empty host (`https:///org/repo.git`).

**Do not embed credentials in `repo_url`.**
A password in a well-formed URL is kept out of the checkout's Git configuration, but it stays in your config file and is passed to Git on every command.
A credential MindRoom cannot reliably separate from the URL, such as `oauth2:TOKEN@gitlab.com:org/repo.git` or `x-access-token:TOKEN@github.com/org/repo.git`, makes the sync fail.
Use `credentials_service` instead; those credentials reach Git only for the duration of each command and are never written to disk.

### Sync Behavior

- An explicit reindex or sync from the dashboard or API syncs Git first, then rebuilds the semantic index or, in files mode, publishes the new file list.
- The checkout is owned by MindRoom: when the branch moves, sync discards local edits to tracked files and restores deleted ones, so edit the source repository and sync instead.
- Dashboard and API file uploads and deletes are rejected for Git-backed bases.
- When `lfs: true`, MindRoom downloads all LFS files after each sync, even ones the indexing filters exclude.
- LFS downloads use the endpoint derived from `repo_url`; an LFS URL set in the repository's `.lfsconfig` is ignored.

### File Filtering with Patterns

Patterns are matched from the repository or folder root.
`*` matches one path segment and `**` matches zero or more segments.

```yaml
knowledge_bases:
  project_docs:
    path: ./knowledge_docs/project
    git:
      repo_url: https://github.com/org/project
      include_patterns:
        - "docs/**"                    # All files under docs/
        - "README.md"                  # Root README only
        - "content/posts/*/index.md"   # Specific nested files
      exclude_patterns:
        - "docs/internal/**"           # Exclude internal docs
```

- If `include_patterns` is empty, all non-hidden files are eligible.
- If `include_patterns` is set, a file must match at least one pattern.
- `exclude_patterns` are applied last and remove matching files.
- On a Git-backed base, a file must pass both the top-level patterns and the `git` patterns.

Pointing several bases at the same path is the preferred way to expose separate views of a large repository without cloning it more than once:

```yaml
knowledge_bases:
  project_docs:
    path: ./knowledge_docs/project
    git:
      repo_url: https://github.com/org/project
      branch: main
      include_patterns: ["docs/**"]
  project_source:
    path: ./knowledge_docs/project
    git:
      repo_url: https://github.com/org/project
      branch: main
      include_patterns: ["src/**"]
```

### Checkout Layout

The knowledge folder holds only the checked-out files.
The Git data lives at `<storage>/knowledge_git/<folder>_<path digest>`, and bases that share a folder share it.
A `.git` written into the knowledge folder is ignored and never indexed.
The folder must be empty when MindRoom first clones into it.
Changing a base's `path` starts a new clone in the new folder; delete the old directory under `<storage>/knowledge_git/` to reclaim its space.
Deleting only the knowledge folder restores its files from the Git data on the next sync; delete both to clone afresh.

Knowledge Git commands ignore system and global Git configuration, repository hooks, and credential helpers, and they do not receive MindRoom's secrets.
Configure proxies, CA bundles, and SSH through environment variables or `~/.ssh/config`, and credentials through `credentials_service`.

### Private Repository Authentication

For private HTTPS repositories, store credentials and reference them in the config.

**Step 1:** Store credentials through the dashboard **Credentials** tab or the API:

```bash
curl -X POST http://localhost:8765/api/credentials/github_private \
  -H "Content-Type: application/json" \
  -d '{"credentials":{"username":"x-access-token","token":"ghp_your_token_here"}}'
```

**Step 2:** Reference the service name in the knowledge base config:

```yaml
knowledge_bases:
  private_docs:
    path: ./knowledge_docs/private
    git:
      repo_url: https://github.com/org/private-repo
      credentials_service: github_private
```

| Fields | Notes |
|--------|-------|
| `username` + `token` | Standard GitHub or GitLab access token |
| `username` + `password` | Basic HTTP auth; the password wins if a token is also set |
| `token` or `api_key` alone | Authenticates as `x-access-token` |

For unattended GitHub access, a GitHub App installation can replace a personal access token.
Store this object under the service named by `credentials_service`:

```json
{
  "auth_type": "github_app",
  "app_id": 12345,
  "installation_id": 67890,
  "private_key_file": "/var/run/secrets/github-app/private-key.pem"
}
```

`private_key_file` must be an absolute path, ideally a read-only secret mount; do not put the PEM contents in the credential object.
`repo_url` must use the canonical `https://github.com/<owner>/<repository>` form.
MindRoom requests short-lived installation tokens limited to that repository with read-only Contents permission, so the App needs Contents read access, and the tokens are never written to disk or logs.

## Open Knowledge Format Bundles

An [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) (OKF) bundle is a folder of markdown files whose YAML frontmatter records who wrote each concept, who verified it, and whether it is current.
Use `mode: files` for OKF bundles, because semantic search cannot filter on frontmatter.
Agents with the bundled `open-knowledge-format` skill report trust and freshness when they answer, and when they edit, they record themselves as the author and add a `verified` entry only when a person confirms the content.

### Reading a Published Bundle

Mirror the bundle's repository inside the agent workspace and name the bundle root in the description:

```yaml
knowledge_bases:
  acme_okf:
    description: Acme Retail finance knowledge, a read-only Open Knowledge Format (OKF) bundle mirrored from Git. Start at knowledge/acme_okf/bundles/acme_retail/index.md.
    mode: files
    path: ${MINDROOM_STORAGE_PATH}/agents/analyst/workspace/okf/acme
    git:
      repo_url: https://github.com/GoogleCloudPlatform/open-knowledge-format

agents:
  analyst:
    display_name: Analyst
    role: Answers questions from the team's knowledge bundles
    tools: [file]
    knowledge_bases: [acme_okf]
    skills: [open-knowledge-format]
```

The mirror is a [Git-backed base](#git-backed-knowledge-bases), so trigger a sync from the dashboard to clone it before the first poll.

### Maintaining a Bundle with Agents

For a bundle that agents edit, create a local folder with a root `index.md` inside the agent workspace, then add it to the agent's `knowledge_bases`:

```yaml
knowledge_bases:
  ops_wiki:
    description: The team's ops wiki, an Open Knowledge Format (OKF) bundle that the team and its agents maintain.
    mode: files
    path: ${MINDROOM_STORAGE_PATH}/agents/analyst/workspace/okf/ops_wiki
```

## Embedder Configuration

Semantic knowledge bases use the embedder configured under `memory.embedder`; files-mode bases use none.
Semantic knowledge and file-memory indexes support the `openai`, `ollama`, and `sentence_transformers` providers, while the Mem0 memory backend also supports `huggingface`.

| Provider | Model example | Notes |
|----------|---------------|-------|
| `openai` | `text-embedding-3-small` | Also works with keyless OpenAI-compatible local endpoints |
| `ollama` | `nomic-embed-text` | Self-hosted |
| `sentence_transformers` | `sentence-transformers/all-MiniLM-L6-v2` | Runs locally in MindRoom; the optional extra installs on first use |

For multilingual text and code, use `google/embeddinggemma-2` with `sentence_transformers`; its first use downloads about 1.4 GB.
MindRoom applies the query and document prompts that a `sentence_transformers` model defines, so knowledge and file-memory searches need no manual task prefixes.

See [Memory](https://docs.mindroom.chat/memory/#embedder) for the embedder fields, API key resolution, and how embedder failures are reported.

## Storage

Semantic indexes are stored under `<storage_path>/knowledge_db/<base_id>_<hash>/`, where the hash comes from the resolved knowledge path.
Each published index is one SQLite file there, holding the chunks, their vectors, and a keyword index, and searches run inside the MindRoom process.
Searching needs numpy, whose `numpy` extra installs on first use unless `MINDROOM_NO_AUTO_INSTALL_TOOLS=1` is set; otherwise install `mindroom[numpy]`.
Private agent knowledge is indexed under `<storage_path>/knowledge_db/private/<requester>/`, outside the private roots that sandboxed workers can write, with one directory per requester and agent.
Removing a private instance leaves its index behind; each index directory's `publication.json` names the `knowledge_path` it was built from under `settings`.
The storage path defaults to `mindroom_data/` next to `config.yaml` and can be changed with `MINDROOM_STORAGE_PATH`.

A shared base's index that earlier MindRoom versions stored in Chroma is converted on its first refresh after upgrading: its chunks and vectors are copied out of Chroma, and only files changed since are embedded again.
MindRoom schedules that refresh for every such base when it starts, so conversion does not wait for the base's first use.
The copy needs the `chromadb` extra, which installs on first use unless `MINDROOM_NO_AUTO_INSTALL_TOOLS=1` is set; without it, the base is rebuilt, which embeds every file again.
Private agent indexes are rebuilt in their new location.
A base cannot be searched until its converted index publishes, which for a very large base can take a while.
A shared base's old `chroma.sqlite3` and segment directories are removed once its new index is published.
Private agent indexes used to live in the agent's state root, `<storage_path>/private_instances/<scope>/<agent>/knowledge_db/`, beside the private root rather than inside it; MindRoom no longer reads that directory, so it can be deleted after upgrading.

## Dashboard and API

Manage knowledge bases, files, reindexing, and Git sync from the dashboard **Knowledge** tab or the API; see [Dashboard](https://docs.mindroom.chat/dashboard/#knowledge) for the tab and the [knowledge API endpoints](https://docs.mindroom.chat/dashboard/#knowledge-api).

# Knowledge Graph

A knowledge graph gives an agent a model of the people, projects, documents, and events its conversations and tools reveal, with when each fact held and where it came from.
MindRoom builds it with [Graphiti](https://github.com/getzep/graphiti) in the background from the agent's finished conversations, and the agent looks facts up with the `knowledge_graph` tool.
To have the agent also explore its data sources on a schedule, add the [`exploration`](https://docs.mindroom.chat/scheduling/#exploration) automation.
[Memory](https://docs.mindroom.chat/memory/) keeps what is worth remembering as text and a [knowledge base](https://docs.mindroom.chat/knowledge/) searches your documents; the graph links the entities that conversations and tool results reveal, with dated facts, so an agent can answer who owns what, how things connect, and what changed.

## Enable It

```yaml
models:
  luna:
    provider: openai
    id: gpt-6-luna

knowledge_graph:
  model: luna          # extracts entities and facts

agents:
  mind:
    display_name: Mind
    knowledge_graph: true
```

`agents.<name>.knowledge_graph` (bool, default from `defaults.knowledge_graph`, which defaults to `false`) turns the graph on for one agent and gives it the `knowledge_graph` tool.
MindRoom installs the optional `knowledge_graph` extra the first time a graph is used; its embedded database ships for macOS and for Linux with glibc 2.39 or newer, and elsewhere it must build from source.
Extraction needs a model with reliable structured output; small local models often fail it; a failed extraction is retried later, and a part of a conversation is skipped once its extraction output was unusable five times or it kept failing for three days.

## Settings

| Field | Default | Description |
|---|---|---|
| `knowledge_graph.model` | none | A key of `models` that extracts entities and facts; required once any agent enables the graph |
| `knowledge_graph.extraction_instructions` | empty | Text with your own guidance for extraction, added after the built-in instructions, such as which topics, systems, or kinds of facts matter in your organization; it applies to what is added afterwards, or to the last `backfill_days` of conversations once you raise `knowledge_graph.generation` |
| `knowledge_graph.ingest.interval_seconds` | `600` (min 30) | How often MindRoom looks for new turns in a graph whose last ingestion pass added nothing; a pass that added something is followed at once |
| `knowledge_graph.ingest.idle_seconds` | `120` (min 1) | How long a conversation must be quiet before its new turns are added |
| `knowledge_graph.ingest.max_sessions_per_cycle` | `10` (min 1) | Conversations each graph adds per pass; the rest wait for the next |
| `knowledge_graph.ingest.max_episode_chars` | `24000` (1000 to 60000) | Largest piece of a conversation sent for extraction at once; a single longer message is cut to this length |
| `knowledge_graph.ingest.max_tool_result_chars` | `4000` (min 100, at most half of `max_episode_chars`) | Characters kept from each tool result |
| `knowledge_graph.ingest.backfill_days` | `7` (min 0) | A new graph starts from conversations active within this many days |
| `knowledge_graph.aliases` | `{}` | The one name the graph uses for a person, agent, or thing, mapped to its other names; see [Names](#names) |
| `knowledge_graph.generation` | `0` (integer, min 0) | Raise it to start every graph over without restarting MindRoom |

With the defaults, new turns reach the graph 2 to 12 minutes after a conversation goes quiet, plus extraction time, and later when many conversations are waiting.
Up to four agents' graphs are filled at the same time, counting each requester of a private agent separately, so the extraction model can receive requests for four graphs at once; graphs take turns for those places pass by pass, so one graph's long backlog does not hold up the others.
Facts are embedded with [`memory.embedder`](https://docs.mindroom.chat/memory/#embedder).
The graph database runs inside MindRoom and keeps every graph in memory, so MindRoom's memory use grows with the graphs.
Changing the embedder or raising `knowledge_graph.generation` starts new, empty graphs that fill again from conversations active within `backfill_days`, which pays for that extraction again, charged to each conversation's requester.
Replies and voice calls already running when you raise the generation keep reading the old graphs until they finish, and setting an earlier generation again reads that generation's graphs as they were left.
The old graphs keep their memory until you stop MindRoom and delete `knowledge_graph/` in the storage directory, which also starts the current graphs over.

## What Goes Into the Graph

- Every finished turn of the agent's Matrix conversations: what people said, what the agent answered, and the tool calls it made with their results.
- From [`exploration`](https://docs.mindroom.chat/scheduling/#exploration) threads, tasks another agent delegated to it, and an automation's turn inside an ordinary conversation, only the tool calls and their results, never the agent's own summary.

Not added: threads started by other automations such as `dreaming`, graph lookups and edits, including those through a shell, the graph facts that `search_knowledge_base` returns, knowledge searches through a shell, the `reasoning` tool's thoughts, answers from subagents, workflows, and Claude Code, results collected from background jobs, conversations through the OpenAI-compatible API, team conversations, and context that hooks attach to messages, such as location.
Credential-like values are removed on a best-effort basis before anything is sent for extraction.
A turn that waits for tool approval is added once it finishes.

Extraction asks the model to sort what it finds into people, agents, organizations, locations, events, projects, products, documents, and accounts, to leave out what fits none of them, and to keep file paths, URLs, and the like inside the facts that mention them, so pets, foods, hobbies, and topics get no entity of their own.
It records what the agent reports as facts about their subjects and leaves out the agent's own lookups and housekeeping, such as what it searched, read, or saved, its access, accounts, tools, and configuration, and the counts and progress of its own tasks; what the agent did for others, such as filing an issue or sending a message, is kept.
This steers the model rather than filtering its output, so an occasional untyped entity, such as a file that only a fact names, or a fact about the agent's own activity can still appear.

Each fact keeps its sources: `conversation` means someone said it, `tool_results` means it came from a tool call, its arguments or what it returned.
When later evidence contradicts a fact, the fact is marked superseded with the date it stopped holding instead of being deleted.
Facts are extracted by a model, so they can be wrong; check a fact's source before relying on it.
A message redacted before its conversation is added stays out of the graph, and so do the turn that read it and every later turn its redaction removes from [history](https://docs.mindroom.chat/configuration/history/#compacted-history-and-redaction).
Redacting a message later does not remove facts already extracted from it; to stop new replies and calls from reading them, raise `knowledge_graph.generation`, or, to delete them too, instead stop MindRoom and delete `knowledge_graph/` in the storage directory, and either way the graphs rebuild from the remaining conversations of the last `backfill_days`, without redacted messages.

## Names

Each agent and person gets one name in the graph, so their facts collect on one entity instead of one per spelling.
An agent or team goes by its `display_name`, and a person by the newest display name their messages in the conversation carried, without emoji and other symbols, or else by their Matrix username.
Extraction is told the other names each one goes by, such as their Matrix ID, username, or agent key, and uses the one name for those too.
`knowledge_graph.aliases` gives one name to the other names of a person, agent, or thing:

```yaml
knowledge_graph:
  aliases:
    Alice Smith: ["@alice:example.org", Ali, alice@example.org]   # her Matrix account, nickname, and email
    Scout: [research]                                             # the agent whose key is research
    Project Atlas: [Atlas, Atlas v2]                              # something without an account
```

A Matrix ID or agent key in an entry gives that person or agent the entry's name everywhere, whatever display name they use; a display name never links an entry.
Someone else whose display name is the name of an entry with a Matrix ID or agent key goes by their username, so the two stay apart.
A speaker whose display name is the name of an entry without one shares that entry's entity and its other names, so an entry named after your own display name needs no Matrix ID.
A username that an entry with a Matrix ID or agent key, or an agent, is already named gets its homeserver added, such as `alice work.example`.
Every other alias is a plain name that extraction maps to the entry's name and that `graph_entity` finds the entity by.
Each alias belongs to one entry and is not another entry's name, in any letter case; names and aliases use single spaces, and a name cannot contain a colon.
Two people with the same display name, or a person named like an agent, share one entity unless an entry gives one of them a name through their Matrix ID.
Every entry is sent with every piece of a conversation for extraction, so a long list makes each extraction prompt longer.
Entities extracted earlier under other names, such as a Matrix ID, stay separate until you raise `knowledge_graph.generation`, which merges them in the graphs rebuilt from the last `backfill_days`, or an agent with [`knowledge_graph_edits`](#letting-the-agent-fix-its-graph) merges them.

## The `knowledge_graph` Tool

```python
search_graph("who owns Project Atlas", limit=10)   # limit 1 to 50
graph_entity("Project Atlas")
```

The agent decides when to look facts up; ask it to search the graph, or to cite each fact's source and date, when you want to be sure.
A graph-enabled agent's `search_knowledge_base` also returns matching graph facts, labelled `knowledge_graph`, beside its knowledge-base results, so the agent has that tool even without knowledge bases.
Each fact comes back with its validity, for example `valid 2026-01-05 until 2026-03-09 (superseded)`, and its sources with dates.
`graph_entity` returns the entity whose name equals the given name in any letter case, and the one with the most facts when several do.
A name listed in [`knowledge_graph.aliases`](#names), the username of a Matrix ID listed there, or an agent's key, Matrix ID, or display name finds the entity under its one name, or the entity named exactly as asked when there is none under that name yet.
Without an exact name it returns the closest match and ends with `Other close matches: ...`, up to four more names to look up when the first is not the one meant.
An agent reaches only its own graph; a [private agent](https://docs.mindroom.chat/configuration/agents/#private-instances) has one graph per requester.

## Letting the Agent Fix Its Graph

An agent with a graph can correct it.
`agents.<name>.knowledge_graph_edits` (bool, default from `defaults.knowledge_graph_edits`, which defaults to `true`) turns this off when `false`, which keeps the agent's lookups read-only; setting it to `true` on an agent without `knowledge_graph` is a config error.
With edits on, its lookups show each fact's and entity's id, `graph_entity` also takes an id and lists, as `Other entities under this name`, the other entities found under the name it was given, so looking up an alias or Matrix ID shows a duplicate extracted under it, and the tool gains four functions:

```python
delete_graph_fact("<fact id>")                       # a fact that never held, or names the wrong person or thing
delete_graph_entity("<entity id>")                   # a file path, URL, or other non-entity, with every fact that touches it
merge_graph_entities("<duplicate id>", "<kept id>")  # the duplicate's facts and their sources move to the kept entity
update_graph_entity_summary("<entity id>", "...")    # a summary that still states a deleted fact
```

The kept entity keeps its name and summary, and facts between the two merged entities are dropped.
An entity's summary is model-written text that collects what was said about it, so a deleted fact stays in it until the agent rewrites the summary.
Ask the agent to fix what it finds wrong, or let an [`exploration`](https://docs.mindroom.chat/scheduling/#exploration) run do it: with edits on, its prompt also asks the agent to fix the mistakes it found in the graph.
Deleting is permanent, and a later conversation or tool result that states a deleted fact again adds it back; the agent is told to keep a fact that held and later changed, because newer evidence marks it superseded.
To approve each edit before it happens, add [`tool_approval`](https://docs.mindroom.chat/tool-approval/) rules for `delete_graph_*`, `merge_graph_entities`, and `update_graph_entity_summary`.
To keep extraction from creating a merged-away name again, list it in [`knowledge_graph.aliases`](#names).

## Privacy

A shared agent's graph holds what it learned from every requester's conversations, including tool results, and anyone who can talk to that agent can query it.
Unless `knowledge_graph_edits` is `false`, anyone who can talk to a shared agent can also have it delete, merge, or rewrite what that graph holds.
Use a private agent when requesters must not see or change each other's facts.

## Cost and Health

Extraction is reported as `knowledge_graph` in [usage tracking](https://docs.mindroom.chat/usage/#token-usage) and makes about 20 to 25 model calls per piece of a conversation.
Each lookup makes one embedding request.
When ingestion fails, `/api/health` includes a `knowledge_graph` block naming the kind of failure until a later pass succeeds with no conversation still waiting to retry a failed attempt, and the MindRoom log has the details.

# Skills

Skills are instruction packs: a `SKILL.md` file with optional scripts and reference documents that guide an agent through a task without adding new code capabilities.
Skills use OpenClaw-compatible metadata.
This page covers writing, installing, and allowlisting skills, skills agents keep in their own workspace, and opt-in automatic skill learning.

## Skill directory structure

A skill is a directory containing:

```
my-skill/
├── SKILL.md         # Required: instructions; YAML frontmatter is recommended
├── scripts/         # Optional: executable scripts
│   └── audit.sh
└── references/      # Optional: reference documents
    └── examples.md
```

## SKILL.md format (OpenClaw compatible)

```markdown
---
name: repo-quick-audit
description: Quick repository audit checklist
metadata: '{openclaw:{requires:{bins:["git"], env:["GITHUB_TOKEN"]}}}'
---

# Repo Quick Audit

1. Check CI status
2. Review open issues
```

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Unique skill identifier; defaults to the skill directory name |
| `description` | string | Brief summary shown to users and models; defaults to the skill name when omitted or blank |
| `metadata` | mapping or JSON5 string | OpenClaw metadata (see [Eligibility gating](#eligibility-gating-openclaw-metadata)) and custom fields |
| `license` | string | Informational only; accepted but not used by the runtime |
| `compatibility` | string | Informational only; accepted but not used by the runtime |
| `allowed-tools` | list | Reserved; accepted but not enforced by the runtime |

A skill without any frontmatter still loads with the name and description fallbacks, but frontmatter gives clearer listings.
Keep each skill focused on one capability, give it a descriptive name such as `code-review`, and declare its dependencies with `metadata.openclaw.requires`.

Workspace skill frontmatter must not use YAML aliases, `%TAG` directives, or unusually large or deeply nested structures, while bundled, plugin, and `~/.mindroom/skills` skills are parsed like any YAML.
A workspace skill whose frontmatter is not valid YAML, for example because an unquoted `description` contains `: `, or breaks these limits is skipped with a warning.

## Installing and managing skills

Install a user skill at `~/.mindroom/skills/<name>/SKILL.md`, or use the dashboard Skills page to create, view, edit, and delete user skills.
Bundled and plugin-provided skills are visible but read-only in the dashboard.

An agent can use a bundled, plugin, or user skill only after you add its name to the agent's `skills:` allowlist in `config.yaml`:

```yaml
agents:
  developer:
    display_name: Developer
    role: A coding assistant
    model: sonnet
    skills:
      - repo-quick-audit
      - code-review
```

If `skills` is empty or unset, the agent gets no bundled, plugin, or user skills.
Skills in the agent's own workspace load without being listed; see [Workspace skills](#workspace-skills).

## Skill locations and precedence

MindRoom resolves skills for each agent from these locations, in this order:

1. Bundled skills shipped with MindRoom
2. Plugin-provided skill directories (see [Plugins](https://docs.mindroom.chat/plugins/))
3. User skills: `~/.mindroom/skills/`
4. Agent workspace skills: `<resolved workspace>/skills/`

For agents without `private`, the workspace skills directory is `<storage>/agents/<agent>/workspace/skills/`.
Private instances use `<storage>/private_instances/<scope-directory>/<agent>/<private.root>/skills/`; see [Private Instances](https://docs.mindroom.chat/configuration/agents/#private-instances).

If several skills share a name, the later location wins: agent workspace over user, user over plugin, and plugin over bundled.

## Workspace skills

Workspace skills are available only to the owning agent or private instance.
They do not appear in the dashboard Skills page or the skills API, because those views are not agent-scoped.
Hidden entries such as `.usage.json`, `.history/`, and `.archive/` are never loaded as skills.

Agents never need write access to a global skill root to create skills, so the bundled, plugin, and user roots can stay read-only, for example in container or Kubernetes deployments.
An agent with workspace-rooted file or shell tools can write a skill to `<workspace>/skills/<skill-name>/SKILL.md`, which its file tools address as `skills/<skill-name>/SKILL.md`.
Such agents are told how in their system prompt through the `WORKSPACE_SKILL_AUTHORING_PROMPT` built-in prompt, which you can change through [Built-In Prompt Overrides](https://docs.mindroom.chat/configuration/#built-in-prompt-overrides).
A new or edited workspace skill is picked up on the agent's next run without a config change.

Being able to write workspace skills does not make an agent create them.
To have agents save reusable workflows on their own, add that guidance to the agent's instructions, or enable [automatic skill learning](#automatic-skill-learning).

MindRoom loads at most 256 workspace skills per agent, skips a skill whose `SKILL.md` exceeds 1 MiB or whose name exceeds 64 characters, and truncates descriptions to 1,024 characters, logging a warning in each case.
Until the file is fixed, the owning agent's system prompt names each workspace skill skipped for its frontmatter or name, and why.

## Eligibility gating (OpenClaw metadata)

If `metadata.openclaw` is present, MindRoom loads the skill only when these rules pass:

- `os: ["linux", "darwin", "windows"]`: the current OS must be listed
- `always: true` bypasses `requires`, but not an OS mismatch
- `requires.env`: each environment variable is set, directly or through its [`_FILE` variant](https://docs.mindroom.chat/configuration/models/#file-based-secrets), or a stored credential with that key exists
- `requires.config`: each config path is truthy, for example `agents.code.tools`
- `requires.bins`: all binaries exist in `PATH`
- `requires.anyBins`: at least one binary exists in `PATH`

Skills without `metadata.openclaw` are always eligible.

## Using skills at runtime

Agents see their available skills in the system prompt and load details with these tools:

- `get_skill_instructions(skill_name)` loads the full instructions for a skill
- `get_skill_reference(skill_name, reference_path)` reads a reference document
- `get_skill_script(skill_name, script_path)` reads a script

`get_skill_script` never runs a script, whichever skill it comes from.
Agents with a shell tool run scripts with it, where their shell commands run.

Added, removed, or edited `SKILL.md` files take effect on the agent's next request, within about a second.
A workspace skill created during a turn becomes available on the next agent run, not in the same response.

## Automatic skill learning

Automatic skill learning follows the self-improvement loop of [Hermes Agent](https://github.com/NousResearch/hermes-agent).
After enough work in a conversation, a background review maintains a small library of class-level skills in the agent's workspace, and unused learned skills are archived.
It is opt-in for each agent:

```yaml
agents:
  assistant:
    display_name: Assistant
    skill_learning:
      enabled: true
      model: null  # null reviews on the response's own model and reuses its prompt cache
      review_interval: 10
      timeout_seconds: 120
      notify: true
      archive_after_days: 30
```

`mindroom config init` and newly provisioned hosted instances enable it for the starter Mind agent; set `agents.<name>.skill_learning.enabled: false` to turn it off.
Reviews incur additional model usage, reported under `kind: skill_learning` in [usage tracking](https://docs.mindroom.chat/usage/#token-usage).
Learned skills are generated from conversation content, so review them before relying on them for sensitive work.

### Settings

All `agents.<name>.skill_learning` fields are optional, and unknown fields are rejected.

| Field | Type | Default | Bounds and behavior |
|---|---|---|---|
| `enabled` | boolean | `false` | Count completed standalone responses, review conversations that reach the interval, and offer the `skill_manage` tool. |
| `model` | string or null | `null` | Review model alias from `models`; null reviews on the model the response used and reuses its prompt cache, while another model replays a digest of the conversation. |
| `review_interval` | integer | `10` | 1–1000 model replies per conversation between reviews, counting each tool-calling step. |
| `timeout_seconds` | integer | `120` | 10–900 seconds per review. |
| `notify` | boolean | `true` | Post an `m.notice` in the conversation when a review changes skills. |
| `archive_after_days` | integer | `30` | 0–3650 days with no use, creation, or `skill_manage` edit before a learned skill is archived; `0` keeps learned skills indefinitely. |

### When reviews run

Each conversation keeps a count of model replies: one for each tool-calling step and one for the final answer.
Only successful standalone-agent responses to a person in Matrix count.
Team responses, responses another agent asked for, scheduled, hook, and external-trigger responses, responses that resume after an approval, and runs resumed after a restart never count.
A response that calls `skill_manage` restarts the count from its last call, except in minimal mode, where `skill_manage` runs through the command line.
Once the count reaches `review_interval`, it restarts and the review starts in the background, so it never delays a reply.
A new response in the same conversation stops a running review.
Counts are per agent or private instance and conversation, not per requester, so a thread shared by several people is reviewed once.
Counts live in memory, so a restart starts every count over, and a review that fails or is stopped is not retried.

### What a review sees

By default, the review resends the final model request of the response that made the conversation due, on the same model with the review prompt appended.
The provider can serve it from its prompt cache, and the review sees the conversation verbatim, including tool calls and results.
This request is sent unredacted, because it is the request the same provider just received.

The review instead replays a digest of the stored conversation on a separate request when `skill_learning.model` names another model, the agent runs in minimal mode, or the final request cannot be reused or is too large for the review's input budget.
A digest shows the newest 24 messages verbatim and older messages shortened, leaves out older tool results, and redacts credential-like values on a best-effort basis.
Without its own `model`, a digest replay runs on the model the response used.

A review's input budget scales with the review model's `context_window`.
Set `context_window` on the agent's model so long conversations can still be reviewed verbatim; without it, a final request over about 30,000 input tokens is replayed as a digest.
A review makes a bounded number of tool calls and stops after `timeout_seconds`.

Only the skill tools run during a review; any other tool the agent has answers that it is not available, and scripts never run.
Provider-hosted tools, such as a provider's web search, stay available because the provider runs them.
The review prompt asks for class-level skills that capture lessons rather than logs, treats user corrections as first-class signals, prefers patches over rewrites, and excludes environment-specific failures, transient errors, one-off narratives, and unresolved attempts.
Override it through the `SKILL_REVIEW_PROMPT` [built-in prompt override](https://docs.mindroom.chat/configuration/#built-in-prompt-overrides).

### The skill_manage tool

Agents with skill learning enabled get a `skill_manage` tool, and the review writes skills with the same tool.
Other agents can list `skill_manage` in `tools` to save skills from chat.
Its actions are `create` a skill from a full `SKILL.md`, `patch` text in `SKILL.md` or a support file, `edit` to replace `SKILL.md`, and `write_file` or `remove_file` for one support file directly under `references/` or `scripts/`.
It cannot delete a skill; unused learned skills are archived, and you remove any skill by deleting its directory.

In chat, `skill_manage` can change any workspace skill, and a skill it creates belongs to the person, not the learner.
Configured bundled, plugin, and user skills are read-only.
Tool approval rules apply to chat calls as to any tool.
`skill_manage` works only on skills in their own `skills/<name>/` directories, so a workspace whose `skills/SKILL.md` makes the whole directory one skill gets no new skills or edits until that file moves into `skills/<name>/`.

A new skill needs a lowercase hyphenated name matching its directory and a description of at most 60 characters.
Files that contain a likely literal credential are refused with the offending line named: a quoted API key, token, secret, or password of at least 20 characters that does not name an environment variable, a private key header, or a GitHub, OpenAI, Anthropic, AWS, or GitLab token.
This check is a heuristic for common formats, not a guarantee.

### Ownership

The review edits only learned skills, never bundled, plugin, or user skills or workspace skills someone else wrote, and its new skills cannot reuse their names.
Ownership is recorded in `skills/.usage.json`, so a learned skill stays learner-owned when the agent or a person later rewrites it.
Skills the review creates also carry a visible marker in their frontmatter:

```yaml
metadata:
  mindroom:
    learned: true
```

Add this marker to a skill you wrote to hand it to the learner.
Add `pinned: true` under `metadata.mindroom` to any skill to keep the review and archival from ever editing or archiving it.
Private agents learn only from and into the requester's private workspace, and shared agents use `<storage>/agents/<agent>/workspace/skills/`.

### History, archive, and notices

Before `skill_manage` replaces or removes a file, in chat or in a review, it saves the previous version under `skills/.history/<skill>/`, keeping the ten newest versions.
Copy a saved version back to restore it.

Before each review, learned skills with no use, creation, or `skill_manage` edit for `archive_after_days` days move to `skills/.archive/<skill>--<timestamp>`; nothing is deleted.
Move a directory back to `skills/<skill>/` to restore it.
Archiving a skill forgets its ownership record, and a deleted skill's record is forgotten at the next review, so a skill restored or recreated after that starts a new inactivity period.
A restored learned skill still carries `learned: true` and stays learner-owned; add `pinned: true` to keep the learner from editing or archiving it.
Loading a workspace skill or one of its files through the skill tools counts as a use, also for agents without skill learning.
Minimal-mode agents read skills through their command line, which records no use, so a learned skill used only in minimal mode is archived after `archive_after_days`.
Archival is logged rather than announced, because other conversations may share the workspace.

With `notify: true`, a review that changed skills posts an `m.notice` in the conversation naming those skills, such as ``💾 Skill review: created `deploy-checks` ``.
The notice is also posted when a new response stopped the review after its changes landed, and it is left out of later model context.
Review log lines carry `kind: skill_learning`, so you can filter logs for them.

# Scheduling

Schedule agents or teams to do one-time or recurring work, such as reminders, daily reports, or periodic checks, using natural language.
Create schedules with the `!schedule` chat command or let an agent create them with the `scheduler` tool.

<video controls playsinline preload="metadata" aria-label="A scheduled task posts a morning brief" style="width: 100%" poster="https://github.com/user-attachments/assets/ada165aa-24ad-407f-9831-30928126d091" data-poster-light="https://github.com/user-attachments/assets/ada165aa-24ad-407f-9831-30928126d091" data-poster-dark="https://github.com/user-attachments/assets/01a8d67f-f1ed-4c6a-b2df-a1681149722e">
  <source src="https://github.com/user-attachments/assets/99f7e664-131d-4831-9c7e-64a2d95b2b6b" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/67fd537c-d810-4a3c-b5a5-d8e307708d56" type="video/mp4">
</video>

## Commands

Use `!help schedule` for inline help on the scheduling commands.

### `!schedule`

Schedule a one-time or recurring task.

```
!schedule <natural-language-request>
```

```
!schedule in 5 minutes Check the deployment
!schedule tomorrow at 3pm Send the weekly report
!schedule Every hour, @shell check server status
!schedule Daily at 9am, @finance market report
!schedule Weekly on Friday, @analyst prepare weekly summary
```

Include `@agent_name` or `@team_name` to choose who answers each run.
MindRoom checks that mentioned agents and teams are available in the room before it creates the task.

A task runs where it was created: in the room timeline for a room-level schedule, or in the current thread for a threaded schedule.
The task text never runs as a chat command, even when it starts with `!`; agents receive it as an ordinary message.

Conditional requests such as `if` or `when` become recurring polling schedules, not event subscriptions.
The condition is part of the task message, so the responder checks it on each run.
Include an explicit polling cadence for predictable behavior.

```
!schedule Every 5 minutes, check if I got an email about "urgent"; if so, @phone_agent call me
!schedule Every 10 minutes, check whether Bitcoin dropped below $40k; if so, @crypto_agent notify me
```

Add phrases to the request to change how each run behaves:

| Phrase | Effect |
| --- | --- |
| `with no history`, `without context`, `context-free` | Each run sees no prior messages; see [History limits](#history-limits) |
| `with only the last 5 messages of context` | Each run sees only recent messages |
| `silently`, `quietly` | Hide the trigger and post only findings; see [Silent delivery](#silent-delivery) |

### `!edit_schedule`

Replace an existing task's timing and content.

```
!edit_schedule <task-id> <new-task-description>
```

MindRoom re-parses the description to update timing and content.
Omitted settings stay unchanged, including the history limit, silent mode, and model.
A task cannot switch between one-time and recurring; cancel it and create a new one instead.
A request that would change nothing about the task is refused, and the task stays as it was.

```
!edit_schedule task42 keep the same schedule but restore full history
!edit_schedule task42 every weekday at 8am check build status with no history
!edit_schedule task42 keep the same schedule but make it silent
```

**Aliases:** `!editschedule`, `!edit-schedule`

### `!list_schedules`

List pending tasks in the current room, or in the current thread when used inside one.
Each entry shows its task ID, timing, mode (`Silent` or `Visible`), and selected model if any.

```
!list_schedules
```

**Aliases:** `!listschedules`, `!list-schedules`, `!list_schedule`, `!listschedule`, `!list-schedule`, `!inspect_schedules`, `!inspectschedules`, `!inspect-schedules`, `!inspect_schedule`, `!inspectschedule`, `!inspect-schedule`

### `!cancel_schedule`

Cancel one task, or every task in the room.

```
!cancel_schedule <task-id>
!cancel_schedule all
```

Use `!list_schedules` to find task IDs.
Only a pending task can be cancelled; a one-time task that already ran, and a task that failed or was cancelled, stay as they are.

**Aliases:** `!cancelschedule`, `!cancel-schedule`

## [`scheduler`]

<video controls playsinline preload="metadata" aria-label="A check asked for in conversation becomes a weekly scheduled task" style="width: 100%" poster="https://github.com/user-attachments/assets/7221a1d4-2429-4591-8b6f-aca242cc152b" data-poster-light="https://github.com/user-attachments/assets/7221a1d4-2429-4591-8b6f-aca242cc152b" data-poster-dark="https://github.com/user-attachments/assets/70ad19a8-6728-4344-9e6d-0258c43b5173">
  <source src="https://github.com/user-attachments/assets/3377307e-268f-4ae0-a0e6-061dd16c1627" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/4423fd02-b6c9-4cf5-bf10-ae50149ebd2e" type="video/mp4">
</video>

The `scheduler` tool lets an agent create, edit, list, and cancel the same tasks as the chat commands.
It is included in `defaults.tools` by default, needs no setup, and has no tool-specific configuration fields.
It only works from a live room or thread; elsewhere it returns `Scheduler tool is unavailable in this context.`

```yaml
agents:
  assistant:
    tools:
      - scheduler
```

| Function | Arguments |
| --- | --- |
| `schedule(request, new_thread, history_limit=None, silent=None, model=None)` | `new_thread` is required |
| `edit_schedule(task_id, request, history_limit=None, silent=None, model=None)` | Omitted arguments keep the task's current settings |
| `schedule_tool_call(tool_name, arguments_json, execute_at, description)` | One approval-gated call that the requester approves now; see [Pre-Approved Tool Calls](#pre-approved-tool-calls) |
| `run_scheduled_call(task_id, arguments_json=None)` | Runs a pre-approved scheduled call when its task asks; see [Pre-Approved Tool Calls](#pre-approved-tool-calls) |
| `list_schedules()` | |
| `cancel_schedule(task_id)` | |

- `new_thread=False` posts each run in the current room or thread.
  `new_thread=True` posts each run as a new room-level message, and the responder answers in a new thread under it with a fresh session.
- `history_limit` caps how many recent messages each run sees: `0` for none, a positive integer for that many, unset for full history.
- `silent=True` turns on [silent delivery](#silent-delivery); unset infers the mode from the request text.
- `model` picks a model alias for each run; see [Model selection](#model-selection).

```python
schedule("tomorrow at 9am @ops check the deployment", new_thread=False)
schedule("every weekday at 8am post the on-call handoff summary", new_thread=True)
schedule("every hour @ops check deployment health", new_thread=False, history_limit=0, model="cheap")
schedule("every 5 minutes check the inbox for urgent mail", new_thread=False, history_limit=0, silent=True)
list_schedules()
edit_schedule("a1b2c3d4", "tomorrow at 10am @ops check the deployment", history_limit=5, silent=False)
cancel_schedule("a1b2c3d4")
```

## History Limits

Scheduled runs normally use the responder's configured conversation history.
Use `with no history`, `without context`, or `context-free` when each run should see no prior room or thread messages; the system prompt and the task message remain available.
Use phrases such as `with only the last 5 messages of context` or `include the last 5 messages` to cap each run to recent context.
In an edit, say `restore full history` or `use unlimited history` to remove a limit.
Agents set the same limit with the tool's `history_limit` argument.

## Silent Delivery

Schedules are visible by default.
Add `silently` or `quietly` to a request when the trigger and routine no-report results should stay out of the room timeline.

```
!schedule Every 5 minutes, quietly check the inbox for urgent messages and report only when one arrives
!schedule Daily at 9am, silently check whether the backup failed and report failures
```

A silent schedule does not show its trigger in the room.
A successful run that returns nothing, whitespace, or only `NO_REPLY` (any case) posts no message.
Findings, failures, and messages that tools send explicitly remain visible.
Silent runs also show no typing indicator, progress placeholder, stop control, or streaming updates.
With `new_thread=True`, a silent schedule posts any finding or failure as a new room-level message.
Say `make this schedule silent` or `make this schedule visible` in `!edit_schedule` to change the mode.

Silent delivery only hides messages from the room; the task text still travels through Matrix and is subject to homeserver retention.

Each silent run writes a JSON receipt to `<agent-workspace>/.mindroom/scheduled_runs/`, so you can check what a silent run did.
A receipt records the prompt, room, thread, `status` (`started` or `completed`), `result` (`reported`, `no_report`, or `suppressed`), the final response text, and UTC start and completion times.
A receipt still in `started` means the run began but never reached a final response.
Team runs write a receipt to each member agent's workspace, and private agents write to the requester's workspace.
Workspace knowledge indexing skips these receipts.

## Model Selection

Pass `model` to `schedule()` to run each scheduled response with a model alias from `models:` in `config.yaml`, for example a cheaper model for simple recurring checks.

```python
schedule("every hour @ops check deployment health", new_thread=False, history_limit=0, model="cheap")
edit_schedule("task42", "keep the same schedule and task", model="cheap")
edit_schedule("task42", "keep the same schedule and task", model="")
```

The model applies to the whole scheduled response, including a team's coordinator and members.
It takes precedence over room and thread model settings without changing them for later messages.
Omitting `model` on creation uses normal model selection; omitting it on an edit keeps the saved choice, and an empty string restores normal model selection.
Parsing the scheduling request itself always uses the default model.

## Pre-Approved Tool Calls

An agent can store one of its own approval-gated tool calls to run later, and the requester approves it while scheduling it, so a gated action such as an external message runs at its time without a second approval.
The agent calls `schedule_tool_call(tool_name, arguments_json, execute_at, description)` from a thread.
`tool_name` must be one of that agent's own tools, `arguments_json` is a JSON object of the call's arguments, and `execute_at` is an ISO 8601 time with a UTC offset, such as `2026-10-04T09:00:00-04:00`.
The `tool_approval` policy must require approval for the call (or the tool must ask for its own confirmation), the scheduler must be an agent rather than a team, and the agent must be able to reply to the requester in that room; otherwise the scheduler refuses it.

```python
schedule_tool_call(
    tool_name="post_slack_message",
    arguments_json='{"channel": "U0123ABCD", "text": "Good morning! The report is ready."}',
    execute_at="2026-10-04T09:00:00-04:00",
    description="Morning report DM",
)
```

MindRoom stores the call and posts an approval card in the thread showing the tool, its arguments, and the send time; only the requester can approve or deny it, and it expires at the send time.
The requester approves either the stored arguments or, when the card offers it, any arguments to the same tool.
Approving any arguments lets the agent decide the arguments for that tool when the task runs, following the task's description, so choose it only when the content must be decided later.
The card offers it only when requesters approve their own calls; operators can stop offering it with [`tool_approval.scheduled_any_arguments`](https://docs.mindroom.chat/tool-approval/), and generic MCP `*_call_tool` calls and tools that ask for their own confirmation are always approved exactly.

When the task runs, it asks the agent to call `run_scheduled_call(task_id)`, which runs the stored call once with the stored arguments, without a new card, and posts an approved receipt naming who approved it, when, and for which scope.
With an any-arguments approval the agent may pass `arguments_json` to `run_scheduled_call` to replace the arguments for the same tool.
The approval can be used once, by that agent for that requester in that thread, from when the task runs until 15 minutes after its scheduled time.
If the card was not approved in time, `run_scheduled_call` runs nothing and the agent can call the tool directly, which asks for approval as usual.
If the requester denies the card, the task does not run, and cancelling the task withdraws the approval.
A pre-approved task cannot be edited; cancel it and schedule the call again.
The arguments are not posted in the task or its trigger message; MindRoom keeps them with the approval, and the card shows them with secrets hidden.
Recurring schedules cannot pre-approve calls.

## Timezone

The top-level `timezone` setting (default `UTC`) is the IANA timezone MindRoom uses for the current date in agent prompts, message timestamps, file-memory daily notes, automation `cron` times, and interpreting and displaying scheduled times.
An agent's own `timezone` overrides it for that agent, so one install can serve people in different timezones; the router and chat commands such as `!schedule` use the top-level setting.
A team reply uses the timezone of the account that posts it: a configured team uses the top-level setting, and an [ad hoc team](https://docs.mindroom.chat/configuration/teams/#ad-hoc-teams) uses the timezone of the agent whose account posts its reply.
An unknown timezone name fails config validation.

```yaml
timezone: America/Los_Angeles
agents:
  atlas:
    display_name: Atlas
    timezone: Europe/Amsterdam
```

A schedule keeps the timezone it was created in: when `atlas` schedules "tomorrow at 9" with its `scheduler` tool, the task runs at 09:00 in Amsterdam, and later chat edits and listings use Amsterdam time too.
The dashboard's Schedules page shows dates and times in the top-level timezone, while every recurring schedule's description names the timezone it is written in, such as `At 09:00 (Europe/Amsterdam)`.
Schedules created before agents had their own timezone keep following the top-level setting.

Recurring schedules run on UTC cron times, so their local clock time shifts when the timezone's UTC offset changes, such as at daylight-saving transitions.
Edit or recreate a recurring schedule after an offset change if it must keep the same local time.

## Permissions and Automatic Cancellation

Schedules belong to the room, not to their creator.
Any authorized participant in the room can edit or cancel any of its schedules by task ID, and listing inside a thread only filters the view.
Each run acts as the schedule's creator, as if the creator had sent the message, so it uses that person's access, per-user credentials, and private-agent workspace.
Editing a schedule with `!edit_schedule` or the `scheduler` tool makes the editor its new creator.

MindRoom cancels a schedule automatically once neither its creator nor any of the creator's [bridge aliases](https://docs.mindroom.chat/authorization/#bridge-aliases) is joined to the room.
Bot accounts and managed identities do not count.
A run waits rather than proceeding when membership cannot be confirmed.

## Persistence and Missed Runs

Schedules are stored in Matrix room state and survive restarts.
In a room MindRoom did not create, a room admin may need to raise the router's power level to the room's state-event level, usually 50, before schedules can be saved; raising the router is enough for every agent, and the error names the account and the level to grant.
MindRoom only acts on schedule state written by its own router, agent, or team accounts; schedule state written by any other account, including a room admin, is ignored and logged.
The homeserver must support `GET /_matrix/client/v3/rooms/{roomId}/state/{eventType}/{stateKey}?format=event` (Matrix spec v1.16), as Synapse, Tuwunel, and Dendrite do; otherwise reading tasks fails.

### Scheduled Task Restoration

The router runs a schedule when, at the time the schedule is saved, it may post its trigger and save schedule state in the room; otherwise the agent that saved the schedule runs it, as in a DM or an agent's personal room.
Every agent restores the schedules it runs in a room when it joins that room, at startup or when it accepts an invitation, and the router also restores the room's pending configuration changes.
Schedules keep running when their agent restarts, for example after a config edit: a one-time task that came due during the restart runs once the agent is back, and a recurring task runs the occurrence that came due if it is still within the catch-up window.
A one-time task missed by less than 24 hours runs once its agent's Matrix sync is ready; an older one is marked failed instead.
When an agent is removed from the config while MindRoom runs, each schedule it runs is marked failed and the agent says so in the schedule's thread, or in the room for a schedule without one; its schedules never move to another agent.
When it is removed while MindRoom is stopped, a bot that may save schedule state in a room marks the agent's schedules there failed and says so when it rejoins that room, and schedules in the agent's DMs stay pending.

### Recurring task recovery

After a restart or a late wake-up, a recurring task runs its latest missed occurrence if it is within the catch-up window, and multiple missed occurrences run once.
Occurrences outside the window are skipped and logged, and later runs keep the normal cadence.

```yaml
scheduler_catch_up_grace_seconds: 3600  # default; 0 disables catch-up
```

Recurring progress is kept in `tracking/recurring_schedules/` under the storage directory, which must persist across restarts.
Without it, or after a schedule edit, MindRoom resumes from the next future occurrence without replaying past ones.
Restarts do not post the same trigger twice.
For `schedule:fired` hooks that run more than once for the same occurrence, see [Hooks](https://docs.mindroom.chat/hooks/#event-notes).

## Automations

Automations are checks MindRoom runs on a cron schedule; when one passes, the agent posts the automation's prompt in its room and answers it with a normal, visible run.
MindRoom ships three, [`prompt_curation`](#prompt_curation), [`dreaming`](#dreaming), and [`exploration`](#exploration), and [plugins](https://docs.mindroom.chat/plugins/#automations) can add more.
The check runs in code, so a schedule that finds nothing to do costs no model call and posts nothing.
Enable them per agent in `config.yaml`, or under `defaults` for every eligible agent:

```yaml
defaults:
  automations: []                # inherited by agents that omit the field

agents:
  mind:
    memory_backend: file
    automations:
      - prompt_curation          # the built-in with its defaults
      # or: {name: prompt_curation, cron: "0 4 * * *", room: personal, trigger_tokens: 30000, model: opus}
      - dreaming
      - {name: weekly_digest, cron: "0 9 * * 1", options: {target: digest.md}}   # from a plugin
```

- An entry is a built-in name, or a mapping with `name` plus overrides; unknown fields fail config load.
- Any other name is an automation a loaded plugin provides: it needs `cron` and takes the `options` the plugin documents; an entry no loaded plugin provides is skipped with a warning in the log and fails `mindroom config validate`.
- `cron` is a five-field expression in the agent's [timezone](#timezone).
- `room` is a room alias or ID; it defaults to the agent's first configured room.
- Each run starts in a new thread, even for an agent with `thread_mode: room`, and a re-check follows up in that thread.
- `agents.<name>.automations: []` turns inherited defaults off for one agent.
- A [private agent](https://docs.mindroom.chat/configuration/agents/#private-instances) runs its automations once for each requester who has used it, as that requester and on that requester's own files, and posts in their [personal room](https://docs.mindroom.chat/personal-rooms/) with the agent.
  Only the agent named by `personal_rooms.agent` can run automations as a private agent, its entries cannot set `room`, and it inherits only default entries without `room`.
  A requester without a personal room, or whom the agent may no longer answer, is skipped.
  Everyone who can read that personal room, such as an agent the requester invited or an adopted room's other participants, sees the prompts and replies, like any other reply there.
- `prompt_curation` and `dreaming` need `memory_backend: file`, and `exploration` needs `knowledge_graph: true`; an agent that lacks what a default entry needs skips it and keeps inheriting the others.
- Edits apply on config reload without restarting the agent.
- The agent posts the prompt in its own name and mentions itself, so it answers even in a room with other agents.
- When the response to a prompt is final, or an hour after the prompt was posted without one, the automation's next step runs: it posts a notice in the prompt's thread or asks the agent again.
- An agent, or each requester's instance of a private agent, runs one automation at a time; one that comes due meanwhile starts when the other ends.
- A check that cannot read what it checks posts a warning in the room.
- A restart skips an occurrence it missed, and prompts posted before the restart get no follow-up.

For conditions that need your own code, write a [plugin automation](https://docs.mindroom.chat/plugins/#automations), or gate an ordinary recurring schedule with a [`schedule:fired` hook](https://docs.mindroom.chat/hooks/#event-notes), which can suppress a fire or rewrite its message.

### `prompt_curation`

Agents append to `MEMORY.md` and their `context_files` far more often than they condense them, and every model call re-sends those files.
`prompt_curation` checks their total size daily and, once it passes the trigger, asks the agent for a gradual cut.

1. The check measures `MEMORY.md` plus the agent's `context_files` with the estimate behind `static_prompt_tokens` (characters / 4); for a private agent these are `MEMORY.md` and `private.context_files` in the requester's private root, because agent-level `context_files` load from the workspace every requester shares.
2. Above `trigger_tokens`, or when the preload has to withhold part of `MEMORY.md` because it passes `memory.file.max_entrypoint_lines` (default `200`) or `memory.file.max_entrypoint_tokens`, it posts a prompt with exact numbers, for example "bring them to at most 19800 tokens in total, but not below 18700", a cut between `min_reduction` and `max_reduction` (10 to 15%).
3. The prompt asks the agent to commit the files to git first, take lines out of a `MEMORY.md` over either preload cap first (the prompt shows its line and token counts against both caps), fold each later correction into the entry it corrects, keep each fact once in the file that owns it, move detail and history verbatim into `memory/` topic files with one-line pointers, and otherwise never invent facts or change their meaning.
4. Once the run ends, verify measures the files again; when any of these holds, it lists them in the thread and mentions the agent once to re-check its change against that commit:
    - a file can no longer be read safely;
    - a file shrank by more than 25%;
    - the files total less than the floor, or did not shrink;
    - `MEMORY.md` was over the line cap and still has as many lines;
    - a file is no longer valid UTF-8;
    - total memory content (the files plus `memory/**`) dropped by more than `max_content_loss` of the files' size, which means detail was deleted instead of moved.

When the files changed and none of these holds, verify reports the new size and marks the thread resolved.
When the prompt files are unchanged, verify only checks that no `memory/` detail was deleted, and otherwise reports that nothing changed.
Verify never changes the files; the agent's answer to a re-check is not verified again, and the next pass comes on the next scheduled check.

| Field | Default | Description |
|---|---|---|
| `cron` | `0 4 * * *` | When to check |
| `room` | first configured room | Where to post the prompt |
| `trigger_tokens` | `20000` (min 1) | Post the prompt once the files exceed this many estimated tokens; a `MEMORY.md` the preload cannot show whole also posts it |
| `min_reduction` | `0.10` (at most `0.25`) | Smallest cut the prompt asks for; no file may shrink more than 25%, so no pass can be asked for more |
| `max_reduction` | `0.15` | Largest cut before verify asks for a re-check |
| `max_content_loss` | `0.05` | Largest net drop in total memory content, as a fraction of the files' size, before verify asks for a re-check |
| `model` | the agent's model | A key of `models` to run the prompt and its re-check with, for example one with a larger context window than the agent's everyday model |

`prompt_curation` needs `memory_backend: file`, because moved detail must stay searchable, and the prompt templates are overridable as `PROMPT_CURATION_PROMPT_TEMPLATE` and `PROMPT_CURATION_RECHECK_TEMPLATE`.

### `dreaming`

`dreaming` keeps the agent's `memory/` files current: each night it reconciles them with new conversations and daily notes, through a proposal that a second run reviews before MindRoom applies it.

1. It runs only when a [thread export](https://docs.mindroom.chat/thread-exports/) or a daily note from before today is new or changed since its last run, so a night without new threaded messages in the agent's rooms and without new daily notes costs nothing.
2. The agent proposes changes in a copy of `memory/`: it updates facts that later evidence corrects, marks superseded lines instead of deleting them, files new durable facts such as decisions, preferences, and stable settings in topic files with their source and date, records conflicting sources with both versions, and removes duplicates.
   It leaves transient tasks in the daily notes and never removes or doubts a fact only because newer notes do not mention it.
3. A second run, in a thread of its own, reviews the proposal against its sources and approves or rejects it.
4. MindRoom applies an approved proposal and marks both threads resolved.

MindRoom never changes `MEMORY.md`, context files, or today's daily note; the agent suggests changes to them in its report.
When the review rejects a proposal, a run stops unfinished or changes a file it may not, or memory changed during the run, nothing is applied; the next run, once a new thread export or daily note arrives, retries the same inputs and starts from any proposal that reached review and its review.
A run handles at most 40 inputs; the rest wait for later runs.
Turning the automation or thread exports on starts from conversations and notes of the last seven days instead of reviewing the older archive.
Each run keeps its agenda and report, and a run that reached review also its patch and verdict, in `.mindroom/dreaming/runs/<run>/` in the workspace, the newest 30 runs at least; undo an applied run with `git apply -R .mindroom/dreaming/runs/<run>/proposal.patch` from the workspace root.

| Field | Default | Description |
|---|---|---|
| `cron` | `15 3 * * *` | When to check |
| `room` | first configured room | Where both prompts are posted |
| `model` | the agent's model | A key of `models` to run both prompts with |

`dreaming` needs `memory_backend: file` and something to work from: [auto-flush](https://docs.mindroom.chat/memory/#file-auto-flush-worker) writes the daily notes it reviews and [thread exports](https://docs.mindroom.chat/thread-exports/) add threaded conversations, so with neither on, and no daily notes the agent writes itself, it never runs.
It also needs a tool that writes workspace files, such as `file`, `coding`, or `shell`, because the agent edits its copy and writes its report and verdict as files; without one, every run stops unfinished.
Its prompt templates are overridable as `DREAMING_PROMPT_TEMPLATE` and `DREAMING_VERIFY_TEMPLATE`.

### `exploration`

`exploration` asks an agent with a [knowledge graph](https://docs.mindroom.chat/knowledge-graph/) to explore its data sources on a schedule, so the graph learns about projects, people, and documents nobody has asked about yet.

```yaml
agents:
  mind:
    knowledge_graph: true
    tools: [mcp_github, duckduckgo]
    automations:
      - name: exploration
        cron: "0 */6 * * *"
        room: "#mind-exploration"
        max_tool_calls: 30
```

1. The check lists the tool calls the agent's conversations made since its last run, such as the repositories and searches people asked about.
2. When there are such calls, or `revisit_days` have passed since the last run and people talked to the agent since, the agent posts a prompt in a new thread to check what the graph knows, re-read those sources for changes, and follow references to what the graph lacks; an agent with [`knowledge_graph_edits`](https://docs.mindroom.chat/knowledge-graph/#letting-the-agent-fix-its-graph) is also asked to fix the mistakes it found in the graph.
3. The agent answers the prompt like any message, with its own tools and [approval rules](https://docs.mindroom.chat/tool-approval/); a shared agent's call that needs approval is denied, because no person asked for the run, while a private agent's call waits in the thread until its requester answers or the request expires.
4. When the run ends, MindRoom marks the thread resolved and adds its tool results to the graph; the agent's own summary is not added, and an interrupted run adds nothing.

The prompt tells the agent to only read, apart from fixing its graph when it may edit it, and to stay within `max_tool_calls`, but nothing enforces either: the agent's `max_tool_calls_per_turn` is the hard limit, and approval rules are what stop writes.
The prompt lists recent tool calls with their arguments, credential-like values removed and mentions disabled so it wakes nobody, so a shared agent's exploration needs a `room` whose members may see what the agent's conversations looked up; a private agent posts in each requester's personal room.
A turn whose message was redacted can still be listed until the agent next replies in that conversation.

| Field | Default | Description |
|---|---|---|
| `cron` | `30 2 * * *` | When to check |
| `room` | required for a shared agent | Where the prompt is posted |
| `max_tool_calls` | `40` (1 to 1000) | Tool calls the prompt asks one run to stay within |
| `max_leads` | `20` (0 to 200) | Most recent tool calls listed in the prompt |
| `revisit_days` | `7` (min 1) | Run without new tool calls once this many days have passed since the last run, if people talked to the agent since |
| `model` | the agent's model | A key of `models` to run the exploration with |

Its prompt template is overridable as `EXPLORATION_PROMPT_TEMPLATE`; for an agent with `knowledge_graph_edits`, its `{fix_step}` and `{fix_exception}` placeholders insert `EXPLORATION_FIX_STEP` and `EXPLORATION_FIX_EXCEPTION`, which are overridable too and start with their own separator, a line break and a comma.
Its model usage counts as ordinary replies, and extracting what it read counts as `knowledge_graph` usage.

# Chat Commands

Chat commands are messages starting with `!` that you type in any Matrix room where MindRoom agents or teams are present.
Use them to get help, manage schedules, switch models or modes, manage room encryption, and, when enabled, change configuration from chat.
This page lists every command and fully covers `!help`, `!hi`, and `!config`; the other commands are documented on the linked pages.

## Command List

| Command | Description | Details |
|---------|-------------|---------|
| `!help [topic]` | Get help on commands or a specific topic | [`!help`](#help) |
| `!hi` | Show the welcome message again | [`!hi`](#hi) |
| `!schedule <task>` | Schedule a task or reminder | [Scheduling](https://docs.mindroom.chat/scheduling/#schedule) |
| `!list_schedules` | List pending scheduled tasks | [Scheduling](https://docs.mindroom.chat/scheduling/#list_schedules) |
| `!cancel_schedule <id>` | Cancel a scheduled task | [Scheduling](https://docs.mindroom.chat/scheduling/#cancel_schedule) |
| `!edit_schedule <id> <task>` | Edit an existing scheduled task | [Scheduling](https://docs.mindroom.chat/scheduling/#edit_schedule) |
| `!jobs` | List the background jobs running in this thread, or in the whole room when sent outside any thread | [Background jobs](https://docs.mindroom.chat/tools/agent-orchestration/#background-jobs) |
| `!cancel_job <id\|all>` | Cancel a background job by the short ID `!jobs` shows, or all of them in this thread, or in the whole room when sent outside any thread | [Background jobs](https://docs.mindroom.chat/tools/agent-orchestration/#background-jobs) |
| `!desktop [setup\|status\|confirm\|rotate\|disconnect]` | Manage your Desktop target for one agent | [Desktop](https://docs.mindroom.chat/tools/desktop/#desktop) |
| `!mode <agent> minimal\|standard\|show\|reset` | Switch one agent between standard tools and a Bash-only interface | [Minimal Agent Mode](https://docs.mindroom.chat/tools/agent-cli/#mode) |
| `!model [name\|list\|reset]` | Show or switch the model used in the current thread | [Models](https://docs.mindroom.chat/configuration/models/#model) |
| `!room_model [name\|list\|reset]` | Show the room model default or switch it (set/reset require a room admin) | [Models](https://docs.mindroom.chat/configuration/models/#room_model) |
| `!thread_mode [room\|thread\|reset\|show]` | Show or switch the thread mode used in the current room (set/reset require a room admin) | [Threads](https://docs.mindroom.chat/configuration/threads/#thread_mode) |
| `!auto_approve [<minutes>\|<hours>h\|off]` | Run your tool calls in this thread without approval for a while, or show or end that window | [Tool Approval](https://docs.mindroom.chat/tool-approval/#auto-approve-everything) |
| `!encrypt [confirm]` | Enable end-to-end encryption for this room (irreversible, room admin only) | [Matrix](https://docs.mindroom.chat/matrix/#encrypt) |
| `!e2ee` | Show encryption diagnostics for this room | [Matrix](https://docs.mindroom.chat/matrix/#e2ee) |
| `!config <operation>` | View and modify configuration (disabled by default, platform administrators only) | [`!config`](#config) |
| `!reload-plugins` | Force-reload all configured plugins (platform administrators only) | [Plugins](https://docs.mindroom.chat/plugins/#reload-plugins) |

Command names are case-insensitive.
Any other message starting with `!` gets `❌ Unknown command. Try !help for available commands.`

## Command Handling

The router answers commands, in the main room and in threads, even in rooms with a single agent.
The exception is `!desktop`, which a private agent may answer itself when the room contains only you and that agent; see [`!desktop`](https://docs.mindroom.chat/tools/desktop/#desktop) for its room requirements.

Commands follow the same [authorization](https://docs.mindroom.chat/authorization/) rules as ordinary messages.
"Room admin" means a Matrix room administrator, while "platform administrator" means a user listed in `administrators`.

Commands must be typed as text; voice messages are never turned into commands.

An agent can send `!help`, `!mode`, `!model`, and the schedule commands on your behalf, and they run with your permissions.
Any other command sent by an agent for you is refused with `❌ Agents cannot run this command for you. Send it yourself.`

## `!help`

`!help` lists all commands, and `!help <topic>` shows detailed usage and examples for one topic.

```
!help
!help schedule
!help config
```

**Topics:** `schedule`, `list_schedules`, `inspect_schedules`, `cancel`, `cancel_schedule`, `edit`, `edit_schedule`, `config`, `mode`, `model`, `room_model`, `room-model`, `roommodel`, `thread_mode`, `thread-mode`, `threadmode`, `auto_approve`, `auto-approve`, `autoapprove`, `reload-plugins`, `reload_plugins`, `encrypt`, `e2ee`, `encryption`

Any other topic shows the general command list.

## `!hi`

`!hi` shows the room's welcome message again: the agents and teams you can address, their roles and tools, and quick-start instructions.

## `!config`

`!config` views and changes `config.yaml` from chat.
It is disabled by default; set `authorization.config_command_enabled: true` (boolean, default `false`) to enable it.
Even when enabled, only platform administrators can use it.
Otherwise it replies `❌ Config command disabled.` or `❌ Admin only.`

```
!config show
!config get agents
!config get agents.analyst.display_name
!config set agents.analyst.display_name "Research Expert"
!config set models.default.id gpt-6-astra
!config set defaults.markdown false
!config set agents.analyst.tools [shell, file]
```

`!config` alone is the same as `!config show`.
Paths use dot notation, such as `agents.analyst.role`, and list items use indexes, such as `agents.analyst.tools.0` for the first tool.
Values are parsed as YAML, so `false`, `42`, and `[shell, file]` become a boolean, a number, and a list.
Quote a value to keep its exact spacing.

Shown values are redacted like `config_manager` output; see [Access, Redaction, and Saving](https://docs.mindroom.chat/tools/agent-orchestration/#access-redaction-and-saving).
`!config set` refuses a value containing the `***redacted***` marker or masked URL credentials such as `***@host`, so copying shown values back cannot overwrite a hidden secret; set the field to its real value instead.

### Configuration Confirmations

`!config set` validates the change against the full configuration and replies with an error ending in `Changes were NOT applied.` when it is invalid.
A valid change gets a preview of the current and new values from the router, with ✅ and ❌ reactions.
Only the user who requested the change can confirm or cancel it, and that user must still be a platform administrator.
Unconfirmed changes expire after 24 hours.

Pending changes usually survive a MindRoom restart; if one cannot, such as a secret value, the confirmation asks you to run `!config set` again.

On confirmation, the change is saved to `config.yaml` and applies to new agent interactions, except [event journal](https://docs.mindroom.chat/deployment/storage/#event-journal) changes, which apply after MindRoom restarts.
If `config.yaml` changes while a confirmed change is being saved, for example through a dashboard save, the change is refused with `Changes were NOT applied.`; run `!config set` again.

## Stop Button

Cancel an in-progress response by reacting with 🛑 on the message being generated.
When `defaults.show_stop_button` is `true` (the default), MindRoom adds that 🛑 reaction to the message for you to click.
Setting `show_stop_button: false` hides only that reaction; a 🛑 reaction you add yourself still cancels.

The stop button only works on messages still being generated, and 🛑 reactions from agents are ignored.
The exception is an agent's [`stop_reply`](https://docs.mindroom.chat/tools/matrix-and-attachments/#stop_reply) call, which stops a reply on behalf of the user it works for.
See [Cancellation and Errors](https://docs.mindroom.chat/streaming/#cancellation-and-errors) for how a cancelled response is finalized.

# Streaming Responses

MindRoom streams agent responses to Matrix by progressively editing a single message, so users see text appear as the model generates it.
This page covers when streaming is used, how to tune or disable it, how streamed messages show tool calls, completion, cancellation, and errors, and how oversized responses are delivered.

<video controls playsinline preload="metadata" aria-label="Three colleagues and one agent in a shared thread, each answer streaming live" style="width: 100%" poster="https://github.com/user-attachments/assets/dc935612-af9a-4556-a502-5c1ffaf3810a" data-poster-light="https://github.com/user-attachments/assets/dc935612-af9a-4556-a502-5c1ffaf3810a" data-poster-dark="https://github.com/user-attachments/assets/d3312f2a-0315-4109-83d6-3470bb2a4247">
  <source src="https://github.com/user-attachments/assets/0545ae90-c825-496d-81ad-45d3cc069822" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/71036d2f-4ccd-4cdd-99c2-518b915f2ba0" type="video/mp4">
</video>



## How It Works

1. MindRoom sends an initial message showing a `Thinking...` placeholder until text arrives.
2. As text arrives, MindRoom edits the same message with the accumulated content.
3. When the response finishes, a final edit holds the complete text.

When a response is not streamed, the placeholder is replaced with the complete response in a single edit.
Agents also show a typing indicator while they work on a response.

## Live Updates

When the homeserver relays live updates, as MindRoom's Tuwunel fork (`mindroom-tuwunel`) does for mindroom.chat, hosted MindRoom, and `mindroom-stack`, MindRoom Chat shows a streamed reply about ten times a second, including its tool calls, worker progress, and [reasoning](#reasoning), without editing the message each time.
While a tool such as `shell` runs, MindRoom Chat also shows the last lines of its output under the call, with credentials masked as in tool results; the message keeps only the tool's result, and team replies do not show it.
The message itself is then edited only every `update_interval` seconds (5 by default) and once at the end, so Element, bridges, and other clients show the reply in steps of about five seconds and end with the same final message.
A reply that grows too large for a live update, as a very long reply with many tool calls can, switches to the progressive edits below for the rest of the reply.
Live updates are encrypted like the reply in an encrypted room.
They reach only accounts on the agents' homeserver; people on other homeservers see the message edits.
MindRoom Chat opened while a reply is streaming shows the last edit until the next live update, at most five seconds later.
A reply that is stopped or ends with an error keeps the text of its last edit, so up to `update_interval` seconds of text MindRoom Chat showed live can disappear above the note.
A homeserver relays them when its `/_matrix/client/versions` lists `io.mindroom.ephemeral_opt_in`, which the fork does unless its `mindroom_ephemeral_events_enabled` option is off; with any other homeserver every client sees the progressive edits below.
Set `defaults.streaming.live_frames: false` to turn live updates off, so every client sees the progressive edits below.

## Configuration

Streaming is enabled by default.
These settings are global-only under `defaults` and cannot be overridden per agent.

```yaml
defaults:
  enable_streaming: true         # Set false to disable progressive edits
  streaming:
    update_interval: 5.0
    min_update_interval: 0.5
    interval_ramp_seconds: 15.0
    max_idle: 2.0
    live_frames: true
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enable_streaming` | bool | `true` | Stream responses via progressive message edits. When `false`, the placeholder is replaced with the complete response in a single edit. |
| `streaming.update_interval` | float, > 0 | `5.0` | Steady-state seconds between edits. |
| `streaming.min_update_interval` | float, > 0 | `0.5` | Seconds between edits at the start of a response. |
| `streaming.interval_ramp_seconds` | float, >= 0 | `15.0` | Seconds over which the interval ramps from `min_update_interval` to `update_interval`. Set `0` to use `update_interval` from the start. |
| `streaming.max_idle` | float, > 0 | `2.0` | When no new text has arrived for this many seconds, the buffered text is shown with the next streaming update. |
| `streaming.live_frames` | bool | `true` | Send [live updates](#live-updates) when the homeserver relays them, and edit the message only every `update_interval`. |

Edits also come sooner than the interval once enough new text has accumulated, and a tool-call marker appears as soon as the tool starts.
While live updates run, edits come only every `update_interval`, and the other timing settings have no effect.
Raise `update_interval` to reduce load on the homeserver, or lower it for smoother updates.

## Presence-Based Streaming

Even when streaming is enabled, MindRoom streams a response only when the user who sent the message is `online` or `unavailable`; for an `offline` requester, the placeholder is replaced with the complete response.
Everyone in the room sees the same edits.
If the presence check fails, for example because the homeserver has presence disabled, MindRoom does not stream.
A reply that continues after a restart always streams, even when streaming is off or the requester is offline, so the part it already showed stays in the message.
When no requester can be identified, MindRoom streams.

## Presence

Running agents appear `online` (or `unavailable` when idle) and `offline` when stopped or disconnected.
Their presence status message shows the model, role, and tool count, for example `🤖 Model: anthropic/claude-sonnet-5-5 | 💼 Code assistant | 🔧 5 tools available`.

## Stream Status

Each streamed message carries its progress in the Matrix event content field `io.mindroom.stream_status`.

| Value | Meaning |
|-------|---------|
| `pending` | Initial message of the response |
| `streaming` | Response is still being generated |
| `approval_pending` | Response is paused until someone approves or denies a tool call (see [Tool Approval](https://docs.mindroom.chat/tool-approval/)) |
| `completed` | Response finished, including one that ends by reporting an error, such as a provider failure, whether or not part of the answer came first |
| `cancelled` | User stopped the response |
| `error` | MindRoom could not finish or deliver the response, for example because of a restart or a failed delivery; a response cut short ends with a note that says why (see [Cancellation and Errors](#cancellation-and-errors)) |

To confirm a response finished, inspect the latest `io.mindroom.stream_status` in a client or event view that shows event content.
MindRoom never appends an ellipsis to generated text, and clients may draw their own progress indicators, so a trailing ellipsis is not a progress signal.

## Tool Calls During Streaming

When an agent calls tools, MindRoom shows inline markers in the message text:

```
🔧 `web_search` [1] ⏳       ← tool call started
🔧 `web_search` [1]          ← tool call completed
```

The number in brackets counts tool calls within the message, and `io.mindroom.tool_trace.events[N-1]` in the event content holds the tool name, argument preview, and result preview for call `[N]`.
Tools that start in quick succession appear together in one edit.
Markers and tool-trace metadata are shown by default; to hide them, set `show_tool_calls: false` on one agent or under `defaults` for every agent that leaves it unset (see [Agent Configuration](https://docs.mindroom.chat/configuration/agents/#defaults)).
With tool calls hidden, the agent still shows typing activity, and a tool that needs an isolated worker may show generic progress such as `Preparing isolated worker... 17s elapsed.` without naming the tool.

## Reasoning

When a model reasons before or between parts of its answer, as with Anthropic extended thinking, OpenAI reasoning summaries, Gemini thinking, or `<think>` blocks, a streamed reply shows that reasoning to everyone in the room.
MindRoom Chat shows it live above the answer and, once the reply finishes, as a collapsed "Thought for N s" block, where N is how long the model reasoned.
The final message keeps it in `io.mindroom.reasoning`; it is never part of the answer text or the agent's history.
Set `show_reasoning: false` on an agent, or under `defaults`, to keep reasoning out of the room.
Reasoning can describe tool calls that `show_tool_calls: false` hides, so set `show_reasoning: false` as well to keep them out of the room.
Long reasoning is shortened on the final message so the reply stays one ordinary message; replies that are not streamed, team replies, and replies split into several messages (`large_message_strategy: split`) keep no reasoning, and a reply that paused for a tool approval shows its reasoning only until the pause and keeps none on its final message.

## Cancellation and Errors

Users can cancel an in-progress response by reacting with 🛑 on the message being generated (see [Stop Button](https://docs.mindroom.chat/chat-commands/#stop-button)).
Tool calls running in a [worker](https://docs.mindroom.chat/deployment/sandbox-proxy/), such as `run_shell_command` or `run_python_code`, stop with it, apart from the exceptions listed there.
The partial text stays in the message, followed by a note explaining how it ended:

| Note | Cause |
|------|-------|
| `**[Response cancelled by user]**` | The user stopped the response. |
| `**[Response interrupted by service restart]**` | MindRoom restarted while the response was being generated; the reply continues below the note once MindRoom is back, or ends there if nothing is left to continue. |
| `**[Response interrupted]**` | The response was interrupted for another reason. |
| `**[Response interrupted by an error: <error description>]**` | MindRoom could not finish the reply; the description says why. |

A crash, an orderly shutdown, or an agent restarted by a configuration or MCP change, including during an approved tool call, keeps the partial reply and its tool calls, and the agent continues it in the same message once MindRoom is back.
The agent is told which tool calls already finished, and which were still running, so it avoids repeating side effects, including calls hidden by `show_tool_calls: false` and calls made while streaming is off.
Nothing blocks a repeated call, so a model can still occasionally run one again.

## Large Messages

A Matrix event holds at most about 64 KB, so `defaults.large_message_strategy` controls how a larger response is delivered.

| Value | Delivery |
|-------|----------|
| `sidecar` (default) | A preview with as much text as fits, plus the full message content attached as `message-content.json` (`message-content.json.enc`, encrypted, in encrypted rooms). |
| `split` | Several complete rich-text messages: the first replaces the streaming placeholder and the rest follow it in the same thread or room. |

The `sidecar` strategy applies to messages over about 55 KB and edits over about 27 KB, and the preview omits inline tool-trace metadata.
With `split`, each segment renders as standalone Markdown, text is cut at paragraph or line boundaries and never inside a fenced code block, and the segments together reproduce the full response text exactly.
Each segment carries the tool calls it shows, numbered from 1; a segment with many tool calls may show them without their input and output previews, or, when even their names do not fit, without them.
Even with `split`, non-text payloads, metadata that alone exceeds the size budget, and a single code block larger than one event use the sidecar.

# Voice Messages

This page covers Matrix voice messages sent to agents and teams: speech-to-text (STT) setup, transcript cleanup, what appears in the room, and what happens without STT.
For live spoken conversations in Matrix calls, see [Voice Calls](https://docs.mindroom.chat/voice-calls/).

<video controls playsinline preload="metadata" aria-label="A voice message becomes a reminder" style="width: 100%" poster="https://github.com/user-attachments/assets/5b7b4f52-ea23-415c-b2d5-50b3fbbb7bc1" data-poster-light="https://github.com/user-attachments/assets/5b7b4f52-ea23-415c-b2d5-50b3fbbb7bc1" data-poster-dark="https://github.com/user-attachments/assets/3fb56349-ad0f-435d-be4e-6c95860e7b9a">
  <source src="https://github.com/user-attachments/assets/4c49c8f1-2b87-45a1-949a-b165d0e18c1a" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/f91d0d7f-b8b0-4039-9fda-c7a9caed7534" type="video/mp4">
</video>

With voice enabled, MindRoom transcribes each voice message, cleans up the transcript, and handles it like a text message prefixed with `🎤`.
The responder also receives the original audio as an attachment.
Without STT, voice messages still reach responders as raw audio; see [Voice Fallback](#voice-fallback-no-stt-available).

## Setup

Configure voice in `config.yaml` or in the dashboard's Voice tab.

### OpenAI transcription

```yaml
voice:
  enabled: true
  stt:
    provider: openai
    model: gpt-transcribe
  intelligence:
    model: default  # Model used for mention normalization and light ASR cleanup
```

Cloud OpenAI transcription uses the `openai` credential service by default, which resolves a stored OpenAI credential or `OPENAI_API_KEY`.
Set `voice.stt.api_key` or a different `voice.stt.credentials_service` to use another key.

### Self-hosted Whisper

Any service that implements the OpenAI-compatible `/v1/audio/transcriptions` endpoint works, such as [faster-whisper-server](https://github.com/fedirz/faster-whisper-server):

```yaml
voice:
  enabled: true
  stt:
    provider: openai_compatible
    model: whisper-1
    host: http://localhost:8080
    api_key: your-custom-api-key  # Optional
```

`host` may be the service root or its `/v1` base URL.
Without `api_key` or `credentials_service`, MindRoom sends a non-secret placeholder key, so keyless local servers work and your OpenAI key is never sent to them.

## Configuration Reference

| Field | Default | Description |
|-------|---------|-------------|
| `voice.enabled` | `false` | Transcribe voice messages; when `false`, voice messages use the [raw-audio fallback](#voice-fallback-no-stt-available). |
| `voice.visible_router_echo` | `true` | Let the router show transcription progress and the transcript in the room; see [Voice Message Processing](#voice-message-processing). |
| `voice.stt.provider` | `openai` | `openai` or `openai_compatible`. |
| `voice.stt.model` | `gpt-transcribe` | STT model name. |
| `voice.stt.host` | none | Service root or `/v1` base URL; must be an HTTP(S) URL. Required for `openai_compatible`. |
| `voice.stt.api_key` | none | Inline API key. |
| `voice.stt.credentials_service` | `openai` for `provider: openai`, otherwise none | Named credential service holding the API key. |
| `voice.stt.extra_kwargs` | `{}` | Extra form fields sent with each transcription request, such as `language`; must not set `api_key`, `base_url`, `client`, or `model`. |
| `voice.intelligence.model` | `default` | Name of a `models` entry used for transcript cleanup. |

`provider: openai` requires `api_key` or `credentials_service`, and `provider: openai_compatible` requires `host`.

Each transcription request has a 60-second limit and is not retried.
On timeout or service error, the message uses the raw-audio fallback.

## Voice Message Processing

What appears in the room depends on whether the router is present and whether the responder is already clear.
Voice messages follow the same mention, thread, and access rules as text from the original sender.

**Router echo.**
With `voice.enabled: true` and `voice.visible_router_echo: true`, the router immediately posts `Router agent is transcribing…` and then replaces it with the cleaned transcript, or with the fallback text if transcription failed.
With `voice.enabled: false`, the router posts the fallback text directly instead.
The echo is display-only: agents answer the original voice message, not the echo.
The echo appears only when the router is in the room and allowed to reply to the sender.
Set `voice.visible_router_echo: false` to hide it; this does not change who answers.

**Responder already clear.**
If only one agent or team can respond, the transcript or caption mentions one, or thread context picks one, that responder answers the voice message directly without a router handoff.

**Router must choose.**
If several agents or teams can respond and none is targeted, the router routes on the transcript and posts a normal handoff such as `@home could you help with this?`.
The chosen responder answers that handoff and still receives the original audio attachment.

**No router, or router cannot reply to the sender.**
Agents and teams handle the voice message directly with the same rules as text, and no router echo appears.
If several responders remain and none is targeted, nobody answers until the user mentions an agent or team.

## Transcript Normalization

The `voice.intelligence.model` cleans up each raw transcript before dispatch:

- Spoken agent or team names become `@agent` or `@team` mentions, using display names such as "HomeAssistant" to produce `@home`.
- Mentions of agents or teams not available to the sender in the room lose their `@`, so they do not trigger a response.
- Spoken text is never rewritten into `!command` syntax.
- Light speech-recognition errors are corrected.

If cleanup fails or times out, MindRoom uses the raw transcript.
Choose a faster or stronger model in `voice.intelligence.model` to trade cleanup speed against accuracy.

## Voice Fallback (No STT Available)

When voice is disabled, STT is unavailable, or transcription fails, the voice message is dispatched as `🎤 [Attached voice message]`, or as `🎤` followed by the message caption when it has one.
The audio is registered as an attachment, so responders with audio-capable models receive it directly and tool-using responders can fetch it with the `attachments` tool.
Attachment IDs follow the context-scoping rules in [File & Video Attachments](https://docs.mindroom.chat/attachments/).
If the audio cannot be downloaded from Matrix, the fallback text is dispatched without an attachment.

Without a transcript, routing has little text to work with, so use explicit `@mentions` or reply in an existing thread in rooms with several responders.

## Limitations and Tips

- Only OpenAI-compatible STT APIs are supported.
- Audio quality and background noise affect transcription accuracy.
- Say the agent or team name first, for example "Hey assistant, what's the weather?"

## Text-to-Speech

Agents can also send spoken replies, which is separate from voice message transcription.
The `matrix_voice_message` tool sends a playable Matrix voice note to the current room or thread; see [Matrix & Attachments tools](https://docs.mindroom.chat/tools/matrix-and-attachments/).
The `openai`, `eleven_labs`, `cartesia`, and `groq` tools generate speech audio; see [AI & Generation tools](https://docs.mindroom.chat/tools/ai-and-generation/).

# Voice Calls

MindRoom agents can join Element Call voice calls (MatrixRTC, also used by Element X and recent Cinny releases) in their rooms and talk with the caller in real time.
Use this page to set up calls, choose a voice backend, and find out why an agent did or did not join a call.
Voice messages (recorded audio clips) are covered on [Voice Messages](https://docs.mindroom.chat/voice/).

Each calls-enabled agent uses one of three backends:

| Backend | What it does |
|---------|--------------|
| `realtime` | OpenAI realtime speech-to-speech runs the conversation with the agent's prompt and tools. |
| `live` | OpenAI Live handles speech and delegates substantive requests to the normal MindRoom agent. |
| `cascaded` | Separate speech-to-text and text-to-speech services wrap the agent's normal MindRoom response, so any model provider works, including fully local setups. |

## Setup

1. MindRoom installs the `matrix_calls` extra automatically when a calls-enabled agent starts, including after a config reload, unless `MINDROOM_NO_AUTO_INSTALL_TOOLS=1` is set.
   If auto-install is disabled or fails, install it yourself with `pip install "mindroom[matrix_calls]"` or `uv sync --extra matrix_calls`.
2. Make sure your Matrix deployment has a MatrixRTC backend; see [Server requirements](#server-requirements).
3. Define call profiles and assign one to each calls-enabled agent:

```yaml
calls:
  enabled: true
  profiles:
    openai-realtime:
      backend: realtime
      model: gpt-realtime-2.1
      credentials_service: openai-voice
      voice: marin
  agents:
    assistant: openai-realtime
  # livekit_service_url: https://rtc.example.org
```

Calls can also be configured in the dashboard's [Voice tab](https://docs.mindroom.chat/dashboard/#voice).
A calls-enabled agent shows `📞 Voice calls` in its Matrix presence status when the extra is installed and its profile's credentials resolve, so clients can offer the call action only where it will be answered.

## Calls configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `calls.enabled` | bool | `false` | Lets agents join voice calls. |
| `calls.profiles` | map of name to profile | `{}` | Reusable, complete voice pipelines; each has a `backend` and the fields below. |
| `calls.agents` | map of agent name to profile name | `{}` | Each calls-enabled agent uses exactly one profile. A [`personal_agents`](https://docs.mindroom.chat/personal-agents/#voice-calls) template key gives every user's agent from that template the profile. |
| `calls.livekit_service_url` | string | discovered | MatrixRTC authorization service URL to use instead of discovering it from the agent homeserver's `.well-known/matrix/client`. |

`enabled` and `livekit_service_url` are global and cannot be set per agent.
Profiles have no inherited defaults and credentials never fall back to another key, so a missing credential service leaves calls unavailable rather than using a different OpenAI key.
Set a dedicated credential service when voice and chat use different OpenAI credentials.

Configuration loading fails with one of these errors when references are wrong:

- `calls.agents references unknown profile(s): ...`
- `calls.agents references unknown agent(s): ...`
- `calls.agents configures multiple agents for room(s): ...`, because a room can have at most one calls-enabled agent.
- `calls.agents names personal_agents.<key>, whose agent lists rooms in the template or a user's override; ...`, because a room takes only one calls-enabled agent.
- `calls.profiles references unknown cascaded model(s): ...` or `calls.profiles references unknown Live agent model(s): ...`, when a profile's model alias is not in the top-level `models` mapping.

### Realtime profile fields

| Field | Required | Description |
|-------|----------|-------------|
| `backend` | yes | `realtime` |
| `model` | yes | OpenAI realtime speech-to-speech model ID. |
| `credentials_service` | yes | Named credential service holding the OpenAI API key. |
| `voice` | yes | Realtime voice preset. |

### Live profile fields

| Field | Required | Description |
|-------|----------|-------------|
| `backend` | yes | `live` |
| `model` | yes | OpenAI Live speech model ID, independent of `agent_model`. |
| `credentials_service` | yes | Named credential service holding the OpenAI API key; Live needs no separate STT or TTS credentials. |
| `voice` | yes | Live voice preset. |
| `agent_model` | no | Name of a top-level `models` entry used for delegated agent turns; omitted keeps normal room and agent model resolution. |

### Cascaded profile fields

| Field | Required | Description |
|-------|----------|-------------|
| `backend` | yes | `cascaded` |
| `model` | no | Name of a top-level `models` entry for call turns; it takes precedence over room and agent models, and omitting it keeps normal model resolution. |
| `stt` | yes | Speech-to-text service. |
| `tts` | yes | Text-to-speech service. |

Each speech service (`stt` and `tts`) has these fields:

| Field | Default | Description |
|-------|---------|-------------|
| `provider` | `openai` | `openai` or `openai_compatible`. |
| `model` | required | Speech model name. |
| `credentials_service` | none | Named credential service holding the API key. |
| `api_key` | none | Inline API key. |
| `host` | none | Service root or its `/v1` base URL; must be an HTTP(S) URL. |
| `extra_kwargs` | `{}` | Provider options such as `language` for STT or `voice` for TTS; must not set `api_key`, `base_url`, `client`, or `model`. |

`openai` services require `credentials_service` or `api_key`.
`openai_compatible` services require `host` and may omit credentials, in which case MindRoom sends a non-secret placeholder key.
STT services must expose `/v1/audio/transcriptions` and TTS services `/v1/audio/speech`, except `gpt-realtime` TTS models, which speak through the OpenAI Realtime API and take only `voice` in `extra_kwargs`.
The two legs may use the same credential service or different ones.

## Choosing a backend

### Realtime

The realtime model runs the whole conversation itself, using the agent's rendered prompt and its tools.
See the example in [Setup](#setup).

### Live with agent delegation

Live answers from the agent's full rendered system prompt, including its context files and caller-specific workspace, and delegates requests that need tools, research, more memory, or actions to the normal MindRoom agent, then relays the result conversationally.
If the agent's prompt plus voice guidance exceeds about 16,000 tokens (a local estimate, not a provider limit), Live instead delegates every substantive request, including questions about identity and preferences, to the normal agent.
Delegated turns keep their history while the call lasts, and each new call starts a fresh delegate session.
Each Live call runs its audio in a separate Python process, so a busy MindRoom process does not interrupt the call's audio; expect one extra process per active call, plus one idle process (about 130 MB) that MindRoom starts ahead of the next call so pickup stays fast.

This example uses GPT-Live for speech and the `chat` model for delegated turns, with a voice credential independent of the agent's model provider:

```yaml
models:
  chat:
    provider: anthropic
    id: claude-opus-5-5

agents:
  assistant:
    display_name: Assistant
    model: chat

calls:
  enabled: true
  profiles:
    openai-live:
      backend: live
      model: gpt-live-1
      credentials_service: openai-live
      voice: marin
      agent_model: chat
  agents:
    assistant: openai-live
```

### Cascaded

Each finalized transcript goes through the agent's normal response path, with the same model resolution, system prompt, instructions, knowledge, skills, hooks, tools, requester identity, and history.
The profile's `model` changes only the LLM; speech services, prompts, history, memory, and tools stay the same.
Turn ends are detected locally, and the caller can interrupt the agent by speaking.

This example uses OpenAI speech services and a faster model for call turns than for text chat:

```yaml
models:
  chat:
    provider: anthropic
    id: claude-opus-5-5
  call_fast:
    provider: anthropic
    id: claude-haiku-5-5

agents:
  assistant:
    display_name: Assistant
    model: chat

calls:
  enabled: true
  profiles:
    fast-cascaded:
      backend: cascaded
      model: call_fast
      stt:
        provider: openai
        model: gpt-transcribe
        credentials_service: openai-voice
        extra_kwargs:
          language: en
      tts:
        provider: openai
        model: gpt-realtime-2.1-mini
        credentials_service: openai-voice
        extra_kwargs:
          voice: ash
  agents:
    assistant: fast-cascaded
```

OpenAI documents [`gpt-transcribe`](https://developers.openai.com/api/docs/models/gpt-transcribe) for transcription and [`gpt-realtime-2.1-mini`](https://developers.openai.com/api/docs/models/gpt-realtime-2.1-mini) for text-to-speech.

### Completely local cascaded setup

This configuration sends STT, LLM, and TTS requests only to localhost services and needs no cloud API key:

```yaml
models:
  default:
    provider: openai
    id: local-chat-model
    extra_kwargs:
      api_key: sk-no-key-required
      base_url: http://127.0.0.1:9292/v1

memory: none

agents:
  assistant:
    display_name: Assistant
    role: Help the caller.
    rooms: [lobby]

calls:
  enabled: true
  profiles:
    local-cascaded:
      backend: cascaded
      stt:
        provider: openai_compatible
        model: whisper-large-v3
        host: http://127.0.0.1:9000
        extra_kwargs:
          language: en
      tts:
        provider: openai_compatible
        model: kokoro
        host: http://127.0.0.1:9001
        extra_kwargs:
          voice: af_heart
  agents:
    assistant: local-cascaded
```

Different agents can use different profiles, for example one agent on `realtime` and another on this local profile.

## When an agent joins a call

An agent joins a call when it starts in one of the agent's configured rooms, in a [personal room](https://docs.mindroom.chat/personal-rooms/) the agent created for its owner, or in an ad-hoc room whose invitation the agent accepted under its [`accept_invites`](https://docs.mindroom.chat/configuration/agents/) setting.
Ad-hoc rooms let a Matrix client create a private voice room and invite one agent without adding the room to `config.yaml`.
A configured room assignment wins over personal rooms and invites; if several calls-enabled agents are in the same room only through those, none of them answers calls there.

The agent stays in a call only while one Matrix user is in it (that user may join from several devices) and only while that user passes the normal room and per-agent reply permissions.
That user is the requester for tools, and requester-private agents use that user's workspace, memory, credentials, history, and knowledge.

Tools that need confirmation, user input, external execution, or [`tool_approval`](https://docs.mindroom.chat/tool-approval/) are unavailable during calls in every backend, because a call has no approval UI.
With `cascaded` and `live`, the agent tells the caller to use text chat for them.
[Deferred tools](https://docs.mindroom.chat/tools/dynamic-tools/) are available from the start of a call in every backend, with no search or loading step.

The agent leaves the call when the caller leaves, the caller loses access to the agent (for example by leaving a `members_of_rooms` grant room), a second Matrix user joins, the voice session ends, or MindRoom shuts down.
A restart also drops it from the call, and so does any configuration change applied while the agent is in a call, except a change limited to the `knowledge_graph` section.

If the agent joins but cannot hear or speak, look for a `Voice call error:` notice in the room, which names the failing service and how to fix it.
For a rejected credential, update the credential MindRoom uses, restart MindRoom, then leave and rejoin the call; retrying with the same credential does not help.

## Calling an agent about a thread

In MindRoom Chat, start the call from a thread's header, or from the agent's profile while a thread is open.
When the agent picks up, it receives a snapshot of that conversation, plus the thread title when the thread has one.
The snapshot is the newest part of the thread that fits the call budget (about 6,000 tokens), with a note when older messages were left out.
Each message is cut to 2,000 characters.
Starting a call from a room's main timeline gives the agent the newest unthreaded messages of that room instead.

The snapshot is taken once, when the agent joins; messages sent during the call are not added.
It works with all three call profiles; with `live`, the voice model skips it when the agent's prompt leaves too little room, but the delegated agent always gets it.

The agent gets the snapshot only when you are a member of the room the call came from, the agent's `access` admits you there, and the agent has joined it.
Otherwise, or when reading the conversation takes longer than 5 seconds, the call starts without it.

When you hang up, the agent posts the call back into that conversation: as a reply in the same thread, or as a new room message for a call started from the main timeline.
The message reads `📞 Voice call · N min` with the spoken transcript collapsed underneath, so later replies in the thread know what was said.
Calls shorter than 10 seconds, calls in which the caller said nothing, calls that started without a conversation snapshot, and calls cut off by a MindRoom restart post nothing.
The transcript holds only what was said; tool use is left out.
Before posting, the agent checks the caller's access to the origin room again, and posts nothing if the caller lost that access during the call.
If the agent's media connection drops and it rejoins the same call, each part of the call posts its own message.

An agent with the `matrix_message` tool is also told how to start longer work during a call.
It sends a self-contained task to the thread or room the call came from, and names itself or another agent as `recipient`, so the work runs there while you keep talking.
A call without an origin makes it ask you which room to use.

Other Matrix clients ask for a conversation snapshot with the `origin` field of the call room's `io.mindroom.agent_call` state event:

```json
{
  "version": 1,
  "agent_user_id": "@mindroom_assistant:example.org",
  "creator_user_id": "@alice:example.org",
  "ephemeral": false,
  "origin": { "room_id": "!abc:example.org", "thread_id": "$root" }
}
```

`thread_id` is `null` when the call starts from the room's main timeline, and `origin` may be left out entirely.
The agent uses `origin` only when the caller sent the event with an empty state key and `version: 1`, with `creator_user_id` and `agent_user_id` naming the caller and this agent, `thread_id` is the thread's root event, and `room_id` is not the call room.
Write `origin` before each call, because the agent reads it when it joins.

## Transcripts and memory

Every call writes a markdown transcript as it goes.

| Agent | Transcript location |
|-------|---------------------|
| Shared, file memory | `calls/` in the agent's workspace, readable later by its file tools |
| Shared, other memory | `<storage>/calls/<agent>/` |
| Requester-private, file memory | `calls/` in the requester's workspace |
| Requester-private, other memory | `calls/` in the requester-scoped state directory |

When the call ends, file memory stores a reference to the transcript, Mem0 stores the transcript as recallable context, and `memory: none` keeps only the transcript file.

## Server requirements

Matrix carries only call signaling; audio flows through a LiveKit SFU deployed next to the homeserver.
Your Matrix deployment needs the standard Element Call backend:

- A [LiveKit SFU](https://github.com/livekit/livekit) reachable by call participants.
- The [MatrixRTC authorization service](https://github.com/element-hq/lk-jwt-service) (`lk-jwt-service`).
- A `.well-known/matrix/client` on the Matrix server-name domain that advertises the service:

```json
{
  "org.matrix.msc4143.rtc_foci": [
    { "type": "livekit", "livekit_service_url": "https://rtc.example.org" }
  ]
}
```

On Kubernetes, the [MatrixRTC chart](https://github.com/mindroom-ai/mindroom/tree/main/cluster/k8s/matrixrtc) deploys LiveKit and the authorization service, and the [client chart](https://github.com/mindroom-ai/mindroom/tree/main/cluster/k8s/client) publishes the `.well-known` announcement and proxies `/livekit/jwt` and `/livekit/sfu` to them.
LiveKit media still needs a static public IP and open TCP and UDP media ports, which the chart README describes.
Element's [self-hosting guide](https://github.com/element-hq/element-call/blob/main/docs/self_hosting.md) covers the full setup, and [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy) enables it with `matrix_rtc_enabled: true`.

The agent joins only calls started on your own MatrixRTC backend, meaning the one in `calls.livekit_service_url` or the agent homeserver's `.well-known`; calls started through a different backend are ignored.
[Managed rooms](https://docs.mindroom.chat/rooms/#room-management) already let every member join calls.
In other rooms, such as ad-hoc rooms, the power levels must let members send `org.matrix.msc3401.call.member` state events, which Element Call-capable clients set up when they create rooms.
Calls work in both encrypted and unencrypted rooms.

Synapse supports the full MatrixRTC stack, including automatic cleanup of stale call memberships.
Tuwunel works with Element Call but does not yet support delayed events ([tuwunel#178](https://github.com/matrix-construct/tuwunel/issues/178)), so memberships of crashed clients linger until they expire.

## Limitations

- Audio only: the agent neither sends nor receives video or screen shares.
- One human Matrix user per call, possibly on several devices.
- Cascaded speech services must be OpenAI-compatible transcription and speech endpoints.
- Legacy 1:1 `m.call.*` calls (non-MatrixRTC) are not supported.

# Image Messages

This page covers how agents and teams see images sent in Matrix rooms, which image formats they receive, and what happens when a model cannot accept an image.
Images follow the same rules as other attachments for captions, routing, encryption, size limits, attachment IDs, and retention; see [Attachments](https://docs.mindroom.chat/attachments/).

## Sending Images

Send an image in a room and mention the agent or team in the caption, for example `@assistant What does this diagram show?`.
No configuration is needed; the image is passed to the responder's model as vision input, so that model must support vision (for example Claude or GPT-6 Astra).
A model without vision answers through the [media fallback](#media-fallback).

Captions from bridges trigger a response when they mention the agent through Matrix mention metadata, an HTML mention pill, or plain `@name` text.

## Image Formats

MindRoom recognizes PNG, JPEG, GIF, WebP, BMP, and TIFF images from their content and sends the recognized type to the model, logging a warning when the client declared a different MIME type.
Other image formats are sent with the client's declared MIME type, and the provider may reject them.

## Media Fallback

When a provider rejects a request containing inline media (images, audio, video, or documents), MindRoom retries it without that media.
The retry adds `[Inline media unavailable for this model]` to the message that carried the media and tells the agent the content was not inspected, so the agent can still open the file through the [`attachments` tool](https://docs.mindroom.chat/attachments/#the-attachments-tool) or explain the limitation.
When the error names the attachments it rejected, the retry leaves out only those, or every attachment of the same type, and the note lists them by file name, with the attachment ID of a stored attachment.
Otherwise, or if the provider rejects that retry too, all of the request's media is left out.
If the provider error says the model does not support that kind of input, such as `does not support image input` or `is not a multimodal model`, later requests to the same model leave that media kind out from the start until MindRoom restarts.
Other rejections, such as an image the provider could not decode, drop the media from that one request only.
The same note replaces only that attachment when an attachment file no longer exists or cannot be read, or when MindRoom cannot download media a tool returned as a URL, including media larger than 20 MiB.
Once a reply has produced text, tool calls, or reasoning, a later provider error is reported instead of retried without media, and provider safeguard refusals and the billing refusals listed under [Retries](https://docs.mindroom.chat/configuration/models/#retries) are never retried without media.

# Attachments

This page covers how agents and teams receive files, images, and videos sent in Matrix rooms, how attachment IDs work, how long attachments are kept, and how to use the `attachments` tool to view, save, and register files.
Attachment handling works automatically for agents and teams in direct messages and threads; no configuration is needed.
Image-specific behavior is on [Image Messages](https://docs.mindroom.chat/images/), and audio messages are transcribed as described on [Voice Messages](https://docs.mindroom.chat/voice/).

## Sending Files to an Agent

Send a file, image, or video in a Matrix room and mention the agent or team in the caption:

- **With caption**: `@assistant Summarize this document` uses the caption as the prompt.
- **Without caption**: the responder receives `[Attached file]`, `[Attached image]`, or `[Attached video]` as the prompt.

A media event's `body` counts as a caption only when the event also has a `filename` field with a different value, following [MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530), so a body that only repeats the filename is not a caption.

The responder answers only when it would answer a text message there (mention, thread participation, or DM).
Without an `@mention`, the router uses the caption to choose among several eligible agents or teams.
Both unencrypted and end-to-end encrypted media are supported.

The responder's model receives the media directly, so it must support that input type (vision for images, file or video input for documents and videos).
Models without that support can still work with the file through the [`attachments` tool](#the-attachments-tool).
When a provider rejects inline media, MindRoom retries without it and tells the agent the content was not inspected; see [Media Fallback](https://docs.mindroom.chat/images/#media-fallback).
Some provider integrations never send certain media kinds, so MindRoom leaves those out from the start with the same note.
Only models using the `google` provider receive video, Claude and OpenAI Responses models (including Codex) also receive no audio, Ollama and Groq models receive only images, and Cerebras models receive no media.
Models using the `google` provider read documents only as PDF or text; other file types, such as Office files and archives, reach them as a note naming the file.

## Attachment IDs

Each received or registered file gets an attachment ID such as `att_abc123`.
Attachments sent with the current message are listed in the prompt with their kind, filename, sender, send time, and event ID:

```
Attachments sent with the current message (use tool calls to inspect or process them by ID):
- att_abc123 (image, "car.jpg", from @user:example.org, sent 2026-06-06 09:00 UTC, event $abc)
```

Only current-message attachments are sent to the model as media.
Earlier attachments appear in the conversation history as annotations, and the agent can open them again by ID with the `attachments` tool:

```text
@user:example.org: check this out
[attachments: att_def456 (image, "house.jpg")]
```

Attachment IDs are scoped to their room and thread, so an ID from one conversation cannot be used in another.
Within a conversation, an authorized response can open files uploaded by any participant.
When a message's file is replaced by an edit, later turns use the new file under a new attachment ID.

## The `attachments` Tool

The optional `attachments` tool lets an agent list, view, save, and register files in the current conversation, including with models that cannot take file input directly.
Add it to the agent's tools; it has no configuration fields, and enabling [`matrix_message`](https://docs.mindroom.chat/tools/matrix-message/) also enables it:

```yaml
agents:
  assistant:
    tools:
      - attachments
```

| Function | What it does |
|----------|--------------|
| `list_attachments(target=None)` | List the attachment IDs available in the current context with their metadata, plus any `missing_attachment_ids`; `target` returns one attachment |
| `get_attachment(attachment_id, mindroom_output_path=None, view=False)` | Return metadata, save the file into the workspace, or show its content to the model |
| `view_file(path)` | Show an image from the workspace to the model |
| `register_attachment(file_path)` | Turn a local file into a new attachment ID for this conversation |

```python
list_attachments()
get_attachment("att_abc123")
get_attachment("att_abc123", view=True)
get_attachment("att_abc123", mindroom_output_path="incoming/plan.pdf")
view_file(path="plots/result.png")
register_attachment("exports/report.csv")
matrix_message(message="Here is the report.", attachments=["att_abc123", "exports/report.csv"])
```

Attachment arguments must be `att_*` IDs available in the current conversation or registered earlier in the same tool run.
Metadata includes the kind, filename, MIME type, size, room ID, thread ID, sender, timestamps, and an `available` flag that reports whether the file still exists.
It also includes the runtime-local path, except for agents whose workspace tools run on a worker, where that path does not exist; their metadata has a `usage` note saying to save the file with `mindroom_output_path` instead.
To post files into Matrix, pass attachment IDs or file paths to `matrix_message`; see [`matrix_message` attachments](https://docs.mindroom.chat/tools/matrix-message/#attachments).

### Viewing Content

`get_attachment(attachment_id, view=True)` sends an image, audio, video, or document (including PDF and plain text) to the model, up to 20 MiB.
The model and its provider must support that media type and may have stricter limits.
`view=True` cannot be combined with `mindroom_output_path`.
If the model cannot take the media, the agent is told so and can save the file and use other extraction or transcription tools instead.

`view_file` shows one image to the calling model in one call; to view an attachment, use `get_attachment(attachment_id, view=True)`, and use `read_file` for ordinary text and code.
Workspace paths resolve inside the agent's worker when it has one, otherwise inside its workspace.
It accepts PNG, JPEG, GIF, and WebP images up to 20 MiB and 40 million pixels.
Images larger than 2048 pixels on the longest edge or 5 MiB are scaled down, animated images show only the first frame, and the result metadata reports these changes.
Image viewing through `get_attachment(view=True)` follows the same limits.
Viewing does not post anything to Matrix; to share the image, pass the returned attachment ID to `matrix_message`.
That ID works within the same tool run; in later turns, view the original path again.
Conversation history keeps up to four recently viewed images within 10 MiB in total, and older ones are replaced by a notice.

### Saving Files to the Workspace

`get_attachment(attachment_id, mindroom_output_path="incoming/file.ext")` saves the file into the agent workspace and returns a receipt with the path, byte count, and SHA-256.
Save attachments this way before processing them with `file`, `coding`, `python`, or `shell` on a worker, because attachment storage is not visible inside the worker.
The path must be a workspace-relative file path; it cannot be absolute, contain `..`, start with `~`, or contain `$` or `%`.
Saves into a worker are limited by [`MINDROOM_ATTACHMENT_INLINE_SAVE_MAX_BYTES`](https://docs.mindroom.chat/deployment/sandbox-proxy/#environment-variable-reference), which defaults to 64 MiB so that every attachment MindRoom accepts can be saved.
See [Sandbox Proxy](https://docs.mindroom.chat/deployment/sandbox-proxy/) for how the workspace appears as `$MINDROOM_AGENT_WORKSPACE` and `~` inside worker tools.

### Registering Local Files

`register_attachment(file_path)` copies a local file into attachment storage and returns a new attachment ID for the current room and thread, usable by later calls in the same run.
Later changes to the source file do not change the attachment.
Registered files are limited to 64 MiB.
Paths, including the targets of symbolic links, follow the agent's [`file_access`](https://docs.mindroom.chat/architecture/security-posture/#file-access) setting:

- With the default `workspace`, paths resolve from the agent workspace and must stay inside it, so pass workspace-relative paths such as `incoming/file.txt`.
  `~` expands to the MindRoom process home, not the worker workspace, so `~/incoming/file.txt` is rejected.
- With `unrestricted`, any file the MindRoom process can read is accepted, including absolute paths.

## Retention

Attachment bytes are stored under `incoming_media/` and metadata under `attachments/` in the storage directory (`mindroom_data/` by default).
MindRoom prunes attachments older than 30 days; deletion is not immediate at the 30-day mark.
Pruning never deletes workspace files, the source files of registered attachments, or copies on the Matrix homeserver.

## Limits

- **Incoming media size**: received files must be at most 64 MiB (67,108,864 bytes), including after decryption; send a smaller file if one is rejected.
  Homeservers and model providers may set lower limits.
- **Earlier media per turn**: a turn fetches at most 10 earlier files, images, or videos that are not stored yet, newest first, and leaves the rest for later turns.
  An earlier file that fails to download is not tried again for an hour.

# Interactive Q&A

Agents can ask users a multiple-choice question that they answer by clicking an emoji reaction or typing the option number.
Use this when an agent needs the user to pick between a few concrete options before it continues.

<video controls playsinline preload="metadata" aria-label="The agent asks a multiple-choice question before it decides" style="width: 100%" poster="https://github.com/user-attachments/assets/2a7ab600-0934-4b53-a47b-ec8fcd80201d" data-poster-light="https://github.com/user-attachments/assets/2a7ab600-0934-4b53-a47b-ec8fcd80201d" data-poster-dark="https://github.com/user-attachments/assets/4ee1ad16-4d36-4aca-932d-49b8447b5239">
  <source src="https://github.com/user-attachments/assets/35bb8221-3405-4efb-b236-304a58f898e8" type="video/mp4" media="(prefers-color-scheme: dark)">
  <source src="https://github.com/user-attachments/assets/35bfe7b2-feab-484f-b5e8-22b26ac92df8" type="video/mp4">
</video>

## Asking a Question

Any agent can ask a question without extra tools or configuration by including an `interactive` code block with JSON in its response:

````markdown
```interactive
{
    "question": "What approach would you prefer?",
    "options": [
        {"emoji": "🚀", "label": "Fast and automated", "value": "fast"},
        {"emoji": "🔍", "label": "Careful and manual", "value": "careful"}
    ]
}
```
````

MindRoom replaces the block with a numbered list and adds each option's emoji as a reaction button:

```
What approach would you prefer?

1. 🚀 Fast and automated
2. 🔍 Careful and manual

React with an emoji or type the number to respond.
```

While a response streams, the question appears once its code block is complete.

To make an agent use questions, describe the format in its `instructions` or `role`:

```yaml
agents:
  assistant:
    display_name: Assistant
    role: A helpful assistant
    instructions:
      - >
        When the user needs to choose between options, present them using
        an interactive code block with JSON containing question and options
        (each with emoji, label, and value fields).
```

### Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `question` | string | No | Question text shown above the options. Defaults to `"Please choose an option:"`. |
| `options` | array | Yes | Option objects; options beyond the fifth are dropped without warning. |
| `options[].emoji` | string | No | Reaction button for the option. Defaults to `"❓"`. |
| `options[].label` | string | No | Text shown for the option. Defaults to `"Option"`. |
| `options[].value` | string | No | Value passed back to the agent. Defaults to the label in lowercase. |

Give every option a different emoji; reacting with an emoji that several options share, including the default `❓`, selects the last of them, so the others can only be chosen by number.

## Answering a Question

Users answer where the question was asked, in its thread or in the main room, in either of two ways:

- **Reaction**: click one of the option emojis on the question.
- **Text**: send just the option number, a single digit from `1` to `5`.
  When several questions there are unanswered, a number answers the oldest question from each agent that asked one; react to answer a specific question.

Either way, the agent that asked receives the question text and the selected option's label and value, and continues the conversation there.
Each question accepts one answer, and questions stay answerable across restarts.
Only human users can answer; reactions and messages from agents are ignored.

## Limitations

- Only the first valid question in a response gets reaction buttons and numeric answers; later ones are shown as plain text.
- A very large question, roughly a few thousand characters of question text, option labels, and option values, is shown without reaction buttons and cannot be answered by number; shorten the question, labels, or values to keep it answerable.
- Questions only work in an agent's normal responses; sending or editing one with the `matrix_message` tool fails with `Interactive prompts are only supported in normal agent responses.`

# Interactive Canvases

An agent can show the user a web page it wrote, such as a dashboard, report, slide deck, form, or picker, in the Canvas panel beside the conversation in MindRoom Chat.
The user can work in the page and send an answer, which reaches the agent as the user's next message.
Use a canvas when seeing or clicking beats reading or typing; for a quick choice between a few options, a plain reply or an [interactive question](https://docs.mindroom.chat/interactive/) is simpler.

=== "Overview · 27 s"

    A narrated film of all four: a scheduling grid, a data fit, a slide deck, and a trip planner.

    <video class="only-light" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/5941fd9d-90d2-4a52-b0e9-abbff53354b4" aria-label="Canvas film: four agent canvases beside the conversation" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/3a2e29e3-90a0-4cf7-a3cb-c0e41765695a" type="video/mp4">
    </video>
    <video class="only-dark" controls playsinline preload="none" poster="https://github.com/user-attachments/assets/dc3e3352-7cef-45ce-95be-3edad3bc7fe5" aria-label="Canvas film: four agent canvases beside the conversation" style="width: 100%">
      <source src="https://github.com/user-attachments/assets/a704564b-be05-45cd-9c74-2b5de4f5c875" type="video/mp4">
    </video>

=== "Scheduling"

    A week grid of four people's availability; the pick goes back with **Send**, and the agent books the meeting after approval.

    <video controls playsinline preload="metadata" aria-label="An agent shows a week grid; the user picks a slot and sends it, and the agent books it" style="width: 100%" poster="https://github.com/user-attachments/assets/21d0d8ad-a7c4-4937-8fac-3d178b047011" data-poster-light="https://github.com/user-attachments/assets/21d0d8ad-a7c4-4937-8fac-3d178b047011" data-poster-dark="https://github.com/user-attachments/assets/1247f0ca-1c73-4759-bfa7-1b06fb8c2635">
      <source src="https://github.com/user-attachments/assets/2ae227fd-8edb-4aa2-af62-f1ef3c99a530" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/3926d694-04cf-4f17-ae4b-70f00dde705e" type="video/mp4">
    </video>

=== "Data"

    An interactive fit drawn in inline SVG with the theme variables; leaving out an outlier refits the curve live.

    <video controls playsinline preload="metadata" aria-label="An agent shows a data fit; the user leaves out an outlier and sends back the fit" style="width: 100%" poster="https://github.com/user-attachments/assets/795ebae7-99dc-4660-bf7c-2754fdc5916d" data-poster-light="https://github.com/user-attachments/assets/795ebae7-99dc-4660-bf7c-2754fdc5916d" data-poster-dark="https://github.com/user-attachments/assets/61718584-7b95-4bac-87f3-2e71b3cb6ab4">
      <source src="https://github.com/user-attachments/assets/7385503a-982b-4811-9146-0a3a0378084d" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/36d79992-fc82-47e2-a012-6bc2c07b4956" type="video/mp4">
    </video>

=== "Slides"

    A deck the agent writes to a workspace file and shows by `path`; after a change request it rewrites the file and the open deck updates in place.

    <video controls playsinline preload="metadata" aria-label="An agent shows a slide deck from its workspace and updates it in place after a change request" style="width: 100%" poster="https://github.com/user-attachments/assets/0a8304cd-77bb-481a-8654-c735d91f5fd4" data-poster-light="https://github.com/user-attachments/assets/0a8304cd-77bb-481a-8654-c735d91f5fd4" data-poster-dark="https://github.com/user-attachments/assets/1e2819a7-8c5a-4095-8749-a7a3610dddc9">
      <source src="https://github.com/user-attachments/assets/c033ea6b-fc57-4a66-a235-be716d33286c" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/6219e504-dcf2-40a7-a919-fc5b372c13d6" type="video/mp4">
    </video>

=== "Trip planner"

    One canvas, two steps: pick a stay, then plan the days against a live budget, updated in place with `canvas_event_id`.

    <video controls playsinline preload="metadata" aria-label="One canvas, two steps: pick a stay, then plan the days against a live budget" style="width: 100%" poster="https://github.com/user-attachments/assets/fbe6bf8f-f1b4-4195-ad37-df3a2dfdbe80" data-poster-light="https://github.com/user-attachments/assets/fbe6bf8f-f1b4-4195-ad37-df3a2dfdbe80" data-poster-dark="https://github.com/user-attachments/assets/c71df2bd-54c9-4946-a8b1-54e191db5c3b">
      <source src="https://github.com/user-attachments/assets/341cd5bd-7fb1-441f-8a6a-470b701359d6" type="video/mp4" media="(prefers-color-scheme: dark)">
      <source src="https://github.com/user-attachments/assets/26870d8e-5303-4cfd-bb10-9fa66c1d6632" type="video/mp4">
    </video>

## Turn canvases on

Canvases need two switches, and both are off by default.

First, give the agent the [`chat_ui`](https://docs.mindroom.chat/tools/chat-ui/) toolkit with `enable_show_canvas: true`:

```yaml
agents:
  planner:
    display_name: Planner
    role: Plan trips and compare the options visually.
    model: default
    tools:
      - chat_ui:
          enable_show_canvas: true
```

`enable_show_canvas` (boolean, default `false`) adds the `show_canvas` function; the rest of `chat_ui` works without it.
It can also be set in the `chat_ui` tool's saved settings for every agent that uses `chat_ui`; an agent's own entry wins, as described in [How Settings Combine](https://docs.mindroom.chat/tools/#how-settings-combine).
`mindroom config init` and newly provisioned hosted instances already turn it on for the starter Mind agent, so Mind needs only the Chat switch.

Second, enable canvases in the MindRoom Chat deployment's runtime `config.json`:

```json
{
  "mindroom": {
    "canvas": {
      "enabled": true
    }
  }
}
```

The shipped MindRoom Chat configuration sets `enabled` to `false`, so only whoever deploys Chat can turn canvases on.

### Let pages load libraries

Pages can also load libraries such as Chart.js, D3, Mermaid, or KaTeX from jsDelivr's npm CDN, `https://cdn.jsdelivr.net/npm/`.
This takes two more switches, both off by default; turn on both, because a page that needs a library breaks where Chat blocks it.

```yaml
agents:
  planner:
    display_name: Planner
    role: Plan trips and compare the options visually.
    model: default
    tools:
      - chat_ui:
          enable_show_canvas: true
          enable_canvas_libraries: true
```

```json
{
  "mindroom": {
    "canvas": {
      "enabled": true,
      "libraries": true
    }
  }
}
```

`enable_canvas_libraries` (boolean, default `false`) tells the agent it may use the source, and Chat's `libraries` lets pages load from it.
Pages may then load scripts, styles, and fonts from `/npm/` paths only; fetching data, external images, workers, and every other site stay blocked, and libraries that evaluate strings, such as Alpine or Vue in-page templates, do not run.
Opening a page that uses a library tells jsDelivr the viewer's IP address and which files they load, which is why Chat keeps libraries off unless its deployment allows them.

## What the user sees

Each canvas appears in the conversation as its title with an **Open canvas** button.
Chat opens the panel by itself under the same conditions as other [Chat UI actions](https://docs.mindroom.chat/tools/chat-ui/#what-the-user-sees); otherwise the user chooses **Open canvas**.
The panel names the agent that made the page, and the user can resize it or expand it to the full width of the conversation.
The room header's **Canvas** button lists the conversation's canvases, newest update first, and reopens one after the user closed it.

When the panel cannot open, the **Open canvas** button is disabled and a message below it says why:

| Message | Cause |
|---|---|
| Interactive panels are turned off in this app. | The Chat deployment has not enabled canvases. |
| Interactive panels are not available in the app yet. Open this conversation in a browser. | The native mobile app does not show canvases; Chat in a browser does. |
| End the call to open interactive panels. | Chat does not run canvases during a call. |

Other people in the room who use MindRoom Chat on the agent's homeserver see the title without the button; everyone else sees only the notice text, `Interactive panel: <title>. Open it in MindRoom Chat to respond.`

### Find a canvas again

A canvas the user comes back to, such as a landing page an agent keeps current or a checklist used every day, does not have to be found in its thread again.
Where canvases are on, Chat's sidebar gets a **Canvases** button once the user has a canvas; it lists the canvases agents made for the user: pinned ones first, then the most recently updated, with the thread each was shown in (by its summary, as the sidebar names it), the room, the agent, and when each was updated and created.
Choosing a title opens the canvas at its latest version, filling the conversation's column; choosing the thread or the room opens that conversation.

Pins are kept in the user's Matrix account data (`io.mindroom.pinned_canvases`, room and event IDs only), so they show on every device.
The rest of the list holds the canvases this browser has received, so a new device starts with recent canvases and older ones appear once their thread is opened.
Users who don't want the button turn off **Show Canvases in the sidebar** under Settings → General → Interface; the choice follows them to every device.
For a page the user returns to, update the same canvas with `canvas_event_id` rather than showing a new one, so its row and pin keep pointing at it.

## Answering a canvas

Typing, toggling, and moving between steps inside the page send nothing.
When the page submits an answer, Chat shows the user exactly what will be sent, and nothing is sent until the user chooses **Send**.
To change a pending answer, the user chooses **Discard** and answers again.

The answer is an ordinary message from the user in the canvas's thread; it mentions the agent and starts the agent's next turn:

```text
@mindroom_planner:example.org Canvas response ($canvas-event, revision $canvas-event): Pro plan
{"plan":"pro"}
```

The first ID is the canvas and the second is the version the user answered; when that is not the latest version, the user answered an earlier page.
MindRoom Chat shows the answer as a one-line receipt that expands to the exact data.
An answer can carry up to 512 KB of JSON data, enough for a long document the user edited in the canvas; larger or non-JSON data is ignored, so no **Send** prompt appears.

When the page throws an error, fails to load a file, or tries to load something Chat blocks, the panel lists up to five of those errors with a **Tell** *agent* button.
Nothing is sent unless the user chooses it; the report reaches the agent like an answer:

```text
@mindroom_planner:example.org Canvas error ($canvas-event, revision $canvas-event):
Uncaught ReferenceError: drawChart is not defined (line 12)
Blocked https://unpkg.com/chart.js (script-src-elem)
```

### Let the agent read the page without a Send button

For a page whose choices the agent needs later, such as a checklist ticked over a few days, the agent shows the canvas with `share_state=True`.
Chat then keeps a copy of what the page saved (its saved state and kept inputs) in the room, once the user has worked in the page and pauses for a moment, and when the panel closes or the tab is hidden.
The copy never starts the agent's turn and never enters its conversation; the agent reads the newest copy when it wants, for example from a scheduled task:

```python
chat_ui.read_canvas_state(canvas_event_id="$canvas-event")
```

It returns the state, the inputs, and when the user's Chat shared them, or says nothing was shared yet; when the newest copy in the room cannot be read, it returns an error rather than an older copy.
Sharing is decided when a canvas is first shown, and its updates keep it; the panel tells the user "Saved in this room: *agent* and others here can read what you enter."
Each copy stays in the room's history like a message, and logging out does not remove it.

## Update a canvas

`show_canvas` returns the canvas `event_id`.
Calling `show_canvas` again with `canvas_event_id` set to that ID replaces the page in place, for the next step of a flow or a new version of a file the agent edited, and the conversation keeps one card.
An update may leave out `title` to keep the title the canvas was first shown with.
An open panel switches to the new page at once, unless the user has been working in it; then Chat shows "*agent* updated this panel." with a **Load update** button, so unsent work is not lost.
Every update stays available: once a canvas has more than one version, the panel header shows *Version n of m* with buttons to go back and forward, and an earlier version offers **Show latest**.
The user can answer an earlier version; switching is unavailable while an answer waits for **Send** or is sending.
An agent can update only canvases it showed to the same user in the same room and thread, or at room level; otherwise the call fails and the agent shows a new canvas instead.

## Write a page

Pass the page as `html`, or as a `path` to an HTML file, which suits pages the agent builds and refines, such as slides.
Paths follow the same rules as [registering local files](https://docs.mindroom.chat/attachments/#registering-local-files); prefer workspace-relative paths such as `slides/deck.html`.

```python
chat_ui.show_canvas(
    title="Choose a plan",
    html="""
<style>
  .plans { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
  .plan { background: var(--mr-surface); border: 1px solid var(--mr-border);
          border-radius: var(--mr-radius); padding: 16px; }
  button { background: var(--mr-accent); color: var(--mr-accent-text); border: 0;
           border-radius: 8px; padding: 8px 14px; cursor: pointer; }
</style>
<div class="plans">
  <div class="plan"><h3>Basic</h3><p>For trying things out.</p>
    <button onclick="mindroom.submit({plan: 'basic'}, {label: 'Basic plan'})">Choose Basic</button></div>
  <div class="plan"><h3>Pro</h3><p>For daily work.</p>
    <button onclick="mindroom.submit({plan: 'pro'}, {label: 'Pro plan'})">Choose Pro</button></div>
</div>
""",
)
```

- **Self-contained:** write inline CSS and JavaScript. The page cannot fetch data or load external images, and it loads libraries only where [allowed](#let-pages-load-libraries); otherwise draw charts with inline SVG or a `<canvas>` element, and always embed images as `data:` URLs.
- **Submitting:** call `window.mindroom.submit(data, {label})` with JSON-serializable `data` and a short `label`, or use a `<form>`, whose fields become the data and whose `data-mindroom-label` attribute sets the label.
- **Responsive:** the panel ranges from narrow to the full conversation width.
- **Kept inputs:** inputs, selects, and textareas with an `id` or `name` keep their values on their own, across a reload, an update, and a switch to another version, matched by that `id` or `name` (a radio group by its name).
  When the page loads, Chat sets the kept values and fires `input` and `change` on each, so a page that redraws on those events shows them.
  Give a field a new `id` when its meaning changes between versions.
  Never kept: password, file, and hidden inputs, fields with `autocomplete="off"`, and texts over 32K characters, which a page can keep with `saveState`.
- **Saved state:** to keep anything else the user did in the page, call `window.mindroom.saveState(value)` on every change; Chat batches the writes, and a value over 256K characters of JSON throws.
  Later loads of the canvas, after a reload, an update, or a switch to another version, start with it in `window.mindroom.state`, which is `undefined` when nothing is saved.
  Read it defensively, since an earlier version of the page may have saved it in another shape.
  It stays in that browser and reaches the agent only through an answer, or [when the canvas shares it](#let-the-agent-read-the-page-without-a-send-button).
  The browser keeps it for the 100 most recently saved canvases and deletes it when the user logs out.
- **Checking a page:** an agent with the [`browser`](https://docs.mindroom.chat/tools/web-scraping-and-browser/#browser) tool can open its page file with `browser_control(action="open", paths=[...])` and take a screenshot before showing it.
  Chat's theme variables and `window.mindroom` are not there, so a page that gives its `var(--mr-...)` colors fallbacks looks closest to the panel.
  The browser also does not block everything the panel blocks.
- **Limits:** the `title` is one line of at most 120 characters, and the page is UTF-8 text of at most 4 MB.
  Each update of a page larger than about 24 KB stores a new copy on the homeserver that users cannot delete, so iterate on large pages sparingly.

Chat exposes its current theme as CSS variables, so a page can match light and dark mode:

| Variable | Use |
|---|---|
| `--mr-bg` | Page background (the default body background) |
| `--mr-surface`, `--mr-surface-raised` | Cards and raised elements |
| `--mr-border` | Borders and dividers |
| `--mr-text`, `--mr-text-muted` | Text (the default body color) and secondary text |
| `--mr-accent`, `--mr-accent-text` | Primary actions and the text on them |
| `--mr-success`, `--mr-warning`, `--mr-danger` | Status colors |
| `--mr-radius`, `--mr-font` | Corner radius and font family |

For choices the variables cannot make, such as a chart palette, `window.mindroom.colorScheme` is `light` or `dark`.

## Privacy and safety

- The page cannot read the user's Matrix account, messages, cookies, or storage, and it cannot open pop-ups or navigate away; a page that tries is stopped, and Chat shows "This panel tried to leave its sandbox and was stopped." with a **Reload panel** button.
- In an encrypted room, the page, the answer, and a shared copy of the page's state are end-to-end encrypted like other messages.
- A canvas shared with `share_state=True` lets its agent, and anyone else in the room, read what the page saved without asking each time; Chat says so in the panel.
  Passwords and fields with `autocomplete="off"` are never kept automatically, but whatever the page passes to `saveState` is shared, so pages must keep secrets out of it.
- A canvas is not a safe place for secrets: browsers do not let a page block WebRTC, and a page that loads libraries can put data in the addresses it requests, so a malicious page could leak what the user types into it.
  Chat therefore names the agent that made each canvas and reminds the user that what they enter may leave the panel.
- Agents should show only pages they wrote, never downloaded or untrusted pages, because every canvas appears as the agent's own.

# Thread Exports

Thread exports write Matrix conversation history to YAML files that agents and operators can search with ordinary file tools.
Only Matrix threads are exported; plain room messages outside threads, including conversations with agents using `thread_mode: room`, are not.
Set `thread_exports` on an agent to keep a continuously updated copy inside its workspace, so its `file` and `shell` tools can grep past threads without Matrix access.
Use [`mindroom threads export`](#threads-export) for a one-shot or watched export to any directory.

```yaml
agents:
  code:
    thread_exports: true            # defaults below
  research:
    thread_exports:
      invited_rooms: false          # config rooms only
      private_room_scope: owner     # private agents only
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `invited_rooms` | bool | `true` | Also export user-created rooms the agent joined through invites. Current membership is always required |
| `private_room_scope` | `owner_and_agent` or `owner` | `owner_and_agent` | Private agents only. Both values require the requester to be joined to an exported room; `owner_and_agent` also checks the agent's membership. The agent must be joined either way to read the room |

## Export Layout and Contents

Workspace exports land at `<storage_root>/agents/<agent>/workspace/thread_exports/<urlencoded room key>/<urlencoded thread id>.yaml`, which agent tools see as `$MINDROOM_AGENT_WORKSPACE/thread_exports/`.
`mindroom threads export` writes the same layout under its output directory.
Each thread file holds `version`, `room` metadata, `thread` metadata including the current thread summary as `thread.summary`, and a `messages` list.
Exported messages match the history agents see in their prompts, including edits, redactions, and long messages.
Each room directory also holds an `index.json` mapping every thread file to its message count, participants, current summary, and last activity, sorted by most recent activity.
A thread file is rewritten only when its content changed, so `exported_at` reflects the last content-changing export.

Size limits:

- A thread file larger than 64 MiB or holding more than 250,000 YAML nodes, roughly 15,000 messages, is indexed without participants or last activity.
- A thread whose messages together pass 128 MiB, or whose file would pass 128 MiB, is not exported; the export reports it as failed and keeps any previous file for it.
- A room whose thread files together pass 256 MiB, or whose index entries together pass 8 MiB of JSON, gets an index of its most recently written threads only, listing the rest under `unindexed_files`, with a logged warning.

## When Workspace Exports Update

MindRoom re-exports a room within about two seconds of a message, edit, redaction, or membership change in it, and runs a full pass at startup and after every config reload.
A full pass removes exports for threads and rooms that no longer exist or that the agent may no longer read, and clears the export tree of any agent whose `thread_exports` was removed.
Agents may edit or delete their exported files; deleted files return on the next pass that touches the room.

If a pass exports no room at all, MindRoom keeps exports of rooms it did not reach, and if a room returns zero threads, it keeps that room's thread files; both cases log a warning.
Rooms excluded by config, such as invited rooms after `invited_rooms: false`, and rooms the agent or requester has left are still removed.
After such a warning, verify the source state and remove the preserved export manually only when the deletion is confirmed.

## Security and Retention

Shared agents export only rooms where the agent's own Matrix account is currently joined.
Private agents (`private:`) get one export tree per instance under `<storage_root>/private_instances/<scope-key>/<agent>/<private root>/thread_exports/`, limited to the agent's configured and invited rooms and to the requester's current room memberships, so one requester's workspace never holds other users' conversations.
A private instance is exported only after MindRoom has created or used it for its requester.

Thread export settings control what future passes write.
Cleanup is not a revocation or data-erasure boundary: previously exported data may remain, or may already have been copied or committed by the agent.

## Semantic Search Over Exports

With an embedder configured (`memory.embedder`), point a knowledge base at the export directory for semantic search through `search_knowledge_base`.

```yaml
knowledge_bases:
  code_threads:
    path: ./mindroom_data/agents/code/workspace/thread_exports
    description: Exported Matrix conversation history for the code agent
    exclude_patterns: ["*/index.json"]
agents:
  code:
    thread_exports: true
    knowledge_bases: [code_threads]
```

For a private agent, index the private-root-relative path instead:

```yaml
agents:
  secret:
    thread_exports: true
    private:
      per: user
      knowledge:
        path: thread_exports
        description: Your exported conversation history
```

The active thread's file is rewritten on every message, so a watching index re-embeds that thread per message.
This is negligible with a local embedder but costs real money with paid embedding APIs in busy rooms.

## threads export

`mindroom threads export` exports Matrix threads to YAML files through a running MindRoom instance; options are listed in the [CLI reference](https://docs.mindroom.chat/cli/#threads-export).
Keep MindRoom running with its API enabled; there is no offline export mode or separate Matrix login.
The CLI calls `--url`, then `MINDROOM_URL` from the selected runtime environment, then `http://127.0.0.1:8765`.
Set `MINDROOM_API_KEY` when API authentication is enabled; hosted deployments require an authorized bearer token.
With `MINDROOM_API_KEY` set, the CLI refuses a remote `http://` URL with `Use HTTPS when sending MINDROOM_API_KEY to a remote endpoint.`; HTTPS and loopback HTTP work.
The selected `--config` and `--storage-path` must match the running installation, and output paths refer to that runtime's filesystem.
Output defaults to `<storage>/thread_exports`.
Rooms joined through authorized invites are exported with the invited entity's own account unless `--no-invited-rooms` is passed.

A complete pass removes exported room and thread files that are no longer present or authorized; a `--room` pass only cleans up the selected room.
The same zero-room and zero-thread safeguards as [workspace exports](#when-workspace-exports-update) apply.
An interrupted pass keeps completed files; rerun the export to finish it and rebuild indexes.

### Output directory rules

MindRoom claims an empty output directory by writing a `.mindroom-thread-exports` marker file.
A populated directory without that marker is refused, skipped for the whole pass, and reported as a target failure.
To use an existing populated directory, create `.mindroom-thread-exports` inside it containing exactly `{"format":"mindroom-thread-exports","version":1}` followed by a newline.
Cleanup removes only exported room directories and thread files; unrelated entries such as `.DS_Store`, a `.git` directory, or your own notes are never deleted.
Output paths ending in `.`, `..`, or an empty name are rejected, as are symlinked output and room directories.

# Bug Reports

When something goes wrong in a conversation, such as an agent stopping halfway, a reply never arriving, or a tool failing, the person who fixes it needs to know exactly what happened.
Collecting that by hand (screenshots, room and message IDs, app diagnostics, server logs) is slow, and users rarely know what to include.
Bug reports make it one click for the user and give the administrator both sides of the story: what MindRoom Chat saw and what the MindRoom backend did.

This is meant for deployments with an administrator who can already read everything, such as a company or family install.
Users keep their privacy from each other: a report is visible only to the person who sent it and the administrators at the time it was sent, and an administrator removed later keeps the reports they already received.

## How it works

1. In MindRoom Chat, the user opens the menu on any message and chooses **Report a bug**.
2. MindRoom Chat collects the evidence and sends it to a private room shared only with the administrators, then opens that room so the user can add details.
3. The administrators' MindRoom Chat joins that room automatically, so nobody has to accept an invite.
4. The administrator runs [`mindroom debug-report`](https://docs.mindroom.chat/cli/#debug-report) on the attached file to collect the backend side of the same moment.
5. Both files go to whoever fixes the issue, for example a coding agent.

## Enable it

Add the administrators to the homeserver's `/.well-known/matrix/client` file:

```json
{
  "m.homeserver": { "base_url": "https://matrix.example.com" },
  "io.mindroom.bug_reports": { "admins": ["@admin:example.com"] }
}
```

With the `mindroom-client` Helm chart, list them in `bugReports.admins` and the chart serves that file; see the [chart README](https://github.com/mindroom-ai/mindroom/blob/main/cluster/k8s/client/README.md#bug-reports).
Nothing changes in `config.yaml`.
Without this key, the menu item is called **Download bug report** and saves the report as a file instead.

## What a report contains

- The room, thread, and message it was sent from, with a link.
- The conversation as MindRoom Chat holds it: the thread root and its newest 200 replies, or the last 50 messages when the message is not in a thread, each with its original content, latest edit, and send status.
- App details such as version, platform, browser, and connection state.
- The same data as **Export diagnostics** under **Settings → About**.

Nothing is redacted, and reports from encrypted rooms are stored decrypted in the report room.

## Where reports go

- Each user gets one private room named `Bug reports · <name>`, and each report is a thread in it, so follow-up questions stay next to the report.
- The room is unencrypted so administrator tools such as `matrix-mcp` can read it.
- Administrators are joined automatically only when they run MindRoom Chat and the invite is for a private report room created by someone on their own homeserver; other accounts, such as bots, accept the invite themselves.
- When an administrator is removed from the list, the next report goes to a new room; reports they already received stay with them.

## Collect the backend side

The report carries the IDs the backend uses, so one command on the MindRoom host gathers the agent runs, tool calls, model requests, deliveries, and log lines for that conversation:

```bash
mindroom debug-report mindroom-bug-report-<time>.json --output backend-report.json
```

Enable `debug.log_llm_requests` in advance if you also want full model requests and successful tool calls in it.
See [`mindroom debug-report`](https://docs.mindroom.chat/cli/#debug-report) for every option and data source.

# Usage Tracking

MindRoom records content-free token usage for agent and team replies, realtime voice calls, and the [helper and internal AI work](#token-usage) listed below, plus call duration for GPT-Live voice calls.
View it in the dashboard [Usage tab](#dashboard-usage-tab), fetch it over [HTTP](#usage-report-api) for reporting or cost tools, or let agents read it with the [`usage_stats`](#usage_stats) tool.
Reports show retained usage, not estimated spend or a billing ledger; to cap spend per user, see [Budgets](https://docs.mindroom.chat/budgets/).

## Dashboard Usage Tab

The **Usage** tab shows organization-wide token usage from the [usage report](#usage-report-api) and uses normal dashboard authentication.

- All-time recorded totals for input, output, and cache tokens
- Daily activity with a UTC date range and an expandable data table
- Searchable agent/team, model, and requester breakdowns, sorted by the selected token counter
- Agent/team detail with requesters, cumulative models, and daily activity
- Requester detail across agents, models, and daily activity, including usage of shared agents
- Model detail across agents, requesters, and daily activity, keeping providers separate

The token selector also offers reasoning and audio counters when the provider reports them.
Select a row in any breakdown to open its detail panel.
Date ranges apply only to daily activity; agent and model totals stay cumulative.
Daily and requester detail can be lower than cumulative totals when older attribution is missing, and undated runs appear only in totals.
A run that used several models counts once in each model's run count, so do not add model run counts together.
Cache tokens may already be included in input counts, depending on the provider.

The tab waits automatically while a report is prepared.
**Refresh** requests the latest report; completed reports can be up to one minute old.

## Token Usage

This section describes what every usage view counts.

**Counters.** Each total has nine counters: `input_tokens`, `output_tokens`, `total_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `audio_input_tokens`, `audio_output_tokens`, and `audio_total_tokens`.
`output_tokens` includes `reasoning_tokens`.
Missing counters are reported as zero, so a zero does not prove a provider used none of that category.
Models are grouped by provider and model; usage that cannot be attributed to a model appears under model `unknown`.

**Runs.** `run_count` counts retained top-level AI replies with usable token metrics, not messages or conversations.
Team member tokens count toward model, requester, daily, and request detail without adding to `run_count`, and a team's session totals already include its members.
Helper work adds tokens without adding runs, including retries and rejected outputs:

| Request `kind` | Work |
| --- | --- |
| `run` | Ordinary agent or team reply |
| `compaction_summary` | Conversation compaction summaries |
| `memory_auto_flush` | Background memory auto-flush extraction |
| `memory_extraction` | Mem0 memory extraction from conversations, the `memory` tool, and voice-call transcripts, when its LLM provider reports tokens; a team's conversation memory is reported under `system:internal` |
| `dynamic_workflow` | Dynamic Workflow `room_agent` participants; a `subagent` participant's tokens appear under its calling agent |
| `claude_agent` | Each Claude Code turn run by the `claude_agent` tool, for its main conversation's responses but not the subagents it starts or requests Claude Code makes for itself, such as compaction summaries and session titles; it appears in totals but not in request rows |
| `skill_learning` | Automatic [skill reviews](https://docs.mindroom.chat/skills/) |
| `knowledge_graph` | Fact extraction for the [knowledge graph](https://docs.mindroom.chat/knowledge-graph/) |
| `realtime_voice` | Speech-model tokens of a realtime voice call, one request per response |
| `routing`, `room_topic`, `schedule_parse`, `team_mode`, `thread_summary`, `voice_normalization`, `voice_transcription` | Internal AI work, reported under the entity `system:internal` |

Helpers are attributed to the conversation and requester that triggered them, and stay unattributed when no requester is known.
`system:internal` usage has no human requester, appears only in organization reports, and transcription is counted only when the provider reports tokens.
Embedding calls and calls to separately configured judgment backends, such as participation or mid-turn judges, are not counted.

**Requesters.** Usage is grouped by canonical Matrix user ID, with known aliases combined; `user_id: null` holds unattributed usage.
Shared and private instances of the same agent are combined in entity rows; `private_agent_breakdown` shows the private part separately.

**Retention.** Recorded usage survives compaction, message edits, and regeneration, and a regenerated reply counts as a separate run.
Erasing a whole session removes its usage; deleted sessions are unavailable.
Reports cover only agents in the current configuration, so removing an agent drops its usage from reports even though its sessions stay stored; removed and ad hoc teams stay included.
Usage from session history that predates usage tracking is imported once at startup; history already lost cannot be reconstructed, and old usage is never given invented dates or requesters.

**Coverage.** Each breakdown has a matching coverage object (`coverage`, `model_coverage`, `user_coverage`, `cumulative_model_coverage`, `daily_coverage`, `request_coverage`, `voice_coverage`, `private_agent_coverage`) with `scanned_sources`, `unavailable_sources`, and an explanatory `note`.
`unavailable_sources` counts sources with missing, unreadable, or incomplete data; it is not a missing-token percentage.
Because of these gaps, model, user, and daily rows do not necessarily sum to the top-level totals.
Totals support cost estimates, but provider-specific charging rules and calls outside retained storage are not captured.

## Reply Cost

With [prices](https://docs.mindroom.chat/configuration/models/#model-config-fields) set on a model, each agent or team reply that uses it records its cost in USD, priced the same way as [budget spend](https://docs.mindroom.chat/budgets/#how-spend-is-counted).
In MindRoom Chat, open **Reply details** from a reply's ⓘ button or message menu to see its cost and the conversation's cost up to that reply.
A reply's cost covers every model call made within the reply, including team members on other models and [native compaction](https://docs.mindroom.chat/configuration/history/#native-compaction).
Subagents it delegates to and separate helper work, such as text compaction summaries, memory-backend extraction, and thread titles, are left out of it but still count toward budgets.
The conversation cost adds up the replies MindRoom Chat has loaded and reads "at least" while older messages are not loaded.
A regenerated reply shows the cost of its latest run only.
Replies on models without prices, and replies that report no token usage, show no cost.

## Usage Report API

`GET /api/usage` returns the organization-wide report under standard dashboard authentication, like other administrator APIs.
A collector service can fetch the same report from `GET /api/usage/export` with its own credential; see [Usage Export Service](#usage-export-service).

Both routes accept two optional query parameters, each defaulting to `false`:

| Parameter | Adds |
| --- | --- |
| `include_daily=true` | `daily_breakdown` and `daily_coverage`, plus a `daily_breakdown` inside every requester and private-agent row |
| `include_requests=true` | `request_breakdown` and `request_coverage` |

### Polling for a Report

The first request for a report returns `202` with `{"status":"pending"}` and `Retry-After: 5`.
Poll the same URL with the same parameters after that delay; once ready, the route returns `200` with the report.
Completed reports are cached for 60 seconds, and configuration changes discard cached reports.
A `503` means the report could not be prepared, for example because the committed configuration is unavailable; retry after a few seconds.
All responses use `Cache-Control: no-store` and contain no conversation content.

### Report Fields

Every completed report includes `schema_version: 1` and `generated_at`, the UTC ISO 8601 time the scan finished; cached responses keep that time.

| Field | Contents |
| --- | --- |
| `totals`, `session_count` | All retained usage |
| `breakdown` | One row per agent or team |
| `model_breakdown` | Retained runs by provider and model, with `run_count` |
| `cumulative_model_breakdown` | Session totals by provider and model, with `session_count` |
| `user_breakdown` | Per requester: `user_id`, `totals`, `run_count`, and its own `model_breakdown` |
| `private_agent_breakdown` | Private-agent usage per owner and agent; see [Private-Agent Rows](#private-agent-rows) |
| `voice_breakdown` | GPT-Live call duration; see [Voice Call Duration](#voice-call-duration) |

Each `breakdown` row has session `totals`, `session_count`, and `cumulative_model_breakdown`, plus `retained_run_totals`, `run_count`, and a `user_breakdown` showing which requesters used that agent or team.
Requester totals sum to `retained_run_totals`, which can differ from the cumulative session `totals`.

`cumulative_model_breakdown` splits each session's `totals` by the models that produced them, so it includes compacted history still in retained sessions but has no dates or requesters.
A session using several models counts once in each model's `session_count`.

### Daily Rows

Each daily row has a UTC `date` (`YYYY-MM-DD`), `totals`, `run_count`, and a `model_breakdown`, sorted oldest first.
Dates come from individual request timestamps, falling back to the run's creation date when request detail is missing, which can shift usage across days.
Each run counts once on its first request date, so a later day can show tokens with `run_count: 0`.
Usage with no usable date is left out of daily rows but stays in all-time totals, so a user with only undated runs has an empty `daily_breakdown`.

### Request Rows

`include_requests=true` is available only on the two organization routes.
Each request row has `entity`, `user_id`, `provider`, `model`, `kind`, epoch-seconds `created_at`, and all nine counters in `totals`, sorted by time.
Rows keep individual provider calls apart so a consumer can apply per-request pricing, such as context-length tiers.
Rows contain no prices, prompts, responses, session IDs, or run IDs.
Only requests whose counters match their run's recorded totals are exported; missing detail marks `request_coverage` incomplete while aggregate totals stay available.

### Voice Call Duration

Each `voice_breakdown` row is one GPT-Live provider session, with `entity`, `user_id`, `provider`, `model`, epoch-seconds `created_at`, `duration_seconds`, and `finalized`.
Reconnecting starts a new row, even within the same call, and duration is not split across UTC days.
`finalized: false` means the duration is the last running total the provider reported, for example after a dropped connection.
Sum `duration_seconds` to group voice use by caller, agent, or model, and price it separately from the tokens of agents the call delegated to.
Voice duration does not add to token totals, request rows, or run counts, and rows contain no audio, transcripts, or session IDs.

### Private-Agent Rows

Each `private_agent_breakdown` row has `agent_name`, `user_id`, session `totals`, `session_count`, `cumulative_model_breakdown`, `retained_run_totals`, `run_count`, and `model_breakdown`.
Session totals are attributed to the private instance's owner, while retained runs keep the user who made the request.
Session totals can include older compacted history that has no model or daily detail.

To attribute usage by ownership instead of by requester, compute this per user and counter over the union of users in both breakdowns, summing a user's private-agent rows first and treating missing rows as zero:

```text
user_breakdown.totals - private_agent_breakdown.retained_run_totals + private_agent_breakdown.totals
```

For example, 60 retained tokens including 20 private-agent tokens, plus a private-agent session total of 50, gives `60 - 20 + 50 = 90`.
This view assigns private-instance usage to its owner: if Bob requested 20 tokens from Alice's private instance with 100 session tokens, Alice gets 100 and Bob gets none.
Keep `user_breakdown` unchanged when reporting who made requests.

### Consuming Reports

Tolerate additional response fields, and keep session totals and retained-run breakdowns distinct.
Treat each report as a snapshot; do not add successive all-time totals together.
Counter values can change between reports as coverage improves.

### Personal Private-Agent Usage

`GET /api/usage/me/private-agents` returns the signed-in user's usage across their own private agents, with the same rows as `get_my_private_usage()` but without `user_id` or `user_breakdown`.
The only accepted query parameter is `include_daily` (default `false`); any other parameter returns `400` `Usage target overrides are not accepted`.

The endpoint requires [trusted upstream auth](https://docs.mindroom.chat/deployment/trusted-upstream-auth/) with a [Matrix identity](https://docs.mindroom.chat/deployment/trusted-upstream-auth/#matrix-identity).
A [Connections portal](https://docs.mindroom.chat/connections-portal/) session from MindRoom Chat does not authorize it.
Without it, requests fail with `403` `Personal APIs require trusted signed authentication` or `Personal APIs require a verified Matrix identity`; an API key alone cannot select a user.
Any signed-in user can call it, and it grants no administrator or export access.

## Usage Export Service

Deployments with trusted upstream auth can let a collector service call `GET /api/usage/export` without giving it a browser or administrator identity.
The route returns the same report, parameters, and polling behavior as [`GET /api/usage`](#usage-report-api).

Configure [trusted upstream auth](https://docs.mindroom.chat/deployment/trusted-upstream-auth/#setup), then add a dedicated service audience and client ID:

```bash
MINDROOM_USAGE_SERVICE_JWT_AUDIENCE=mindroom-usage-export
MINDROOM_USAGE_SERVICE_CLIENT_ID=usage-export-client.example.org
```

The service sends its assertion in the trusted-upstream JWT header on every request, including polls.
The assertion must be signed with `RS256` by the trusted-upstream issuer and JWKS, and contain valid `exp`, `iat`, `iss`, and `aud` claims, where `aud` is `MINDROOM_USAGE_SERVICE_JWT_AUDIENCE` rather than the browser audience.
It must also contain `type=app`, `common_name` exactly equal to `MINDROOM_USAGE_SERVICE_CLIENT_ID`, and an empty `sub`.

| Status | Meaning |
| --- | --- |
| `503` `Usage export service authentication is not configured` | Trusted upstream auth, one of its JWT settings, or a service setting is missing |
| `401` `Invalid usage service JWT` | Missing or invalid assertion |

A service assertion authorizes only `/api/usage/export`, not `/api/usage`, personal usage, configuration, or other administrator routes.

## `usage_stats`

The `usage_stats` tool lets an agent answer usage questions in chat; it is read-only.

| Function | Reports | Availability |
| --- | --- | --- |
| `get_my_usage()` | The caller's usage of the current agent; for a private agent, the caller's own instance | Always |
| `get_my_private_usage()` | Usage across all the caller's currently configured private agents | Always |
| `get_all_usage()` | Organization-wide usage of currently configured agents, all stored teams, and `system:internal` | Requires `admin_scope: true` and a caller listed in `administrators` |

Give an agent the self-service functions by adding the tool:

```yaml
agents:
  usage_assistant:
    display_name: Usage Assistant
    role: Report the caller's retained MindRoom usage
    tools:
      - usage_stats
```

`mindroom config init` and newly provisioned hosted instances give the starter Mind agent these self-service functions.

Enable `get_all_usage()` with the per-agent `admin_scope` option (boolean, default `false`), which cannot be set in `defaults.tools`:

```yaml
administrators:
  - "@usage-admin:example.com"

agents:
  usage_admin:
    display_name: Usage Administrator
    role: Report retained MindRoom usage across configured entities
    tools:
      - usage_stats:
          admin_scope: true
```

All three functions accept `include_daily` (default `false`), which adds [daily rows](#daily-rows) and `daily_coverage`.

### Tool Responses

Responses are JSON with `status` and `tool` fields, plus `scope`, `totals`, `session_count`, `breakdown`, `coverage`, `model_breakdown`, and `model_coverage`.
`get_all_usage()` returns the organization report fields described in [Report Fields](#report-fields), except request rows.
Self reports leave `breakdown` empty.
`get_my_private_usage()` adds `cumulative_model_breakdown` and `private_agent_breakdown` without user IDs.
`get_my_usage()` omits cumulative model and private-agent fields and excludes the in-progress tool call.
On a shared agent it counts only the caller's retained runs, so it can miss usage after compaction.
On a private agent it reports the whole session totals, which include compacted usage and can exceed the retained model or daily detail.

Errors use the same envelope with a `code` and `message`:

| Code | Cause |
| --- | --- |
| `authorization_error` | `get_all_usage()` called without `admin_scope`, from another agent, or by a non-administrator |
| `context_unavailable` | No active requester or agent context, such as outside a conversation |

# Budgets

Budgets cap how much each Matrix user can spend on AI replies per calendar month.
A user at their cap keeps chatting, but replies that would use a priced model use a cheaper fallback model until the next month starts.

## Setup

Budgets need prices for the models you pay for and a `budgets` section:

```yaml
models:
  astra:
    provider: openai
    id: gpt-6-astra
    pricing:
      input: 5.0
      output: 30.0
      cache_read: 0.5
  luna:
    provider: openai
    id: gpt-6-luna
    pricing:
      input: 0.2
      output: 1.25

budgets:
  monthly_limit_usd: 20
  fallback_model: luna
```

Prices are USD per million tokens, set with the `pricing` field described in [Model Config Fields](https://docs.mindroom.chat/configuration/models/#model-config-fields).
Give each paid model prices on one of its model entries: they apply to every entry with the same provider and model ID, including entries that differ only in API transport or ID prefix.
The dashboard **Budgets** page edits the same settings and prices.
To give a user a cap other than the default, see [Per-User Caps](#per-user-caps).

## Fields

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `monthly_limit_usd` | number ≥ 0 or `null` | `null` | Cap in USD for each user without a [cap of their own](#per-user-caps); `null` leaves those users uncapped |
| `fallback_model` | model name | required | Model that replies use instead of a priced model once the user reaches their cap |

A cap of `0` sends every priced reply for that user to the fallback model.
Bridge aliases share their canonical user's cap and spend.
Removing the `budgets` section turns budgets off.
Changes to caps, prices, and the fallback model apply without a restart.

## Per-User Caps

A user's own cap replaces `monthly_limit_usd` for that user.
Set it on the [Budgets page](#dashboard-budgets-page) or through the [Budget API](#budget-api), including for users who have not used MindRoom yet.
Per-user caps are not part of `config.yaml`: they survive restarts, and config edits or redeploys leave them unchanged.
Only people have caps; agents, bot accounts, and MindRoom's internal user cannot get one.
Caps can be set only while budgets are on, and they stay stored if budgets are turned off.

## How Spend Is Counted

Spend is the cost of the user's [recorded token usage](https://docs.mindroom.chat/usage/#token-usage) since the first day of the current month in UTC, at the configured prices.
It includes the user's agent and team replies, delegated agents, and helper work they triggered, such as compaction summaries.
Usage of models without prices, such as local models or subscription logins, costs nothing.
`claude_agent` coding sessions are charged at the prices of the Anthropic model entry with the same model ID, however Claude Code is signed in.
Memory extraction with `memory.llm.provider: openai` is charged at the prices of an `openai` model entry with the same model ID.
A realtime voice call's speech tokens are charged at the prices of an `openai` model entry with the same model ID, and a live call's billed duration is not charged.
Reasoning and thinking tokens are charged at the output price.
Audio tokens count at the model's text prices.
Internal work with no requester (`system:internal`) and usage without a date or requester are not charged.
Spend updates within about a minute of a chat reply finishing and within five minutes of other usage, such as voice calls, so a user can go slightly over their cap before the fallback applies.
With a lot of retained usage, recounting spend takes longer, and updates wait about ten times that long so the recount does not slow replies.
A reply that already started finishes on its model.

## Over-Budget Replies

Once a user reaches their cap, replies that would use a priced model use `fallback_model` instead.
This covers agent and team replies including every team member, delegated agents, Dynamic Workflow participants, scheduled tasks, the agent's replies in voice calls, and [OpenAI-compatible API](https://docs.mindroom.chat/openai-api/) keys mapped to a requester.
The speech model of a realtime or live call profile is never swapped.
Models without prices keep running, because they add no spend.
A `claude_agent` tool without its own `model` starts Claude Code on the reply's model, so set its `model` when `fallback_model` is not a Claude model.
The fallback model's usage still counts toward spend, but the fallback is never blocked.
Requests without a human requester, such as unauthenticated OpenAI-compatible calls, are never budgeted.
Users get no chat message when the fallback applies; the Budgets page shows who is over budget.

## Dashboard Budgets Page

The **Budgets** page turns budgets on or off, sets the default cap and the fallback model, edits per-user caps, and edits model prices.
It lists each user with spend or a cap of their own, their month-to-date spend, whether they are over budget and using the fallback model, and who last set their cap.
It also lists models used this month that have no prices.
A per-user cap is saved as soon as you leave its field or press Enter, and clearing the field returns the user to the default cap.
Click **Save** to write the other changes to `config.yaml`.

## Budget API

The dashboard routes use standard dashboard authentication, and the same routes under `/api/budgets/service` use the [budget service](#budget-service) credential.

| Route | Effect |
| --- | --- |
| `GET /api/budgets` | Budget settings and spend, or `{"enabled": false}` with budgets off |
| `PUT /api/budgets/users/{user_id}` | Create or replace one user's cap with a body like `{"limit_usd": 50}` |
| `DELETE /api/budgets/users/{user_id}` | Remove one user's cap so the default applies; returns `204`, also when the user had no cap |
| `GET /api/budgets/service` | Same as `GET /api/budgets` |
| `PUT /api/budgets/service/users/{user_id}` | Same as `PUT /api/budgets/users/{user_id}` |
| `DELETE /api/budgets/service/users/{user_id}` | Same as `DELETE /api/budgets/users/{user_id}` |

URL-encode `user_id` in the path, for example `/api/budgets/users/%40alice%3Aexample.com`.
`limit_usd` is a JSON number of at least `0`; no other body fields are accepted.
`PUT` returns `user_id`, `limit_usd`, `updated_at`, and `updated_by`, where `user_id` is the canonical user the cap belongs to.
`updated_by` is `dashboard` for a cap set on the Budgets page or its routes, and `budget-service` for the budget service.

`GET` returns these fields:

| Field | Contents |
| --- | --- |
| `period_start`, `period_end` | First day of the current UTC month and of the next month |
| `generated_at` | When spend was last computed, or `null` before the first computation |
| `default_limit_usd`, `fallback_model` | The configured default cap and fallback model |
| `users` | One row per user with spend this month or a cap of their own, highest spend first, with `user_id`, `spend_usd`, the applied `limit_usd`, `over_budget`, and the user's own cap as `override_usd`, `updated_at`, and `updated_by`, which are `null` when the default applies |
| `unpriced_models` | Rows with `provider`, `model`, and this month's `total_tokens` for models without prices |
| `coverage` | `scanned_sources` and `unavailable_sources`, as in [usage coverage](https://docs.mindroom.chat/usage/#token-usage) |

| Status | Meaning |
| --- | --- |
| `503` `Budget monitor unavailable` | The agents are not running in the same process as the API, so no spend or caps are available |
| `409` `Budgets are off` | Turn on budgets before setting or removing caps |
| `422` `User ID must be a concrete Matrix user ID` | Use a full Matrix user ID like `@alice:example.com` |
| `422` `Caps apply only to people, not agents or bots` | The ID belongs to an agent, a bot account, or MindRoom's internal user |
| `422` with a list of errors | `limit_usd` is missing, negative, not a finite number, or the body has other fields |

## Budget Service

Deployments with trusted upstream auth can let a service, such as a company spend dashboard, read budgets and set caps without a browser or administrator identity.
Configure [trusted upstream auth](https://docs.mindroom.chat/deployment/trusted-upstream-auth/#setup), then add a dedicated audience and client ID:

```bash
MINDROOM_BUDGET_SERVICE_JWT_AUDIENCE=mindroom-budget-service
MINDROOM_BUDGET_SERVICE_CLIENT_ID=budget-service-client.example.org
```

The service sends an assertion that follows the rules of the [usage export service](https://docs.mindroom.chat/usage/#usage-export-service), with this audience and client ID instead.
Give the `/api/budgets/service` path its own access application, so the service's audience never applies to dashboard routes.
A budget service assertion authorizes only the `/api/budgets/service` routes, and a usage export assertion does not authorize them.

| Status | Meaning |
| --- | --- |
| `503` `Budget service authentication is not configured` | Trusted upstream auth, one of its JWT settings, or a budget service setting is missing |
| `401` `Invalid budget service JWT` | Missing or invalid assertion |

# Matrix Integration

This page covers how MindRoom appears on Matrix: the accounts it creates, mentions, end-to-end encryption, managed avatars, TLS trust, and sync tuning.
Room creation and room policy are covered in [Rooms](https://docs.mindroom.chat/rooms/), and Matrix environment variables in [Configuration](https://docs.mindroom.chat/configuration/#matrix).

## Why Matrix?

Every agent is an ordinary Matrix user, so you can reach it from any Matrix client, on any federated homeserver, in end-to-end encrypted rooms, and from Discord, Slack, Telegram, and other networks through [bridges](https://docs.mindroom.chat/deployment/bridges/).

## Agent Users

Each agent, team, and the router has its own Matrix user, created automatically at startup.
Account credentials are stored in `matrix_state.yaml` in the storage directory (default `mindroom_data/`).

In chat, address an agent or team by its configured key, such as `@assistant`.
MindRoom requests localparts such as `mindroom_assistant`, but the account recorded in `matrix_state.yaml` is authoritative, so an agent's actual Matrix ID (for example `@assistant_live:example.com`) can differ from the requested one.

`MATRIX_MANAGED_ACCOUNT_AUTH` selects how these accounts authenticate:

- `password` (default) generates a separate password for every account and uses normal Matrix registration and login.
- `appservice` registers passwordless accounts inside an exclusive application-service namespace and gives each account its own device token for encryption and sync.
  First register an application service with the homeserver whose exclusive user namespace covers every account MindRoom manages, then supply that registration's `as_token`.
  Provide exactly one of `MATRIX_APPSERVICE_TOKEN` or `MATRIX_APPSERVICE_TOKEN_FILE`; setting either one without `MATRIX_MANAGED_ACCOUNT_AUTH=appservice` stops startup.
  The application-service token is used only to register accounts and log in new devices.
  After switching an existing install to `appservice`, stored passwords are removed from `matrix_state.yaml` once each account logs in.

## Internal User

The optional `mindroom_user` section configures MindRoom's internal Matrix user account; omit it for hosted or public profiles.

```yaml
mindroom_user:
  username: mindroom_user
  display_name: MindRoomUser
```

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `username` | string | `mindroom_user` | Matrix localpart, without `@` or a domain, using lowercase letters, digits, `.`, `_`, `=`, `-`, or `/`; it must not match an agent, team, or router account |
| `display_name` | string | `MindRoomUser` | Display name, changeable at any time |

Set `username` before first startup.
Changing it after the account exists stops startup with `mindroom_user.username cannot be changed after the internal Matrix account has been created.`
Hosted provisioning can return a different actual Matrix ID, which MindRoom then uses.

## Mentions

In agent replies and other messages MindRoom sends, two forms become real Matrix mentions that render as clickable links and notify the agent or team:

- `@calculator`, the configured agent or team key.
- `@actual_calculator:localhost`, the agent's current full Matrix ID.

`@mindroom_calculator`, the `mindroom_` prefix plus the key, also mentions the agent's or team's current account.
Any other bare account localpart, such as `@actual_calculator`, is not a mention.
Any other valid full Matrix ID, such as a human user or an agent's old account like `@mindroom_calculator:localhost`, becomes a mention of that exact account and does not reach the agent's current account.

## End-to-End Encryption

Agents fully participate in encrypted rooms: they read encrypted text, media, and thread history, and they reply encrypted.
To encrypt managed rooms, set `encrypted: true` in their [room policy](https://docs.mindroom.chat/rooms/#room-policy).
Enabling encryption on a Matrix room is irreversible.

Each agent cross-signs its own device, so clients that only share room keys with cross-signed devices (MSC4153) keep sharing them with agents.

### `!encrypt`

Enable end-to-end encryption for the current room.

```
!encrypt
!encrypt confirm
```

`!encrypt` explains what enabling encryption means for the room without changing anything.
`!encrypt confirm` enables encryption and is restricted to Matrix room admins.
The responding bot also needs permission to change room state; if it lacks that permission, a room admin can enable encryption from their own Matrix client.
People joining an encrypted room later cannot read messages sent before they joined.

### `!e2ee`

Show encryption diagnostics for the current room.

```
!e2ee
```

The report includes the room's encryption state, the responding bot account and device, the encryption store status, cross-signing status, and decryption-failure counters since startup.
Use it when an agent seems to ignore messages in an encrypted room.

### Troubleshooting Undecryptable Messages

When an agent cannot decrypt a message from an authorized sender, it logs a `matrix_event_decryption_failed` warning and posts one notice for that message's encryption session asking the user to resend.
Only one agent posts the notice, even in multi-agent rooms.
Resending normally fixes it, because the sender's client then shares a new key with the agent.
Decryption-failure counters also appear on `/api/health` under `e2ee`.

If a bot's encryption store under `mindroom_data/encryption_keys/` is lost, that agent does not start, and `mindroom doctor` reports the missing store.
Restore the matching storage backup before restarting, as described in [Device recovery](https://docs.mindroom.chat/deployment/upgrades/#device-recovery-after-the-upgrade).

### Delivery Policy

Encrypted messages from agents are always shared with the room members' unverified devices.
MindRoom has no device-verification flow, so requiring verified devices would make every reply in an encrypted room fail.

## Managed Avatars

Every agent, team, and managed room gets a Matrix avatar by default, chosen from the painted stock avatars in the [MindRoom assets repository](https://github.com/mindroom-ai/assets/blob/main/avatars/painted/README.md).

- An agent named after a stock avatar (for example `mind`, `code`, `research`, `writer`, or `email`) uses that picture, and any other agent or team receives a stable stock avatar chosen from its name.
- A room served by exactly one agent or team shows that entity's avatar, and any other room receives a stable stock avatar chosen from its key.
- The root Matrix Space uses `avatars/spaces/root_space.png` when present and otherwise the stock `mind-logo` image.
- Avatars are set only where the Matrix profile or room has none, so pictures you set yourself are kept.
- Room avatars are chosen only when a room is created, so run `mindroom avatars sync` to fill in existing rooms.
- A failed stock avatar download is retried at the first start after 24 hours, or immediately for rooms and the root Space by `mindroom avatars sync`.

To choose a picture, place a PNG at `avatars/<agents|teams|rooms|spaces>/<name>.png` next to `config.yaml`; containerized deployments read these overrides from `<storage>/avatars/` instead.
Run `mindroom avatars sync --force` to reset every managed room and the root Space to its default avatar: its own file, its single agent's or team's avatar, or its stock pick.

`mindroom avatars generate` creates a custom avatar file for every entity without a workspace or bundled avatar file, using `gpt-6-astra` to write prompts and `gpt-image-2.5-sunburst` to render them; it requires only `OPENAI_API_KEY` or `OPENAI_API_KEY_FILE`.
Run `mindroom avatars generate --force` to overwrite existing workspace avatar files after changing prompts or styles.
Override the generation styles with the root `prompts` block:

```yaml
prompts:
  AVATAR_CHARACTER_STYLE: "professional AI avatar portrait, abstract geometric silhouette"
  AVATAR_ROOM_STYLE: "minimalist wayfinding icon, precise geometry, strong silhouette"
  AVATAR_AGENT_SYSTEM_PROMPT: "You are creating distinctive visual elements for a professional AI agent avatar."
  AVATAR_TEAM_SYSTEM_PROMPT: "You are creating distinctive visual elements for a professional AI team avatar."
  AVATAR_ROOM_SYSTEM_PROMPT: "You are creating a refined, minimalist icon design for a room avatar."
```

## TLS Trust

MindRoom trusts OpenSSL's default CA file and directory plus certifi's public roots for the homeserver.
For a homeserver behind a private CA, set `SSL_CERT_FILE` or `SSL_CERT_DIR`; each replaces the matching OpenSSL default location, and setting either one stops MindRoom from adding certifi's roots.
A homeserver certificate that fails verification stops startup with an error naming the homeserver and the verification failure.

Requests to the hosted provisioning service always verify its certificate, whatever `MATRIX_SSL_VERIFY` says.
For a provisioning service behind a private CA, set `SSL_CERT_FILE` or `SSL_CERT_DIR` in the process environment.

## Matrix Sync

```yaml
matrix_sync:
  mode: classic
  sliding_timeline_limit: 100
  max_response_bytes: 16777216
  max_pending_bytes: 67108864
```

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `mode` | `classic` or `sliding` | `classic` | `classic` uses `/v3/sync`; `sliding` uses MSC4186 Simplified Sliding Sync and requires a homeserver that advertises it |
| `sliding_timeline_limit` | integer ≥ 1 | `100` | Per-room timeline window for Sliding Sync |
| `max_response_bytes` | integer ≥ 1 | `16777216` (16 MiB) | Maximum HTTP sync response size per bot |
| `max_pending_bytes` | integer ≥ 1 | `67108864` (64 MiB) | Maximum size of received events waiting to be processed per bot |

Choose `mode` before first startup, because each bot's stored sync state stays bound to the mode it first used.
Changing `matrix_sync` through config reload restarts running agents.

If a large initial sync exceeds the response limit, raise `max_response_bytes` enough to fit it.
If processing received events exceeds the pending limit, raise `max_pending_bytes`; stored events can take more space than the HTTP response, so raising the response limit may also require raising the pending limit.
Each bot has its own sync session, so allow enough memory and disk space when raising these limits.
Large initial syncs may also need more startup time: `MINDROOM_MATRIX_SYNC_STARTUP_TIMEOUT_SECONDS` sets the first-sync allowance.

# Rooms & Spaces

This page covers the Matrix rooms MindRoom creates and manages, their join, listing, encryption, invitation, and admin settings, and the root Matrix Space that groups them.
Who may talk to an agent in a room is a separate setting covered in [Access Control](https://docs.mindroom.chat/authorization/).

## Configuration

List room keys under an agent's or team's `rooms`, and optionally set per-room policy under `rooms.<key>`:

```yaml
room_defaults:
  join_policy: invite
  listed: false
  invite_users:
    - "@owner:example.com"
  admins:
    - "@owner:example.com"

rooms:
  research:
    display_name: Research Lab
    description: Literature reviews and experiment notes
    encrypted: true
    invite_users:
      - "@owner:example.com"
      - "@scientist:example.com"

agents:
  assistant:
    rooms: [lobby, dev]

teams:
  research_team:
    rooms: [research]
```

Every room key listed by an agent or team, or under top-level `rooms`, is a managed room, created on startup or config reload if it does not exist yet.
Entries starting with `!` are existing Matrix room IDs, which are joined as they are and are not created or managed.

## Room policy

`room_defaults` sets the policy for every managed room, and `rooms.<key>` overrides it for one room.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `join_policy` | `invite`, `knock`, or `public` | `invite` | How people may join the room |
| `listed` | bool | `false` | Publish the room in the server's room directory |
| `encrypted` | bool | `false` | Enable end-to-end encryption; once enabled it cannot be turned off |
| `invite_users` | list of Matrix user IDs | `[]` | Users invited to the room |
| `admins` | list of Matrix user IDs | `[]` | Users granted Matrix admin power (level 100) in the room |
| `rooms.<key>.display_name` | string | the key with underscores as spaces, title-cased | Matrix room name |
| `rooms.<key>.description` | string | `""` | Room purpose shown in the dashboard |

`invite_users` and `admins` accept only concrete Matrix user IDs, no wildcards, plus the pairing owner placeholder.
A field set under `rooms.<key>` replaces the default, and a list replaces the whole default list instead of merging with it, so an empty list removes the inherited invitations or admins for that room.
MindRoom logs `Administrators, room invitees, or room admins are on another homeserver; remove them unless you trust them` when any of these users is on a different homeserver.

MindRoom applies this policy to new and existing managed rooms on startup and on every config reload:

- `invite_users` is the desired invitation list, so a listed user who leaves or is kicked is invited again; remove the user from configuration before intentionally removing access.
- `admins` grants missing admin power but never demotes anyone, so removing a user from `admins` only stops future grants; lowering an existing admin requires a Matrix account with higher power.
- `listed: true` requires the router to have moderator or admin power in the room.
- `encrypted: true` turns on encryption in existing rooms, and setting it back to `false` never disables it.

## Room Management

The router creates and maintains managed rooms:

- Creates missing rooms with the alias `#<key>:<server>` (plus the `MINDROOM_NAMESPACE` suffix when set), a topic, and a [managed avatar](https://docs.mindroom.chat/matrix/#managed-avatars).
- Uses the room's `description` as its topic, and generates a topic with the `default` model only when the description is empty.
- Invites the configured agents and teams and the room's `invite_users`.
- Sets the room name from `display_name`, and adds a topic only when the room has none, so changing `description` never replaces an existing topic.
- Lets every room member write thread tags and call membership state, which requires the router to be joined and able to edit power levels.
- Applies the [room policy](#room-policy) above.

Agents and teams leave rooms they are no longer configured for, except direct-message rooms and rooms they joined by accepting an invitation.

On a shared homeserver, any account can create or repoint an alias such as `#lobby:<server>`, so MindRoom only uses an aliased room it recorded itself or one the router created and aliased.
Otherwise it logs `managed_alias_target_refused`, never joins that room, and creates its own room without the alias.
A recorded room stays in use even when its alias now points elsewhere, which logs `managed_alias_points_elsewhere`.
If the alias of a recorded room is deleted, the router restores it, and creates a replacement room only when it can no longer read or is no longer joined to the recorded room.

## Room Cleanup

On startup, MindRoom removes Matrix accounts of agents and teams that were deleted from `config.yaml` from the rooms they had joined.
Direct-message rooms are left untouched, and no manual cleanup is needed.

## Matrix Space

MindRoom creates and maintains a root Matrix Space that contains every managed room, so users can find all MindRoom rooms in one place in their Matrix client.



```yaml
matrix_space:
  enabled: true
  name: MindRoom
```

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | bool | `true` | Create and maintain the root Space |
| `name` | string | `"MindRoom"` | Space display name; cannot be empty |

Rooms added later are linked into the Space automatically, and the name follows the configured value on every startup.
Every user in any managed room's `invite_users` is invited to the Space, without Space admin power.
Room `admins`, `administrators`, and other access settings do not cause Space invitations.
MindRoom never removes existing Space admins, and adding, removing, or reordering Space children in a Matrix client requires sufficient power in the Space; manage those permissions in a Matrix client.
Setting `enabled: false` stops maintaining the Space but does not delete it or remove its child links.
The Space avatar is described in [Managed Avatars](https://docs.mindroom.chat/matrix/#managed-avatars).

# Personal Rooms

The optional `personal_rooms` section gives each eligible human a private, unlisted room with one selected agent, created when they join an onboarding room such as a lobby or send a configured command there.

## Setup

```yaml
rooms:
  lobby: {}
agents:
  helper:
    display_name: Helper
    rooms: []
    access:
      members_of_rooms: [lobby]
personal_rooms:
  agent: helper
  onboarding_rooms: [lobby]
  commands: ["!personal"]
  welcome_dispatch: true
```

The router watches the onboarding rooms, so the selected agent does not need to join them.
To give each user their own agent instead of one shared agent, see [Personal Agents](https://docs.mindroom.chat/personal-agents/).
The human must still be allowed to use the agent under its access rules; listing the onboarding rooms in `access.members_of_rooms`, as above, grants that (see [Responder access](https://docs.mindroom.chat/authorization/#responder-access)).

## Configuration

`personal_rooms` is an object, or `null` (the default) to disable onboarding.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `agent` | string | Required | Configured agent, or a [`personal_agents`](https://docs.mindroom.chat/personal-agents/) template, that owns and answers in personal rooms; a template gives each user their own agent. |
| `onboarding_rooms` | list of strings | Required | Nonempty list of configured rooms whose human joins trigger onboarding. |
| `commands` | list of strings | `[]` | Exact self-onboarding commands accepted in onboarding rooms; each starts with `!` and contains no whitespace or arguments. |
| `alias_prefix` | string | `"personal"` | Room alias prefix matching `[a-z0-9_-]{1,40}`, followed by a hash of the user ID and the installation namespace. |
| `name` | string | `"Personal room for {user}"` | New room name template, up to 1,000 characters. |
| `topic` | string | `"Private conversation with {agent}."` | New room topic template, up to 1,000 characters. |
| `welcome` | string | `"Welcome {user}! This is your personal room with {agent}."` | Welcome message template, up to 10,000 characters; empty disables the welcome. |
| `welcome_dispatch` | boolean | `false` | Send the welcome to the agent as a trusted message on the human's behalf once the human joins, so the agent starts the conversation; otherwise post it as a notice right away. |
| `confirmation` | string | `""` | Optional one-time notice in the onboarding room where the human was onboarded, visible to everyone there, up to 10,000 characters. |
| `backfill` | boolean | `false` | Also onboard current eligible members of the onboarding rooms on startup and config reload. |
| `auto_join_requester` | boolean | `false` | Join the human into a newly created personal room instead of only inviting them. |
| `requester_admin` | boolean | `false` | Grant the human Matrix room admin; when `false`, no admin is granted and existing power levels, including admin granted earlier, stay unchanged. |
| `avatar` | string or `null` | `null` | Room avatar file, relative to the configuration file; takes priority over `avatar_from_requester`. |
| `avatar_from_requester` | boolean | `false` | Copy the human's profile avatar into the room when `avatar` is not set. |

Templates accept only `{user}` (full Matrix user ID), `{room}` (room alias), and `{agent}` (agent display name); any other placeholder fails config validation.
Avatars are set only on rooms that have none.
The welcome is sent once, and changing `welcome` or `welcome_dispatch` later does not change a welcome that is already pending.

`auto_join_requester: true` requires the router's Matrix account to be allowed to use the homeserver's Synapse-compatible admin join API.
It applies only when MindRoom creates the room, and never joins someone who already joined, left, or was banned.

## Onboarding Behavior

- Joining an onboarding room, or sending one of the `commands` there, onboards only that human.
- MindRoom's own router, agent, and team accounts, accounts listed in `bot_accounts`, and the internal `mindroom_user` account never get personal rooms.
- A human who leaves or is banned from their personal room is not re-added by restarts or backfill.
  Leaving and rejoining an onboarding room re-invites them to their existing personal room, except after a ban or for an adopted room.
- In a personal room MindRoom created, the owner may invite other MindRoom agents, and an invited agent sees only messages sent after its invite.
  Anyone else invited into such a room is removed with a notice explaining that the room is private.
  Adopted rooms instead allow only the participants listed in their record, and any other MindRoom agent invited there is removed the same way, because an adopted room may share its earlier history.
- Personal rooms survive restarts and room cleanup, even after `personal_rooms` is removed.
  Disabling the feature stops new onboarding but does not remove existing rooms or access.

## Troubleshooting Personal Rooms

An onboarding failure affects only that human, and the onboarding room keeps working for everyone else.
Failed onboarding is retried automatically, first after 30 seconds and then at doubling intervals up to one hour, and a config reload retries it immediately.
Onboarding blocked by an adopted room's unlisted members is also retried as soon as that room's membership changes.
A personal room that fails its ownership or membership checks keeps retrying only while its reconciliation is still failing; otherwise it is checked again on restart or config reload.
A join or command that arrives before the personal-room agent connects is handled once it does.
Revoking the human's access to the agent, or disabling `personal_rooms`, cancels a welcome that has not been sent yet.

| Log message | Meaning |
| --- | --- |
| `Personal-room validation failed for an onboarding trigger` | An existing personal room failed ownership or membership checks, for example because another account already holds the alias, or a guest the agent cannot remove was raised to the agent's power level. |
| `Personal-room imported roster has unattested members` | An adopted room contains someone other than a MindRoom agent that its record does not list; the warning names every unlisted member, and retries repeat it only when those members change. MindRoom does not change the room, and that human's onboarding, such as the invite, admin grant, and welcome, waits until the room holds only participants its record lists. |
| `Personal-room validation failed for an owner membership event` | The owner joined or changed their profile in a personal room that fails these checks; the agent keeps answering there. A room whose reconciliation is still failing keeps retrying up to hourly; otherwise the room is checked again on restart or config reload. |
| `Personal-room onboarding trigger failed` | Any other failure, such as an invite refused by the human's homeserver. |
| `Personal-room reconciliation failed` | An automatic retry failed; the log shows the attempt number and the delay before the next try. |

## Adopting an Existing Room

Operators can turn an existing room into someone's personal room, keeping its room ID and history.

1. Stop the runtime.
2. Have the selected agent send an `org.mindroom.personal_room` state event in the room with an empty state key and exactly `{"user_id": "<requester>", "agent_user_id": "<agent>"}` as content.
3. Make sure the agent is joined with room admin power, the room is not published in the room directory, and its join rule is invite-only.
4. Write the record with `personal_room_record_path(runtime_paths, agent_name, user_id)` and `write_personal_room(path, record)` from `mindroom.matrix.personal_room_store`.

For a room with shared history and one existing guest, a record looks like this:

```json
{
  "user_id": "@alice:example.test",
  "alias": "#personal-alice:example.test",
  "source_room_id": "!lobby:example.test",
  "room_id": "!existing:example.test",
  "welcome_completed": true,
  "adoption": {
    "creator_user_id": "@router:example.test",
    "agent_user_id": "@helper:example.test",
    "router_user_id": "@router:example.test",
    "expected_history_visibility": "shared",
    "additional_user_ids": ["@guest:example.test"]
  }
}
```

- `room_id` is required; an alias alone never adopts a room.
- `source_room_id` is the onboarding room, and the requester must still be a member of it and allowed to use the agent.
- `creator_user_id` must match the room's creator, and `agent_user_id` must match the selected agent's Matrix ID.
- `router_user_id` (optional) must be this installation's router account and lets it stay in the room, including during room cleanup after onboarding is disabled.
- `expected_history_visibility` is `invited` (default), `joined`, or `shared` and must match the room's current setting; `world_readable` rooms cannot be adopted.
- `additional_user_ids` (default `[]`) lists the only other participants allowed to stay joined, invited, or knocking; it does not invite anyone or grant power.
- Set `welcome_completed: true` to skip the welcome; otherwise MindRoom sends the configured welcome.

If the room's history setting stops matching the record, or anyone other than a MindRoom agent it does not list joins, is invited, or knocks, onboarding for that human fails with one of the log messages above until someone fixes the room.
MindRoom does not change an adopted room's name, topic, or history setting, and removes only MindRoom agents the record does not list.
It invites the requester if they are not in the room yet and applies the configured `requester_admin`, avatar, and welcome settings.

# Personal Agents

A [`private`](https://docs.mindroom.chat/configuration/agents/#private-instances) agent keeps one copy of its state for each requester, but everyone still talks to a single Matrix account, so nobody can address "Bob's agent", add it to a team, or delegate to it.
The optional `personal_agents` section gives each person onboarded through [`personal_rooms`](https://docs.mindroom.chat/personal-rooms/) their own agent with its own Matrix account, all built from one template whose defaults you control.
Use `private` when one shared agent with separate state per requester is enough, and `personal_agents` when each person needs an agent others can mention or put in a team.
A generated agent is a personal agent in the sense of the [egress broker](https://docs.mindroom.chat/deployment/egress-broker/) and [MCP gateway](https://docs.mindroom.chat/deployment/mcp-gateway/) pages only when its worker scope, from the template's `agent` or `defaults.worker_scope`, is `user` or `user_agent`.

## Setup

```yaml
rooms:
  lobby: {}
personal_agents:
  mind:
    name: "{username}_mind"
    matrix_username: "{username}.mind"
    agent:
      display_name: "{user_display_name}'s Mind"
      role: Personal assistant for {user}.
      model: sonnet
      memory_backend: file
    users:
      "@alice:example.org":
        model: opus
personal_rooms:
  agent: mind
  onboarding_rooms: [lobby]
```

`mind` is the template key, never an agent itself, and `personal_rooms.agent: mind` gives each user's personal room that user's own agent.
Because `access` is omitted, only the owner and platform administrators may use each generated agent.

## Configuration

`personal_agents` maps template keys to objects.
A key follows the [agent naming rules](https://docs.mindroom.chat/configuration/agents/#naming-rules) and does not equal an agent or team key.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `name` | string | `"<template key>_{username}"` | Agent key pattern; it contains `{username}` exactly once and renders to letters, digits, and underscores. |
| `matrix_username` | string | `mindroom_<agent key>` | Matrix localpart pattern; it contains `{username}` exactly once plus at least one other character, so no agent takes a person's bare localpart, and renders to `[a-z0-9._=/-]`. `_<MINDROOM_NAMESPACE>` is appended when a namespace is set, and the default account carries the same suffix. |
| `agent` | object | Required | Any [agent](https://docs.mindroom.chat/configuration/agents/#configuration-options) fields; the defaults for every user's agent. |
| `users` | object | `{}` | Per-user overrides keyed by concrete Matrix user ID, with no patterns. Each override is an agent field that replaces the template's value whole, so a list or object is not merged. |

In the template's `delegate_to`, the template key names each user's own agent, so `delegate_to: [mind]` lets every user's agent run fresh copies of itself as [subagents](https://docs.mindroom.chat/tools/agent-orchestration/#agent-delegation).

Placeholders are replaced as exact tokens, so other braces in prompts stay literal and a replaced value is never replaced again.

| Placeholder | Value | Where |
| --- | --- | --- |
| `{user}` | The user's full Matrix ID. | `display_name`, `role`, and each `instructions` item |
| `{username}` | The localpart lowercased, every character outside `[a-z0-9_]` replaced by `_`, cut to 40 characters, or `user` when empty. | `display_name`, `role`, each `instructions` item, `name`, and `matrix_username`; in the last two it is the only placeholder |
| `{user_display_name}` | The user's Matrix display name at onboarding, without control characters, cut to 100 characters, or the localpart when unset. | `display_name`, `role`, and each `instructions` item |

If the rendered `name` or Matrix account is already taken, `{username}` becomes the slug plus `_` and the first 6 hex characters of the SHA-256 of the user ID, for example `alice_86f01a` for `@alice:example.org`.
Taken means an authored agent, team, or template key, including one saved but not yet reloaded, `router`, `user`, `_shared`, another generated agent, or an account MindRoom already manages.
A user on another server than MindRoom's always gets this hashed form, so nobody elsewhere can take a local person's plain name.
A second clash fails that user's onboarding.

Every generated agent also gets these settings, whatever the template says:

- The owner is added to `access.users` and `credential_managers` when missing.
- `access.members_of_rooms` is `[]` when missing or null, so membership of the agent's rooms grants nothing unless you list rooms yourself.
- `allow_self_config` is `false`, even when `defaults.allow_self_config` is true, because `self_config` edits `agents.<name>`, which a generated agent does not have, so owners use `update_agent` instead.

Config validation rejects `private` and `allow_self_config: true` in the template's `agent` and in any `users` override, because a personal agent already belongs to one user.
A template or override that would produce an invalid agent fails validation as soon as you save or load it, even before anyone has onboarded, with `personal_agents would generate an invalid agent for a new user:` and the reason.

## Generated Agents

Generated agents are ordinary agents in the running config, and workers receive them too.
They never appear in the saved config file, and the dashboard and `config_manager` edit only what you authored.

- Editing the template changes every user's agent at the next reload.
  The name, Matrix account, and `{user_display_name}` stay fixed at what onboarding stored.
  A field its owner changed with [`update_agent`](#updating-a-generated-agent) keeps the owner's value until the owner restores it.
- An authored agent or team that uses a generated agent's name fails validation with an error such as `agents.mind_alice collides with the agent generated from personal_agents.mind for @alice:example.org`.
  The same error rejects a dashboard or `config_manager` save that edits a generated agent in place, so use `personal_agents.<key>.users` for per-user changes.
- A team member or `delegate_to` entry can name a generated agent only once it exists, because validation rejects an unknown agent name before then.
- Removing a template stops its agents but keeps their roster entries and Matrix accounts, and bringing the template back with the same key starts them again.

## Letting Agents Talk to Each Other

By default nobody except the owner and administrators can use a generated agent, so agents do not reach each other.
To let them, set `access` under the template's `agent:`, for example `members_of_rooms: [lobby]` or `current_room_members: true`.
With `members_of_rooms: [lobby]` and the lobby as the onboarding room, every onboarded user can use every other user's agent.

> [!WARNING]
> **Whoever `access` admits works with the owner's agent.**
> Anyone the template's `access` admits can work with the owner's agent, including its memory, its files, and its shared credentials.
> That includes the admitted person's own agent acting for them, because an agent mentioned in a reply acts for the person who requested that reply (see [Agents mentioning other agents](https://docs.mindroom.chat/authorization/#agents-mentioning-other-agents)).
> Admit only people you would trust with everything an owner gives their agent.

## Voice Calls

Name the template key in [`calls.agents`](https://docs.mindroom.chat/voice-calls/#calls-configuration) to let every user call their own agent:

```yaml
calls:
  enabled: true
  profiles:
    openai-live:
      backend: live
      model: gpt-live-1
      credentials_service: openai-live
      voice: marin
  agents:
    mind: openai-live
```

Each generated agent then uses that profile in its owner's personal room, including agents of people onboarded after the config was loaded.
The saved config keeps `mind`, not the generated agent names.
A generated agent that `calls.agents` also names directly keeps the profile given there.
Neither the template's `agent` nor its `users` overrides may list `rooms`, because a room takes only one calls-enabled agent; config loading fails with `calls.agents names personal_agents.mind, whose agent lists rooms in the template or a user's override`.

## Onboarding Behavior

The router handles a join, a `commands` entry, or a `backfill` hit in an onboarding room like any [personal-room](https://docs.mindroom.chat/personal-rooms/#onboarding-behavior) trigger, with these differences:

1. MindRoom registers the agent's Matrix account and starts the agent without a restart; a failed registration leaves no agent behind.
2. The usual personal-room flow runs with that user's own agent once the agent has finished its first sync.
   Until then the trigger is pending, which does not hold up the onboarding room for anyone else, and the router logs `Personal agent is starting`.
3. The user keeps the agent, its account, and its room when they leave the onboarding room or are banned, and MindRoom has no command that removes them.

A [bridge alias](https://docs.mindroom.chat/authorization/#bridge-aliases) onboards onto its person's agent, which gives the alias account its own personal room.
A Matrix ID containing a space, `*`, `?`, `[`, or `]` gets no agent, because access settings cannot name it exactly.

The roster of generated agents is `tracking/personal_agents.json` in the storage directory; back it up with the storage, because an agent's name and Matrix account never change once recorded there.
It also holds the agents that agents [create](#created-agents-and-self-updates) and every owner's edits.
Stop MindRoom before editing it by hand; a file MindRoom cannot read fails config loading with `Personal agent roster <path> is invalid:` and the reason.

> [!WARNING]
> **Everyone who joins an onboarding room gets an agent on your model keys.**
> Every human who joins an onboarding room, sends one of its `commands`, or is picked up by `backfill` gets a Matrix account and a running agent, and nothing caps how many.
> The only gate is membership of the onboarding room, so control it through that room's [`join_policy`](https://docs.mindroom.chat/rooms/#room-policy) and invitations.
> An open or federated lobby gives an agent to anyone who joins it, and `backfill: true` creates agents for every current member at once.
> Bridged puppet accounts count as people unless you list them in [`bot_accounts`](https://docs.mindroom.chat/authorization/#bot-accounts).

MindRoom's own accounts and the internal `mindroom_user` never get an agent.

The dashboard's Settings page edits `personal_agents` next to Personal rooms.
Its `personal_rooms.agent` dropdown lists the configured agents and the `personal_agents` templates.
The Agents page lists every agent generated from a template, read-only, with its owner, Matrix ID, template, and whether it is active, pending, or stopped because its template was removed.
Agents that agents [create](#created-agents-and-self-updates) do not appear there.

## Switching From a Single Personal-Room Agent

When `personal_rooms.agent` changes from an agent to a template, existing personal rooms stay with the old agent, and a user's new agent cannot take over a room the old agent created.
Set a new `personal_rooms.alias_prefix` at the same time so that each user gets a new personal room with their own agent; otherwise their onboarding fails with `Personal-room alias ... which this agent does not own`.
The old agent's `access` no longer limits who gets an agent: everyone in an onboarding room gets one.

## Created Agents and Self-Updates

A template gives every person the same agent.
A person cannot narrow its tools or reword its role, and cannot get a second agent with a narrower job unless an administrator edits `config.yaml` for them.
The [`agent_manager`](https://docs.mindroom.chat/tools/agent-orchestration/#agent_manager) tool covers both, and MindRoom adds it by itself:

- Every generated agent, personal or created, can show and change its own `display_name`, `role`, `instructions`, and `tools`, and set its avatar, for its owners.
- An agent with `created_agents_per_owner` above 0 can create new agents for the people who ask it.

Every call that creates, changes, or deletes an agent waits for the person asking to approve it on a card; see [Approval Cards](#approval-cards).

### Updating a Generated Agent

Ask the agent to change itself, for example to answer more briefly or to drop a tool.
It calls `agent_settings` to read its current values and the tool names it may choose from, then `update_agent` with only the fields to change.

- Each value replaces the whole field, so send a changed `instructions` list in full.
- `null` restores the template's value, or for a created agent its creator's value.
- `tools` are toolkit names such as `calculator`, never function names, chosen from the `allowed_tools` that `agent_settings` returns, which are the tools of the template or creator.
  A chosen tool keeps the settings it has there, and so does each tool it brings along, such as `attachments` with `matrix_message`; any other tool that was not chosen is left out, even one from `defaults.tools`.
- A change that the next config load would reject is refused with `The agent would not load with this change, so nothing was stored: <reason>`, and so is a new agent from `create_agent`.
- A field with no edit follows its template or creator, so a later change to either reaches it.

To change the agent's picture, attach a PNG, JPEG, GIF, or WebP image in a conversation with the agent and ask it to use the image as its avatar.
It calls `set_avatar`, which crops the image to its center square, and the new picture applies without a reload.
An animated image keeps only its first frame.

Only the agent's owners may ask, and anyone else is refused with `Only <owners> may change <agent>`.
A personal agent has one owner, the person it was made for, and cannot be deleted.

### Letting an Agent Create Agents

Set `created_agents_per_owner` on the agent that people may ask for helpers.
It applies to that agent only, never through `defaults`, and under a template's `agent:` it lets each person's agent create agents.

```yaml
agents:
  mind:
    display_name: Mind

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.