Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,48 @@
All notable changes are documented here. Versions follow semver; the CLI,
the `mimir-mem` crate, and the on-disk schema move together.

## [Unreleased]

### Added

- **Recall says when a hit is weak.** A ranked list always has a #1, so the
score cannot tell "the answer" from "the least-bad thing in the store".
Each hit is now labelled by evidence instead: `strong` when it contains the
query's words and both the lexical and semantic legs rank it in their own
top 10, otherwise `weak` — one word on the compact line (CLI and MCP),
`"strength"` in `--json`. Calibrated against the labelled retrieval corpus
with the real model: weak hits are relevant ~2% of the time, strong ~55%.
A three-tier version (with `likely`) and two stricter matching rules were
measured and rejected; the numbers are in `strength.rs`, and
`eval::strength` gates the rule in `cargo test`.
- **`mimir misses` — what agents asked that the store couldn't answer.** A
recall with no strong hit is recorded (query text, count, last asked), most
asked first; the row resolves itself when the same question later gets a
strong answer, and reopens if that answer goes away. `mimir report` gains an
"unanswered queries" row. Explicit recalls only — the inject hook never
feeds it. Per-machine (not synced or exported), never stores a query the
secrets scanner flags, ages out with `[learn] event_retention_days`, and
`[learn] log_misses = false` turns it off.

- **Co-activation edges (phase 1).** Two memories opened within 30 minutes
of each other strengthen a derived `co_recalled` edge (Hebbian, one fire
per pair per day, 30-day half-life, at most 32 per node). Recorded only —
ranking is untouched. Not synced or exported; `link` refuses to author one.
- **Shadow ranking — `mimir shadow`.** A candidate ranking runs beside the
live one on every explicit recall; where the orders differ both are kept,
and each later `opened` is scored against the page it came from (MRR of the
opened hit, wins/losses, exact sign test). Recall output never changes and
nothing promotes itself. First candidate: lift memories co-recalled with
one opened in the last 30 minutes. The report says plainly that the
comparison favours live. `[learn] shadow_ranking = false` to disable.

### Changed

- **Schema v13** adds the `recall_miss` table, **v14** the `shadow_rank`
table. As with every schema bump, a
store opened by this binary is refused by older ones (`SchemaTooNew`):
reinstall, then restart running `mimir mcp` servers.

## [0.17.1] - 2026-10-15

### Fixed
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,63 @@ mimir link --scan --all-projects # ~600 links on a mature store
`mimir doctor` reminds you if you have code graphs and have never scanned,
and goes quiet once you have.

### Weak matches and the miss ledger: what the store couldn't answer

Every ranked list has a #1, including the list for a question the store knows
nothing about. So recall labels each hit by the *evidence* behind it rather
than its score: a hit is `strong` when it contains the query's words **and**
both the lexical and the semantic search rank it in their own top 10.
Anything else gets one word on its line:

```
m:46MBBK [decision 10-15 weak] Release process: bump versions, update CHANGELOG…
```

Measured on the labelled retrieval corpus with the real embedding model, a
`weak` hit is relevant about 2% of the time and a `strong` one about 55%
(`eval::strength`; numbers and the two rules tried and rejected are in
`crates/mimir-core/src/strength.rs`). `--json` carries `"strength"` on every
hit.

A recall whose page has no `strong` hit is a **miss**, and misses are kept:

```sh
mimir misses # most-asked unanswered queries, this project + global
mimir misses --all # every project
mimir misses --dismiss 12 # it was noise, not a gap
```

That list is what to write down next. A row resolves itself the next time the
same question gets a strong answer, and `mimir report` counts what's open.
Only explicit recalls (CLI and MCP) feed it — never the per-prompt inject
hook. The query text stays on this machine (not synced, not exported), a query
the secrets scanner flags is never written, and rows age out with
`[learn] event_retention_days`. Turn it off with `[learn] log_misses = false`.

### Shadow ranking: test a ranking idea on real use before it ships

The retrieval eval proves a ranking change doesn't break the questions
someone wrote down; it can't say whether the change helps the questions
agents actually ask. Shadow mode runs a candidate ranking beside the live one
on every recall, keeps its order when the two differ, and scores both on what
the agent opened from that page — while recall keeps returning the live
order.

```sh
mimir shadow # candidate vs live: MRR of the opened hit, wins/losses, sign test
mimir shadow --days 30
```

The one candidate today is phase 2 of co-activation: lift a memory that is
co-recalled with one you opened in the last 30 minutes. Two honest limits.
The comparison **favours live**, because agents open what they were shown
near the top, so a shadow lead is real evidence and a shadow deficit isn't
evidence of harm. And it needs data: a co-activation edge only counts once
the pair has been opened together on several separate days, so expect
nothing for the first week or two. Nothing promotes itself — a winning
candidate goes live as an ordinary ranking change, through the eval gate.
`[learn] shadow_ranking = false` turns it off.

### The fast path (recommended setup)

Three commands get you the lowest-latency configuration Mimir has — measured
Expand Down
Loading
Loading