Muninn Features
Source-of-truth feature inventory for Coabai Muninn. Tracks what is shipped in main at the latest tag. Roadmap items live in the issue tracker and per-version project_muninn_* notes — not here.
Current baseline:
v0.12.0shipped + deployed (2026-05-30). Tip-of-mainhas run well ahead of the last tag (schema v31). On top ofv0.12.0it adds: themusictool, conversation replay capture +muninn replay, the reflective "dream" pass; DeepSeek as the primary brain with a fallback brain + round-robin peer pools; presence / co-location with a family map (people layer + custom pins); open-house voice mode; thecalendar/contacts/youtube/researchtools;subtitles(Bazarr),mastodon,photos,geocode/routemaps tools,media_request/music_request(Jellyseerr / DJ request),code_task(delegate coding to Huginn over NATS),scratchpad(per-task working memory),identity/workspace_edit/self_restart; one-time (run_once) schedules; an Alertmanager → Matrix relay with generic auto-remediation; and a failure-learning loop (run-outcome ledger + nightly review → constraint memories).
Legend: [x] shipped · [?] partially shipped or behind a feature flag · [ ] planned (kept out of this file).
Transports
- Matrix — first-class transport. Olm/Megolm E2EE, cross-signing, device verification, sync loop, allowed-user gate.
- TUI — terminal chat surface (
muninn-tuilauncher). Separate config + DB, peer-reads the daemon's Matrix DB read-only so memories are shared. - Web — third surface inside the daemon. Axum HTTP, OIDC bearer auth (any issuer with a
userinfoendpoint), default bind127.0.0.1:7792. Also serves the family map JSON endpoints (see Presence & Family Map):/map/people(live GeoJSON people layer) and/map/pins(GET list / POST create / DELETE), both behind the samepresence_map_allowed_usersallowlist via a sharedauthorize_map()gate — used by a self-hosted server. - Voice (M2) — fourth surface. Axum HTTP
POST /api/voice/eventfrom voice-hub; resolvespyannote_id → user_idanddevice_id → roomvia the M1a/b registries; optional shared-secret auth; reply path TTS-routes via the nativetts::TtsRouter(HAtts.speak,[tools.tts]) or the standalone voice-server/speakfallback, using the device'smedia_player. Default bind127.0.0.1:7793. - Embedded docs server — static HTML in
docs/served by axum on:7790(daemon) /:7791(TUI). Edit + refresh, no recompile. - Pluggable transport trait —
Transport+IncomingMessageinsrc/transport/mod.rs; new transports drop in alongside Matrix. - Session persistence —
data/session.jsonfordevice_id+access_token;data/matrix-store/SQLite crypto store. Restores on startup, falls back to fresh login on failure.
Agent Loop
- OpenAI-compatible LLM client (
src/llm.rs) —chat_with_tools()for the main loop,send_request_raw()for sub-agents,chat_with_system()for extraction. Primary brain in prod is DeepSeek (muninn.toml.local), Qwen stays a peer/fallback. - Fallback brain —
[llm] fallback_url/fallback_model/fallback_api_keyon a separate host: on a transport error or 5xx from the primary, the non-streaming path retries once on the fallback (5s connect-timeout so a dead primary fails over in seconds; read timeout stays unset so long generations aren't cut off). Logs the failover at WARN. Unset = no failover (back-compat). - Round-robin peer pools —
[llm] api_peers/fallback_peers: extra interchangeable endpoints (same model on another host) round-robined alongside the primary so conversation / tool / extraction load spreads across boxes; a dead peer is skipped. Classifier/fast clients drop the peers (different small models, own hosts). - Per-tool progress + thinking preface —
ChatResult::ToolCallscarries a preface; a "→" progress line is emitted per tool call (with the binary basename + subcommand for shell, privacy-safe), so the user sees what's happening before each tool fires. - Mid-task steering —
!stopplus natural-language steering (SteerMsg/SteerRegistry; a classifier routes steer-vs-new-message, fails open). Background tasks get content-derived names (gen_bg_name). - Single
agentrouter tool in main context (~150 tokens) — saves ~70% on tool definition tokens vs exposing all tools. - Sub-agent execution — ephemeral context, full tool registry, results returned to the main LLM as the
agenttool result. - Three-round tool loop — both main and sub-agent. Last round strips tool defs to force a text reply.
- Per-request sampling params —
temperature,top_p,top_k,presence_penalty,reasoning_budget(config-driven, model-specific). - Static system prompt — user/location facts baked in to preserve llama.cpp KV cache across the day.
-
reasoning_contentcapture — model thinking logged when the server emits it. - SSE streaming via
futures-utilfor incremental responses. - Markdown → HTML rendering via
pulldown-cmarkfor Matrix HTML message bodies. - Garbage guard — drops replies that are pure tool-syntax leaks, JSON fragments, or identifier dumps.
- Engagement-fishing strip — trims trailing "Want me to…" questions added by the LLM.
- Loop detector — short-circuits runaway tool patterns (same call repeated, etc.).
Tools (agent router + inner tools)
Sub-agent inner tools, registered in main.rs. The agent router is the only tool the main LLM sees; a few benign tools (e.g. geocode, route, photos) are also flattened onto the main LLM so it doesn't have to delegate for them. Each tool is registered only when its config block is present, so the live surface depends on the deployment. Page fetching is no longer a native tool — it moved to the muninn-fetch MCP server (see Misc), so web_fetch was removed.
Information & Web
-
weather— Environment Canada via HA REST API (~11 ms); falls back to Open-Meteo when HA is unavailable. -
web_search— SearXNG self-hosted, Tor-proxied (~1–3 s). -
geocode/route— (optional integration — needs[tools] maps_urlpointing at an OSM maps stack; absent → not registered) read-only wrappers over a self-hosted OSM maps stack (Nominatim/Photon + Valhalla behind Caddy).geocodedoes forward (place/address → coords) and reverse (coords → address) lookups via worldwide Photon.routereturns distance + time between two places/coords by car/bike/foot (resolves place-name endpoints through the geocoder first), pins units to km, and attaches a traced map snapshot — it decodes Valhalla's leg shapes, decimates to ≤150 points, and renders the line over the Protomaps basemap (tileserver-gl/render), attaching the PNG via the same media queue ascamera(best-effort; a render failure still returns the text answer). Tiles are planet-wide; routing covers North & Central America + Europe. Both are always-on (benign, likeweather) and flattened onto the main LLM.
Compute & Shell
-
calculate— Python expression eval, sandboxed subprocess (env_clear, stdin pipe, 10 s timeout). -
shell— Bash with two-gate safety: hardcoded fast-reject + LLM safety judge. -
code_run— Python code execution (TUI sandbox mode).
Filesystem
-
file_read/file_write/file_list— sandboxed FS access. -
project— project file enumeration.
Home Assistant
-
home_query— HA/api/conversation/processread side ("is the basement light on?"). Soft regex guard rejects action verbs. -
home_action— HA/api/conversation/processwrite side ("turn off the basement lights"). Split from legacyhomeat M1c so M1d's role filter can gate writes independently. -
camera— Frigate snapshot + the GPU server-vision description, parallel fetch of motion / person / face sensors, uploads JPEG to Matrix. -
music— Music Assistant control via HA. Play modes: by name; favorites+random ("play some music"); discover (random library minus favorites — "surprise me"); radio (radio_modesimilar-tracks station — "more like this / X radio"); genre (playlist search — "play some jazz / 90s music"). Plus pause/resume/stop/next/previous, volume (set/nudge/mute), shuffle, favorite/dislike (skip + remember), transfer, synced join/unjoin (multi-room), "pause everything", and status (track + volume). Room-aware targeting (config map → HA area → device → fuzzy); grouped-pause doesn't cascade; slow-device playback is backgrounded so the turn never hangs.[tools] music_assistant_config_entry_idenables search/library/genre. Full reference: MUSIC. - DJ mode (radio host) — (optional —
[tools.dj] enabled, off by default; needs Music Assistant + a configured TTS router) themusictool'sdjaction ("be a DJ for the basement" / "stop") flips radio-host mode on per player. A background worker polls each active player (~3s) and, ~5s into every new track, speaks a short on-air intro over the music — ducked via theTtsRouterannounce path, so the song dips under the voice and recovers (no mute/jump). The announcer persona is picked by the song's detected language; the patter vibe rotates each song (round-robin) so it never repeats. Personas + vibes live in an editable markdown file ([tools.dj] md_file, defaultdj.md), re-read per announcement so you retune without a rebuild (missing/unparseable → baked-in defaults). Optional web grounding (web_grounding, default on) runs a quick SearXNG search so the patter cites real artist/song facts instead of hallucinating (6s budget → fact-free fallback; inert withoutsearxng_url). The active-player set is persisted (JSON) so DJ mode survives a restart; a forgotten session auto-expires afteridle_timeout_secs(default 30 min) of non-playback. Config[tools.dj]:enabled,md_file,poll_secs,announce_after_secs,idle_timeout_secs,web_grounding. -
photos— (optional integration — needs[tools.photo_displays]+ Home Assistant + an Immich slideshow on a wall display; absent → not registered) show/hide an Immich photo slideshow on an esphome-android wall display. Resolves a place →[tools.photo_displays]prefix → HAswitch.<prefix>_show_photos(ON viaswitch.turn_on, OFF viaswitch.turn_off). Place defaults to the voicectx.room; asks if a typed command omits it. Flattened onto the main LLM. admin/adult/child. -
presence— (optional integration — needs[tools] presence_enabled+ Home Assistantdevice_trackers; off by default) "who and what is where". Thin wrapper over the sharedPresenceEngine(src/presence.rs): fuses configured users' HAdevice_trackers, auto-detects cars, and clusters co-located people (named zone, or haversine distance underpresence_colocate_meters). Enforces GPS freshness — a fix older thanpresence_stale_secsreads as "location unknown" rather than a ghost location. Answers "is anyone home?", "where is a family member?", "are Oz and Maria together?". Config[tools] presence_enabled. Allowed for admin/adult/child (read-only household presence is benign).
Media Generation
-
imagine— ComfyUI image gen (Flux 2 Klein 9B). Text-to-image and image-to-image. Async with placeholder image, swapped when generation lands. LLM caption held and attached. -
speak— native HAtts.speakviatts::TtsRouter([tools.tts]): speaker/voice resolution, Kokoro↔OmniVoice engine dispatch,notify.*phone targets,strip_noise+fix_pronunciation; standalone voice-server fallback when unconfigured. -
transcribe— STT. Default path is the HA STT REST API; when[tools] stt_modelis set it POSTs audio straight to an OpenAI-compatible/v1/audio/transcriptionsendpoint (speaches/Parakeet on the GPU server), skipping the HA + Wyoming hops and the entity-name fragility (multipart file+model+language; omits the auth header when the token is empty so the HA bearer never leaks).
-
email— IMAP read/search/flags + SMTP send over one or more mailboxes ([tools.email], default gateway a self-hosted server / Mailcowa self-hosted server). Actions:accounts,list(folder, unread-only),read(by uid, non-destructive PEEK),search(IMAP TEXT),send(to/cc/bcc),reply(threaded via In-Reply-To/References, optional reply-all),mark_read/mark_unread,archive,delete(Trash or permanent),folders,extract_events. Multi-account: one[[tools.email.accounts]]block per mailbox; calls pick thedefault(or named) account. Sent copies are IMAP-appended (best-effort).imap/lettreare blocking → run inspawn_blocking. Admin-only at the role gate; masterenabledtoggle gates registration. Reference: docs/references/odysseus/. -
email→extract_events— appointment mining, confirm-first: scans oneuidor the recent INBOX (unread_only/limit), runs a per-email LLM pass (date-grounded to Atlantic, mail treated as untrusted) →[{title, when, all_day, location, notes, confidence}], and returns a numbered proposal list. Never writes the calendar itself — the agent confirms with the user, then callscalendaraddper pick. Dedup (v28): each processed message is recorded inemail_calendar_extractions(Message-ID keyed) so a re-scan skips it — no re-proposing, no wasted LLM pass;rescan=true overrides. Prompt adapted from Odysseusemail_pollers.py.
Calendar
-
calendar— appointments + timed reminders.add(NLwhenlike "next Tuesday at 3pm" → resolved against Atlantic time via the utility LLM; optional location/notes/duration/reminders),list,remove. Local SQLite is source of truth (calendar_events+calendar_reminders, schema v25); best-effort mirrored to SOGo CalDAV (a self-hosted server) so events show in real calendar clients. Default reminders 1 day + 1 hour before (configurable, per-event overridable); the calendar reminder worker (src/tools/calendar.rs, 60s tick) posts each due reminder to the originating Matrix thread via the samesend_noticepath as the scheduler. Multi-account via[[tools.calendar.accounts]]. One shared calendar serves all users: every event records who added it (created_by) and a public/privatevisibility(v26) —listshows public events to everyone plus the caller's own private ones (🔒). Recurring events (v27): NL → iCal RRULE (rrulecrate, DST-safe), pushed to CalDAV as a series; the worker rolls a 60-day horizon each tick to materialize per-occurrence reminders;removecancels the whole series. Admin-only.
Contacts
-
contacts— CardDAV name→email/phone lookup + add over the SOGo Personal Address Book ([tools.contacts]). Actions:search(match name/email/org — resolves "email a contact" to an address),list,add(name+email/phone). Oneaddressbook-queryREPORT pulls every vCard inline, parsed by a hand-rolled vCard reader (FN/EMAIL/TEL/ORG, structured-Nfallback,mailto:stripping) — no CardDAV/vCard crate.addPUTs a vCard 3.0. Admin-only. The agent resolves a name here, then callsemailsend— no cross-tool coupling.
Media
-
youtube— summarize a YouTube video from its transcript ([tools.youtube]).yt-dlp(host binary, argv — no shell) pulls auto-captions as VTT; a hand-rolled VTT cleaner strips timing/tags and collapses the rolling-duplicate caption lines → transcript → LLM summary (TL;DR + key points + notable moments). URL is host-validated to YouTube (no open-fetch SSRF). 90s subprocess budget; graceful errors for no-captions / not-found / timeout. admin/adult. -
subtitles— (optional integration — needs[tools.bazarr]+ a running Bazarr;[tools.jellyfin]further optional; absent → not registered) conversational front-end over Bazarr for "get subs for<title>". Resolves a movie (/api/movies+/api/providers/movies) or TV show (/api/series+/api/episodes+/api/providers/episodes) in Bazarr, runs a live provider search, and downloads the best match per wanted language. It re-checks what actually landed rather than trusting the POST (Bazarr discards below-threshold/failed-sync subs even after a 204). TV argsseason/episode→ one episode / a whole season / all missing episodes (bounded inline). A whole-season/series ask bigger than the inline cap runs in a background worker when a Matrix room is available (mirrorscode_task): the tool replies "working on it" and posts a summary back to the room when every episode is done; voice/scheduler contexts keep the bounded inline behavior. After a successful download it triggers a Jellyfin library scan (the only thing that re-discovers external subs; incremental, ~12s). admin/adult/child. -
media_request— (optional integration — needs a Jellyseerr config block + a running Jellyseerr; absent → not registered) request a movie/show via Jellyseerr (gates "on Jellyfin ✅" only on a realjellyfinMediaId). Kids can ask; child requests are held for owner approval. admin/adult/child. -
music_request— (optional integration — rides on themusicstack, so it needs Music Assistant installed in Home Assistant) request-side companion to the music/DJ feature: kids can ask for music, owner-gated. admin/adult/child. -
mastodon— (optional integration — needs[tools.mastodon]+ a Mastodon account/instance; absent → not registered) native Mastodon tool, replacing the retired OpenClaw cron for the daily "On This Day in History" image posts. Two actions:recent(today's Atlantic date + the account's last N posts, for self-contained dedup of which event to post) andpost(generate an image via the shared imagine/Ideogram pipeline or take a local path, upload it as media, then post the status). Routed to the QUALITY tier (multi-step compose). Admin-only. Driven 3×/day by the scheduler.
Research
-
research— deep research ([tools.research]): its own bounded plan→search→fetch→synthesize loop → cited markdown brief. A grounded planner (one preliminary SearXNG search informs the decomposition) splits the question; each sub-question hits SearXNG; top unique URLs are fetched concurrently (SSRF-guarded — public http(s) only) and reduced to text. Embeddings context compression (ports GPT-Researcher'sEmbeddingsFilter): each page is chunked + embedded via the[memory]Ollama embedder, keeping only the chunks most similar to the query withinmax_page_chars— synthesis sees the relevant slice, not the first N chars (fails open to truncation without embeddings). Iterative deepening (deepen_rounds, default 1): after the first gather, the LLM reflects on coverage gaps and runs targeted follow-up search rounds (raises the source ceiling 10→14; fails closed). Synthesized into an in-depth, multi-section report (synthesis runs atsynthesis_max_tokens, default 6000, so it isn't clipped) with an auto-appended## Sourceslist. Bounded (≤10/14 sources, 40k synth chars, 8s/fetch); slow → run under!bg. Direct-return (not re-summarized). admin/adult.
Memory & Meta
-
memory_lookup— direct human-readable memory search. -
progress— tool execution progress reporting. -
scratchpad— per-task working memory (set/append/get/list/clear) — fixes sub-agent statelessness across rounds. Backed bytask_memory(runtime_kv), scoped to (channel_id, topic_id), 3h TTL, bounded, fails open, unknown senders excluded. Auto-carried into every sub-agent run as a## Working memoryblock. -
schedule— cron schedule create / list / delete, plus one-time (run_once) schedules (v31): a NL time like "remind me at 3pm tomorrow" resolves to an absolute UTC instant (Atlantic-grounded vianl_to_oneshot), pins the cron to that minute, and the row is archived after its single fire (so a mid-run crash retries). -
agent— router that delegates to a sub-agent (the only tool in main context).
Self-Modification & Delegation
-
code_task— delegate a coding job to Huginn over NATS, instead of writing runnable code inline.create_workspace=truemakes Huginn create a brand-new named workspace for a fresh artifact (script / small app / canvas demo);false(default) targets an existing repo (Huginn refuses an unknown name, catching a typo'd repo). Live progress relay, structured outcomes, and mid-flight steering (code_task+ a steer tool). When it hands off, Muninn publishes aSpawnChildenvelope on the Hermes feed bus (coabai-agent-protocol) keyed to the job id, so the Hermes console opens an interactive Huginn child tab that fills with the job's real progress (best-effort; dropped when Hermes isn't listening). Admin-only. -
identity/workspace_edit/self_restart— self-modification tools (persona overlay; edit own config/persona workspace files; bounce own service with a detached safe restart + loop-guard). Admin-only.
Memory System
Persistent SQLite-backed memory at data/muninn.db, FTS5 + sqlite-vec, schema v31.
Storage & Search
- Append-only
messagesraw log. - Mutable
memoriestable withimportance,strength,access_count,source_ids,archived_at,user_id,scope,corrects. -
vec_memoriesvirtual table — 768-dim embeddings (Ollamanomic-embed-text). -
memory_linksgraph:co_extracted,consolidated,semanticlink types with strength. - FTS5
messages_fts+memories_ftskept in sync via SQL triggers. - Per-channel
maintenance_statefor incremental workers. -
conversation_summariescache — one rolling summary per channel, keyed bylast_message_id. -
vec_skillstable — embeddings on extracted skills. - Hybrid retrieval — FTS5 + vector RRF, 1-hop graph augmentation, strength-weighted BM25 (
strength × (1 + importance)).
Lifecycle Workers
- Async fact extraction — every exchange spawns a tokio task; LLM extracts facts, dedups via FTS5, upserts, embeds. Does not memorize network/host facts — the extraction prompt forbids IP/port/VLAN/host-spec/scan/status facts (an explicit "remember
<net fact>" still wins), and a deterministic backstop (is_transient_infra,mentions_ip_or_vlan) drops scan/status/inventory forms and auto-extracted IP/VLAN facts so heavy SSH/scan sessions stop leaking transient tool output into never-decaying "facts". Network inventory lives in the workspace network-map file instead. - Correction detection —
correctsfield; substring-matched contradicted memories are auto-archived before the new fact is inserted. - Maintenance worker (30-min timer):
- Episodic summaries (groups of 6+ messages within 60-min windows → LLM summary).
- Fact consolidation (Jaccard-similar memories → LLM merge, originals archived with
source_idsprovenance). - Ebbinghaus decay (
strength × 2^(-days/14), archived below 0.1; access resets the clock). - Semantic linking (cosine > 0.85 creates links).
- Embedding backfill.
- Link cleanup.
- Skill curator (v0.12) — archives skills that are unused since creation, low-success after enough uses, or idle past
archive_idle_after_days; toggles + thresholds under[memory] skill_curator_*. - Per-user dialectic profile (v0.12) — distills each user's top-importance personal memories into a hard-capped (
profile_max_chars, default 1500) "About<user>" block; refreshes onprofile_refresh_after_hours; injected into the system prompt on every turn from that sender.
- [?] Reflective "dream" pass (v24, schema
maintenance_state.last_dream) — opt-in nightly consolidation that re-reads recent messages and distils higher-order memories/links beyond the per-cycle extraction. Config:[memory] dream_enabled(default false),dream_interval_hours(24),dream_max_messages(120). - Rolling conversation summaries — older messages aged out of recent window are summarised, cached, refreshed asynchronously when stale, prepended to context as
[Earlier in this conversation]. - Skill extraction worker — distills successful sub-agent trajectories into procedural memory; toggle:
[memory] skill_extraction_enabled. - Skill curator (v0.12) — periodic janitor for the skills table (see Maintenance worker above). Manual run:
!curate-skills. Inspection:!archived-skills. - User profile worker (v0.12, Honcho-lite static phase) — generates and refreshes the
user_profilestable (schema v22). Manual ops:!profile [user_id],!regenerate-profile [user_id]. - Query-aware profile slicer (v0.12, full Honcho) — when
[memory] profile_slicer_enabled = true, every turn from a known user makes one extra LLM call againstclassifier_llm(the GPU server-small in prod) to extract the slice of the cached profile most relevant to the incoming message. Three outcomes: relevant slice → inject sliced; nothing relevant → omit profile this turn; LLM failure → fall back to full profile. Default OFF — opt in after observing baseline latency from B1. - Failure-learning loop (v30/v31) — the failure mirror of the skill loop (which only captured successes). Every sub-agent run records a
RunOutcome(ok / ok-with-failures / round-limit / empty / error) plus its signals (steps, guard nudges, tool failure, round-limit hit, tools used) to therun_outcomesledger; capture is best-effort and never recorded for unknown speakers. A run is also markedcorrectedwhen the user corrects Muninn in the next exchange (hooked into the existing correction detection) — a corrected "ok" is a quiet failure, excluded from the clean-rate.failures.rsanalyzes the ledger (outcome distribution, clean rate, round-limit / tool-failure rates, unhealthy-tool hotspots) →!failures. Nightly review → constraints:!review-failuresreads recent unhealthy/corrected runs, distils up to 3 recurring anti-patterns into shortconstraintmemories that surface back through normal retrieval (failure → constraint → surfaced → avoided); opt-in auto via[memory] failure_review_enabled(default OFF, throttled to ~once/24h).
Scoping
- Per-user isolation — personal / room / global tiers via
user_id+scope. - Per-topic scoping — see Topics & Threads.
Identity Registry
Canonical user_id bridging every surface a person speaks through into one row. M1a of the wake-word-free voice work — see project_wake_word_free_voice memory.
-
userstable (schema v19) —user_idPK,display_name,matrix_mxid,pyannote_id,tts_voice,role,allowed_rooms(JSON),source, timestamps. Unique partial indexes onmatrix_mxidandpyannote_id. - Config seed —
[[users]]blocks inmuninn.tomlupsert at every boot, markedsource='config'. Configured fields always win on conflict. -
UserRegistry(src/identity.rs) —by_id,by_matrix_mxid,by_pyannote_id,list,insert_runtime. Holds its own SQLite connection on the samedata/muninn.db. - Roles —
admin|adult|child|guest|self. - [?] Lookups not yet wired — registry held by
BotCore, but resolver calls land in Phase M2 (voice transport) and M1d (role-filtered ToolRegistry).
Device Registry
Stable per-surface ids that voice events arrive under, mapping to room and HA media_player. M1b of the wake-word-free voice work.
-
[[devices]]config seed — in-memoryDeviceRegistry(src/devices.rs) loaded from muninn.toml at boot. No DB persistence (devices are infrastructure). - Resolvers —
by_id,in_room,list. Synchronous. -
media_playerpolymorphism — accepts voice-server alias (the workstation,kitchen,pixel 6a) or rawmedia_player.*entity. -
room = "varies"— supported for mobile devices (phones); voice transport will need a per-utterance room hint. - [?] Lookups not yet wired — registry held by
BotCore, callers land in Phase M2 (voice transport).
Presence & Family Map
Optional — off until [tools] presence_enabled is set; the map layers further assume Home Assistant device_trackers and a self-hosted OSM map front-end (e.g. a self-hosted server). A shared PresenceEngine (src/presence.rs) plus a custom-pin store, surfaced both as the presence tool (above) and as JSON layers for the map.
- Co-location engine — fuses configured users' HA
device_trackers, auto-detects cars, and clusters co-located people by named zone or haversine distance (presence_colocate_meters). GPS freshness enforced: a fix older thanpresence_stale_secsreads as "location unknown" rather than a ghost location. - People layer —
GET /map/peoplereturns bearer + allowlist-gated GeoJSON (Markers). When a stationary phone stops reporting coords (last fix aged out) but HA still places it in a known zone, the marker falls back to the zone center (approx: true) so family members stay visible; only genuinely-unknown (away, no fresh fix) is omitted. - Custom pins —
PinStore(own SQLite table indata/muninn.db, mirrorsUserRegistry): drop / label / delete places on the family map.GET /map/pins(list) ·POST /map/pins(create) ·DELETE /map/pins/{id}.created_byis the authenticated user (never the request body); label + lat/lon validated. Samepresence_map_allowed_usersallowlist as the people layer via the sharedauthorize_map()helper. - Config —
[tools] presence_enabled,presence_stale_secs,presence_colocate_meters,presence_map_allowed_users.
Open-House Voice Mode
-
[tools] open_house(default false) — when on, every speaker (recognized, guest, or unknown) may use the house-control surface:home_query/home_action,music,photos,presence,speak, andmedia_request/music_request/subtitles. Family-private surfaces stay gated even in open mode —camera(surveillance) andmemory_lookup(personal data) keep their normal role gate, as do all admin/dev tools. Goal: low-friction adoption + usage/clip data collection. Enforced intools::agent::role_allowsvia a process atomic set at boot (set_open_house); reversible withopen_house=false+ restart.
Role-Based Tool Filter
M1d. The sub-agent's tool surface is filtered per request by the caller's role.
-
role_allows(role, tool_name)— pure function insrc/tools/agent.rsencoding the permissions matrix (admin / adult / child / guest / unknown / self). -
AgentTool::resolve_role— looksctx.senderup inUserRegistry(Matrix mxid first, then canonical user_id); misses →"unknown". - Visibility filter —
all_tool_defs(ha_access, role)excludes denied tools from the sub-agent's system prompt andtoolsarray. - Execute-time gate —
execute_tool()rechecksrole_allows()and returns"Access denied: role 'X' may not invoke Y."as defense in depth. - AND with
ha_allowed_users— the older sender-explicit allowlist for HA tools is layered with the role gate. A user must pass both. - MCP gating — admin-only for now; per-server promotion case-by-case.
- [?] Finer-grained gating (room-limited home_action, own-voice-only speak, own-scope memory_lookup) — allowed at the AgentTool layer; tightening lands inside each tool with the M2 voice transport.
Ephemeral Sessions (Unknown Speakers)
M3a. RAM-only conversation state for sender == "unknown" voice events. Bypasses the memory DB entirely.
-
EphemeralSessionStore(src/sessions.rs) — keyed bydevice_id, TTL 5 min idle, bounded turn buffer (16 turns / 8 pairs). Background purge worker on TTL/2 tick. -
BotCore::handle_unknown_message— dedicated path that short-circuits BEFORE topic resolution, memory log, and memory extraction. Single-shot LLM call with theagentrouter; tool surface restricted by the M1d role gate to weather + calculate + transcribe + progress. - Privacy-guard system prompt —
EPHEMERAL_SYSTEM_PROMPTreplaces the workspace persona for unknown turns. Decline household questions; suggest contacting a resident for state-changing requests. - Lost on Muninn restart by design.
Onboarding (M3b)
Deterministic state machine on each ephemeral session. Runs as a parallel track — the conversation isn't blocked on identification.
- States —
NotStarted→AskedName→NameCaptured(name)→ClaimedKnownName|AwaitingApproval. - Triggers — Strategy C (identity-class regex on user text) + Strategy B (8-turn chit-chat safety net). Weather / time / public info don't trigger.
- Name extraction — regex patterns for
I'm X,my name is X,this is X,X here, plus bare-word reply.is_plausible_namerejects greetings/fillers. - Impersonation defense — claimed name matched against
display_nameinUserRegistry(case-insensitiveby_display_name_ci). On match, system-prompt addendum tells the LLM to acknowledge warmly but never confirm. - [?] Cross-channel approval (DM the owner, 5-min timeout, pyannote /enroll) — stubbed in v0; the audit table from M3c is the foundation for the DM correlator.
Auth Events Audit Log (M3c)
Append-only audit log for unknown encounters and onboarding state transitions. Schema v20.
-
auth_eventstable —id,ts,device_id,event_type,pyannote_id,pyannote_confidence,claimed_name,matched_user_id,transcript_preview(capped at 200 chars). -
AuthEventStore(src/identity.rs) — async writer holding its own SQLite connection; failures warn-logged and swallowed (never deny service for an audit hiccup). - Event vocabulary —
unknown_encounter,name_asked,name_captured,claimed_known,enrollment_requested, plus reservedenrollment_approved/enrollment_deniedfor the future approval flow. - Wired —
BotCore::handle_unknown_messagewrites one row per unknown turn + one per state transition.
Topics & Threads
- LLM topic classifier — picks an existing topic or proposes a new one per inbound message.
- Topic-scoped memory retrieval.
- Topic lifecycle — dormant aging.
- User-initiated threads only (schema v18) — classifier tags but never spawns Matrix threads.
- General-topic posts go to room root, not threaded.
- Commands:
!topics,!topic,!archive,!retag.
Vision & Media Pipeline
- Frigate snapshot fetch via HA.
- Vision describe via the GPU server LLM (same llama.cpp instance).
- Parallel Frigate sensor fetch (motion, person count, face recognition).
- Image-to-image style transfer from camera snapshots.
-
SharedMediaQueue— tool → bot image hand-off. - Background job queue for async generation with placeholder image swap.
- Matrix JPEG / PNG upload.
Home Assistant Integration
- Weather:
GET /api/states/weather.*+POST /api/services/weather/get_forecasts?return_response. - Conversation API:
POST /api/conversation/process. - HA-side entity resolution — Muninn passes natural language, no entity-ID knowledge required.
- Long-lived access token via
[tools] ha_token.
Scheduling
-
schedulestable with cron + room + thread + task fields (+run_once, v31). - Tick worker — wakes on the minute, fires due cron tasks via sub-agent. Claim-first double-fire fix: a schedule's
next_run_atis advanced before the task runs (claim_schedule), so a multi-minute task isn't re-fired by later 60s ticks. - One-time (
run_once) schedules — fire once at an absolute time, then retire. The row is archived after the fire (not at claim) so a mid-run crash retries. - 5-field cron expressions (
croncrate). - Per-schedule room and thread delivery.
- Heartbeat worker — reads
HEARTBEAT.md, evaluates pending tasks, posts ack. Addresses the user by[heartbeat] address_as(name woven into the eval prompt + reminder greeting; name-neutral when unset). -
scheduletool for LLM-driven schedule management.
Observability & Export
-
⏱timing instrumentation — memory context, LLM calls, tool exec, sub-agent rounds, end-to-end. Visible injournalctl --user -u coabai-muninn. - Context logging —
data/logs/YYYY-MM-DD_<room>.log, async writes, 1 K line cap. Internal extraction/maintenance calls excluded. Tool args/results redacted unless[llm] log_tool_results = true. - Replay capture (
[llm] replay_capture, default on) — every conversation LLM exchange written as a replayable JSONL record underdata/logs/replay/, one file per session (delimited by!clear), last 5 sessions per channel retained. Each record holds the exact built request (system prompt, messages, tool defs, sampling params) + the raw response — POSTable straight back to the LLM. -
muninn replayCLI — re-run a captured record against the LLM, optionally swapping the system prompt (--system-file) to A/B-test prompt changes;--list/--line Nselect the record. -
muninn export-trajectoriesCLI — Atropos-compatible JSONL, runs alongside the daemon (read-only). - Export filters:
--since,--channel,--topic,--min-tool-calls,--include-system. - Status reactions on Matrix messages: 🤖 agent, 🔍 search, 🌐 fetch, 💻 shell, 🧮 calc, 📷 camera, 🎨 imagine.
- Credential scrubbing — regex redaction of tool output before LLM and before persist.
Infra Alerts & Auto-Remediation
Optional — off until an [alertmanager] block is configured; assumes an external Prometheus/Alertmanager pointed at the webhook, and each remediation block names its own command.
- Alertmanager → Matrix relay (
src/alertmanager.rs) — axum webhook receiver that turns firing Prometheus/Alertmanager alerts into Matrix messages in an ops room; infra alert text names the failed service/pod. - Generic auto-remediation — a firing alert whose
alertnamematches an[[alertmanager.remediation]]block runs that block'scommand(argv, no shell, 20s timeout, per-alertname cooldown) and appends the outcome to the Matrix alert.RemediationConfig { alertname, command, cooldown_secs, description }. Reusable for any self-heal — add a config block, no code change. - First use: Onkyo AVR WiFi self-heal — the
OnkyoUnreachablealert (labelremediation: onkyo-deauth) SSHes to AP.6via a key locked to a single forced-deauth command to kick the receiver into re-associating + pulling a fresh DHCP lease (~4s recovery); if that fails, the 12m critical rule escalates to a phone push.
Deployment & Configuration
- systemd user unit (
coabai-muninn.service.example→~/.config/systemd/user/). -
./deploy.shrebuild + restart. - TOML config with
MUNINN_CONFIGenv var precedence (falls back tomuninn.toml). - Sections:
[matrix],[llm],[memory],[tools],[tools.tts],[tools.email],[tools.calendar],[tools.contacts],[tools.youtube],[tools.research],[tools.bazarr],[tools.jellyfin],[tools.mastodon],[tools.dj],[tools.music_rooms],[tools.photo_displays],[docs],[tui],[web],[voice],[heartbeat],[audio_companion],[alertmanager], and array tables[[users]],[[devices]],[[persona_voices]],[[tools.mcp_servers]],[[tools.email.accounts]],[[tools.calendar.accounts]],[[alertmanager.remediation]]. -
data/andmuninn.toml.localgitignored. - Two-step UIA cross-signing bootstrap in
setup_e2ee(). - Shared peer-DB pattern for the TUI surface (read-only against daemon DB).
Commands (Matrix / TUI)
| Command | Action |
|---|---|
!ping |
Health check |
!help |
List commands |
!status |
Uptime, memory stats, link count, embedding coverage, token counts |
!clear |
Reset conversation context (preserves raw log + memories) |
!memories |
Show stored memories with IDs and strength % |
!forget <q> |
Delete memories matching FTS5 query |
!links <id> |
Show memory graph connections |
!consolidate |
Trigger episodic summaries + fact merging |
!decay |
Memories at risk of fading (strength < 30 %) |
!graveyard |
Recently archived memories |
!embed |
Backfill embeddings for unembedded memories |
!topics / !topic / !archive / !retag |
Topic management |
!skills / !skill <id> / !forget-skill |
Skill management |
!curate-skills / !archived-skills |
Skill curator manual run + archive inspector |
!failures |
Failure-learning review of recent sub-agent runs (mirror of !skills) |
!review-failures |
Distil recurring failures into "constraint" memories that surface via retrieval |
!profile [user] / !regenerate-profile [user] |
Inspect / regenerate per-user dialectic profile |
!schedules / !schedule <id> |
List scheduled tasks / show one |
!pause-schedule <id> / !resume-schedule <id> / !delete-schedule <id> |
Pause / resume / archive a schedule |
!bg-list / !bg-cancel <id> |
List / cancel backgrounded tasks (prefix any message with !bg <message> to force-background it) |
!stop |
Stop the current task; or steer it mid-flight with plain language |
Misc
- MCP client in
src/mcp.rs— both stdio JSON-RPC and HTTP / streamable-http (FastMCP) transports ([[tools.mcp_servers]]withkind = "stdio" | "http"). MCP tools are admin-gated.muninn-fetch(HTTP MCP) supplies page-fetch/screenshot/extract, replacing the removed nativeweb_fetch. - Voice persona stack —
[[persona_voices]]map a transcript opener (e.g. a character name) to a TTS voice + a per-turn system-prompt addendum; persona sticks per-device for[voice] persona_ttl_seconds. Topics are persona-scoped (schema v21) so personas don't share conversation history. - Audio companion —
[audio_companion]: when an inbound Matrix message was a voice note, send a TTS audio reply alongside text; long replies summarised first. - Atomic message batching per topic.
- Standalone OIDC test page (
web-test/) for the local auth server.
Versioned Highlights
| Tag | Theme |
|---|---|
v0.6.1-foundation |
Topic-thread foundation (schema v11 + v12 migrations) |
v0.6.2 |
Topic threads end-to-end |
v0.7.0 |
Hermes-inspired core (dialectic profile groundwork) |
v0.8.0 |
Skill creation loop (procedural memory from sub-agent trajectories) |
v0.9.0 |
Schedule tool (cron tick worker, NL schedules) |
v0.10.0 |
Trajectory export CLI (Atropos-compatible JSONL) |
v0.11.0 |
Per-task sub-agent isolation; web transport; user-initiated threads only (schema v18); credential scrub; garbage guard |
v0.12.0 |
Skill curator (Hermes-inspired active→archive lifecycle) + per-user dialectic profile (Honcho-lite, schema v22) with optional query-aware slicer on classifier_llm; voice persona stack + persona-scoped topics (schema v21); MCP HTTP/streamable-http transport |
post-v0.12 (main) |
music tool (Music Assistant); conversation replay capture + muninn replay A/B harness; reflective "dream" consolidation pass (schema v24); skill-flywheel revival (tools_used-aware skill embeddings, schema v23); memory bullets stamped with relative age; matrix-sdk 0.9 → 0.17 migration |
| post-v0.12 cont. (schema v25–v31) | email / calendar / contacts / youtube / research tools; subtitles (Bazarr), mastodon, photos, geocode / route maps (+ route map snapshot), media_request / music_request, DJ mode (radio-host announcer over the music), code_task (delegate to Huginn over NATS + Hermes SpawnChild), scratchpad (per-task working memory), identity / workspace_edit / self_restart; DeepSeek primary brain + fallback brain + round-robin peer pools; presence / co-location engine + family map (people layer + custom pins); open-house voice mode; one-time (run_once) schedules; Alertmanager → Matrix relay + generic auto-remediation (Onkyo self-heal); failure-learning loop (run-outcome ledger + nightly review → constraints, schema v30/v31); transcribe direct OpenAI/speaches STT; extraction stops memorizing network/host facts |
Roadmap (not in this file)
Tracked in the project_muninn_* notes outside the repo and the issue tracker. Items currently being scoped:
- Voice satellite (LVA → Matrix)
- Speaker ID via pyannote ECAPA + sqlite-vec
- Speaker-gated wake-word-free voice
- Family-scoped multi-bot deployment
- Conversation-quality and proactive-HA polish
Update this file when a roadmap item lands and a tag ships.