From f593d5f6214fddc253199280debee19bc688e30f Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 6 Nov 2026 17:28:48 +0100 Subject: [PATCH 1/2] Skill fixes: warn when a fix drops a rule, and say when the skill is one you chose --- crates/mimir-core/src/db/mod.rs | 10 ++++++++ crates/mimir-core/src/db/skills.rs | 37 ++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/mimir-core/src/db/mod.rs b/crates/mimir-core/src/db/mod.rs index 1eeb5194..b9a9d296 100644 --- a/crates/mimir-core/src/db/mod.rs +++ b/crates/mimir-core/src/db/mod.rs @@ -1049,6 +1049,16 @@ mod tests { let b = d.skill_propose(s, 1, "Pay invoices", "When paying invoices", "1. Open bank\n2. Pay", None).unwrap().unwrap(); d.skill_decide(s, b, true).unwrap(); assert!(d.skill_merge_propose(s, &[a, b], "Book padel", "x", "y", "r").is_err(), "Book padel is another active skill"); + // A fix that drops a rule says so; one of a skill you saved says you chose it. + let saved = d.skill_saved_propose(s, 1, "Pay safely", "When paying", "Never pay before checking the invoice.\nOpen the bank app.", "you asked", None).unwrap().unwrap(); + d.skill_decide(s, saved, true).unwrap(); + let fix = d.skill_patch_propose(s, saved, "Never pay before checking the invoice.\nOpen the bank app.", "Open the bank app.", "faster").unwrap().unwrap(); + let reason = d.skills(s).unwrap().into_iter().find(|k| k.id == fix).unwrap().reason; + assert!(reason.contains("drops a rule: “Never pay before checking the invoice.”") && reason.contains("You chose “Pay safely”"), "{reason}"); + let merged = d.skill_merge_propose(s, &[a, b], "Pay invoices", "When paying", "1. Open bank\n2. Pay the bill", "Same job").unwrap().unwrap(); + let reason = d.skills(s).unwrap().into_iter().find(|k| k.id == merged).unwrap().reason; + assert!(!reason.contains("drops a rule") && !reason.contains("You chose"), "{reason}"); + assert_eq!(super::skills::dropped_rules("Don’t pay twice.\nOnly on weekdays", "Only on weekdays, mornings"), vec!["Don’t pay twice."]); // Facts nobody looks up can leave standing memory; looking one up counts. let e = d.store(s, "knowledge", "Old wifi hint", "the cat is called Bob", "user said", None).unwrap(); diff --git a/crates/mimir-core/src/db/skills.rs b/crates/mimir-core/src/db/skills.rs index 5ce4deb3..6d714e33 100644 --- a/crates/mimir-core/src/db/skills.rs +++ b/crates/mimir-core/src/db/skills.rs @@ -284,7 +284,27 @@ impl Db { if body.chars().count() > limit || body.trim() == k.body.trim() { bail!("the fixed skill would be too long, or unchanged"); } - self.skill_insert(scope, k.bot_id, &k.name, &k.description, &body, None, "patch", reason, &[]) + let reason = self.fix_reason(scope, std::slice::from_ref(&k), &k.body, &body, reason)?; + self.skill_insert(scope, k.bot_id, &k.name, &k.description, &body, None, "patch", &reason, &[]) + } + + /// A fix's reason, with what a quick approval could miss: rules it drops, and that you chose the skill yourself + /// (saved it or added it from the library, in any version of it). + fn fix_reason(&self, scope: &str, skills: &[Skill], before: &str, after: &str, reason: &str) -> Result { + let mut out = reason.trim().to_string(); + for rule in dropped_rules(before, after) { + out.push_str(&format!(" Warning: this fix drops a rule: “{rule}”.")); + } + for k in skills { + let chosen = self + .c() + .prepare("SELECT 1 FROM skills WHERE scope=?1 AND bot_id=?2 AND lower(name)=lower(?3) AND kind IN ('saved','library') AND status IN ('active','removed')")? + .exists(params![scope, k.bot_id, k.name])?; + if chosen { + out.push_str(&format!(" You chose “{}” yourself.", k.name)); + } + } + Ok(out) } /// What this conversation actually did lately: the successful actions of the bot's last few runs here (not memory, @@ -364,7 +384,9 @@ impl Db { if self.skills_active(scope, skills[0].bot_id)?.iter().any(|k| k.name.eq_ignore_ascii_case(name.trim()) && !ids.contains(&k.id)) { bail!("another skill already has that name"); } - self.skill_insert(scope, skills[0].bot_id, name, description, body, None, "merge", reason, ids) + let before = skills.iter().map(|k| k.body.as_str()).collect::>().join("\n"); + let reason = self.fix_reason(scope, &skills, &before, body, reason)?; + self.skill_insert(scope, skills[0].bot_id, name, description, body, None, "merge", &reason, ids) } /// Propose retiring a skill nobody has used for a while (it's kept, and can come back). @@ -423,3 +445,14 @@ impl Db { Ok(self.c().execute("UPDATE skills SET status='removed' WHERE id=?1 AND scope=?2 AND status='active'", params![id, scope])? > 0) } } + +/// Rule lines of `before` ("never", "only", "always", "unless", "don't", "do not", "must") that `after` no longer has. +/// A line kept inside a longer one ("Never pay" → "Never pay twice") isn't dropped. +pub fn dropped_rules(before: &str, after: &str) -> Vec { + let rule = |line: &str| { + let lower = line.to_lowercase().replace('’', "'"); + let words: Vec<&str> = lower.split(|c: char| !c.is_alphanumeric() && c != '\'').collect(); + words.iter().any(|w| matches!(*w, "never" | "only" | "always" | "unless" | "don't" | "must")) || words.windows(2).any(|p| p == ["do", "not"]) + }; + before.lines().map(str::trim).filter(|l| !l.is_empty() && rule(l) && !after.contains(*l)).map(str::to_string).collect() +} From 68763395f5a77572fb9f9769815491d4d9389905 Mon Sep 17 00:00:00 2001 From: Willem Date: Wed, 11 Nov 2026 20:09:03 +0100 Subject: [PATCH 2/2] 0.8.0 Co-Authored-By: Claude Fable 5.2 --- CHANGELOG.md | 23 ++++++++++++++++++++++- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c337a81..253290d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,28 @@ What changed for people using Mimir. The newest release is at the top; "Unreleased" is what the next one brings. -## Unreleased +## 0.8.0 + +### The app +- **The chat in the middle:** a new layout. The sidebar lists your bots, then your chats, with search (⌘K jumps to a + bot, a chat or a place) and new bot at the top, and Needs you, Meetings, Computer and Settings at the bottom. +- **Replies as bubbles:** one face and name per run of replies, your own messages tinted in the bot's colour, and a + time on a new day or after an hour's gap. The composer has attach and hold-to-talk beside it. +- **Everything else opens over the chat:** Memory, Skills, Routines and Activity are icons in the chat's corner and + share one sheet with tabs; Settings, Needs you, Meetings, Computer and a bot's page open the same way. Esc, × or a + click beside the sheet closes it, and the sidebar stays live meanwhile. On a phone, sheets fill the screen. +- **Settings by category:** a list of sections that follows your scroll. + +### Skills +- **A fix that drops a rule says so:** when a proposed fix or merge of a skill leaves out a line with "never", + "only", "always", "unless", "don't" or "must", its card warns you and quotes the rule. It also says when the + skill is one you saved or added from the library yourself. + +### AI accounts +- **What tools and memory cost:** `mimir doctor` and a bot's page (under *What it can do*) show about how many tokens + the offered tools and the standing memory take in every request, and warn when tool servers take a lot. +- **Ollama's window too small:** a bot on Ollama warns when the model's context window is smaller than Mimir's tools + and memory need, with the `OLLAMA_CONTEXT_LENGTH` to start Ollama with. ### Routines - **What I noticed:** a new library skill for a daily quiet check: mail older than two days that asks you something, diff --git a/Cargo.lock b/Cargo.lock index 3ba02ca1..57e637f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3167,7 +3167,7 @@ dependencies = [ [[package]] name = "mimir" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "axum", @@ -3179,7 +3179,7 @@ dependencies = [ [[package]] name = "mimir-core" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "async-imap", @@ -3223,7 +3223,7 @@ dependencies = [ [[package]] name = "mimir-desktop" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 03a7f2ca..9dd1f404 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/mimir-core", "crates/mimir", "crates/mimir-desktop"] # The one place the version lives: every crate inherits it, and the desktop app takes it from its crate (tauri.conf.json # has none). The release workflow checks the tag against it. [workspace.package] -version = "0.7.0" +version = "0.8.0" [profile.release] strip = true