# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Releases before `2.0.0` are archived in [docs/CHANGELOG-archive.md](docs/CHANGELOG-archive.md).

## [Unreleased]

## [2.12.15] - 2026-07-06

### Fixed

- Add MCP descriptions for every core-profile tool so directory quality
  scanners and MCP clients receive useful tool metadata instead of empty
  descriptions.

## [2.12.14] - 2026-07-06

Post-Wave-5 robustness fast-follow for the history importers (from the Wave 5
whole-branch review).

### Fixed

- **Per-record error isolation in streaming importers.** A single malformed
  record no longer aborts the whole import. `iter_codex/opencode/chatgpt/
  claude_export_exchanges` now skip-and-continue on any per-record parse error
  (previously e.g. a non-object `message.data` in opencode raised an uncaught
  `AttributeError`). Valid records still import; matches the already-guarded
  mem0/zep path.

### Added

- **Source provenance on imports.** Memories imported by the streaming
  importers (codex / opencode / chatgpt / claude-export) now carry
  `extra.source = "imported:<source>"`, so imports are attributable and
  cleanable later — matching the `imported:mem0` / `imported:zep` markers.

## [2.12.13] - 2026-07-06

Ecosystem roadmap **Wave 5** (final) — benchmarks (workstream I, 7 tasks) +
maintenance-extras (workstream K, 7 tasks) + multimodal-import (workstream H,
8 tasks). **This completes the 11-workstream / 92-task ecosystem-learnings
roadmap** (Waves 1–5). Every new flag is default-off; the recall hot path is
untouched (empty diff); full non-MLX suite green (2960), mypy clean,
whole-branch opus review = SHIP.

### Added

- **Benchmark harness (`memo eval bench`).** LoCoMo / LongMemEval dataset
  schema + parsers, fetch/cache into an isolated store, an idempotent ingestion
  adapter (provenance + back-dating), per-category retrieval scoring via the
  shared `rank_hits` path, and a pluggable QA judge (local MLX default, API
  judge env-gated). `eval_recall` gains ranked metrics **Recall@K / NDCG@K /
  MRR** alongside precision@K.
- **Chunk→parent recall** (`MEMO_SEARCH_CHUNK_PARENT`, default off, eval-gated).
  In explicit `Memory.search`, a chunk hit maps to its parent memory. The 5s
  recall hook is untouched and excludes the reference tier, so this is a no-op
  there even when enabled.
- **Entity de-duplication.** MinHash+LSH blocking (pure-Python, entropy-gated)
  before any LLM merge, `graph.list_entities` / `merge_entity_pair`, and a
  nightly entity-canon dream pass (`MEMO_DREAM_ENTITY_CANON`, default off) that
  records merge counts in the receipt.
- **Consolidation bounds** — per-topic size invariant + synthesis body cap
  (memobase-style; `MEMO_SYNTHESIS_MAX_MEMBERS` / `_BODY_MAX_CHARS`, default 0 =
  off) and a per-folder vault-abstract dream pass (`synthesis_kind=folder_abstract`).
- **Multimodal & imports.** mlx-vlm image captions + mlx-whisper audio
  transcription into ingest (guarded optional deps, deferred imports, SHA256
  cache, opt-in `--include-audio` / caption flags); history importers for
  **Codex / opencode / ChatGPT / claude-export / mem0 / zep** (`memo import …`,
  CLI-gated, normal save path with dedup + provenance).

### Removed

- Placeholder CLIP multimodal stub — captions/transcripts flow through the text
  index, which is the real cross-modal retrieval path.

- Cold-start importers: `memo import codex` (Codex CLI rollouts, resumable
  cursors), `memo import opencode` (opencode SQLite), `memo import
  chatgpt` / `memo import claude-export` (data-export conversations.json) —
  all replay through the same mine-history extraction pipeline — plus
  `memo import mem0` / `memo import zep` store migrators (invalidated Zep
  facts are skipped).
- Ingest: optional mlx-vlm caption pass for images whose OCR yields
  little/no text (`MEMO_VLM_CAPTION_ENABLED`, default off) — text-free
  diagrams/photos become searchable via `<!-- VLM: … -->` blocks and
  caption-bodied orphan-image records (`vlm-caption` tag). Install deps
  with `pip install "mlx-memo[multimodal]"`.
- `memo ingest --include-audio`: vault audio files (m4a/mp3/wav/…) are
  transcribed via mlx-whisper (SHA256-cached) and indexed through the same
  pipeline as notes/PDFs, keeping the raw file as provenance. Opt-in.

### Removed

- The placeholder multi-modal store (`memo multimodal` CLI group and the
  `memo_multimodal_add_image/add_audio/search_images/search_audio/search_all`
  MCP tools). Its "universal embeddings" were a hash-based CLIP stub —
  cross-modal search over them was noise. VLM captions and whisper
  transcripts indexed through the normal text pipeline replace it.
  `memo_ocr_image` is unaffected.

## [2.12.12] - 2026-07-05

Ecosystem roadmap **Wave 4** — profile distillation (workstream B, 5 tasks) +
interfaces (workstream G, 14 tasks). Every new flag is **default-off** except
`MEMO_BRIEFING_PROFILE` (default on but a no-op until the default-off dream pass
produces a profile). Retrieval hot path untouched (empty diff); full non-MLX
suite green, mypy clean.

### Added

- **profile.md distillation (Tier-1 #1).** A nightly `memo dream run` pass
  (`MEMO_DREAM_PROFILE_ENABLED`, default off) distills preference/feedback/
  decision/synthesis memories into char-budgeted, rewritten-in-place profile
  documents (global + per-project) under `memory_dir/_profile/` with memory-id
  provenance, plus a **Standing rules** block graduated from grounding.log
  (cited in ≥K distinct sessions) and retired on resolved contradictions. El
  Briefing injects them at SessionStart via a zero-MLX file read
  (`MEMO_BRIEFING_PROFILE`, opt-out) — the "facts you wouldn't think to search
  for" channel that similarity recall structurally misses.
- **Typed knowledge-graph edges.** `- relation_type [[target]]` link grammar +
  bare `[[wikilinks]]` parsed into the crossref backlinks table at
  save/update/delete/reindex (`MEMO_CROSSREF_INDEX`, default off), with
  prefix-aware reverse traversal and cascade-aware warnings on supersede
  (`memo maintain`) and `memo_delete`.
- **Surgical edits.** `Memory.update(replace=(old,new))` / `append=` +
  `memo_update` params — edit a memory's body without a full rewrite.
- **`memo_offload` / `memo.offload`.** Content-addressed offload of large
  payloads to a `reference`-tier memory with a deterministic typed synopsis
  (recall-excluded; retrievable via drill-down).
- **`memo.integrations.wrap(client)`.** Wrap an LLM client for automatic
  pre-call recall injection + post-call capture (sync + async, idempotent
  re-wrap).
- **Prompt overrides.** All LLM system prompts route through `resolve_prompt`
  (`state_dir/prompts/<name>.md`), byte-identical to the built-in default until
  a user drops an override; captured memories carry a `prompt_version` stamp.
- **`memo maintain undo`.** `memo maintain` is now a Click group with
  timestamped receipts; `maintain undo [--run <ts>]` batch-restores from them.
- **FastMCP tool annotations.** `ToolAnnotations` (readOnly/idempotent/
  destructive hints) swept across every registered MCP tool, with a
  completeness gate.

## [2.12.11] - 2026-07-05

### Fixed

- **Recall daemon starvation on the GPU flock.** The daemon's embed thread
  could block indefinitely in `flock()` on the machine-global GPU lock behind
  batch MLX jobs (capture-stop, refresh-summary, idle-daemon, test suites)
  while holding its internal PriorityLock — every recall bailed at 2.5s
  (`recall_lock_bail` storms) and hooks degraded to subprocess+bm25. The
  daemon now takes a priority lane (`memo-mlx-gpu.prio.lock`): it holds the
  priority flock while waiting for / holding the main GPU lock, and
  non-priority processes probe it and back off, yielding at their next chunk
  boundary. Liveness is kernel-managed (flock drops on process death).
- New `mlx_gpu.gpu_deadline(seconds)` context manager; the daemon wraps all
  MLX work in it so a busy GPU raises `TimeoutError` instead of wedging the
  PriorityLock indefinitely.
- **Reindex `UNIQUE constraint failed: meta.path` skips.** With soft delete
  enabled, a deleted memory's tombstone row still occupies the
  `UNIQUE(meta.path)` index; when a new file (new id) reclaimed the same
  path, the path-collision guard could not see the tombstone and the INSERT
  failed. The guard now looks up stale rows with `include_deleted=True` and
  purges them with `hard_delete`.

## [2.12.10] - 2026-07-05

Ecosystem roadmap **Wave 3** — retrieval UX (workstream F, 13 tasks) + feedback
& eval instrumentation (workstream D, 9 tasks). Every new flag is **default-OFF
or opt-out**; retrieval ranking is unchanged at the shipped defaults (full suite
2813 passed, mypy clean, retrieval eval baseline prec@5 / noise@5 0.0). This
release also carries concurrent default-off recall/token-metering levers already
merged to master (precision-gate, intra-injection dedup, per-session budget
decay, token-meter).

### Added

- **Temporal retrieval.** `save()` can normalize relative dates
  (`MEMO_SAVE_NORMALIZE_DATES`); `store.search` / `Memory.search` gain
  `date_from` / `date_to` filters; `memo_search` accepts a natural-language
  `when` range (new leaf `nl_dates.py`).
- **Timeline primitives + `memo_around`.** `chunks_adjacent` /
  `records_around_created` + `Memory.around()` and an MCP `memo_around` tool for
  neighbor/around-a-memory navigation (`idx_meta_created` now back-fills on
  existing DBs).
- **Epistemic recall labels** (`MEMO_RECALL_EPISTEMIC_LABELS`) — presentation-only
  `⟨type · YYYY-MM⟩` / `⟨~inferred⟩` / `⟨?unverified⟩` prefixes; ranking untouched.
- **Omissions tail** (`MEMO_RECALL_OMISSIONS_TAIL`) — a budget-checked `+N more`
  line so agents don't read budget-trimmed hits as absent.
- **Recency band** (`MEMO_RECALL_RECENCY_BAND_DAYS`) — khoj-style union of the
  newest durable memories at the min_sim floor (eval-gated, default 0/off).
- **Unmatched-term honest-empty gate** (`MEMO_RECALL_UNMATCHED_TERM_GATE`) —
  inject nothing when the top score is weak and no distinctive term matches.
- **Multi-round ask** (`MEMO_ASK_MULTI_ROUND`) — one flag-gated extra retrieval
  round in `_build_ask_context` (recall hook untouched).
- **Next-turn verdicts → implicit feedback.** A heuristic ES+EN reaction
  classifier correlates the user's next turn to the prior recall and, behind
  `MEMO_VERDICT_ENABLED` (Stop-hook only), writes implicit `source_feedback`
  (never clobbers a manual vote).
- **Ablation instrumentation.** `MEMO_RECALL_DISABLE` turns are now stamped as a
  `via="disabled"` cohort; `memo roi` / `memo tokens` report with-vs-without
  cohort deltas (per-cohort turns, grounded-per-turn, re-ask rate).
- **Eval/tuner depth.** Negative labels (`avoid_ids` + `harvest_negative_labels`
  from verdict.log, `memo eval harvest --negatives`); a HyDE eval seam
  (`Cfg.flag_overrides` env-pin + named config `J`); a nightly HyDE A/B pass
  (`MEMO_DREAM_HYDE_TUNE_ENABLED`, overlay-applied, hybrid-hook vetoed); and an
  MLX paraphrase label expander (`memo eval expand-labels`).
- **Quarantine + graduation.** `_uncertain` captures are recall-excluded
  (`MEMO_RECALL_EXCLUDE_UNCERTAIN`, opt-out) and a nightly graduation pass
  (`MEMO_DREAM_GRADUATION_ENABLED`) promotes corroborated ones.

## [2.12.9] - 2026-07-04

Internal simplification of the Wave 2 code — behavior-identical (full suite,
mypy, ruff, and the retrieval eval all unchanged: baseline prec@5, noise@5 0.0).

### Changed

- **Corroboration bump deduplicated.** The `if MEMO_SUPPORT_COUNT: suppress:
  bump_support_batch(..., lift=support_lift())` block was copy-pasted at four
  call sites (near-duplicate save, `topic_key` upsert, and both consolidation
  merge paths). Collapsed into one `record.bump_support_if_enabled(store, ids)`
  helper that owns the flag gate + lift resolution — the store SQL layer stays
  flag-free and the policy lives in exactly one place.
- **Capture provenance deduplicated.** The identical provenance-bag construction
  in `run_capture` and `run_capture_incremental` (session/turn stamp + opt-in
  tool-file arrays) is now one `capture._capture_provenance()` helper.

## [2.12.8] - 2026-07-04

Ecosystem roadmap **Wave 2** — confidence lifecycle (workstream C) + capture
depth (workstream E). Every new flag is **default-OFF** except the signed-off
pure counter `MEMO_SUPPORT_COUNT`; nothing changes retrieval ranking at the
shipped defaults.

### Added

- **Corroboration counter (`support_count`).** A re-asserted memory now
  *counts* instead of discarding the signal: a new `support_count` column in
  `memory_health` (survives `reindex --rebuild`, syncs cross-machine via
  `dump_signal`/`merge_signal` with `max()` merge) is bumped at the three
  existing corroboration sites — near-duplicate save, `topic_key` upsert, and
  consolidation merge. Gated by `MEMO_SUPPORT_COUNT` (default on, pure counter).
- **Bounded confidence lift from corroboration.** `MEMO_SUPPORT_CONFIDENCE_LIFT`
  (default `0.0` = off) lets a re-assertion restore confidence toward — never
  past — neutral `1.0`, so a repeatedly-confirmed fact recovers from an earlier
  contradiction/quality penalty. A ranking input only when enabled.
- **Supersede gate.** `MEMO_SUPERSEDE_SUPPORT_GATE` (default `0` = off): when
  set, `memo maintain` refuses to auto-archive the losing side of a
  contradiction whose `support_count` meets the gate — the pair stays open for
  triage and is reported under `flagged_for_review` in the receipt.
- **Supersede provenance.** Archiving a contradiction loser now stamps
  `superseded_by` + `superseded_at` into the archived `.md`'s `extra` bag
  (portable, best-effort — a stamp failure never un-archives).
- **Mutability classes.** `MEMO_CONTRADICT_MUTABILITY` (default off): an LLM
  "contradiction" verdict between two volatile-class bodies (ports, versions,
  status) is downgraded to an `evolution` (a normal update, not a conflict).
  The volatile regex is deliberately conservative on Spanish text.
- **Absorb-on-recurrence.** `MEMO_SAVE_ABSORB` (default off): a near-duplicate
  save folds into the *existing* record via one bounded LLM call routed through
  the versioned `update()` (rollbackable), growing `proof_count` — instead of
  creating a near-copy. Best-effort; any failure falls back to warn-and-create.
- **`memo invalidate <pattern> --reason`.** Reversible bulk confidence
  weakening: one event (a stack migration, a port change) weakens every
  matching memory (confidence penalty + `_invalidated` tag/stamp), writes a
  receipt of prior confidences, and `memo invalidate --undo` restores them.
  Preview-only without `--yes`. Flag `MEMO_INVALIDATE_PENALTY` (default `0.3`).
- **Procedural memory types.** New durable types `procedure` (how-to workflows)
  and `failure_pattern` (structured Pattern/Context/Wrong/Right mistake notes);
  the capture extractor now mines both.
- **Capture provenance.** Every capture- and mine-extracted memory is stamped
  with `session_id` / `transcript_path` / `turn_hash`, plus structured
  `files_read` / `files_modified` arrays from the session's tool stream (gated
  by the existing `MEMO_CAPTURE_TOOL_EVIDENCE`).
- **By-file search lane.** `Memory.search_by_file` + a `file=` parameter on the
  MCP `memo_search` tool: a high-precision post-filter over the captured
  file arrays. `search()` itself is unchanged.
- **`memo mine-git`.** A deterministic (no LLM) miner that turns a repo's
  fix/revert commits into `failure_pattern` seed memories with commit-SHA
  provenance; resumable per repo.
- **PreCompact capture flush.** A `PreCompact` hook runs `capture-tick --force`
  at the compaction boundary so a long session's insight reaches `.md` before
  early context is destroyed, and the briefing re-fires after a compaction.
  Wired in the plugin and self-healed into `settings.json`.

### Changed

- `bug` memories now join `failure_pattern` and `procedure` in
  `EVICTION_PROTECTED_TYPES` — hard-won failure/how-to knowledge is exempt from
  the nightly prune-floor and LFU-eviction passes even when rarely accessed.

## [2.12.7] - 2026-07-04

### Added

- **Privacy layer (default-on secret redaction).** A new pure `memo.redact`
  module masks provider-prefixed API keys (AWS, GitHub, OpenAI, Anthropic,
  Slack, GCP) and PEM private-key blocks to `****<last4>` and strips
  `<private>…</private>` spans. Redaction runs before persisting on every
  capture path (Stop-hook capture, `capture-tick`, `mine-history`) and on
  `memo ingest` index rows — the vault `.md` on disk is never rewritten, and
  redaction is deterministic so `reindex` reproduces identical masked rows.
  Redacted memories carry a `_redacted` tag. Flags: `MEMO_REDACT_SECRETS`
  (on), `MEMO_PRIVATE_MARKERS` (on), `MEMO_REDACT_ENTROPY` (off, opt-in
  high-entropy tier; hex hashes/ids always exempt).
- **Pre-push secret gate for git sync (`MEMO_SYNC_SECRET_GATE`, on).** Before
  the sync commit, staged `.md` additions are scanned (pattern tier only);
  on a hit the commit+push are blocked, `sync_pending` is stamped with the
  reason, and `memo sync status` / `memo doctor` surface it. Blocks the
  commit (not just the push) so a secret never enters git history.
- **Per-call write scope on `memo_save` (MCP).** New `scope` param:
  `"global"` skips the auto `project:<repo>` tag (memory lands in the global
  recall tier); `"project"`/omitted keep auto-detection; an explicit
  `project:` tag always wins.
- **Project→global retag dream pass (`memo dream retag`, default-off).**
  `MEMO_DREAM_RETAG_GLOBAL_ENABLED` promotes memories that were grounded from
  ≥ `MEMO_DREAM_RETAG_MIN_PROJECTS` other projects to global via the
  tag-only update path (no re-embed, reversible with `memo version rollback`).

### Fixed

- **Git worktrees no longer mint a bogus project tag.** `project.py` now
  canonicalizes a linked-worktree `.git` file to the main repo's toplevel, so
  memories saved from a release worktree (e.g. `/tmp/rel`) tag as the real
  project instead of `project:rel` forever. Pure file reads — the 5s
  recall-hook budget is untouched. Submodule and non-worktree `.git` files
  keep their own-basename behavior.

## [2.12.6] - 2026-07-03

### Fixed

- Pin `transformers<5.13` on the Apple Silicon install: transformers 5.13
  broke `AutoTokenizer.register(str)` which mlx-lm (≤0.31.3) still calls at
  import, so any fresh `mlx-memo` install crashed on the first embed
  (`AttributeError: 'str' object has no attribute '__module__'`). 5.12 and
  below verified working; lift the cap when mlx-lm ships a compatible release.

## [2.12.5] - 2026-07-03

### Fixed

- eval recall now mirrors the hook's reference-tier SQL exclusion
  (`MEMO_RECALL_EXCLUDE_REFERENCE`): the eval's explicit `mem.search()` pool
  included bulk `reference` chunks the live hook never surfaces, so ingested
  vault content (e.g. WhatsApp conversations) crowded top-K and false-failed
  the regression gate (prec@5 0.884 → 0.845 with zero production impact).
  With the exclusion mirrored the gate passes at 0.903.

## [2.12.4] - 2026-07-03

### Fixed

- `build_labels()` (tuner objective) now passes the curated document's
  `noise_tags`/`noise_path_fragments` into the LabelSet — same fix as the
  curated gate: the knob line-searches' noise@K was a vacuous 0.0.
- Capture `retyped` counter compares against the normalized claimed type —
  a whitespace-only extractor type no longer counts as a spurious retype.

### Changed

- Coverage floor ratcheted 64 → 68 (measured 70% after the Q3 programs).

## [2.12.3] - 2026-07-03

### Fixed

- **Recall daemon latency tail.** Two real causes fixed: (1) priority drift —
  the `embed_query` branch acquired the daemon lock at priority 1 (same as
  interactive recall) with a 60s timeout, so an embed burst (memflow vec
  indexing, eval runs, grounding scoring) queued at recall's own priority and
  burned the hook's 2.5s budget; `embed_query`/`search`/`embed_batch` now run
  at priority 0 and interactive recall genuinely outranks them. (2) Cold MLX
  load no longer counts against queued recalls: the model warms in a
  background thread at daemon start; while warming, recall ops bail `{}` fast
  (hook falls back to subprocess, same as socket-absent) and non-latency-bound
  ops wait the event out. The socket still binds immediately, so
  `memo recall-daemon start`'s probe succeeds on cold starts. Observability: a
  recall waiting >500ms or bailing on lock-busy emits one structured stderr
  line (`recall_lock_wait`/`recall_lock_bail`/`recall_warming` with `held_by`).
  A timed-out high-priority waiter now wakes re-slept priority-0 waiters
  (liveness gap).
- `memo dream status` renders the `capture_weights` fragment; `memo
  debug-recall` renders the `synthesis_boost`/`mmr` explain stages; capture
  gains a `retyped` counter; `type_weights.json` staging file is pid-suffixed;
  the tuner's curated gate now measures real noise@K (document-level
  `noise_tags`/`noise_path_fragments` reach the LabelSet).

## [2.12.2] - 2026-07-03

### Fixed

- Hook-side session writers (`stamp_recall_turn`, `mark_ids_recalled`) now
  refresh the snapshot's `updated` timestamp — a session that only ever got
  hook stamps sorted oldest-by-`updated` and the session-cap GC evicted it,
  wiping its `recalled_ids` before Stop-hook cited-grounding could match.

## [2.12.1] - 2026-07-03

### Fixed

- **Cited-grounding was dead on the daemon (production) recall path.** Only
  the subprocess fallback marked session `recalled_ids`; the warm daemon —
  which serves virtually every recall — never did, so `match_cited` always
  compared against an empty map and not one `method="cited"` grounding row
  existed (409/409 recalled-never-cited). `_recall_logic` now mirrors the
  subprocess path exactly: session dedup (already-recalled hits are not
  re-injected — also a token saving every turn) + `mark_ids_recalled`.
  End-to-end regression test covers daemon recall → citing answer →
  `method=cited` row.

## [2.12.0] - 2026-07-02

### Added

- **Recall-faithful eval for boosts.** `knobs_from_flags()` is the single
  source of `RankKnobs` resolution (env > tuned overlay > registry default),
  shared by the daemon recall path, the eval harness, and the hook; the
  extracted `apply_injection_filters()` (skip-below / gap trim) is shared by
  hook and eval so injection semantics cannot diverge. The eval grid gains
  MMR / synthesis variants (`E mmr/0.3` … `I synth/0.10`) and
  `Cfg.knob_overrides`, so any ranking knob is measurable offline.
- **Project-aware grounding and labels.** `grounding.log` entries record the
  session's `project` tag; harvested eval labels carry it through, and the
  eval ranks project-carrying labels with the hook-faithful `project_tag` —
  project/global boosts are now measurable offline per-label.
- **Nightly tuner searches the new knobs.** `MEMO_RECALL_MMR_LAMBDA`
  (0/0.3/0.5/0.7) and `MEMO_RECALL_SYNTHESIS_BOOST` (0/0.05/0.10) join the
  line-search under `MEMO_DREAM_TUNE_ENABLED`, with a curated no-regression
  gate, a latency gate (candidate p50 must stay within +25% of baseline),
  the single-apply guard (one knob change per night across all knobs),
  per-knob baselines, and the knob-generic online revert.

### Changed

- **Recall-hook subprocess fallback unified onto `rank_hits`.** The inline
  ranking chain that predated `rank_hits` is gone; daemon and subprocess
  paths now produce identical injections for identical inputs (parity-tested,
  including with MMR/synthesis/preference knobs on).
  `MEMO_RECALL_STALENESS_DAYS` and `MEMO_RECALL_ADAPTIVE_CONTEXT` had no
  daemon-path consumer and are now inert — marked deprecated in the registry.

### Fixed

- Tuner online-revert now merges into the scalar overlay, so bool/str levers
  (e.g. `MEMO_RECALL_MODE`) survive a revert instead of being dropped.

## [2.11.0] - 2026-07-02

### Added

- **Recall observability pipeline (Q3 Mes 1).** Two nightly dream passes:
  `harvest_labels` mines eval labels from real citations in `grounding.log`,
  `eval_recall` runs a retrieval-only eval (prec@K / noise@K) against
  curated + harvested labels, recording to the dream receipt and
  `state_dir/eval/history.jsonl` (`MEMO_DREAM_EVAL_ENABLED`, default on;
  `MEMO_DREAM_EVAL_MAX_LABELS`, default 200).
- **Recall-hook latency metrics.** Both hook paths (warm daemon and
  subprocess fallback) stamp `total_ms`/`path`/`hits` to
  `state_dir/recall_metrics.jsonl` (`MEMO_RECALL_METRICS`, default on);
  `memo stats` shows p50/p95/p99 per path over the last 7 days.
- **`memo debug-recall <prompt>`.** Diagnostic command reproducing the
  recall pipeline outside a session: per-hit vec / BM25 / rerank-fused
  scores, boost deltas, floor verdicts, and active thresholds
  (including `skip_below` / `gap_threshold`); `--json` for scripting.
- **TUI recall-quality panel.** prec@5 trend sparkline, top cited
  memories, and recalled-but-never-cited count in `memo tui`.
- **Capture hygiene (Q3 Mes 2).** Meta-commentary filter drops process
  narration and trims filler openers keeping the substance
  (`MEMO_CAPTURE_META_FILTER`, default on); intra-batch near-dup window
  collapses retry twins before the store check
  (`MEMO_CAPTURE_BATCH_DEDUP`, default on); type-classification
  confidence stamped in `extra.capture_confidence`, low-confidence
  captures tagged `_uncertain` below `MEMO_CAPTURE_MIN_CONFIDENCE`
  (default 0.0 = off).
- **Citation-type feedback.** Nightly `capture_weights` dream pass
  computes per-type citation rates into
  `state_dir/capture/type_weights.json`; the capture classifier consults
  them as a tie-breaker in ambiguous classifications
  (`MEMO_CAPTURE_TYPE_FEEDBACK`, default off).
- **Reference-tier search floor.** `MEMO_REFERENCE_SEARCH_FLOOR` (default
  0.0 = off) keeps bulk-ingested reference chunks out of
  search/ask results unless they clear a higher score bar; durable
  memories unaffected.
- **Ranking knobs, evidence-gated (Q3 Mes 3), all default off.** MMR
  diversity re-rank (`MEMO_RECALL_MMR_LAMBDA`), synthesis-type boost
  (`MEMO_RECALL_SYNTHESIS_BOOST`), and cited-weighted outcome utility
  (`MEMO_OUTCOME_CITED_WEIGHT`, active only inside the
  `MEMO_OUTCOME_RANKING_ENABLED`-gated path).

### Fixed

- `packaging/mcpb/manifest.json` was left at 2.10.1 by the v2.10.2
  release; version-sync test now green.
- Recall-metrics `hits` now counts the post-dedup injected hits on both
  hook paths (comparable daemon vs subprocess).
- Nightly eval cross-dedups harvested labels against the curated set so
  a prompt never double-weights prec@K.

## [2.10.2] - 2026-07-02

### Changed

- Test suite: `ruff format` pass across all 143 test files (style-only, no logic changes).

## [2.10.1] - 2026-07-02

### Fixed

- Presence polish pass: per-process tmp file for `presence_today.json` writes
  (no lost updates between concurrent writers); `systemMessage` titles are
  forced single-line; cited-id parsing accepts uppercase hex (normalized);
  memories cited from earlier turns are grounded even when the current turn
  had no recall hits; statusline date extraction is GNU/BSD-portable;
  "stale memories archived" label; broader statusline + full-hook test
  coverage.

## [2.10.0] - 2026-07-02

### Added

- **Presence program** — memo's background work is now visible to the human:
  - Recall hook emits a human-visible `systemMessage` line (`🧠 memo · N: titles`)
    on every prompt with recall hits, on both the subprocess and warm-daemon
    paths (`MEMO_RECALL_SYSTEM_MESSAGE`, default on).
  - Injected recall block asks the model to cite used memories inline by short
    id (`MEMO_RECALL_CITE_INSTRUCTION`, default on); cited `[id8]` references
    are validated against session-recalled ids and logged as the strongest
    grounding signal (`method="cited"`, used_score=1.0), feeding roi/usefulness.
  - Statusline badge shows today's live activity —
    `[Memo <ver> · 🧠recalls · 💾saves · ~tokens saved]` — from a new
    `presence_today.json` state file written by the recall hook, `Memory.save()`,
    and capture-stop (`MEMO_STATUSLINE_ACTIVITY`, default on).
  - SessionStart briefing gains a one-shot "☾ Last night" digest of the nightly
    dream receipt (`MEMO_BRIEFING_DREAM_DIGEST`, default on).
  - `memo install-mcp --write` seeds one real memory recording the install and
    the first briefing surfaces it once ("🧠 memo remembers…") — onboarding
    proof powered by the real recall mechanism.

### Fixed

- Cited-id grounding matches the 8-char truncated ids stored in
  `recall_hook.log` (prefix-normalized) — no dead comparisons, no duplicate
  grounding rows.
- Install-seed side effect is isolated from the shared test state dir.

## [2.9.8] - 2026-07-02

### Fixed

- **Sync can no longer destroy memories through a stale interrupted rebase.** A rebase killed mid-conflict (git timeout, machine sleep, session teardown) left `.git/rebase-merge` behind; the next sync matched `--skip` in git's "already a rebase in progress" error and ran `git rebase --skip` — silently dropping the local memories commit and committing raw conflict markers into `.md` files, which then pushed to every other machine. Sync now aborts a stale rebase before pulling (reported as `stale_rebase_aborted`), `_commit_local` refuses to commit unmerged paths, and the recovery loop treats "already a rebase" as a hard error instead of a skippable conflict.
- **`memo sync pull` / `memo sync push` now take the machine sync lock.** Both called the git layer directly, bypassing the `.sync.lock` flock that `memo sync once` holds — a SessionStart pull could drive or abort a concurrent Stop-hook rebase mid-flight. They now route through the same locked coordinator and soft-skip when another sync owns the lock. Git subprocess timeouts (`subprocess.TimeoutExpired`) are wrapped as `SyncGitError`, so `sync_once`'s "never raises" contract and the `--quiet` hook paths hold under a blackholed network.
- **Path traversal via `project:` tags is closed.** A tag like `project:../../evil` was used verbatim as the on-disk bucket, letting `memo save`/`memo_save` create directories and plant `.md` files outside the vault (and `memo migrate --bucket-by-project` rename memories out of it — data loss on the next reindex). The derived folder is now slugified (the tag itself is stored unchanged), and both the save and migrate paths enforce a containment check against `memory_dir`.
- **Recall daemon startup is race-free.** `recall-daemon start` fired concurrently (SessionStart hooks, launchd respawns) could orphan a live MLX daemon on an unlinked socket and later have the orphan's shutdown unlink the survivor's socket, silently killing warm recall. `run_server` now takes the same non-blocking startup flock as the maint/ingest daemons, and daemon cleanup only removes socket/pid files it owns (a live foreign owner's files are left alone). The idle-capture daemon got the same child-side guard against duplicate loops.
- **`gpu_guard(timeout=...)` actually times out across processes.** The deadline only covered the in-process RLock; the cross-process file lock was a plain blocking `flock`, so one stuck MLX pass wedged every other memo process (recall daemon, CLI, MCP) machine-wide with no `TimeoutError`. The flock acquisition is now bounded by the same deadline.
- **Token ledger survives concurrent Stop hooks.** `write_ledger` used a fixed tmp filename, so two sessions rolling up simultaneously could replace the ledger with a half-written file — `read_ledger` then silently reset to empty and historic days beyond the grounding-log window were permanently lost. Writers now use unique temp files and the read-merge-write runs under a sidecar flock, matching the repo's dashboard-logs pattern.
- `memo release bump`/`sync` now edit `packaging/mcpb/manifest.json` (version + pinned `mlx-memo` spec) — `release check` validated it but `bump` never wrote it, so every release tripped the check.
- **`device_id` first-run mint is atomic.** Concurrent first sessions could mint divergent machine ids (phantom attribution in history events) or read an empty id mid-write; the id is now published via an exclusive atomic link and losers adopt the winner's id.

## [2.9.7] - 2026-07-01

### Fixed
- Release version alignment now includes the internal Codex plugin bundle, with `memo release sync` to realign drifted manifests to `pyproject.toml` and `memo update` refreshing installed static agent artifacts for Claude Code, Codex, and Devin after a successful runtime update.
- The retired desktop-agent surface was removed from CLI, docs, tests, and installer flows; use Devin Desktop via `--client/--agent devin-desktop`, `DEVIN_DESKTOP_MCP_CONFIG`, and `~/.devin/mcp.json`.
- The HTTP API no longer hardcodes its OpenAPI and `/health` version; both use the runtime `memo.__version__`.
- `memo release bump` detects surplus version fields again (the `re.subn` count cap had silently disabled over-match detection), and `memo release check` validates every `server.json` package instead of only the first.
- `memo update` no longer masks a failed `claude plugin install` as "already handled" — a real failure now surfaces as a warning and the refresh is reported honestly.
- Three dead flags are actually wired now: `MEMO_TANTIVY_ENABLED=0` really forces FTS5-only (it previously did nothing — only `MEMO_FTS_BACKEND` gated the index), `MEMO_SEARCH_JSON_BODY_CHARS` drives the default `--body-chars` of `memo search`/`memo recall`, and `MEMO_DREAM_MINE_LIMIT` caps the labels mined per dream tuning pass (was hardcoded at 200).
- Unix-socket daemons (recall/ingest/maint) can no longer hang indefinitely on SIGTERM: handler threads are daemonic, so the shutdown `join_timeout` is a real bound (stdlib `ThreadingMixIn` defaults joined in-flight handlers unbounded in `server_close()`).
- Dream `eviction`/`compress` DB errors now propagate into the receipt's `errors` list instead of reading as "nothing to do".
- The HTTP API's lazy `Memory` init is thread-safe — concurrent first requests no longer construct duplicate `Memory` instances (duplicate sqlite connections + embedder load).
- Historical CHANGELOG entries no longer retroactively claim Devin Desktop support for releases that actually shipped the retired client's paths.
- Test hygiene: runtime-isolation tests run against a sandboxed `$HOME` (they wrote real `~/.memo/bin` shims and `~/.zshrc` PATH snippets on every run) and no longer depend on the developer machine's live index for `MEMO_EMBEDDER_DIMS`; the agent-artifact "skip" test is hermetic and uses recording stubs that can actually fail.

## [2.9.6] - 2026-07-01

### Fixed
- **`install-mcp` now merges JSONC agent configs.** Comments (`//`, `/* */`) and trailing commas are tolerated when merging an existing config (Zed `settings.json`, VS Code, Cursor). Previously these raised "not valid JSON". Comments are not preserved on rewrite; the config data is.

## [2.9.5] - 2026-07-01

### Fixed
- **CI `mypy` step is green again.** `fastapi` / `uvicorn` are optional (`[http]`) dependencies not installed in the default `[dev]` CI runtime, but `server_http.py` / `cli_http.py` import them at module level — so `mypy src/memo` failed with `import-not-found` (a pre-existing gap, unrelated to the 2.9.4 changes). Added them to the `[[tool.mypy.overrides]]` `ignore_missing_imports` list, matching how the `[cpu]`/MLX optional extras are already handled. No runtime change.

## [2.9.4] - 2026-07-01

### Added
- **MCP server surface is now under test.** 21 new `tests/test_server_*.py` files cover the previously-untested `server_*` domains (analytics, asof, backup, cache, collaborative, contextual, contradict, core_history, entities, episodes, feedback, graph, import_export, links, multimodal, query, reflect, repo, sync, temporal, version) — each mocks `Memory`, invokes every tool through its `register()`, and asserts the returned envelope. Measured suite coverage rose ~60% → 67%, and the coverage floor (`fail_under`) is ratcheted **58 → 64**.

### Fixed
- **`delete()` rollback no longer drops the sqlite-only dedup keys.** `topic_key` / `normalized_hash` live only in the sqlite index (never in the `.md` frontmatter), and `store.get()` omits them — so a failed-final-unlink rollback restored the row *without* them, and a later same-topic save would then create a **duplicate** instead of updating in place. A new `VecStore.get_dedup_keys()` (mirroring `get_embedding_blob` / `get_fts_body`) pre-fetches them so the rollback restores the row faithfully.
- **`delete()` mutates derived/audit state only after the authoritative unlink.** History logging, graph-edge drop, and receipt/event emission now run strictly *after* the canonical `.md` is removed. A failed unlink therefore rolls back cleanly with **no spurious `delete` audit event** and no dropped graph edges for a memory that actually survives the failure.

### Changed
- **`cli_dream.py` god-file thinned** (1228 → 1142 LOC): five extractable helpers (`_state_path`, `_older_id`, `_corpus_fingerprint`, `_make_progress`, `_render_run_summary`) moved into `cli_dream_passes.py` and re-exported, so existing imports keep resolving. Behaviour-preserving; the CLI file stays wiring-only per the repo convention.

## [2.9.3] - 2026-07-01

### Fixed
- **`memo dream run` no longer emits misleading warnings for by-design behavior.** Two `WARNING`-level log lines fired on every manual dream run despite the run succeeding (`receipt["errors"]` stayed empty):
  - *Near-duplicate save nag.* The save-time dedup advisory (`consider `memo update` instead`) is only actionable for an interactive human, but dream's signal-gather/synthesize passes save near-duplicates by construction (the same run's consolidate pass merges them). A new `derived_save_scope()` (a `ContextVar` in `memory/record.py`) marks batch/derived saves; `dream run` wraps its whole pipeline and `apply_merge` wraps its merged save, so the nag drops to `DEBUG` in those paths. A human's direct `memo save` still gets the warning.
  - *Consolidation merge-proposal JSON unparseable.* The retry only flipped decode temperature (0.0→0.3), which cannot recover a proposal truncated by too small a token budget. The retry now escalates **both** temperature and `max_tokens` (1536→3072) so a long merged body completes instead of being cut mid-value; skipping the cluster remains the safe final fallback (originals untouched, retried next night).

## [2.9.2] - 2026-07-01

### Fixed
- Recover `transcript_path` by `session_id` when hook payloads omit it — since 2026-06-27 some Stop/UserPromptSubmit hook payloads stopped carrying `transcript_path` while `session_id` kept arriving, silently starving `capture-stop`'s grounding scoring (and therefore the token-savings ledger `memo tokens` reads), session autosave's payload persistence, and session checkpoint's snapshot fields. Recovered via a `~/.claude/projects/*/<session_id>.jsonl` glob fallback.

## [2.9.1] - 2026-07-01

### Changed
- The nightly `memo dream run` LaunchAgent template now enables the self-improving recall tuner by default (`MEMO_DREAM_TUNE_ENABLED` + `MEMO_DREAM_TUNE_BOOST_ENABLED`). Every applied change is verified against real grounding by the online proof loop and reverted if it regresses, so on-by-default is safe. Reversible by removing the flags from the plist.

## [2.9.0] - 2026-07-01

### Added
- **F4 consolidate-reuse metric** (`memo dream consolidate-reuse`): read-only report of whether the `type=synthesis` memories the episodic-consolidation pass creates actually get grounded/reused in real recall (n_consolidated / n_reused / reuse_fraction). Measures the value of consolidation with real data.
- **Online-only project-boost explorer** (`MEMO_DREAM_TUNE_BOOST_ENABLED`, OFF by default): nudges `MEMO_RECALL_PROJECT_BOOST` and lets the online proof loop confirm/revert it against real grounding. Boosts are not offline-measurable (the label eval has no project context), so this knob is tuned purely by real outcomes — no offline gate. Rides the generic proof loop (generic per-knob revert). Direction is hill-climbed from the ledger (repeat confirmed, reverse reverted).

### Fixed
- Online proof-loop revert self-heals the overlay's `_meta.prev` so the offline rollback-guard cannot resurrect a just-reverted config under index drift.

## [2.8.2] - 2026-07-01

### Fixed
- Online proof-loop revert now self-heals the tuned-params overlay's one-step `_meta.prev`, so a later offline rollback-guard can no longer resurrect the config the online loop just reverted away under index drift. (Gated behind `MEMO_DREAM_TUNE_ENABLED`, OFF by default.)

## [2.8.1] - 2026-07-01

### Fixed
- Proof-loop deferral is now checked BEFORE the (expensive) MLX search in the graph tuner passes: when a min_sim change is being proven or a revert cooldown is active, the graph passes skip the search entirely instead of grid-searching and only then deferring. Surfaced by end-to-end empirical testing of the proof loop. (Still gated behind `MEMO_DREAM_TUNE_ENABLED`, OFF by default.)

## [2.8.0] - 2026-07-01

### Added
- **Generic online proof loop** for the recall self-tuner — out-of-sample grounding verification now covers any tuned knob, not just `min_sim`. The graph-proximity-weight tuner joins the proof loop: each applied change is confirmed or reverted by real grounding under its new params version, with a knob-generic revert that restores the correct per-knob offline baseline. A one-cycle revert cooldown stops a co-gated pass from re-applying a just-reverted value the same night. (All gated behind `MEMO_DREAM_TUNE_ENABLED`, OFF by default — no default behavior change.)
- Proof-loop ledger, `memo dream status`, and `memo dream timeline` now label each entry by the tuned knob (`min_sim` / `graph_proximity_weight`).

## [2.7.0] - 2026-07-01

### Added
- **Self-improvement proof loop** for the recall self-tuner — all gated behind `MEMO_DREAM_TUNE_ENABLED` (OFF by default), no default behavior change. Each nightly `min_sim` change the tuner applies is now judged out-of-sample by the real grounding accumulated under its new tuned-params version, reverted if that regresses, and recorded in a durable ledger.
  - `params_version` attribution stamped on every grounding row; `memo eval baseline` snapshots offline precision/noise + online grounded/tokens (7d/30d) + the active params version.
  - Online-guarded confirm/revert/wait/expire verdicts (`resolve_pending`), with a self-contained revert that restores the pre-apply floor and offline baseline.
  - One overlay change per proof cycle: the graph tuner passes defer (`deferred_pending`) while a `min_sim` change is being proven, so its grounding cohort is never orphaned.
  - Graduation-readiness checker surfaced in `memo dream status`; `memo dream timeline` renders the proof-loop history with realized online impact.
- Flags: `MEMO_DREAM_TUNE_MIN_COHORT` (20), `MEMO_DREAM_TUNE_ONLINE_EPS` (0.02), `MEMO_DREAM_TUNE_GRADUATION_K` (5).

## [2.6.11] - 2026-06-30

### Fixed
- `memo init` now exits cleanly with guidance on a non-TTY / non-interactive shell instead of crashing inside the interactive picker.
- LLM features (`ask` / `synthesize` / `dream`) on the CPU (non-MLX) backend now print the "requires the MLX runtime" guidance as a clean error instead of an uncaught traceback.
- MCP `serverInfo.version` now reports memo's own version instead of the FastMCP framework version.

### Changed
- README: corrected the CLI command count (95 → 105) and the full MCP surface count (123 → 126).

## [2.6.10] - 2026-06-30

### Fixed
- Dashboard historic tokens-saved now reads from the durable per-day ledger (`token_ledger`) instead of the capped `grounding.log`, so the gerencial headline keeps growing like `memo tokens` instead of plateauing once old grounded rows rotate out of the log.

### Performance
- Reranker: reverted the batched cross-encoder forward (it regressed the configured 4B model) while keeping the per-pair head-slice in `score()` — projecting only the last token through the LM head.

## [2.6.9] - 2026-06-30

### Added
- `memo tokens` — TUI showing how many tokens memo saved today / this month / all-time, with big-number panels (HOY/MES/HISTÓRICO) plus daily and monthly bar charts and a month-over-month growth indicator (`--json` for machine output). Savings are attributable to memo alone: it counts *grounded* recalls — surfaced memories the answer actually used (re-derivations memo prevented) — times `MEMO_ROI_TOKENS_PER_GROUNDED`, so the total rises as memo accumulates more useful memories.
- Durable token-savings ledger (`token_ledger.py`, `state_dir/token_savings_daily.json`). `grounding.log` is capped (~12 days) and rotates, so an all-time total read from it alone would plateau; the ledger folds grounded events into a monotonic per-day file before they evict, giving `memo tokens` a durable, ever-growing historic total. Rolled up on the Stop hook and on demand.

## [2.6.8] - 2026-06-30

### Added
- `[Memo <ver>]` badge in opencode. opencode has no native statusline/tagline slot for custom text (plugin status-bar widgets are an open feature request), so `startup-banner --agent opencode` now stamps the live memo version into opencode's `username` config (`<base> · [Memo <ver>]`), shown next to each user message. Idempotent upsert into the pure-JSON `opencode.json` (merges with `opencode.jsonc`); no-op when opencode is absent.

## [2.6.7] - 2026-06-30

### Added
- `memo recall-daemon restart` subcommand (was only start/stop/status). Launchd-aware: SIGTERM via stop, then waits up to ~5s for a KeepAlive respawn — if a new live PID appears it defers to launchd instead of spawning a competing daemon; otherwise it starts a fresh one. Use after a runtime upgrade so the daemon reloads new code.
- Successful memo updates notify Codex with `Plugin updated: memo · Run /reload_plugins to apply`.

### Changed
- Startup banner and statusline badges now render as `[Memo <version>]` instead of `[MEMO <version>]`.

### Fixed
- Codex shims now also emit a delayed `[Memo <version>]` Codex/Supacode notification so the memo version remains visible after the Codex TUI takes over the terminal.
- Agent shims now always render memo's own startup banner before delegating to downstream wrappers or binaries, and refresh stale PATH snippets so memo stays ahead of downstream wrappers.

## [2.6.6] - 2026-06-30

### Fixed
- Recall-daemon `embed_query` no longer bails after a hardcoded 5s wait for the shared embedder lock — shorter than a single 4B `embed_batch` chunk hold (60s). A heavy job like `memo dream run`, routing through the auto-detected warm daemon, self-contended: its own batch embed held the lock while its interleaved `embed_query` timed out at 5s and paid a redundant ~2s in-process cold MLX load (the `timeout acquiring lock` → `falling back to in-process` warning pair). New `MEMO_EMBED_LOCK_TIMEOUT_MS` (default 60000) matches the `embed_batch` hold so `embed_query` waits out the in-flight chunk instead of bailing. Mirrors the existing `MEMO_RECALL_LOCK_TIMEOUT_MS` pattern; background callers aren't latency-bound so the longer wait is free.

## [2.6.5] - 2026-06-30

### Fixed
- The MCP-registry `server.json` description is back under the registry's 100-character limit (2.6.4's longer Linux-mentioning description was rejected with HTTP 422, so the registry stayed at 2.6.3). Shortened to "Memory for AI agents — MLX (Apple Silicon) or CPU (Linux), sqlite-vec + BM25, zero cloud." — still names the Linux path. PyPI/plugin descriptions (no length cap) keep the fuller wording.

## [2.6.4] - 2026-06-30

### Changed
- Documentation sweep for Linux/Ubuntu: the PyPI/plugin/MCP-registry package descriptions, the README (hero, hybrid-search line, diagram alt text), and `docs/reference.md` + `docs/install-new-mac.md` now state that memo runs on Apple Silicon (MLX) **or** Linux/Ubuntu (CPU `sentence-transformers`), instead of implying macOS-only. The "when not to pick memo" note no longer lists "not on Apple Silicon" as a disqualifier (memo runs standalone on Linux; only the reranker + LLM features are MLX-only).

## [2.6.3] - 2026-06-30

### Fixed
- The MLX-only cross-encoder reranker is now correctly forced **off** on non-Apple-Silicon hosts. The default `MODEL_PROFILES` set `reranker_enabled: True`, which overrode the platform-aware field default — so on Linux every hybrid search tried to load the MLX reranker, failed (`No module named 'mlx_lm'`), and fell back. `Config.from_env()` now gates it: an explicit `MEMO_RERANKER_ENABLED` wins, otherwise it is off when MLX is unavailable. Found by the new `linux-cpu-smoke` CI job running a real Ubuntu `[cpu]` install.

### Added
- `linux-cpu-smoke` CI job (ubuntu-latest): real `pip install ".[cpu]"`, sqlite-vec/FTS5 load, the actual Qwen3-Embedding-0.6B model, `memo doctor`, a save→search roundtrip, and `memo-mcp` build — the end-to-end Linux validation the (stubbed) unit suite can't give.

## [2.6.2] - 2026-06-30

### Fixed
- `memo install-mcp claude-desktop` writes to the per-OS Claude Desktop config path (`~/.config/Claude/...` on Linux, `%APPDATA%` on Windows) instead of the hardcoded macOS path.
- The MCP-config scan (`memo doctor` / mcp-config) now also checks the Linux Windsurf path (`~/.config/Windsurf/User/mcp_config.json`).

### Added
- Linux systemd **user** units in `systemd/` (`memo-dream.service` + `memo-dream.timer` for nightly maintenance, `memo-watch.service` for auto-reindex) — the counterpart of memo's macOS launchd agents. See `systemd/README.md` and `docs/ubuntu.md`.

## [2.6.1] - 2026-06-30

### Fixed
- `memo doctor` / `memo doctor --json` no longer false-fail (exit 1) on a healthy Linux/Ubuntu `[cpu]` install: the import probe is now backend-aware — it checks `sentence-transformers` on the CPU backend instead of MLX, and the model-cache report lists `st_embedder_model`.
- `memo install-watcher` / `uninstall-watcher` give a clean "launchd is macOS-only" message on Linux instead of an uncaught `FileNotFoundError` (no `launchctl`).
- `memo search --rerank` warns and skips on non-Apple-Silicon instead of trying to load the MLX-only cross-encoder.
- `RepoCorpus` (and the opt-in ingest daemon) routes the embedder through `make_embedder`, so it uses the CPU backend on Linux instead of hard-constructing `MLXEmbedder`. SessionStart prewarm likewise warms the active backend.
- Obsidian vault auto-detection reads the per-OS registry path (`~/.config/obsidian/obsidian.json` on Linux, `%APPDATA%` on Windows).

## [2.6.0] - 2026-06-30

### Added
- Linux/Ubuntu (and Intel mac) support via a CPU `sentence-transformers` embedder backend (`STEmbedder`), auto-selected off Apple Silicon. Semantic search, recall, and save work without MLX as a **standalone** corpus. New `embedder_select.make_embedder` is the single MLX-vs-CPU decision point, routed through the `Memory` facade, the `embedder_client` daemon fallback, and `memo ingest`.
- `MEMO_EMBEDDER_BACKEND` (`auto`/`mlx`/`st`) and `MEMO_ST_EMBEDDER_MODEL` (default `Qwen/Qwen3-Embedding-0.6B`, 1024-dim — same family/dims as the MLX quant, so the vec0 schema and asymmetric query prefix are unchanged).
- `pip install "mlx-memo[cpu]"` extra, `scripts/install-ubuntu.sh` (uv/pipx), and `docs/ubuntu.md`.

### Changed
- `reranker_enabled` now defaults ON only on Apple Silicon (the cross-encoder is MLX-only); on other hosts hybrid search returns fusion-ranked results without the rerank pass.
- LLM features (`ask`/`synthesize`/`dream`) raise a clear `MemoError` off Apple Silicon instead of an opaque import failure; search/recall/save are unaffected.
## [2.5.0] - 2026-06-30

### Added
- Graph-native program: associative recall nudge (spreading activation over the merged entity + codegraph graph with IDF hub-damping), entity-centric "Knowledge map" briefing section, nightly community-synthesis dream pass (off by default, `MEMO_DREAM_COMMUNITIES_ENABLED`), and `memo graph explore` / `memo_explore` knowledge exploration.
- codegraph migration: memo navigation merges colbymchenry/codegraph's symbol graph as the primary layer (`MEMO_GRAPH_USE_CODEGRAPH`).

### Fixed
- Associative recall nudge now renders on the warm-daemon (primary) recall path, not only the subprocess fallback (it was dormant in normal operation).
- `compute_centrality` rebuilt the graph adjacency O(N²) times (hung on real corpora); now builds it once.
- Storage: `get_by_path` / `get_by_path_ci` and prune/eviction candidate queries no longer return soft-deleted rows (path-collision false positives, UNIQUE constraint crashes, inflated eviction pools).
- HyDE search reuses the shared MLX chat instead of cold-loading a second copy of the LLM.
- `llm.chat_stream` no longer holds the GPU lock across the entire streamed response.
- `AmbiguousIdError` guards on `memo_related` (and the HTTP/CLI paths); restore tolerates a corrupt manifest; numerous CLI/MCP/flags/dream correctness fixes from a whole-project adversarial audit.

### Changed
- English-only user-facing strings (associative nudge); removed dead code (`find_node_fuzzy`, `auto_update_on_commit`, `get_session_metadata`); gitignore scratch output.

## [2.4.3] - 2026-06-29

### Added
- **Dream convergence guard**: `memo dream run` stamps a corpus fingerprint and, on a re-run where nothing changed (signal-gather found nothing new and the corpus is unchanged), skips the expensive contradict/synthesize/consolidate passes — a re-run on an idle corpus is now near-instant instead of redoing identical LLM work. `--force` runs every pass regardless.
- **Dream single-owner lock**: a second `dream run` (manual, or the `com.memo.dream` LaunchAgent firing while one is in flight) now skips instead of racing on the shared sidecar DBs and clobbering the receipt.

### Changed
- Dream signal-gather now uses the exact last-run timestamp for its transcript lookback instead of a day-rounded window that re-mined ~1–2 days every run.

## [2.4.2] - 2026-06-29

### Changed
- Removed the Spanish command aliases `memo historia` / `memo mapa` (use `memo record-history` / `memo map`); the CLI surface is now English-only.

### Fixed
- Silenced HuggingFace hub progress bars globally (set `HF_HUB_DISABLE_PROGRESS_BARS` at import). Model loads, prewarm, daemon startup and self-update no longer leak repeated "Fetching N files / Download complete 0.00B" noise for already-cached models.

## [2.4.1] - 2026-06-29

### Fixed
- **Split graph DB healing**: an install that ran an interim build (the table rename without a migration) ended up with both `entity_memoria` (legacy data) and an empty `entity_memory`, splitting the knowledge graph and losing historical entity links to recall. The migration now folds the legacy rows into `entity_memory` (deduped) and drops the legacy table when both are present, not only when the new table is absent.

## [2.4.0] - 2026-06-29

### Changed
- **English-only codebase** for public release: Spanish identifiers renamed to English across the code (DB table `entity_memoria`→`entity_memory`, column `memoria_id`→`memory_id`, output/API keys), remaining UI strings translated, and the `engram` name removed (module → `session_patterns`). On-disk corpus dir default `memorias/`→`memories/`.

### Fixed
- **Existing-database migrations**: pre-rename SQLite DBs are auto-migrated in place (`pairs.memoria_id_a/b`, the `entity_memoria` table + `memoria_id` column, `versions.memoria_id`) so upgrades no longer crash with `no such column` (which surfaced as a warn in `memo dream`) or silently orphan graph data.
- **Soft directory migration**: new installs use `memories/`; existing `memorias/` installs keep working — config, sync clone/bootstrap, backup/restore and import all read both names.
- 24 bugs from an exhaustive line-by-line review: `memo backup`/`memo health` crashing on their non-JSON paths, consolidation resurrecting archived duplicates, the `mem_review` SQL query, the tantivy rebuild for >5000-memory corpora, the synapse `conflicts` CLI args, and more.
- Dropped a personal locale default ("Spanish rioplatense") from the ask prompt; answers follow the question's language generically.

## [2.3.12] - 2026-06-29

### Fixed
- Dream cross-session consolidation (`memo dream consolidate-episodes`) now saves its synthesized memories — a keyword-only `save()` was being called positionally, raising `TypeError`. Regression-tested.

## [2.3.11] - 2026-06-29

### Added
- **Dream v2 — self-improving recall tuner** (off by default): nightly `MEMO_RECALL_MIN_SIM` line-search from ground-truth-by-use labels, gated by the regression set, auto-applied and auto-reverted on a later regression (`memo dream tune`).
- **Dream v2 — episodic→semantic consolidation** (off by default): abstracts recurring cross-session work into one durable synthesis memory (`memo dream consolidate-episodes`).
- **Dream v2 — anticipatory pass** (off by default): surfaces recurring knowledge gaps and hot queries into the briefing and pre-warms their embeddings; never fabricates (`memo dream anticipate`).

## [2.3.10] - 2026-06-29

### Changed
- `memo doctor` auto-repairs a stale MCP config path (a dead pipx/uv launch path is repointed to the stable `~/.local/bin` shim, with a `.bak` backup). `--check` and `--json` stay read-only.

## [2.3.9] - 2026-06-29

### Changed
- The injected recall block now carries a relevance gate and stronger prompt-injection framing — recalled memory is treated as data, never as instructions.

## [2.3.8] - 2026-06-28

### Added
- Per-project memory storage (default on): new memories are filed under `memory_dir/<project>/`, while search stays global. `memo migrate --bucket-by-project` re-buckets an existing install.
- 3-tier recall relevance: current project > global / cross-cutting > other projects.

## [2.3.7] - 2026-06-28

### Fixed
- `ask` now surfaces both sides of a contradiction instead of silently picking one, and respects `k`.
- Spanish accented queries keep their relevance boost (diacritics folded).
- The embed socket handles full batches on 4B/8B models without truncating.

## [2.3.6] - 2026-06-28

### Fixed
- The statusline no longer duplicates the `[MEMO <version>]` badge when wrapping an existing statusline.

## [2.3.5] - 2026-06-28

### Fixed
- Broad CLI hardening: corrected contradiction/temporal/stale defaults, short-id resolution in `links`, populated `version history/diff/rollback`, date validation, and crash guards across `feedback`, `import`, `eval`, `query`, and the TUI.

## [2.3.4] - 2026-06-28

### Fixed
- **CRITICAL:** archive no longer deletes memories (the `.md` is moved to an archive dir before the index row is dropped).
- **CRITICAL:** zip-slip path traversal in `memo restore` and `../` traversal in Obsidian image embeds are blocked.
- Concurrency fixes, atomic config/state writes, subprocess timeouts, and correct exit codes across the runtime.

## [2.3.3] - 2026-06-27

### Added
- The statusline self-heals its wiring on session start (gated, default on).

### Fixed
- `memo install-statusline` now **wraps** an existing statusline instead of skipping it, so the `[MEMO]` badge coexists with other tools' badges.

## [2.3.2] - 2026-06-27

### Removed
- Reverted the cross-machine version-file auto-update added in 2.2.0 — it was redundant with the existing tag-based auto-update and shipped broken. Tag-based auto-update is unaffected.

## [2.3.1] - 2026-06-27

### Added
- The `memo resume` picker now previews what changed in a session's cwd since you last worked there, plus that session's open loops.

## [2.3.0] - 2026-06-27

### Added
- `memo episodes search` (CLI) and `memo_episodes_search` (MCP) find past work sessions by meaning.

## [2.2.0] - 2026-06-27

### Added
- Semantic Resume: `memo resume` searches the full session history by meaning via a derived episode index.

## [2.1.1] - 2026-06-27

### Added
- `memo resume` opens an interactive cross-agent picker in a TTY (resumes `claude` / `codex` / …); piped or `--json` still emits the candidate list.

### Fixed
- `memo update` from an editable/dev install now refuses with clear guidance instead of silently updating the wrong runtime.

## [2.1.0] - 2026-06-27

### Added
- `memo eval harvest` mines ground-truth recall labels from real grounded outcomes.
- The outcome loop closes nightly so ranking learns from real usage; continuous entity extraction on save; the unified briefing composes from memo's own corpus.

### Changed
- The reranker sees more candidates (`rerank_input_k` 5→30; validated precision@5 0.786→0.834), with candidate bodies served from the index instead of re-reading `.md` files.

### Fixed
- Hybrid recall mode is usable (its similarity gate compares the true vec cosine).
- 34 correctness bugs across store, memory ops, sync, embedder, capture, temporal, and analytics.

## [2.0.0] - 2026-06-26

First stable release.

### Added
- **Time-machine** — rewind the corpus to any past date (`memo as-of`, `memo diff`).
- **Contradiction radar** — `memo contradict scan` / `triage` detects and resolves conflicting facts.
- **Synthesis pipeline** — `memo synthesize` and the nightly `memo dream` infer cross-cluster insights (opt-in).
- **Cross-Mac git sync** — `memo sync bootstrap <url>` shares a corpus over a private git remote (rebase-before-push, single-owner `flock`).
- **Obsidian vault as source-of-truth** — `MEMO_MEMORIES_IN_VAULT=1`; human edits win on the next reindex.
- **Knowledge graph** — entity extraction, neighbors/path/centrality/communities, backlinks/outlinks.
- **Health scoring & eval gates** — `memo health` and `memo eval recall --gate` for CI or pre-commit.
- **Multi-modal ingestion** — images and audio (macOS Vision OCR), searchable.
- **Session continuity** — open loops, session state, `memo resume`.
- **Warm daemons** — recall (<200 ms) and idle-capture for hookless MCP clients.
- 95 CLI commands and 109 MCP tools across three surface profiles (slim / agent / full).
- `install.sh` uv-first installer and `memo doctor --strict-runtime`.

### Changed
- `MEMO_AUTO_UPDATE` is on by default (tag-based self-update); the `agent` MCP profile (5 tools) is the default.

### Removed
- Purged experimental surfaces (encryption, sharing, federation) and the unused GLiNER entity backend.

---

Releases before `2.0.0`: see [docs/CHANGELOG-archive.md](docs/CHANGELOG-archive.md).
