Architecture
For contributors: how the pieces fit together.
Crates #
| Crate | What it is |
|---|---|
crates/mimir-core |
Everything that isn't a UI shell: the database, bots, providers, tools, channels, routines, meetings |
crates/mimir |
The CLI and the self-hosted server (server.rs: HTTP API + web UI) |
crates/mimir-desktop |
The Tauri desktop app: window, tray, keychain, quick ask, notifications, meeting recorder |
ui/ |
React + Vite UI, shared by the desktop app and the server's web UI |
site/ |
The heymimir.app website (Vite, three.js WebGPU), including /docs/ built from docs/*.md: cd site && npm install && npm run dev; npm run build writes site/dist |
mimir-core modules #
| Module | Does |
|---|---|
db |
SQLite (one file): memories with FTS5 search and versions, bots, chats, messages (FTS5 too, for chat_search), routines, runs, tool calls, approvals, rules, suggestions, skills (versions, uses, proposals by kind), meetings |
host |
Host::call(cmd, args): every command the UI sends; starts and stops each bot's runtime; the secret store |
runtime |
Per-bot channel listeners (Telegram, Slack, Discord), the 30 s routine scheduler, approval cards, the daily digest |
telegram, slack, discord |
The messaging apps (outbound connections, streaming replies) |
provider |
AI accounts: starts claude -p / codex exec / grok -p, or calls an OpenAI-compatible API; streams replies |
lib.rs (turn) |
One run: builds the instructions (role, standing memory, skills), calls the provider, records the run; /skill commands (skill_command) |
tools |
Tool definitions, the allow / ask / deny gate, taint, plans, and the built-in tools' code |
mcp |
The MCP server the CLIs start for each run (<app> mcp --bot B --chat C --run R, plus --read-only for quiet checks, --no-tools, --depth for hand-offs), exposing Mimir's tools |
ext |
Tool servers (MCP clients over stdio and Streamable HTTP), the browser, saved logins |
tidy |
The daily memory tidy-up (on tidy_model if set): corrections, skills from good runs, fixes for skills that went wrong, weekly upkeep (retire / merge / move to background), the weekly review; teaching a skill from a source or a SKILL.md |
computer |
The bots' computer (Xvfb, xdotool, scrot, Chromium), self-hosted Linux only; recording a showing for teach by showing (page details through Chromium's DevTools port) |
transcribe |
Speech to text (whisper.cpp built in, or whisper-cli, or an API), voice activity, speaker separation |
calendar |
iCal links: parsing, recurrence (rrule), the meeting on now, events between dates (calendar_list) |
email |
IMAP (async-imap, mail-parser): search, read, drafts by APPEND, all EXAMINE/PEEK; SMTP sending (lettre); every operation within 60 s |
files |
The per-chat file workspace: path checks, versions under .history/, outside-content marks keyed by canonical name and modification time |
library |
Mimir's skill library: the SKILL.md files in crates/mimir-core/skills/, compiled in, with what each needs and a suggested schedule |
fmt, live, openrouter |
Message formatting per app, "what it's doing now", OpenRouter sign-in |
How a message becomes a reply #
- A channel listener (or the UI's
send_local) receives a message for a bot and chat. turnbuilds the instructions: the bot's role, standing memory, accepted skills, the conversation.providerruns the bot's AI account. For the CLIs, it passes MCP arguments so the CLI starts<app> mcp …and sees Mimir's tools; for APIs, tools are function definitions handled in-process.- Each tool call goes through
tools::call: the allow / ask / deny rule, taint (outside content read in this conversation), the bot's allowlist, then the tool itself. "Ask" creates an approval and waits for your answer from any app. - The reply streams back to where you asked; the run and every tool call are recorded.
Desktop and server #
Both run the same Host. The desktop calls it in-process through one Tauri command (api); the server exposes it as
POST /api/<cmd> with a bearer token. The UI's invoke() goes to whichever is active, so the same React app is the
desktop UI and the web UI. A few commands are handled by the desktop itself, before the host, because they belong to
this computer: meeting (recording, the meetings list), keep_awake, quick_shortcut, selftest.
Meetings #
mimir-desktop/src/meeting.rs records (cpal for the microphone; a Core Audio process tap on macOS 14.4+, else
ScreenCaptureKit / WASAPI loopback / parec for the computer's sound), chunks at pauses, and calls transcribe (whisper.cpp through whisper-rs, with Silero VAD run
separately because whisper-rs's per-state call skips whisper.cpp's own). After Stop, speaker separation runs through
sherpa-rs (feature diarize, linked in, macOS and Windows) or, on Linux, sherpa-onnx's shared libraries downloaded on
first use and loaded at runtime (feature diarize-dl: transcribe::sherpa_dl, structs mirroring its C API). Call detection (mic_in_use) is per OS. See
Meetings.
Build notes:
- whisper.cpp builds with cmake (and libclang on Linux).
.cargo/config.tomlsetsGGML_NATIVE=OFF(+ AVX2 on x86) so builds run on any CPU of the platform.- The desktop
build.rsadds the/usr/lib/swiftrpath that ScreenCaptureKit's Swift bridge needs on macOS. - Speech models must be freed before exit (
transcribe::unload), or Metal aborts.
Learning #
Bots can't be fine-tuned (they run on your own subscription), so they improve only through memory and skills, and
everything learned is a proposal you accept, with its provenance recorded. Signals are objective (corrections,
stops, denials, failed actions), never the model judging its own success. Nothing learned can change permissions,
tool grants, saved logins or schedules. See Memory for the user-facing side, and reports/ (local, not
published) for the research behind it.
Security rules in the code #
The Security model in terms of where it lives. Keep these when changing the code.
- Outside content.
ToolCtx::taintmarks the run (runs.tainted) and the conversation (conv_taint).is_taintedalso re-reads the run, so the tools server sees what the parent process marked.- What carries the mark:
routines.tainted,approvals.tainted(restored byrun_parked),suggestions.tainted,digest_items.tainted, andDb::recent_outsidefor replayed history (only after thefresh:{bot}:{chat}marker). - A file's or forward's text is wrapped by
with_document(its position math uses ASCII lowercasing).
- The gate.
tools::call:precheck(limits, before any card), thengate, thenleaks_out(data-carrying reads), then the approval.card_textbuilds every card.site_of/ext::host_allowedparse hosts withurl::Urland refuse user info.
- Learning.
Db::bootstrapand automatic recall skip unconfirmed memories, andEntry::lineflattens text.- The tidy-up sees unconfirmed memories by title only, and labels outside lines.
suggestion_decidecarries the mark into accepted entries.
- Channels.
Core::sender_ok: only the person who paired a chat (chats.owner;chats.dmfor direct chats).- Every Telegram, Slack and Discord entry point passes a
Sender. - The listener loop recovers from panics.
- Processes.
- The AI CLIs get instructions through private files (
Private), never the command line: Claude--append-system-prompt-file, Codexmodel_instructions_file, Grok a rules file in a trusted per-bot-and-chat folder (grok_trust,grok_rules; Windows keeps--rules). - PDFs are read by
<binary> read-pdf(pdf_helper_main). - The AI CLIs' transcripts are removed by
Provider::forget_session. Host::newdoes startup cleanup (incognito_leftovers,prune_old,tmp/, old media). Never put that inDb::open: the tools server opens the database on every turn.
- The AI CLIs get instructions through private files (
- Downloads.
transcribe::download(url, path, min, sha256)with pinned hashes. Models come from a fixed Hugging Face revision.
Data #
One SQLite file in the data folder (~/.mimir, or MIMIR_HOME). Schema changes are additive (add_col in
Db::open), so an older database opens in a newer build. Installs from before the rename to Mimir (~/.botpoc,
botpoc.db) are moved over once on startup.
Environment variables #
| Variable | |
|---|---|
MIMIR_HOME |
Data folder (default ~/.mimir) |
MIMIR_PROVIDER, MIMIR_MODEL |
CLI bot: claude (default), codex, grok, openrouter, xai, local, openai; model override |
OPENROUTER_API_KEY, XAI_API_KEY, OPENAI_API_KEY, OPENAI_BASE_URL |
Keys for the CLI bot; OPENAI_BASE_URL also points local elsewhere (LM Studio: http://localhost:1234/v1) |
TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN + SLACK_APP_TOKEN, DISCORD_BOT_TOKEN |
Messaging apps for mimir serve |
MIMIR_BOT |
Which bot the CLI acts as (default 1) |
MIMIR_ADMIN_TOKEN |
Server: a fixed admin token, at least 24 characters (else one is generated on first run) |
MIMIR_UI_DIR |
Server: the web UI folder (default ui/dist or /app/ui) |
MIMIR_COMPUTER, MIMIR_COMPUTER_TOKEN |
Server: the bots' computer as a separate service (computer:7300, mimir computer) and the control channel's secret (24+ characters, on both sides) |
MIMIR_TELEGRAM_API |
Telegram Bot API base URL (a self-hosted Bot API server, or the test mock) |
MIMIR_CLAUDE_BIN, MIMIR_CODEX_BIN, MIMIR_GROK_BIN, MIMIR_NPX |
Paths to the CLIs, if not on PATH |
Releases #
.github/workflows/release.yml builds the installers when a version tag is pushed:
- Set
versionincrates/mimir-desktop/tauri.conf.json(the tag must match it; the workflow checks). git tag v0.1.0 && git push origin v0.1.0.- The workflow drafts a GitHub release and attaches the
.dmg,.exe/.msi,.deb/.rpmand.AppImage(macOS on Apple silicon; Linux built on Ubuntu 22.04 so it runs on older systems). - Read the draft over, paste the "Unreleased" section of
CHANGELOG.mdinto its notes (installed apps show them with the update), rename that section to the version, and publish.
The Mac app is ad-hoc signed (signingIdentity: "-"), which Apple silicon needs to open a downloaded app at all;
it isn't notarized, and Windows builds aren't signed.
Updates (crates/mimir-desktop/src/update.rs, Tauri's updater plugin). Apps read latest.json from the newest
published release (plugins.updater.endpoints in tauri.conf.json; drafts aren't served), so publishing the draft is
what ships an update. With the TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD secrets set, each
build signs its update files (.app.tar.gz, the installers) and a last job writes latest.json from those signatures;
without them, releases still build but installed apps don't see them. Apps refuse anything not signed by the private
key whose public half is in tauri.conf.json: losing that key means users must reinstall by hand once, so keep a copy
somewhere safe. End-to-end test on a Mac: build 0.1.1 signed and 0.1.0 pointed at a local server
(--config '{"plugins":{"updater":{"endpoints":["http://127.0.0.1:8899/latest.json"],"dangerousInsecureTransportProtocol":true}}}'),
serve the first, and start the second with MIMIR_UPDATE_TEST=1: it installs and restarts as 0.1.1.
Tests #
cargo test --workspace # unit and integration tests (needs cmake for the desktop crate)
cd ui && npx tsc -b # the UI typecheck (`tsc -p .` checks nothing here)
Test hooks and opt-in tests:
MIMIR_SELFTEST=1 <desktop binary>: starts the real app, reports what rendered, exits 0 only without UI errors (CI runs this on all three OSes).MIMIR_MEETING_TEST=<seconds>(+MIMIR_MEETING_TEST_QUIT=1): records for that long, then stops (or quits) and prints what was saved. With PulseAudio null sinks in a container, this tests the whole meeting pipeline on Linux.MIMIR_WHISPER_DEBUG=1: prints voice-activity stretches and chunk timings.scripts/mock-telegram.py: a fake Telegram Bot API; setMIMIR_TELEGRAM_APIto it.scripts/learning-eval.py <mimir binary> [model] [--control] [--web]: does learning work end to end? A simulated person with three hidden preferences talks to a real bot for three "days", correcting it; the tidy-up runs between days. Prints how many replies broke a preference each day (with learning it should drop to about zero by day 2).--controllearns nothing, for comparison;--webforces a web search on day 1 (learning must survive it).MIMIR_UPDATE_TEST=1: checks for an update after 3 s and installs what it finds (the update test, see Releases).- Ignored tests that need real models or files (run with
-- --ignored):builtin_real,diarize_real,speakers_real,calendar_real,mic_probe. Each says which variables to set.