# Changelog

All notable changes will be documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [Unreleased]

## [0.2.4] — 2026-06-04

Patch release on top of 0.2.3: a multi-project **Context Portal**,
Langfuse-style **artifact version snapshots + label pointers** for agents and
commands (ADR-0022), per-project **sync enrollment**, a new `mm tags` CLI, CLI
session tracing on Langfuse v4, and broad Web UI / i18n / accessibility
hardening — alongside a session-trace secret-redaction fix and retry/webhook
reliability fixes.

- **Secrets are now redacted in session-trace metadata sinks.** Session command
  tracing wrote span/trace metadata to its observability sinks without running
  the redaction pass, so a secret-shaped value captured in command context could
  reach the trace backend in clear text. The metadata now goes through the same
  redaction funnel as the rest of the trace payload.

- **Context Portal — multi-project state board (ADR-0021).** The Context Gateway
  gains a read-only runtime/client registration registry, a multi-project board
  section, per-CLI runtime chips with a provider filter, project health + label
  rename + lazy artifact counts, and a "Sync All" flow with per-phase progress, a
  result summary, and (project, tier) pinning so each phase re-resolves against
  the intended scope. The gateway now lands on the Projects Portal by default
  (#1191–#1193, #1195–#1197, #1200–#1202).

- **Artifact version snapshots + label pointers for agents and commands
  (ADR-0022).** Canonical agents/commands can keep Langfuse-style
  `versions/vN.md` snapshots with movable label pointers (e.g. `production`,
  `staging`) instead of a single flat file. New MCP tools `mem_context_version`
  and `mem_context_promote`, plus label-aware `mem_context_sync`, expose this
  over MCP; the Web UI adds a detail-panel version/label manager, list-card label
  chips (`GET /context/{type}?include=versions`), a versioned create layout, and
  an opt-in "enable versioning" migration that adopts existing flat web-created
  artifacts into the versioned layout (#1206, #1212–#1216, #1218).

- **Per-project sync enrollment.** Project discovery is split from sync: matching
  projects are auto-displayed, but artifacts only fan out once a project is
  explicitly enrolled. Adds enroll / pause / resume UI, sync-eligibility gating
  on the matrix and Sync-All, and a write-guard that returns a structured `409`
  (with localized detail) on runtime-writing routes for paused or not-enrolled
  projects (#1203, #1205, #1208, #1210, #1211).

- **`mm tags` — list / rename / delete / merge tags (#688).** New CLI verb with a
  matching Tags tab in the Web UI for the same rename / merge / delete actions.
  Dry-run sampling is now global so the preview sample matches the count and the
  applied set (#1175–#1177).

- **CLI session command tracing + Langfuse v4 (#1199).** CLI command runs can
  emit session traces through a new `observability/session_tracing.py`, updated
  to the Langfuse v4 client surface.

- **Kimi Code install detected via the `~/.kimi-code` home dir (#1204).** The
  install probe also checks `~/.kimi-code` (the CLI data home set by
  `KIMI_CODE_HOME`), fixing an ADR-0021 false-negative where a present Kimi
  install was reported as absent.

- **Retry and webhook reliability fixes.** `with_retry` now honors string
  `retry_after` attributes instead of ignoring them (#1178), and
  `_validate_webhook_url` URL-safety behavior is pinned by tests (#1179).

- **Web UI accessibility + UX consolidation.** Context Gateway roster
  consolidation (Overview matrix removed, Sync routed through the Projects
  Portal), active-project + tier controls hoisted into one gateway header bar, a
  single capability-mapped source for artifact-section toolbars, artifact lists
  scoped to the active project with a "Show all projects" toggle, Sync/Import
  dry-run confirm previews with count/destination threading, and a11y fixes
  (settings-nav `<nav>` landmark + `aria-current`, un-nested project-remove `×`,
  labeled Hooks controls) (#1217, #1219–#1223).

- **Localization (EN/KO).** Command palette labels, the Context Gateway
  create-form name placeholder, Search history and recent chips, settings
  recovery messages, Decay scan status, Index streaming progress, and the
  embedding-mismatch banner are now localized (#1184–#1190).

- **Internal.** Unused `noqa` directives removed across src and tests; validated
  `TargetScope` casts replace return-value `type: ignore`s; the gc orphan-project
  report renderer is typed (#1180–#1183).

## [0.2.3] — 2026-06-01

- **`mm memory doctor` — read-only hygiene report for memory stores.** A
  registered `memory_dir` can be barely indexed (the fs watcher only reacts to
  live events, so files that landed while the server was down stay invisible
  until a forced re-walk) and its index/TOC file can drift from what's on disk
  — `mem_search` then silently can't find the un-indexed files. The new
  `mm memory doctor` makes the 3-way drift between disk, the agent index file
  (e.g. `MEMORY.md`), and the searchable DB visible: per configured dir it
  reports DB coverage gaps, DB chunks whose source file was deleted, index/meta
  files indexed as content, broken index links (classified
  `missing_target`/`outside_root`, leaving `url`/`anchor` alone), files absent
  from the TOC, hot-cache budget overruns, and never-accessed "cold" files.
  Human output by default, `--json` for CI; exits `1` only on definite
  inconsistencies (deleted-source chunks, convention violations, broken links).
  Report-only by default — the report never writes disk, DB, or config (loads
  config with `migrate=False` so it can't trigger the legacy auto-discover
  rewrite). A narrow, opt-in `--fix` then removes *only* broken
  `missing_target` index links (`outside_root`/`url`/`anchor` left alone),
  dry-run by default with `--apply` to write — byte-exact line-splice under a
  sidecar lock with fresh re-validation, removals count-bounded to what the
  report saw, per the ADR-0020 subtractive write contract (#1172, #1173).

- **Provider index-file conventions enforced on every index path (fixes
  `MEMORY.md` pollution).** The exclude set for agent index/TOC files — Claude
  Code's `MEMORY.md`/`README.md` and Codex's `README.md` — was previously
  honored only by `mm ingest`, so the general `memory_dirs` walk, the file
  watcher, and `mm index <dir>` indexed `MEMORY.md` as searchable content: a
  pointer-only table of contents that then surfaced as a high-score duplicate
  on every query. The conventions now live in one table
  (`config._PROVIDER_INDEX_CONVENTIONS`) consulted by the shared
  `_path_is_excluded` funnel, so ingest, the engine walk, the watcher, and
  `mm purge --matching-excluded` all skip the same files. `mm purge
  --matching-excluded --apply` consequently reclaims `MEMORY.md` chunks indexed
  before this fix. The convention is provider-scoped — a `MEMORY.md` in a plain
  user folder is still indexed as real content.

- **MCP server definitions in the Context Gateway (#1165).** A new **MCP Servers**
  gateway section manages canonical definitions under
  `.memtomem/mcp-servers/<name>.json` and fans them out to the project
  `.mcp.json` `mcpServers` object (create / read / update / delete / diff /
  Sync All, with an overview tile and EN/KO i18n). Writes and sync run through
  the privacy write-guard, so a secret-shaped `env` value is refused rather than
  copied into the shared `.mcp.json` — use `${VAR}` references for secrets. v1 is
  intentionally narrow: only the `project_shared` tier is writable (reads on
  other tiers return empty, never an error), and only stdio servers (a non-empty
  `command` field) are accepted — network (SSE/HTTP) transports, user-home client
  configs, and reverse import are deferred to a follow-up that needs stronger
  host-write and secret-handling policy.

- **Antigravity CLI (`agy`) documented as an MCP target; Gemini CLI marked
  deprecated (#1167).** Google is replacing Gemini CLI with the Go-based
  Antigravity CLI; Gemini CLI stops serving free/Pro/Ultra individual tiers on
  2026-06-18 (enterprise Gemini Code Assist keeps it). Antigravity CLI registers
  MCP servers in its own `~/.gemini/antigravity-cli/mcp_config.json` (key
  `mcpServers`, stdio entries with `"type": "stdio"`) — distinct from both Gemini
  CLI's `~/.gemini/settings.json` and the Antigravity IDE's
  `~/.gemini/antigravity/mcp_config.json`. The `mm init` paste-hints plus the
  getting-started, mcp-clients, uninstall, llm-providers, and PyPI README guides
  now surface this path and the deprecation date. The shared Gemini-compatible
  surfaces are intentionally unchanged: Antigravity CLI reads `~/.gemini/GEMINI.md`,
  so `mm ingest gemini-memory` and the `.gemini/` context fan-out keep working —
  no runtime removed, no config repathed.

- **`mm init` splits Codex memories into per-subdir namespaces (#1164).** The provider
  preset for `~/.codex/memories/` now generates three ordered namespace rules
  instead of one flat `codex` rule: `codex:rollout_summaries` (per-session
  episodic recaps), `codex:extensions` (the ad-hoc note inbox), and
  `codex:global` (the consolidated top-level memory) as the catch-all. Folding
  those three distinct memory classes into one namespace defeated per-class
  search and time-decay. Rules use literal namespaces only, so the RFC #304
  placeholder lock is unaffected. Re-running `mm init` after upgrading migrates
  an existing flat `codex` rule in place: the stale catch-all is removed and the
  split takes its position (reported with a `↻` line), so the new subdir rules
  resolve correctly under first-match-wins instead of being shadowed. A
  hand-edited codex rule (custom namespace) is never touched. Existing indexed
  data is migrated by re-running `mm index ~/.codex/memories --force`.
- **Context Gateway adds Kimi CLI runtime support.** Skills now fan out to
  `.kimi/skills/`, agents render to Kimi `agent-file` YAML under `.kimi/agents/`,
  hooks sync into Kimi `config.toml` through a memtomem-managed TOML block, and
  `mm init --mcp kimi` writes `~/.kimi/mcp.json` (or `$KIMI_SHARE_DIR/mcp.json`).
  Kimi custom commands stay explicitly unsupported until Kimi documents a
  file-backed custom-command format.

## [0.2.2] — 2026-05-31

Patch release on top of 0.2.1: adds network MCP transports, a `mm web`
background daemon, hooks fan-out to the Codex and Gemini runtimes, and a new
artifact-migration MCP tool, alongside broad Context Gateway / Web UI
accessibility and reliability hardening.

- **New `mem_context_artifact_migrate` MCP tool (#1147, B5-1).** Exposes the
  CLI `mm context migrate` verb over MCP for agents / commands / skills: flat→dir
  layout normalization (`to_scope` omitted) and ADR-0011 scope-tier moves
  (`to_scope` set). Dry-run by default (`apply=True` to execute); `force=True`
  migrates dirty flat files (flat→dir only); `confirm_project_shared=True` is
  required to write the git-tracked tier. Reuses the same pure functions and
  gate semantics as the CLI; memory-tier migration stays on
  `mem_context_memory_migrate`.
- **`mem_context_migrate` renamed to `mem_context_memory_migrate` (#1147, B5-2).**
  The tool only ever covered *memory*-tier migration, but its bare name implied
  parity with the full CLI `mm context migrate` (which also does artifact
  flat→dir and scope-tier moves). `mem_context_migrate` remains as a deprecated
  alias that forwards every argument unchanged and will be removed in a future
  major release; the `mem_do` action `context_migrate` likewise resolves to
  `context_memory_migrate`.
- **`memtomem-server` now supports network MCP transports.** Adds
  `--transport sse|http` plus `--host`, `--port`, `--url`, `--allowed-host`,
  `--allowed-origin`, and `--disable-dns-rebinding-protection` for deployments
  that need SSE or streamable HTTP instead of stdio. Treat sse/http transports
  as trusted-network only; place an authenticated reverse proxy in front before
  exposing publicly.
- **`memtomem-server` direct TTY launches now exit with guidance.** Running the
  stdio MCP server directly in a terminal prints MCP-client setup and
  network-transport examples instead of waiting on stdin. Network transport
  defaults are also tightened and the startup banner ordering fixed (#1092).
- **`mm web` can run as a background daemon (#1028).** Lets the Web UI keep
  running detached from the launching shell.
- **Hooks now fan out to the Codex and Gemini runtimes (ADR-0018, #1109).**
  memtomem-owned hook rules are marked so re-sync can update them in place
  (ADR-0019, #1111), duplicate same-matcher conflicts resolve by exact rule
  identity (#1115), and the sync confirmation discloses the actual runtime
  target files with a scope-drift guard (#1114).
- **Custom Commands are promoted to the production tier in Context Gateway
  (#1108).**
- **LLM and embedding requests retry on HTTP 429 (#1056).** Adds shared
  `RateLimitError` handling with `Retry-After` parsing (#1084) and validates
  `max_delay` before sleeping to avoid negative sleeps (#1041).
- **`mem_dedup_merge` defaults to `dry_run=True` for safety parity (#1045).**
  Dedup scan timeout is aligned to 120s and the 408 path is covered (#1085).
- **`mm init` no longer gates HTTP embedding providers on unused client
  packages (#1148).**
- **Context Gateway and Web UI accessibility + reliability hardening.** Focus
  trap/restore and `aria-modal` across modals, a skip-to-main link, polite
  `aria-live` results, reduced-motion support, accessible names for icon-only
  buttons and form inputs, and modal-aware shortcut gating (#1053 series);
  plus project-switch scope/draft race fixes, deep-link cold-load routing,
  single-column mobile layout, and localized error / empty / diff text
  (#1116, #1117, #1120–#1122).
- **Section parser hardened against round-trip data loss; settings TOCTOU and
  the skill-promote rollback leak closed (#1132, #1145).**
- **`/api/reindex` no longer 500s** when index roots need path coercion
  (#1058).

## [0.2.1] — 2026-05-13

Patch release focused on Web UI stability and release polish after
0.2.0.

- **Context Gateway project switching is safer.** Adds the Gateway
  project switcher and fixes stale-response / draft-isolation races
  around scoped project changes, Hooks sync, and project-normalized
  requested scope handling.
- **Home and Sources views are more reliable.** Fixes Home dashboard
  aggregate counts across visible sources, mobile overflow, pinned
  chunk navigation, sampled activity heatmap labeling, source-tree
  navigation, namespace chart actions, and search-filter removal UX.
- **Runtime reranker settings stay in sync.** Fixes runtime reranker
  config synchronization and async hot-reload lazy loading.
- **Add Project and Web copy polish.** Fixes Add Project picker
  discovery scope, localizes Home dashboard states, clarifies Home
  quick-action flow, updates Web docs/comments, and embeds the README
  hero GIF.

## [0.2.0] — 2026-05-13

The 0.2.0 release closes the **ADR-0011 memory scope axis** epic
(per-tier memory writes and reads — `user` / `project_shared` /
`project_local`) and the **Tiered Context Gateway v2** epic (#868 —
ADR-0016 tier-aware Context Gateway + ADR-0017 project-root field
rename) and flips the **Web UI CSRF / Origin / Host guard to enforce
mode** (RFC #787 stage 2). The release notes below are split into the
0.2.0 highlights (post-PR-F work, #930-#972) and the underlying
ADR-0011 PR-A through PR-F changes that landed earlier in the cycle.

### Highlights

- **Tiered Context Gateway v2 — ADR-0016 (#868 epic complete).** The
  web Context Gateway is promoted to a top-level main tab (#962/#967)
  and the surface is now tier-aware end-to-end: write affordances dim
  + intercept on non-shared tiers with a banner (#945), the overview
  dashboard surfaces `project_root` + detected runtimes (#947),
  sync-direction pointers per row (#951), a "last sync" freshness
  indicator (#954), and tier-correct copy in the header (#955) and
  empty-state hint (#957). Tile clicks deep-link to the filtered /
  highlighted leaf via `?section=&filter=&artifact=` URL carriers
  (#834 / #950). Per-rule Hooks detail panel + detail-meta surface
  (#968), Hooks scope-aware sync labels with `needs_confirmation`
  surfacing (#966), no_source state polish (#969), and a guard for
  tier-aware Hooks sync stale-response handling (#972).
- **Project-root field rename — ADR-0017 (#922).** Renamed the
  `project_context_root` BaseSettings field to `project_root` with a
  kept env alias so existing configs keep working. The kept-alias
  contract covers four moving parts that `AliasChoices` alone does not
  (tolerant CLI unset pop, loader env-alias enumeration, Web query
  both-set precedence, and a 4-cell env × persisted test matrix).
  Lands three months early via PR #953.
- **CSRF / Origin / Host guard flipped to enforce mode (RFC #787
  stage 2).** PR #958 flips the soft-warn observation phase to enforce
  for all Web UI mutators. PR #961 folds in a codex review that found
  five SPA mutators that bypassed the threading (including one
  lazy-import call site) and adds an AST-level architectural guard
  with a frozenset of protected entry points, a depth-aware
  fetch-args scanner, and identifier-bound CSRF binding verification.
  Production-loopback Host pin is observed via the central log. This
  is a behavioral change for any embedded / proxied deployments — see
  the RFC for the migration path.
- **Scope-aware audit / rescan (#934).** `mm context rescan` learns
  `--scope=user|project_shared|project_local` and the audit closes a
  cross-project SQLite leak in `iter_chunks_for_audit`. The walker
  targets `canonical_artifact_dir` per tier and unions
  `detect_agent_files` only for `project_shared` (per ADR-0011 §3 the
  project-local tier has no runtime fan-out). MCP parity for the
  audit-class verbs is intentionally deferred (audit is a CI gate,
  not an agent-runtime path).
- **`mem_context_migrate` MCP wrapper (#887 / #926).** The fifth and
  final context MCP tool — all five (`init`, `sync`, `generate`,
  `diff`, `migrate`) now expose the canonical artifact `scope=` axis
  and mirror the CLI's `scope_explicit` gate, click-exception
  handling, per-call output buffers, and stderr-tail preservation.
- **FTS5 query tokenization hardening (#963).** Hardens the FTS5
  query path against the keyword / quote / unicode edge cases that
  previously surfaced as silent zero-result responses.
- **Deep-link from overview tile to filtered / highlighted leaf
  (#834 / #950).** Overview tile click sets a `?section=&filter=&
  artifact=` URL carrier that the leaf view replays — `?artifact=` is
  a DOM-remove (count==1 negative pin), `?filter=` is the `hidden`
  attr (so reset round-trips without refetch). Tier swaps clear the
  carrier; unknown filter values fall through.
- **Web "last sync" freshness indicator + sync-direction pointers
  (#832 / #833 / #951 / #954).** Each row gets a relative-time
  freshness indicator and an arrow pointer for the direction of the
  most recent sync. Skill-dir mtime is read from the manifest file
  rather than the directory itself so any aux-file write does not
  advance the timestamp.
- **Pending chunk highlight retry across same-source loads (#680 /
  #971).** Timeline → Source jumps that land outside the initial
  100-chunk page now retain the pending target across the next
  fetch, so a Load All retry can highlight the intended chunk. The
  pending target is source-scoped via a new
  `pendingActivateChunkSourcePath` companion field.
- **Shared `chunk_progress` throttle helper (#659 / #959 / #660 /
  #964 / #965).** Lifts the 100ms throttle + final-tick bypass +
  boundary-reset shape out of `runIndexStream` and `mdReindexOne`
  into `makeChunkProgressRenderer`. Browser tests pin the consumer
  shape and the locale-toggle mid-stream template via
  `add_init_script` + a `langchange` flag + `wait_for_function`
  (the `documentElement.lang` attribute is statically served and is
  not a usable init-done signal).
- **Browser + jsdom test harness landed.** Playwright `tests/web/`
  with 16 specs reaches a stable shape (shared `install_default_stubs`
  in `conftest.py`, lifespan=None app, `page.route` stubs,
  browser-marker auto-skip) and a companion `tests-js/` Vitest +
  jsdom track for fast UI logic specs is documented in CONTRIBUTING
  alongside a Python-route-vs-browser-test decision rule (#970).
- **Deferred ADR tracker convention (#922 / `docs/adr/TRACKER.md`).**
  ADR PRs that defer open questions append a single line per deferred
  question into `TRACKER.md` in the same PR, with the
  `adr-feedback` label and tracking-issue comments as the signal
  channels.
- **CLA Assistant native workflow (#960).** Replaces the third-party
  CLA action with a native GitHub workflow gate; the CLA status
  comment failure is tolerated so a transient failure to post the
  status comment no longer blocks merges.
- **Misc Web polish.** Empty-state CTA helper formalized + namespaces
  layout regression fix (#864); tier filters polish (#940); rescan
  USERPROFILE-pinned tests for Windows; emptyState i18n authoring
  carve-out for technical domain vocabulary.

### Behavioral changes — please read before upgrading

- The Web UI now **enforces** CSRF, Origin, and Host on all mutators
  (RFC #787 stage 2). Integrators that proxy or embed the Web UI must
  ensure the CSRF token is threaded through every mutating fetch and
  that the Origin header is preserved or matches the configured
  loopback Host. The soft-warn observation phase that shipped in
  0.1.x is gone.
- The `project_context_root` setting is renamed to `project_root`
  (ADR-0017). The env alias is kept so existing
  `MEMTOMEM_PROJECT_CONTEXT_ROOT=` configs keep working in 0.2.x; the
  tracking row in `docs/adr/TRACKER.md` schedules the alias-removal
  review for 2026-08-12.
- `iter_chunks_for_audit` previously cross-leaked between projects on
  the shared SQLite path. `mm context rescan` and the audit class
  callers now filter by `(project_root, scope)` and defensively raise
  on `scope='user' + project_root != None` (#934).

### Added

- **Web tier badges + `/api/add` project-tier rejection hint (ADR-0011
  PR-F Web/docs slice, #929).** Memory sources/chunks and context
  Skills/Commands/Subagents list rows now carry literal tier badges
  (`user`, `project_shared`, `project_local`) per ADR-0016. Web list
  routes accept `?target_scope=` for canonical tier selection and keep
  `project_local` hidden by default unless explicitly requested; context
  rows in `project_local` annotate the draft/no-runtime-fan-out behavior
  inline. `POST /api/add` now rejects unconfirmed
  `scope=project_shared` writes with `403 detail=blocked_project_shared`,
  a CLI remediation hint, and docs URL, mirroring MCP Gate B. Gate A also
  receives the resolved scope so `force_unsafe` cannot bypass
  `project_shared` protections through Web writes.
- **Memory scope axis schema (ADR-0011 PR-B).** First-time start of an
  upgraded server runs a one-shot SQLite migration that adds two new
  columns to ``chunks``: ``scope TEXT NOT NULL DEFAULT 'user'`` (one
  of ``user`` / ``project_shared`` / ``project_local``) and
  ``project_root TEXT`` (the registered project root for project-tier
  rows; ``NULL`` for user-tier). Every existing row backfills to
  ``scope='user'`` so behaviour for current memtomem deployments is
  unchanged on upgrade. Two new indexes
  (``idx_chunks_scope (scope, project_root)``,
  ``idx_chunks_project_root (project_root) WHERE project_root IS NOT
  NULL``) cover the always-on default-merge filter shape. Migration is
  idempotent — DB files already on the new schema are no-ops.
- **`--scope` filter on `mm mem recall`, `mem_search`, `mem_recall`
  (ADR-0011 PR-C).** Optional explicit scope filter accepts a single
  value (``user``), a comma list (``user,project_local``), or a glob
  (``project_*``). Without ``--scope`` the search falls into the
  project-aware default merge (see Changed entry below).
- **`mm mem add --scope` + `--confirm-project-shared` / `--yes` (CLI)
  (ADR-0011 PR-D).** The CLI ``add`` command now writes to one of three
  directories based on the resolved scope: ``~/.memtomem/memories``
  (user, default), ``<project>/.memtomem/memories`` (project_shared,
  git-tracked), or ``<project>/.memtomem/memories.local``
  (project_local, gitignored). Project-tier writes need a registered
  project context (`project_memory_dirs` covers the current cwd);
  without one the CLI exits with a clear error. ``project_shared``
  writes prompt for explicit confirm naming the git-tracked target
  path; ``--yes`` and ``--confirm-project-shared`` skip the prompt for
  scripted use.
- **`mem_add(scope=..., confirm_project_shared=...)` and
  `mem_batch_add(scope=..., confirm_project_shared=...)` (MCP)
  (ADR-0011 PR-D).** The same scope axis is exposed to MCP callers.
  Default scope ``user`` keeps existing behaviour; ``project_shared``
  requires the explicit confirm flag (Gate B). Critically, the write
  target now resolves per scope on the MCP path too — ``mem_add(
  scope='project_shared')`` lands in the project's
  ``.memtomem/memories/`` directory, not the user-tier path. Closes
  the CLI/MCP divergence flagged during PR-D review. ``mem_batch_add``
  routes each entry through ``enforce_write_guard`` per-entry (the
  pre-ADR-0011 inline-scan path is removed); ``force_unsafe=True`` is
  hard-refused on ``project_shared`` per Gate A; the transactional
  reject contract is preserved (clean siblings of a flagged batch
  do not record a ``pass``).
- **`mm context memory-migrate <source> --from <scope> --to <scope>`
  (CLI) (ADR-0011 PR-D).** v1: chunk-id-stable, single-DB rename of
  one markdown memory file between scope tiers. Chunk UUIDs and
  ``chunk_links`` lineage are preserved via a transactional UPDATE on
  the chunks table; no re-index is triggered. Default is dry-run;
  ``--apply`` mutates disk. ``--to project_shared`` re-runs the
  privacy guard on the file content (Gate A on migrate); secret hits
  reject the migration with no force bypass — git history is forever.
  If the DB UPDATE fails after the filesystem move, the move is
  reverted (best-effort) so the source path remains canonical.
  Cross-DB migration, glob/multi-file inputs, and partial chunk-link
  lineage preservation are deferred.
- **Multi-device sync guide (`docs/guides/multi-device-sync.md`).**
  Documents the namespace-aligned layout, `.gitignore` recipe, post-pull
  workflow, and anti-patterns for syncing markdown memories across
  personal devices via a private git repo. Anchors `mm sync-doctor`
  (shipped in 0.1.36 via #838) and the home-rooted `config.json`
  serialization (#836). Inbound links from `README.md`,
  `docs/guides/getting-started.md`, and
  `docs/guides/configuration.md` (Moving `config.json` between machines).
- **`mm context sync --scope=...` + canonical-side Gate A + skills
  staging-dir-first scan (ADR-0011 PR-E3).** ``mm context sync`` (and
  ``mm context generate``) thread the resolved canonical-artifact
  scope through the three include surfaces (``--include=agents`` /
  ``--include=skills`` / ``--include=commands``); the helpers in turn
  call ``generate_all_*`` with the new ``scope=`` kwarg. The default
  remains ``project_shared`` so pre-PR-E3 invocations are
  byte-identical. ``--scope user`` reads ``~/.memtomem/{agents,
  skills,commands}/`` and fans out to ``~/.{claude,gemini,codex}/...``;
  ``--scope project_local`` short-circuits to
  ``NO_PROJECT_FANOUT_FOR_RUNTIME`` skips per runtime (ADR §3 — the
  draft tier has no runtime equivalent). New
  ``context/privacy_scan.py`` runs ``enforce_write_guard`` per file
  (sync direction; ``force_unsafe`` is hardcoded ``False`` per ADR §5
  — sync has no escape valve, unlike init's
  ``--force-unsafe-import``). ``project_shared`` hits raise
  :class:`click.ClickException` with a remediation hint pointing at
  ``mm context migrate`` (PR-E4); ``user`` / ``project_local`` hits
  emit ``PRIVACY_BLOCKED`` skips. Skills fan-out now uses a
  staging-dir-first flow (``_stage_skill`` builds at
  ``dst.parent/.staging-…tmp`` for same-fs atomic
  :func:`os.replace`; vendor SKILL.md override applies BEFORE the
  scan so the scan walks the bytes that will actually be promoted;
  ``_promote_staging`` swaps with rollback). Skill auxiliary files
  (``scripts/``, ``references/``, ``assets/``) stay byte-equal to
  canonical even when an override is staged for ``SKILL.md`` —
  ``test_override_only_touches_skill_md_not_scripts`` invariant
  preserved. The Web PR-F slice in #929 replaced the transitional
  hardcoded route scope with request-driven tier selection.
- **`mm context init --scope=...` + Gate A/B for canonical artifact
  seeding (ADR-0011 PR-E2).** ``mm context init`` (no flag) keeps the
  pre-PR-E2 failure-mode shape — same context.md path, no Gate B
  prompt, no project-signal hard-error from outside a real project —
  with idempotent canonical sub-dir seeding (``agents/``, ``skills/``,
  ``commands/``) added under ``<proj>/.memtomem/`` (or ``<cwd>/`` when
  no project root is found, in which case a yellow hint suggests
  ``--scope=user`` for the cross-project case). All new gating
  behaviour is restricted to EXPLICIT ``--scope`` invocations. ``mm context init`` gains
  three new flags:
  ``--scope user|project_shared|project_local`` (the canonical artifact
  tier to seed), ``--confirm-project-shared`` (Gate B — required when
  ``--scope`` is explicitly ``project_shared``), and
  ``--force-unsafe-import`` (Gate A bypass valve for the runtime-import
  path; user / project_local destinations only). Without ``--scope`` the
  command preserves pre-PR-E2 behaviour: writes context.md and seeds the
  implicit ``project_shared`` canonical tree under
  ``<proj>/.memtomem/{agents,skills,commands}/``. With ``--scope user``
  the command instead reads ``~/.claude/agents`` etc. and seeds
  ``~/.memtomem/{agents,skills,commands}/``. With
  ``--scope project_local`` the command seeds the gitignored draft tier
  ``<proj>/.memtomem/{agents,skills,commands}.local/`` and idempotently
  appends a comment-marked block to ``<proj>/.gitignore`` (covering
  ``.memtomem/*.local/`` and ``.memtomem/.staging/``). Gate A re-scans
  every imported file's bytes via ``enforce_write_guard``: ``user`` /
  ``project_local`` destinations skip-and-warn on hits (or honour
  ``--force-unsafe-import`` with audit-log + raw bytes through);
  ``project_shared`` destinations hard-abort with a
  :class:`click.ClickException` on any hit, with or without
  ``--force-unsafe-import`` (ADR §5: git history is forever). Skill
  imports walk the entire source skill tree (``scripts/``,
  ``references/``, ``assets/``) — one blocked file aborts the whole
  skill atomically. Gemini command imports scan the converted Markdown
  body (where the source ``prompt`` field lands) rather than the raw
  TOML.

### Changed

- **(CLI) ``mm context memory-migrate`` accepts glob input** (e.g.
  ``mm context memory-migrate 'memories/*.md' --from user --to project_local``).
  Quote the pattern to prevent shell expansion. Glob runs an
  all-or-nothing pre-flight (privacy scan + per-file lockfile
  acquisition) before any FS move; on mid-batch DB failure, the
  failing file is reverted (ADR-0011 §5), already-completed files
  stay migrated, and remaining files are left untouched so the user
  has a deterministic resumption point. Single-file input behaviour
  is unchanged. (#886)
- **(CLI) ``mm context memory-migrate`` plan output reports actual
  ``chunk_links`` neighborhood size.** Previously hard-coded
  ``chunk_links lineage: 0 dropped (chunk-id-stable single-DB
  rename)``; now ``chunk_links lineage: N preserved, 0 dropped``
  where N is the count of links touching the moving source's chunks.
  Backed by a new ``count_chunk_links_for_source`` storage helper
  and a regression test pinning lineage preservation across rename.
  (#886)
- **In-project default search merge — behaviour change (ADR-0011 PR-C).**
  ``mem_search`` / ``mem_recall`` running with an MCP server cwd inside
  a registered project (``project_memory_dirs`` covers the cwd) now
  scope by default to ``user`` rows + the current project's
  project_shared / project_local rows. Previously the same call
  returned a cross-project union (every project's rows visible). The
  fragment is composed via the new ``scope_context_sql`` helper and
  applied unconditionally in the storage layer, so a caller cannot
  accidentally drop the boundary by omitting an explicit scope.
  Out-of-project searches still return user-tier only. **Upgrade
  impact:** users who run memtomem from inside a project_memory_dirs-
  covered cwd will see fewer results on the same query than they did
  on 0.1.36 — the dropped rows lived in *other* projects'
  project-tier directories. Pass ``--scope=project_*`` (CLI) /
  ``scope='project_*'`` (MCP) to opt back into a cross-project search.
- **`mem_edit` / `mem_delete` infer scope from the chunk's persisted
  `metadata.scope` (MCP) (ADR-0011 PR-D).** Editing or deleting a
  chunk that lives in ``project_shared`` is gated by the same
  hard-refusal as ``mem_add(scope='project_shared')`` — a client
  cannot bypass Gate A on edit by omitting an explicit ``scope``
  kwarg. ``mem_delete`` adds Gate B: deleting a project_shared chunk
  requires ``confirm_project_shared=True``. Bulk
  ``mem_delete(source_file=...)`` probes the scope set of affected
  chunks via a new ``list_scopes_by_source`` storage method (using
  ``SELECT DISTINCT scope`` rather than the row-limited
  ``list_chunks_by_source``) and rejects all-or-nothing if any
  matched chunk is project_shared without explicit confirm.
- **`mem_consolidate_apply` rejects mixed-scope groups and requires
  explicit `confirm_project_shared=True` for project_shared
  consolidation (MCP) (ADR-0011 PR-D).** Source chunks are loaded
  by ``chunk_ids`` (the truth source — robust to source rename /
  re-index between ``mem_consolidate`` and the apply call) via
  ``get_chunks_batch``, not by re-resolving ``group["source"]``.
  Mixed scope sets skip with a user-visible "Skipped group N: mixed
  memory scopes (...)" message in the MCP return string (in addition
  to the existing logger.warning); single-scope groups inheriting
  ``project_shared`` skip the same way unless ``confirm_project_shared
  =True`` is passed. The summary is written via ``_mem_add_core``
  with the inherited scope so it lands in the matching tier
  directory.
- **`mm context generate` warns when Cursor/Codex/Copilot would merge
  Rules + Style.** These three runtimes fold the canonical `Rules` and
  `Style` sections into a single block via `_compact_rules`, so the
  split does not survive a hand-edit of the generated file followed by
  `mm context init` re-extraction. The CLI now emits a single yellow
  stderr notice naming only the affected runtimes that intersect with
  the target set (no warning for `--agent=claude`/`--agent=gemini`, no
  warning when only one of Rules/Style is populated). Generated file
  format is unchanged; `context.md` remains the source of truth.
- **`config.json` writers serialize home-rooted paths as `~/...`.**
  `indexing.memory_dirs` and `storage.sqlite_path` (and any future
  path-typed config field) are written in tilde form when they sit
  under `$HOME`, restoring portability for the documented "moving
  `config.json` between machines" workflow at
  `docs/guides/configuration.md:96`. Outside-`$HOME` paths stay
  absolute. Loaders are unchanged — `Path.expanduser()` already runs
  at use sites, so the round-trip is symmetric. Legacy absolute-path
  configs continue to load on the same machine; the next save through
  any writer (`mm config set`, the Web UI, `mm init`) rewrites them
  into tilde form. New helper `_portable_path_str` plus
  `_relativize_config_paths_in_place` centralize the transform; all
  four writer call sites (`save_config_overrides`,
  `_persist_auto_discover_migration`, `mm config unset`, `mm init`'s
  `_write_config_and_summary`) now invoke it before
  `_atomic_write_json`.

### Fixed

- **ADR-0011 PR-D review round 12 — `mm context memory-migrate`
  handles nested project-tier sources.** The pre-fix project_root
  inference assumed a fixed depth of three
  (``<root>/.memtomem/memories[.local]/<file>``) and hardcoded
  ``source.parent.parent.parent``. Files in subdirectories like
  ``<root>/.memtomem/memories/notes/foo.md`` left
  ``project_root=None``, AND the fallback to ``_find_project_root()``
  only ran for ``to_scope != "user"`` — so migrating a nested
  project_shared file BACK to user scope errored out at
  ``resolve_memory_scope_dir`` before any FS / DB mutation. Valid
  nested project-tier memories were unmigratable. Now walks the
  source's parents looking for the ``.memtomem`` ancestor, supporting
  arbitrary subdirectory depth.
- **ADR-0011 PR-D review round 11 — engine cooperates with migrate
  sidecar lock.** Round 10's ``mm context memory-migrate`` lock was
  one-sided: only the migrate command itself acquired the sibling
  ``.<name>.lock`` advisory file. The watcher path (``index_file``)
  never asked for the lock, so a concurrent ``mm web`` watcher firing
  ``index_file(target)`` between migrate's ``shutil.move`` and the
  DB UPDATE could still INSERT duplicate chunks at the destination.
  ``IndexEngine.index_file`` now wraps its ``_index_file`` call in
  the same ``_file_lock(_lock_path_for(...))`` so the lock pattern
  is genuinely transitive. New pin
  ``test_engine_index_file_acquires_sidecar_lock_for_watcher_cooperation``
  spies on ``_file_lock`` to confirm the entry path takes the
  expected lockfile.
- **ADR-0011 PR-D review round 11 — direct `dense_search` callers
  thread project context.** Round 9 closed the
  ``SearchPipeline.search`` and ``recall_chunks`` direct-caller gaps
  but ``dense_search`` had its own set of direct callers that the
  always-on storage scope filter still routed to user-tier-only:
  - ``GET /api/chunks/{chunk_id}/similar`` — pin to the source
    chunk's own ``metadata.project_root`` so similar-chunk results
    respect the chunk's tier.
  - ``search/conflict.py:detect_conflicts`` — accepts
    ``project_context_root`` kwarg; ``mem_conflict_detect`` MCP
    tool threads it via ``_resolve_project_context_root(app)``.
  - ``search/dedup.py:DedupScanner._find_near_duplicates`` —
    per-chunk ``chunk.metadata.project_root`` so scans honour
    each chunk's own project tier.
  - ``search/expansion.py:expand_query_headings`` — accepts
    ``project_context_root`` kwarg; ``SearchPipeline.search``
    threads it from the outer search's ``project_context_root``.
  - ``IndexEngine.is_duplicate`` — accepts the kwarg for
    forward-compat (no in-tree callers today).
  Architectural guard test (``test_scope_context_threading.py``)
  now scans ``dense_search`` and ``bm25_search`` direct callers in
  addition to ``SearchPipeline.search`` / ``recall_chunks``, so a
  future regression on any of the four read-surface methods trips
  CI.
- **ADR-0011 PR-D review round 10 — `mem_consolidate_apply` rejects
  project-tier groups with no source `project_root`.** Round 7 added
  the cross-project leak guard for groups whose sources span >1
  distinct ``project_root``, but the zero-root case slipped past:
  every source chunk with ``project_root=None`` (legacy rows
  pre-PR-B backfill, or any decode that left the column NULL) made
  ``project_root_override=None`` and ``_mem_add_core`` then resolved
  the destination via the server cwd, silently leaking summaries
  into whatever project the server happened to be in. Now rejects
  with an explicit ``no source chunk carries a persisted project_root``
  message naming a ``mm reindex`` recovery path.
- **ADR-0011 PR-D review round 10 — `mm context memory-migrate`
  watcher race + transaction lock-up.** The migrate command now
  holds a sidecar advisory lock on both the source and target paths
  spanning ``shutil.move`` + ``update_chunks_scope_for_source`` (the
  ``feedback_sidecar_lockfile_for_replaced_files.md`` pattern), and
  ``update_chunks_scope_for_source`` wraps its SELECT-then-UPDATE
  pair in an explicit ``BEGIN IMMEDIATE`` transaction. Without
  these, a concurrent ``mm web`` watcher firing
  ``index_file(target)`` between the FS move and the DB UPDATE could
  INSERT duplicate chunk rows at the destination, defeating the
  chunk-id-stability guarantee the migrate command promises.
- **ADR-0011 PR-D review round 10 — `mm context memory-migrate
  --yes` parity.** ``--to project_shared`` now requires an explicit
  ``--confirm-project-shared``; ``--yes`` alone is no longer
  sufficient. Mirrors the round-7 fix on ``mm mem add`` for CLI/MCP
  parity with the MCP ``confirm_project_shared=True`` requirement.
- **ADR-0011 PR-D review round 10 — engine `_apply_scope` unchanged-
  chunk drift documented.** Hash-diff means unchanged chunks are not
  re-UPSERTed on a regular reindex, so a previously project-tier
  file whose project is later deregistered keeps stale ``scope`` /
  ``project_root`` rows in storage. Documented the
  ``mm reindex --force`` recovery path inline at the engine site
  and via the existing CHANGELOG project-tier migration entry.
- **ADR-0011 PR-D review round 9 — read surfaces thread project
  context onto the always-on scope filter.** Round 7 introduced the
  always-on scope-context fragment in
  ``storage/sqlite_scope.scope_context_sql`` so missing
  ``project_context_root`` defaults to ``scope='user'`` only. The
  primary ``mem_search`` / ``mem_recall`` callers were updated, but
  every other read surface kept calling ``search_pipeline.search`` /
  ``storage.recall_chunks`` without the kwarg — silently dropping
  project_shared / project_local rows for any caller running inside a
  registered project. Threaded the resolver through:
  - MCP tools: ``mem_ask``, ``mem_temporal_search``,
    ``mem_procedure_list``, ``mem_agent_search``,
    ``_mem_add_core``'s post-write duplicate check, the
    ``recall_chunks`` calls in ``mem_session_summary`` and
    ``mem_reflect_save``.
  - CLI surfaces: interactive shell ``search``, ``ask``, and
    ``recall`` commands.
  - Web routes: ``GET /search``, ``GET /timeline``.
  - LangGraph integration: ``MemtomemStore.search``.
  Web routes that don't have an ``app``/``comp`` wrapper use a new
  ``_resolve_project_context_from_dirs(project_memory_dirs)`` helper
  alongside the existing ``_resolve_project_context_root(app)``
  variant. New AST-scanning guard in
  ``tests/test_scope_context_threading.py`` fails CI if a future
  call site forgets the kwarg.
- **ADR-0011 PR-D review round 7 — cross-project leak in
  `mem_consolidate_apply`.** `mem_consolidate` enumerates source files
  globally so a project-tier group can come from a project that is not
  the MCP server's current cwd. The apply path now derives the
  destination project root from the source chunks' persisted
  `metadata.project_root` (rejecting groups that span multiple
  projects) and threads it into `_mem_add_core` via a new
  `project_root_override` kwarg, so the summary lands in the source
  project's `.memtomem/...` tier instead of being silently routed to
  the server-cwd project.
- **ADR-0011 PR-D review round 7 — `mm mem add` user-tier base.**
  `cli/memory.py` now reads `comp.config.indexing.memory_dirs[0]`
  to derive the user-tier base, matching MCP `_mem_add_core` and
  `mm context memory-migrate`. The previous hardcoded
  `Path("~/.memtomem/memories")` literal split CLI/MCP writes for
  any user who remapped `memory_dirs` — exactly the divergence PR-D
  was meant to close.
- **ADR-0011 PR-D review round 7 — web PATCH/DELETE Gate B.**
  `PATCH /api/chunks/{id}` now infers scope from the loaded chunk's
  `metadata.scope` and feeds it into `enforce_write_guard`, so a
  `force_unsafe` edit on a project_shared chunk hits the same hard
  refusal MCP `mem_edit` enforces (mirrors memory_crud.py:406-413).
  `DELETE /api/chunks/{id}` adds a `confirm_project_shared` query
  parameter and refuses without it for project_shared chunks, mirroring
  the MCP `mem_delete` round-3 fix.
- **ADR-0011 PR-D review round 7 — `--yes` no longer satisfies Gate B.**
  `mm mem add --scope project_shared --yes` (without
  `--confirm-project-shared`) now exits with a clear error rather than
  silently writing to the git-tracked tier. `--yes` is a generic
  "skip prompts" flag users alias for unrelated reasons; treating it
  as an explicit project-shared opt-in broke CLI/MCP parity (MCP
  `mem_add` requires `confirm_project_shared=True` regardless).
- **ADR-0011 PR-D review round 7 — `StorageBackend` Protocol drift.**
  `bm25_search` / `dense_search` / `recall_chunks` Protocol signatures
  in `storage/base.py` now declare `scope_filter` and
  `project_context_root` kwargs (default `None`), aligning with the
  `sqlite_backend` implementation. Without these, alternate backends
  silently dropped the always-on scope-context fragment.
- **ADR-0011 PR-D review round 7 — `idx_chunks_project_root` partial
  index.** New partial index `(project_root) WHERE project_root IS
  NOT NULL` covers the dominant in-project filter shape
  `(scope='user' OR project_root=?)`. The composite
  `idx_chunks_scope (scope, project_root)` could not serve the OR's
  second leg because `project_root` is the trailing column; once a
  user opts into project tiers and accumulates rows, that leg
  degraded to a full table scan. Partial index keeps storage cheap
  for the user-tier majority case.
- **ADR-0011 PR-D review round 7 — Windows CI green.** Two test fixes:
  the `_ALLOWED_DIRECT_ACCESS_SUFFIXES` allowlist in
  `test_all_index_roots.py` now matches via `Path.as_posix()` so
  Windows backslash paths still hit forward-slash entries; the
  `scope_context_sql` parameter assertions in
  `test_search_scope_filter.py` use `str(Path(...))` so the test
  matches native-path stringification on every OS.

### Documentation

- **Obsidian as editor on top of git transport.** New section in the
  multi-device sync guide (`docs/guides/multi-device-sync.md`) describing
  vault layout (vault root = synced repo vs. vault contains a `memories/`
  sub-folder), the required `**/.obsidian/**` exclude_patterns step (vault
  metadata includes JSON which memtomem indexes by default), `.gitignore`
  guidance for vault-local state, and plugin-generated formats (`.canvas`,
  `.excalidraw.md`). Cross-references to the existing one-shot
  `mem_do(action="import_obsidian", …)` flow in `reference.md` clarify
  that they cover different use cases (live sync vs. one-shot ingest).
  Also adds an `**/.obsidian/**` example to the
  `configuration.md#exclude-patterns` block.

- **`configuration.md#exclude-patterns` example is now copy-paste-safe
  (#854).** The block previously used a ` ```jsonc ` fence with a
  path-label comment on the first line and `//` comments inside the
  array, but the fragment loader at
  `packages/memtomem/src/memtomem/config.py:1157` calls strict
  `json.loads`, so a verbatim copy into `~/.memtomem/config.d/*.json`
  was dropped after only a single startup-log WARN — practically silent
  to most users. The example is rewritten to mirror the canonical PR
  #853 shape: prose lead-in, pure-JSON fence, per-pattern `Why` table
  underneath, and an explicit strict-loader note. A new
  `TestNoJsoncFenceInPublicGuides` regression in
  `packages/memtomem/tests/test_docs_guards.py` fails CI if any
  `docs/guides/**/*.md` reintroduces a ` ```jsonc ` fence.

## [0.1.36] — 2026-05-06

### Added

- **Windows MCP server target (#818, closes #817).** `memtomem-server`
  now starts and serves over stdio on Windows. The two `fcntl.flock`
  callsites in `server/__init__.py` (legacy-flock probe + XDG-runtime
  pid lock) are swapped onto `portalocker`, mirroring the #652 swap
  that landed for the CLI side. The legacy-flock probe early-returns
  on Windows because pre-0.1.25 servers were Linux-only by
  construction (the `mm` CLI itself didn't load on Windows until
  #652 / 0.1.34). `_install_sigterm_handler` now POSIX-gates the
  `signal.signal(SIGTERM, ...)` registration; on Windows the teardown
  path is FastMCP's stdin-EOF + `atexit`, since the C runtime does
  not deliver SIGTERM. The atexit cleanup closes the lock fd
  *before* unlinking the pid file on Windows (NTFS refuses to delete
  an open or locked handle, WinError 32) while POSIX still keeps the
  unlink-before-close ordering for the #437 inode-identity invariant.
  Pid files on Windows now land at
  `%LOCALAPPDATA%\Temp\memtomem-0\server.pid`; `~/.memtomem/.server.pid`
  is intentionally never created on Windows.

- **`mm index` streams discovery + chunking progress through a
  `click.progressbar` (#748, closes #656).** Indexing now emits
  per-source events (`discovered`, `parsed`, `chunked`) that drive a
  bar with `Discovered N / Indexed M` so a long index of thousands of
  files no longer feels stuck. The bar length is sized from the
  engine's discovery event, not a heuristic, so the percentage is
  accurate.

- **Tags rename / delete / merge service exposed on Web + MCP (#795,
  PR1 of #688).** A new `tag_management` service unifies the three
  operations behind `mem_tag_rename` / `mem_tag_delete` /
  `mem_tag_list` MCP tools and the Web `PATCH /api/chunks/{id}/tags`
  delegation. Before this, rename and delete were ad-hoc SQL behind
  the search panel only. The Web chunk-edit flow now goes through
  the same service so there's one tag-graph mutation surface to
  reason about.

- **JS unit-test layer for static modules (vitest + jsdom, closes
  #641).** `web/static/`'s pure modules — markdown rendering, tag
  pill construction, sort comparators, i18n key resolution — are
  now exercised under vitest with jsdom, run from a new `js-unit`
  CI job. Closes a real coverage gap: previously these helpers were
  only touched by Playwright e2e specs, which couldn't catch a
  regression in a pure helper without going through the full SPA
  load.

- **CSRF / Origin / Host guard middleware for `mm web` (#787 stage 1,
  observe-only mode).** Logs same-site policy violations on every
  state-changing request without rejecting yet; pins the metric so a
  later stage can flip to `enforce` with confidence the false-positive
  rate is tolerable. The middleware lives in
  `web/middleware/csrf_guard.py` and is wired before the routers.

- **Privacy redaction at the LTM trust boundary covers every user-driven
  write surface (#784, #789, #802).** Previously the redaction guard
  only ran on `mem_add` and the indexing pipeline; the SPA's "Add
  Memory" form, `/api/import`, the Sources upload toast, and the
  redaction-blocked 403 confirm-and-retry path now all flow through
  the same trust-boundary scan. The Add-Memory dialog wires
  `force_unsafe` so users with a confirmed PII-class hit can opt in
  per-write rather than disabling redaction globally.

- **Browser test harness for tag-filter mutation sites (closes #751).** A
  new `packages/memtomem/tests/web/` directory runs a tiny uvicorn-in-thread
  server against a stub-routed `pytest-playwright` page and pins the
  click → DOM-state contract for the three sites in `app.js` that mutate
  `tag-filter` (`_attachResultTagRow`, `renderTagChips`,
  `_searchByTag`). The #672 regression — the Tags Cloud pill click
  silently writing the tag into `search-input` — is now caught
  automatically; previously it relied on manual review. A `browser`
  pytest marker auto-skips on machines without `pytest-playwright` or
  Chromium, mirroring the existing `ollama` pattern, so
  `uv run pytest -m "not ollama"` stays green for contributors who
  haven't run `uv run playwright install chromium`. CI gets a new
  `test-browser` job alongside `test-golden-path`.

### Changed

- **Context Gateway editor surfaces a 3-button conflict dialog on 409
  mtime mismatch instead of silently discarding the user's edits
  (closes #763).** Previously `PUT /api/context/{type}/{name}` returning
  409 (`status: aborted`, mismatched `mtime_ns`) reduced to a single
  toast (`settings.ctx.mtime_conflict`) followed by a quiet
  `loadCtxDetail` that overwrote whatever the user had typed. With
  parallel sessions and the RFC-761 Settings prod expansion bringing
  more concurrent writers, that default destroyed work whose only sin
  was racing another editor. The save handler now opens
  `#ctx-conflict-modal` with side-by-side previews (user buffer vs the
  freshly-fetched on-disk content) and three explicit choices:
  *Reload* (discard buffer, refresh detail), *Open diff editor*
  (renders the user-buffer-vs-on-disk LCS diff inline above the
  textarea so the user can hand-merge — `mtime_ns` is bumped to the
  freshly-read value so the next Save no longer 409s), or
  *Force save* (re-PUTs with the new opt-in `force: true` body field).
  The buffer is stashed in `sessionStorage` on every 409 entry so an
  Escape-out / accidental tab close does not destroy work — the next
  mount of the same `(type, name)` rehydrates the textarea and shows
  a `conflict_draft_restored` info toast. Backend: each of the three
  PUT routes (`context_skills`, `context_agents`, `context_commands`)
  gains a `force: bool = False` field on the Pydantic update model;
  when `force=True` the mtime guard is bypassed and the bypass is
  logged at `WARNING` with the path plus both client and server
  `mtime_ns` values for an audit trail. The 409 response shape itself
  is unchanged (RFC-761 PR-2 / #770 just pinned it). The legacy
  `settings.ctx.mtime_conflict` i18n key is no longer used by code
  but kept for one release as a deprecated alias to avoid churn for
  downstream translators.

- **Tag pill clicks no longer overwrite the search query (closes #672).**
  Clicking a tag in the Tags Cloud or List view now sets only `tag-filter`,
  leaving any text already in `search-input` intact. Previously
  `_searchByTag` copied the tag string into both fields, so the backend
  ran a confusing dual-axis search where the BM25 query and the tag
  filter were the same string — documents that merely *mentioned* the
  tag in prose got boosted on top of the tag-filter constraint, and the
  search bar looked as if the user had typed the tag themselves. With
  the BM25 query left alone, a tag pill click is now a pure narrowing
  filter on whatever the user was already searching for.

- **Tag pill click runs a tag-only search on a fresh session (closes #750).**
  After #672 stopped `_searchByTag` from copying the clicked tag into
  `search-input`, a fresh-session click left `search-input` empty and
  the search early-returned — landing the user on the Search tab with
  `tag-filter` populated but no results, requiring an extra keystroke
  to actually run. The frontend `doSearch()` / `load-more-btn` guards
  now allow a search whenever either `q` *or* `tag-filter` has a
  value; the `/api/search` route's `q` parameter is optional
  (rejecting only "no axis at all" with a 400); and
  `SearchPipeline.search` routes empty-query calls through a new
  `_filter_only_search` branch that enumerates via
  `storage.recall_chunks(tag_filter=…)` and skips
  BM25/dense/expansion/rescue/rerank — the post-filter stages
  (validity → decay → access boost → importance boost → context
  window) still apply so the rank reflects recency × access ×
  importance. `recall_chunks` gained an optional `tag_filter` kwarg
  with the same comma-separated OR semantics as the keyword path's
  post-fusion tag filter.

- **Settings Hooks sync surface promoted to prod web UI (refs #761).**
  Phase D of the Context Gateway (additive merge of canonical
  `.memtomem/settings.json` hooks into `~/.claude/settings.json`) is
  now visible in the default `mm web` surface, alongside Skills /
  Commands / Agents. Previously gated behind `MEMTOMEM_WEB__MODE=dev`.
  The promotion satisfies ADR-0001 §5 readiness criteria — round-trip
  + soft-abort HTTP-layer test fixtures pinned in PR #770; i18n
  parity already covered by `test_i18n.py` auto-discovery; the
  helper-level merge / mtime / atomic-write paths covered by the
  existing 21 tests in `tests/test_context_settings.py`. Rollback:
  `git revert` the gate-flip commit restores dev-only gating.

### Fixed

- **`mm uninstall` deletion is now transactional via stage-then-rmtree
  (closes #757).** Previously a partway failure (permission flip mid-walk,
  external lock acquired during deletion) left the state directory in a
  half-wiped condition with no clean recovery — the user had to manually
  audit what survived. The new path stages the entire deletion to a
  sibling `~/.memtomem.uninstall-staging/` via `os.replace`, then a
  single `rmtree` finalizes; mid-stage failures roll back via reverse
  `os.replace` and the user-visible state is unchanged. Cross-FS staging
  upfront-refuses with a clear message (`os.replace` would EXDEV) and a
  late-firing EXDEV during the rename loop also rolls back. POSIX/Windows
  parity tests use `as_posix()` keys so the snapshot survives both path
  separators.

- **`mm uninstall --force` refuses cleanly on Windows when a writer is
  alive (closes #730).** The POSIX `--force` semantics rely on
  `unlink-while-open` to wipe a path the calling user no longer wants
  even if a process holds the inode; NTFS refuses (WinError 32). Rather
  than half-wipe and fail mid-walk, Windows now refuses upfront with an
  actionable hint pointing at Sysinternals `handle.exe` / Resource
  Monitor / `Get-Process`. POSIX/Windows behavior is now pinned by a
  paired test.

- **Windows runtime dir resolution is NTFS-aware (closes #637).**
  `_runtime_paths` was using POSIX mode-bit gates against NTFS-synthesised
  permissions, which meant `ensure_runtime_dir` refused its own previously
  created directory on every second invocation. The owner-uid +
  mode-`0o700` gates now skip on `os.name == "nt"` and rely on
  `%LOCALAPPDATA%\Temp\` already being per-user; symlink rejection and
  the fresh-creation-with-mode path remain cross-platform.

- **Windows pytest cleanup of components fixture (closes #206).**
  ORT-backed embedder `close()` now forces `gc.collect()` before
  releasing the file handle, so `tmp_path` can rmtree the model dir on
  Windows without lingering handle errors. Tmp-path fixtures gained
  `mkdir(exist_ok=True)` to absorb the same-test reuse pattern.

- **Concurrent-merge abort test forces distinguishable mtimes (closes
  #645).** Windows clock resolution can produce identical `st_mtime_ns`
  for two writes in the same millisecond, breaking the test's contention
  assumption. The setup now bumps mtime explicitly between the two
  writes via `os.utime(..., ns=...)`.

- **Context Gateway editor 409 echoes the current `mtime_ns` on Settings
  resolve abort (#782).** Previously the abort response carried the
  client-submitted (stale) `mtime_ns`, so the next save still 409'd
  even when the client wanted to retry. The route now reads the file
  again post-abort and returns the fresh value so the SPA's resolve
  flow can re-arm without an extra GET.

- **Web i18n race-free dispatch + listener registration (closes #698).**
  `applyDOM` was clobbering already-rendered live data on `langchange`
  because the dispatch happened mid-render. Listeners now register at
  module top-level (not inside conditional setup) and dispatch via
  `setLang` so the order is deterministic. Tests pin both the dispatch
  contract and the listener-registration site.

- **Web a11y: `role="button"` + `aria-label` on row-as-clickable
  surfaces (#700, batched as #808 / #809 / #810).** Search results,
  Timeline rows, and source-detail chunk cards now announce as buttons
  with descriptive labels for screen readers; previously they were bare
  `<div>`s with click handlers, which read as plain text.

- **`/api/sources` resolves `memory_dir` paths instead of returning raw
  config strings (closes #675).** The Sources panel's grouping now
  matches the on-disk path the indexer used, so a `~`-prefixed
  `memory_dirs` entry no longer renders as a separate group from its
  resolved twin.

- **`mm init` filesystem ops wrap into the wizard's `fail_step` (closes
  #664).** `_step_memory_dir` and `_step_settings` now propagate IO
  errors through the wizard's structured error path instead of dumping
  a Python traceback at the user.

- **Web tab activation re-applies `hidden=true` on deactivation (closes
  #699).** Previously a freshly-activated tab could leave the
  just-deactivated sibling visible briefly, producing a flash. Settings
  Runtime group header is now hidden in prod tier (#701).

- **Sync All classifies every non-`ok` Settings status, not just
  `needs_confirmation` (closes #799).** `generate_all_settings` returns
  one of five per-result statuses — `ok` / `skipped` / `error` /
  `needs_confirmation` / `aborted`. #774 (PR #798) added a branch for
  `needs_confirmation` only, so an `error` (canonical or target file
  malformed JSON) or `aborted` (concurrent-write mtime conflict) result
  still toasted `sync_success` even though the merge never landed —
  the same class of "resp.ok hides per-result failure" bug, just with
  different status values. Sync All now surfaces `error` as an
  error-level toast carrying the route's `reason` and `aborted` as a
  warning-level `mtime_conflict` toast — both reusing the same i18n
  keys the per-target Sync flow already uses, so no new strings to
  translate. Severity ladder: `error` > `aborted` >
  `needs_confirmation` > all-`ok`/`skipped` (success). The Settings
  panel's `Sync Now` host-write confirmation flow stays out of scope
  per #774.

### Security

- **Privacy scan covers full content at the LTM trust boundary, not just
  the first 10K chars (#792).** Previously the redaction guard sampled
  only a prefix to keep the scan cheap on large pages, but a determined
  attacker could push secrets past the cutoff and have them ingested.
  The scan now runs against the full content; the perf cost on typical
  prose is negligible and the trust-boundary contract is restored.

- **DNS resolution pinned in `mem_fetch` to defeat rebinding (#794).**
  The URL fetcher now resolves once and pins the IP for the connection,
  preventing a DNS-rebinding attacker from steering a `https://example.com`
  fetch toward `127.0.0.1` between resolution and connect. Combined
  with the existing private-network refusal, this closes the rebinding
  variant of SSRF for the indexer.

- **Sync All no longer lies when Settings host writes need confirmation
  (closes #774).** The Context Gateway "Sync All" button posts to four
  endpoints sequentially; the Settings hop's `POST /api/context/settings/sync`
  body-less request defaults `allow_host_writes` to false, and the route
  returns HTTP 200 with `{"results": [{"status": "needs_confirmation",
  ...}]}` for host-write targets like `~/.claude/settings.json` — the
  merge is skipped. Previously the JS only checked `resp.ok`, so the
  post-merge `sync_success` toast lied about a merge that never
  happened. After RFC #761 (#771) flipped Settings to prod, this
  silent-skip behavior reached every prod user instead of just the
  `mm web --dev` audience. The frontend now inspects per-result
  `status` and surfaces an info-level toast (`Sync All complete except
  Settings — confirm host writes in the Settings panel.`) with an
  `Open Settings` action that navigates to the Settings panel where
  the user can drive the host-write confirmation. The Skills /
  Commands / Agents fanout keeps its all-or-nothing failure shape.

## [0.1.35] — 2026-05-02

### Added

- **Model-readiness banner for `mm web` (PR #703, closes #696).** A new
  `GET /api/system/model-readiness` endpoint reports per-component
  (embedder + reranker) load state derived from `_loading` /
  `_load_error` flags on `OnnxEmbedder` and `FastEmbedReranker`, plus a
  filesystem probe of the fastembed cache. The header banner polls the
  endpoint and renders `Downloading bge-m3 (~2300 MB)…` /
  `Loading model…` / `Model failed to load — check Settings` so the
  first search after a cold-cache boot no longer feels like a hung UI.
  Boot hydrate, `visibilitychange` re-hydrate, and `doSearch()`
  pre-flight cover the three entry points; non-onnx/fastembed providers
  short-circuit to `skipped`.

- **i18n wired up across Export, Import, Tags sort, Auto-Tag, and
  Timeline (PR #695, PR #727).** All target keys already lived in
  `en.json` / `ko.json`; the matching `data-i18n` /
  `data-i18n-placeholder` attributes were missing on Export filters,
  Import bundle/result rows, Tags sort buttons, the Auto-Tag form/result,
  and Timeline date-range/filter labels — Korean-mode users saw mixed
  English/Korean labels (`Source Filter`, `Today`, `Last 7 days`,
  `Count↓`, `Dry run`, `Limit`, etc.). The "Load All" button on the
  Sources chunk browser is also localized via a new
  `chunks.load_all` key (closes #681).

### Fixed

- **Duplicate chunk inserts on shared SQLite DBs (#691, PR #705).** When
  `mm web`'s file watcher and a separate `mm` MCP / CLI process indexed
  the same file at the same time, both processes' independent
  `asyncio.Lock`s let them each `INSERT` chunks with fresh UUIDs that
  shared `(namespace, source_file, content_hash, start_line)`. The
  differ then reused only one of the IDs per re-index, leaving the rest
  as silent ghosts — one user's DB had 77 such groups (~5% of total
  chunks) before the fix. The storage layer now enforces
  `UNIQUE(namespace, source_file, content_hash, start_line)` and uses
  `INSERT OR IGNORE` so race losers are silently dropped at insert time.

  **Migration is automatic on first startup**: existing duplicate groups
  are collapsed to one row each (keeper rule: highest accumulated
  `access_count + use_count`, tie-break on oldest `created_at` then
  `id`), with the matching `chunks_fts` and `chunks_vec` sidecar rows
  removed in lockstep. The migration body is wrapped in
  `BEGIN IMMEDIATE` / `COMMIT` so two processes booting simultaneously
  don't both run the cleanup. A log line — `Cleaned up N duplicate
  chunk row(s) across M group(s)` — is emitted once per DB; subsequent
  startups short-circuit on the new `idx_chunks_unique_content` index.

  `tools.export_import.import_chunks(on_conflict="duplicate")` is
  preserved for back-compat but the storage invariant is now
  authoritative — the mode no longer materialises duplicate rows.

- **`mm web` SPA + `/api/docs` work fully offline (PR #706, PR #712,
  closes #693).** The SPA pulled DOMPurify, marked, and Prism (core +
  five language plugins + theme) from `cdnjs.cloudflare.com`, and
  FastAPI's default `/api/docs` pulled Swagger UI bundle/css from
  `cdn.jsdelivr.net` plus a favicon from `fastapi.tiangolo.com` — three
  failure modes hit at once on every page load: (a) silent breakage on
  offline / firewalled / air-gapped deployments, (b) per-load beacon to
  the CDN with the visitor's IP / UA / request time, (c) no `integrity=`
  hashes so a CDN compromise served arbitrary code into the same origin
  as `/api/...`. All eleven assets (DOMPurify 3.1.6, marked 9.1.6, Prism
  1.29.0 + 5 lang plugins + tomorrow theme, swagger-ui-dist 5.32.5
  bundle.js + css) are now vendored under `web/static/vendor/` with
  pinned SHA-256 in `THIRD_PARTY_LICENSES.md`. CSP tightens to
  `default-src 'self'; script-src 'self'; style-src 'self'
  'unsafe-inline'; img-src 'self' data:; connect-src 'self';
  frame-ancestors 'none'` and a paired positive/negative regression
  guard pins it. `/api/docs` is hand-rolled (instead of
  `get_swagger_ui_html`) so the bootstrap is an external
  `swagger-init.js` and the favicon reuses the SPA's own — no
  `'unsafe-inline'` needed. Footprint: ~1.7 MB total minified
  (~89 KB SPA + ~1.6 MB Swagger UI).

- **Context overview badge surfaces non-`in_sync` states (PR #704,
  closes #692).** The Context tab's overview badge previously checked
  `inSync < total`, which under-counted multi-runtime divergence —
  e.g. three commands tracked under both `claude_commands` (in sync)
  and `gemini_commands` (no target) reported `3/3 synced` while
  `missing_target` was 3. The badge now sums `missing_target +
  missing_canonical + out_of_sync + parse_error` and surfaces the most
  actionable status (precedence:
  `parse_error → missing_target → missing_canonical → out_of_sync`) so
  the user sees `3 missing` in `badge-warning` colour. Five new
  `settings.ctx.badge_*` i18n keys (en + ko).

- **Home bar chart shows full namespace on hover (PR #694).** Long
  auto-namespaces like `claude:-Users-<user>-Work-<project>` truncated
  to `claude:-Users-...` under the 120 px CSS clip with no way to see
  the rest. A `title` attribute on each row makes the full string
  reachable via the browser's native tooltip.

- **Context: `installed_at` captured from filesystem mtime (PR #733,
  closes #732).** On Windows, capturing `installed_at` via Python's
  wall clock and comparing against NTFS `FILETIME` could place
  just-installed files strictly *later* than the captured stamp,
  false-positiving every fresh install as `dirty`. Capture now reads
  `max(st_mtime_ns)` from the destination tree itself, and ceiling-
  divides ns→µs before ISO-8601Z formatting so the round-trip stays
  monotonic across NTFS's 100-ns residual. POSIX is byte-identical for
  ordinary writes; the `dirty.py` strict-`>` invariant is preserved.

- **`mm init` wizard no longer crashes on Windows consoles (PR #728,
  cluster H-1 of #643).** The default Windows codepage (cp1252 / cp437)
  cannot encode the box-drawing and em-dash glyphs the wizard prints,
  so `mm init` raised `UnicodeEncodeError: 'charmap' codec can't encode
  character '─' (U+2500)` at the very first banner line. The CLI entry
  point now reconfigures `sys.stdout` / `sys.stderr` to UTF-8 with
  `errors="replace"` on `sys.platform == "win32"` (POSIX is a no-op),
  so missing glyphs degrade to `?` instead of crashing.

- **Indexing: Windows memory_dir prefixes match stored chunk paths
  (PR #717, closes #647).** `norm_dir_prefix` appended a hardcoded `/`
  to a `Path.resolve()`-ed path, yielding `C:\Users\foo/` on Windows
  that no `chunks.source_file` row could match under
  `target.startswith(prefix)`. This bricked
  `resolve_owning_memory_dir` (every chunk classified as orphan),
  `memory_dir_stats` (all dirs returned 0), the `/api/sources` `kind`
  attribution, and `delete_chunks=true` removal. Fix uses `os.sep` so
  the prefix shape matches `norm_path`'s native-separator output. No
  DB migration required.

- **Search: `source_filter` normalises path separators across seven
  MCP tools (PR #722, closes #720).** `source_filter` was substring
  / glob-matched directly against stored paths with no separator fold,
  so a caller-supplied filter like `/tmp/keep/` never matched a chunk
  stored as `\tmp\keep\policy.md` on Windows. Three contract-specific
  helpers in `search/pipeline.py`
  (`match_source_filter` / `match_source_filter_substring` /
  `match_source_filter_glob`) now centralise the fold across
  `mem_search`, `mem_list`, `mem_consolidate`, `mem_decay`,
  `mem_auto_tag`, `mem_export_chunks`, and `mem_entity_scan`. Persisted
  `chunks.source_file` rows are unchanged — only the comparison picks
  a canonical form.

- **Config: Windows backslash paths in `categorize_memory_dir`
  (PR #716, closes #316).** The provider-pattern table is forward-slash
  regex (`r"/\.claude/projects/[^/]+/memory/?$"`, etc.) but
  `categorize_memory_dir` matched against `str(path)` directly — on
  Windows that's backslash-separated, so every Windows provider
  directory silently fell through to `"user"` and the `mm init`
  wizard could not auto-detect Claude Code / Codex / Gemini memory
  folders. Fix normalises the input via `.replace("\\", "/")`; UNC
  paths and mixed separators covered.

- **Config: `~/...` `path_glob` rules expand via `as_posix()`
  (PR #726, cluster E of #643).**
  `NamespacePolicyRule._expand_and_validate_glob` stored the expanded
  glob via `str(Path(v).expanduser())`, which yielded backslashes on
  Windows. The downstream consumer `pathspec.GitIgnoreSpec` interprets
  gitignore syntax — gitignore is POSIX-only and treats `\` as an
  escape character, so any rule with a leading `~/` (a common config
  shape: `~/.claude/projects/**`, `~/Documents/notes/**`) silently
  matched zero files on Windows and fell back to `default_namespace`
  with no warning. `Path.expanduser().as_posix()` keeps the tilde
  expansion and forces `/` separators; POSIX no-op for absolute paths.

- **Wiki: `mm wiki {skill,agent,command} override` prints consistent
  separators (PR #719).** The `Seeded …` line interpolated a `Path`
  object (native separator) while the adjacent `git add …` hint
  hardcoded `/` — same logical path, two different shapes on Windows.
  Now `Seeded {rel.as_posix()}`. Absolute paths handed to `$EDITOR`
  via `click.edit(filename=str(...))` and shell pipelines via
  `click.echo(str(result.path))` intentionally stay platform-native.

### Documentation

- **`mm embedding-reset` warns about the same-dim model-swap race
  (PR #710, closes #707).** PR #705's `INSERT OR IGNORE` path keeps
  whichever embedding commits first when two processes embed the same
  chunk under *different* models; the dim-mismatch gate catches
  cross-dimension swaps but not same-dimension model swaps. A callout
  in `configuration.md#reset-flow` and a one-line warning in
  `embeddings.md` "Switching Models on an Existing Index" document the
  single-process invariant — stop `mm web` (and any other MCP / CLI
  process) before invoking `mm embedding-reset`.

### Internal

- **Windows test compatibility sweep (refs #643, #644).** A
  cross-cutting cleanup of POSIX-only assumptions in the test suite so
  the Windows informational CI job can graduate to required: a
  `set_home` helper plus a sweep of ~133 HOME monkeypatch sites
  (PR #714); `skipif` markers on POSIX-only mode-bit / `fcntl` /
  `signal` / `pwd.getpwuid` / unlink-semantics assertions (PR #713,
  PR #721, PR #725, PR #729); path-separator normalisation in
  cross-platform assertions (PR #711, PR #718); `shutil.which` for the
  `mm` entry-point lookup (PR #723); and a Windows-friendly
  `test_init_cmd.py` (PR #724).

## [0.1.34] — 2026-05-02

### Added

- **`mm context migrate` (PR-D C4, ADR-0008)** — converts agents and
  commands from the legacy flat layout (`<type>/<name>.md`) to the
  canonical directory layout (`<type>/<name>/agent.md` or
  `<type>/<name>/command.md`) introduced in PR-C. Pre-PR-C installs and
  reverse-imports left flat files on disk; this verb normalizes them so
  the dir-only paths can simplify in a future cleanup PR.

  - **Three signatures**: `mm context migrate` (every flat asset across
    `agents/` and `commands/`), `mm context migrate <type>` (one type),
    `mm context migrate <type> <name>` (single asset). Skills are always
    directory layout (Agent Skills spec); invoking the verb on
    `skills` exits 0 with an informational message rather than an error.
  - **Dry-run by default**; `--apply` mutates the filesystem via
    `os.replace` (atomic single-rename). The lockfile is not touched —
    layout is inferred from the filesystem authoritatively
    (`list_canonical_agents` / `list_canonical_commands`), and
    `installed_at` is preserved so dirty detection (Invariant 2) keeps
    working across migrations.
  - **Eight-row truth table**: the classifier surfaces every combination
    of `flat? × dir? × lockfile-entry? × dirty?` as one of six states
    (`migrate`, `noop`, `cleanup_flat`, `refuse_dirty`, `skip_manual`,
    `skip_orphan`). Manual flat files (no lockfile entry) and orphan
    lockfile entries are surfaced and left untouched — those are out of
    scope for the install/upgrade lifecycle.
  - **Dirty handling mirrors `mm context update --force`** — flat files
    with `mtime > installed_at` are refused unless `--apply --force` is
    passed. With `--force`, a `.bak` sibling is written before mutation
    so the user's edits survive in a forensic snapshot. For `flat+dir`
    collisions the dir layout is left untouched (it carries the
    canonical wiki bytes per PR-C policy); the user reviews the `.bak`
    manually if they want to merge.
  - Pairs with `mm context status`: status walks the dir-only dest tree
    (`is_asset_dirty` is dir-scoped) and so flat-only installs surface
    as `missing` rows; `mm context migrate` is the verb to normalize
    them in place.

- **Multi-project read-only discovery for `mm web` Skills/Commands/Agents
  (PR2 of the multi-project context UI series).** Each tab now renders
  collapsible scope groups so a user running `mm web` from `memtomem`
  can also browse skills/commands/agents under `~/Edu/inflearn/` (or
  any other project root) without restarting the server.
  - **`GET /api/context/projects`** enumerates every discovered scope
    with the response shape `{scope_id, label, root, tier, sources,
    missing, experimental, counts: {skills, commands, agents}}`. Sources
    union "server-cwd", "known-projects", and the opt-in
    "claude-projects" scan; the `scope_id` is `p-<sha12>` of the
    case-normalized resolved path so it survives refresh and restart.
  - **`POST /api/context/known-projects`** registers a project root
    (atomic write + sidecar lockfile). Validates absolute path +
    `is_dir()`; returns a `warning` field but still HTTP 200 when no
    `.claude`/`.gemini`/`.agents`/`.memtomem` marker is present.
  - **`DELETE /api/context/known-projects/{scope_id}`** drops a
    registration (stale entries are removable too).
  - **`GET /api/context/{skills,commands,agents}`** accept an optional
    `?scope_id=` query that targets a different project root; without
    it the legacy single-cwd contract is preserved exactly. Mutating
    routes (POST/PUT/DELETE/sync/import) stay cwd-only — multi-scope
    writes ship in PR3.
  - **Web UI** gains an "Add Project" button in each tab header and
    renders `<details>` scope groups with item counts, source/missing/
    experimental badges, and a per-scope remove (×) button. Server CWD
    items keep their click-to-edit behavior; non-cwd scope items render
    as read-only cards in PR2.
  - **Config** adds `[context_gateway]` with `known_projects_path`
    (default `~/.memtomem/known_projects.json`),
    `experimental_claude_projects_scan` (default `false`), and
    `user_tier_enabled` (default `false`, forward-compat for PR3).

- **Web UI Skills/Commands/Agents empty-state surfaces "why nothing
  happened"**. Previously `mm web` → Settings → Skills/Commands/Agents
  showed a generic `Sync completed` toast even when the canonical root
  (`.memtomem/skills/` etc.) was empty, and a generic `Import completed`
  even when no `.claude/skills/`, `.gemini/skills/`, or `.agents/skills/`
  directory existed under the project root. The toast looked successful
  but nothing actually moved on disk, so users perceived the buttons as
  broken. Now:
  - **`POST /api/context/{skills,commands,agents}/sync`** responses gain
    `canonical_root: str` and each entry in `skipped[]` carries
    `reason_code: "no_canonical_root" | "unknown_runtime" | "parse_error"
    | …` (machine-readable, stable across i18n changes).
  - **`POST /api/context/{skills,commands,agents}/import`** responses gain
    `project_root: str` + `scanned_dirs: list[str]` so the UI can name
    the cwd and the runtime directories that were inspected. Each
    `skipped[]` entry carries `reason_code` (`invalid_name`,
    `already_imported`, `canonical_exists`, `toml_parse_error`).
  - **Web UI** matches on `reason_code` (not the human prose) and shows
    info-tone toasts: `No canonical {type} under {canonical}. Create one
    first.` for empty-canonical syncs, and `No runtime {type} found in
    {project_root}. Scanned: {scan_dirs}.` for empty-runtime imports.
  - **Empty list state** under each tab now points at the canonical and
    runtime paths instead of the generic "Create one or import from
    existing runtimes." line — users see exactly where to drop a
    `SKILL.md` / `*.md` / `*.toml` to populate the panel.
  - **Sync handler** also now reads `data.skipped` (in addition to
    `data.dropped`); previously skills' `skipped` was silently ignored
    because skills don't carry `dropped` field-level omissions.

  All response changes are additive — existing clients that only read
  `imported`/`generated`/`skipped[].name|runtime|reason` keep working.
- **`mm index --debounce-window` / `--flush` / `--status` flags** (closes
  PR #536 documented gap). `mm index` now exposes a file-system-backed
  debounce queue under `~/.memtomem/index_debounce_queue.json`
  (flock-protected). Three new flags:
  - `--debounce-window <SECONDS>` records PATH and drains any entries
    silent at least SECONDS. Designed for `PostToolUse[Write]` hook
    callers — rapid consecutive writes to the same file restart the
    window, so a codegen burst indexes the final state once at the end
    rather than once per Write.
  - `--flush` synchronously drains every queued entry. **Blocks until
    each queued file is indexed (or recorded as an error).** Worst-case
    latency ≈ queue depth × per-file index cost. Plugin's `Stop` hook
    now chains `mm index --flush` before `mm session end --auto` so the
    final burst doesn't get left in the queue.
  - `--status` prints a snapshot of the queue (depth, oldest entry).
    **Race-prone by design** — concurrent hooks may add or drain
    entries between the read and any caller action. Use as telemetry
    only; `--flush` is the only correctness primitive for "drain the
    queue".

  Indexer errors leave the entry in the queue for retry on the next
  hook fire. Last-write-wins for `--namespace`/`--force` when the same
  path is enqueued twice. `MEMTOMEM_INDEX_DEBOUNCE_QUEUE` env var
  overrides the queue path (test-only).

  **Future-extensibility (RFC-B PreCompact, deferred):** when the
  PreCompact hook contract lands and a checkpoint handler wants to
  flush only the files Claude Code reports as in-flight, `--flush`
  will gain a `--paths <list>` form for selective drain. The current
  `--flush` (drain all) remains the default; the API in
  `memtomem.indexing.debounce.drain_all` already accepts an optional
  `paths=` filter so adding the CLI surface is additive.

  Plugin `hooks.json` and the `claude-code.md` Hooks Automation Setup
  snippet are updated byte-for-byte (parity test catches drift). The
  PostToolUse[Write] hook now calls `mm index --debounce-window 5`;
  the Stop hook chains `mm index --flush` before
  `mm session end --auto`.
- **Web UI Memory Dirs sort** — Sources tab → Memory Dirs panel now
  exposes a sort dropdown on any product leaf with at least 6 entries
  (the dominant case is "Claude projects" once a few dozen
  per-project dirs are auto-discovered). Six modes: newest first,
  oldest first, path A→Z, most files, most chunks, recently indexed.
  Selection persists per product via `localStorage`. Backend
  `/api/memory-dirs/status` gains `created_at` (OS filesystem birth
  time, ISO-8601 UTC) and `last_indexed` (max `chunks.updated_at`
  under the dir prefix) — both `null` for missing or un-indexed dirs.
  Linux without statx-birthtime falls back to `st_ctime`, which can
  shift on `chmod` / `chown` but is monotonic for fresh dirs in
  normal workflows.
- **Memory Dirs item-level metadata** — each row now shows the
  directory creation date (locale-formatted) and a `{files} files ·
  {chunks} chunks` badge mirroring the group total, so sort modes
  like "Most files" / "Recently indexed" can be verified row-by-row.
  Hovering the date shows full ISO timestamps for both `created_at`
  and `last_indexed`.
- **Optional chunk cleanup on memory_dir remove** —
  `POST /api/memory-dirs/remove` accepts `delete_chunks: bool` (default
  `false`). When `true`, every chunk whose `source_file` is under the
  resolved dir prefix is dropped (cascades to FTS / vector / link
  tables); the underlying files on disk are never touched. The Web
  UI's delete confirm shows an opt-in checkbox labeled "Also delete N
  indexed chunks" only when the dir has chunks — the safe default
  remains unregister-only.

### Changed

- **`mm init` wizard now prompts on failed steps instead of silently
  advancing (#626).** When `_step_embedding`'s Ollama branch hits a
  failure (server unreachable, model status indeterminate, `ollama pull`
  errored), the wizard surfaces a red `✗` line and a
  `Retry, back, or quit? [R/b/q]` prompt — pre-fix it printed a warning
  and walked into the next step (Reranker), which masked the real
  problem. Mechanism is reusable: new `StepRetry` exception +
  `fail_step()` helper in `memtomem.cli.wizard`. Tri-state
  `_ollama_has_model` returns `bool | None` so server-error is no longer
  conflated with "model absent". **Behavior change**: the OpenAI
  `--provider openai` "API key invalid + decline to continue" path now
  exits **0** (cancelled by user) instead of **1** (error). Scripts
  parsing the exit code to detect a bad-key cancel must switch to
  parsing stdout for `Wizard cancelled.` or use `--yes` mode (which
  retains the non-zero exit on missing extras). Other wizard steps
  still use the pre-#626 silent-continue pattern; an audit and follow-up
  PR will migrate them onto `fail_step` next.

- **`embedding.threads` default flipped from `0` (= ORT default = all
  physical cores) to `4`** so a bulk reindex doesn't pin every core and
  starve the web server / other apps. Live diagnosis of #640 confirmed
  the prior default made a normal indexing run feel like a hang because
  nothing else on the machine could respond. Existing installs without
  a `threads` override will see ONNX use 4 cores after upgrade — slower
  on machines with > 4 cores but visibly responsive. To restore the
  previous behavior on dedicated machines, set
  `embedding.threads = 0` in `~/.memtomem/config.json` (or
  `MEMTOMEM_EMBEDDING__THREADS=0`) and restart `mm web`. Network-bound
  providers (Ollama, OpenAI) ignore the field. Field is restart-required
  per the `MUTABLE_FIELDS` exclusion noted in `EmbeddingConfig`.

- **Force-reindex preserves chunk identity and per-chunk personalization
  (`mem_edit` / `mem_delete` / `mm index --force` / `POST /reindex`).**
  Pre-fix the force path called `delete_by_source` followed by fresh
  `upsert_chunks`, generating new UUIDs and resetting `access_count`,
  `use_count`, `last_accessed_at`, `importance_score` for every chunk
  in the file — including chunks the caller never touched. Now the
  force path runs the same hash-aware diff the non-force path uses,
  preserves IDs for hash-matched chunks, and re-embeds them
  (the `force=True` semantics) via the existing-row UPDATE clause —
  which intentionally does not rewrite personalization columns.
  User-visible: agents that cache chunk IDs across `mem_edit` calls
  stop seeing silent invalidation; access-frequency boost
  (`search/access.py`) and importance scoring
  (`server/tools/importance.py`) no longer drop to 0 when a sibling
  chunk is edited. `chunk_links` rows pointing at unchanged chunks
  also survive — pre-fix the blanket `delete_by_source` cascaded via
  `ON DELETE CASCADE` (target_id) / `ON DELETE SET NULL` (source_id),
  silently dropping consolidation-summary and provenance edges
  whenever a sibling chunk was edited. Contract recorded in
  `docs/adr/0005-force-reindex-metadata-contract.md`. (#582 item 4.2)

- **`POST /api/memory-dirs/add` indexes the registered directory by
  default now.** PR #571 introduced opt-in `auto_index` (default
  `False`) for backward compatibility; the Web UI sent
  `auto_index=true` while CLI/direct-API callers kept the two-step
  (register, then `/api/index`). PR #576 flips the default to
  `True` so omitting the field also indexes — register + index in
  one call for the common case. Callers that want register-only must
  now send `auto_index=false` explicitly (JSON `null` is also
  treated as opt-out, distinct from field omission). Response shape
  is unchanged, but callers that omit `auto_index` will now receive
  an `indexed` object (or `indexed: {"error": ...}` on failure)
  where they previously received `indexed: null` — schema-strict
  clients should expect a populated value. **Performance note:**
  `index_path()` runs in the request/response cycle, so direct-API
  callers indexing large directories will see longer `add` response
  times than before; pass `auto_index=false` to keep the historic
  register-only timings. Watcher invariant and lock boundary are
  unchanged (indexing still runs outside `_config_lock`); indexing
  failures still surface as `indexed: {"error": ...}` instead of
  bubbling a 500 (registration is preserved).

- **`showConfirm` modal** gained an optional `extraOption: { id, label,
  defaultChecked }` parameter that renders an opt-in checkbox below
  the message. Callers that pass extras receive `{ ok, extras }`
  instead of a plain boolean; existing callers without extras get the
  boolean shape unchanged (backward compat).

### Fixed

- **`mm web` Sources page per-row stats badge now renders for
  config-raw memory_dirs.** `/api/memory-dirs/status` was the only
  `/api/memory-dirs/*` endpoint that returned the path with
  `expanduser()` only — every sibling endpoint and `/api/config` use
  `expanduser().resolve()`. When `mm init` (or any wizard pass) wrote
  a tilde-prefixed entry like `~/memories` or a path under a symlinked
  prefix like macOS `/tmp` to `config.indexing.memory_dirs`, the
  frontend's `STATE.memoryDirs` (resolved) and
  `STATE.memoryStatusByPath` keys (unresolved) diverged, the per-row
  lookup missed, and the file/chunk/created badge was skipped. Status
  now resolves on read so the badge renders regardless of how the
  config row was originally persisted. (#666)
- **`mm web` Sources tab now shows orphan-indexed files instead of
  silently dropping them.** The `/api/sources` response carries rows
  with `memory_dir=null, kind=null` for two paths — Index-tab uploads
  saved to `~/.memtomem/uploads/` (which isn't a configured
  `memory_dir`) and chunks whose owning dir was removed from
  `memory_dirs` post-indexing. The server already grouped these as
  "general" with the explicit intent that orphans "ride along" so
  users can find and prune them, but the Sources tab's
  `_renderMemorySourceTree` keyed grouping by `s.memory_dir || ''` and
  then filtered out the empty key (`if (k) allDirs.add(k)`), so the
  orphan rows ended up assigned to no vendor group at all and never
  rendered. The user-facing failure mode: a file uploaded via Index
  tab appeared in `mem_search` and the namespaces tab, but clicking
  its source link from namespaces landed on a Sources tab that didn't
  contain it. The renderer now collects orphan rows separately and
  appends them as a collapsed "Other (unregistered)" sub-section under
  the `user` vendor's active panel, with file rows sharing the same
  click-to-browse-chunks behaviour as indexed dirs. The user vendor's
  sub-tab badge and the empty-state guard count orphans too, so a
  user with only Index-tab uploads sees a populated `user` tab
  instead of "user memory not found".
- **`mm web` now starts a `FileWatcher` and runs a startup backfill.**
  Two gaps fixed together because they presented as one bug ("files
  added to a `memory_dir` don't show up in Sources"):
  - `mm web`'s lifespan previously did not wire `FileWatcher` at all
    (only the MCP server's `AppContext` did), so `mm web` ran with no
    fs watcher — files added while the server was up were never
    auto-picked-up. The lifespan now starts and stops a `FileWatcher`
    in the same way `server/context.py` does, gated on the same
    degraded-mode check (skipped when embedding is broken).
  - `FileWatcher.start()` previously only registered watchdog
    observers — files that landed before `start()` (server was down,
    or the dir was newly added to `memory_dirs`) stayed invisible
    until manual reindex. A one-shot startup backfill task now walks
    each watched dir via `IndexEngine.index_path(recursive=True)`,
    gated by the new `indexing.startup_backfill` flag (**default
    False**). Content-hash dedup makes already-indexed files no-ops,
    so an enabled backfill costs the changed-file count rather than
    tree size. Default-off because an unconditional startup walk
    reintroduces the PR #295 failure mode (silent multi-minute CPU
    embed job blocking the server on first install) — the `mm init`
    wizard's opt-in seed is the user-driven path that resolves the
    same gap with a visible progress bar; users who want backfill on
    every restart can flip the flag explicitly. `mm index <dir>` and
    the web UI's per-dir Reindex button cover ad-hoc indexing without
    flipping the flag.

- **`mm init` wizard now offers `indexing.startup_backfill` as an
  opt-in toggle.** After a successful inline seed the wizard adds a
  second prompt — "Auto-index new files on every server restart?" —
  defaulting to No (same default-skip discipline as the seed prompt).
  Yes writes `indexing.startup_backfill: true` to `config.json`; the
  unset / declined / non-TTY paths leave the key out and preserve
  the `IndexingConfig` default. Closes the discoverability gap from
  the FileWatcher backfill PR — users who actively sync a memory_dir
  from elsewhere (cloud sync, periodic git pull) no longer need to
  hand-edit `config.json` to enable it.

- **`FileWatcher` startup backfill now logs progress.** A
  `Startup backfill: walking N memory_dir(s)...` line at start and a
  `Startup backfill complete: M new chunks indexed` line at end —
  without these the only backfill log was a per-dir summary that only
  fired when something was actually indexed, so opt-in users on slow
  embedders couldn't tell whether the walk was hung or just busy.

## [0.1.33] — 2026-04-29

### Added

- **`mm upgrade` CLI** (#443) — wraps `uv tool install --refresh
  --reinstall memtomem` with process-level hygiene so an in-memory
  pre-upgrade `memtomem-server` can't keep running the old code next
  to the freshly written disk bytes (the v0.1.25 → v0.1.26 stale
  `.server.pid` repro that motivated this command). Probes the server
  pid lock, sends `SIGTERM`, escalates to `SIGKILL` after `--grace`
  seconds (default 5), unlinks the stale pid file, then runs the
  reinstall. **Extras are preserved automatically** by reading the
  current `uv tool` receipt — a `memtomem[all]` install stays `[all]`
  across upgrades instead of silently regressing to a BM25-only base;
  override with `--extras onnx,web` or suppress with `--extras none`.
  `--version X.Y.Z` pins a release; `--dry-run` prints the plan;
  `--json` emits a structured result. On Windows the kill stage is
  skipped automatically (POSIX advisory flock + signals are
  unavailable) and the reinstall runs alone with a warning.
- **`chunk_links` provenance from session summary → source chunks**
  (RFC P1 Phase B-2). When the auto-summary path runs on
  `mem_session_end`, the server now writes `link_type="summarizes"`
  rows from the new `archive:session:<id>` chunk back to each source
  chunk it summarized, bounded by `session_summary.max_summary_links`
  (default 50, newest first, tail dropped per RFC Open-Question-1).
  Manual `summary=` callers do not collect source chunks and so do
  not write links. Each row is best-effort: a single `add_chunk_link`
  failure is logged and skipped rather than aborting `mem_session_end`.
- **Auto LLM session summary on `mem_session_end`** (RFC P1 Phase B-1).
  When `mem_session_end` is called without `summary=` and an LLM
  provider is configured, the server summarizes chunks added during
  the session and persists the result through Phase A's
  `archive:session:<id>` chunk path. New `session_summary` config
  block (`auto`, `min_chunks=5`, `max_summary_tokens=500`,
  `max_input_chars=60000`) gates the behavior; skip reasons
  (`disabled`, `no llm`, `below min_chunks`, `too large`,
  `empty output`, `llm error`) surface in the tool response. See
  [Session Summary](docs/guides/configuration.md#session-summary).
- **`mm session start` SessionStart hook primitives** (#541, RFC
  `memtomem-docs#24`). New flags so a Claude Code SessionStart hook
  can resume the active session instead of orphaning it on every
  start: `--idempotent` returns the existing active session for the
  same `--agent-id` (cross-agent collisions auto-end the prior
  session); `--auto-end-stale=<duration>` (e.g. `24h`, `7d`) closes
  active sessions older than the duration before the idempotency
  check, with a 100-row per-call cap so a backlog of orphans drains
  across multiple hook fires instead of stalling boot synchronously;
  `--json` emits one line of `{"session_id": ..., "resumed": bool,
  "auto_ended": [...]}` for hook parsing — the list groups both
  cutoff-based stale cleanup and cross-agent forced-end UUIDs;
  ``sessions.metadata.reason`` (`stale` or `cross_agent`) carries the
  per-row distinction. The plugin's `hooks.json`
  ships a SessionStart entry calling `mm session start --idempotent
  --auto-end-stale 24h --agent-id claude-code` and the
  `claude-code.md` Hooks Automation Setup snippet matches byte-for-byte
  via the `TestPluginHooksDocsParity` guard added in #536.
  Single-process safe; concurrent SessionStart fires are not locked
  (Claude Code's hook runner serializes them per session, which is
  the supported case).

### Changed

- **Plugin `PostToolUse[Write]` hook now filters by extension and
  path** — the inline `mm index` command in
  `packages/memtomem-claude-plugin/hooks/hooks.json` was indexing every
  `Write` regardless of file type or location, which fanned out to
  `node_modules/`, `dist/`, `__pycache__/`, lock files, binaries, and
  images in monorepo checkouts (embedding-cost amplifier + search
  noise). The hook now allowlists canonical source extensions
  (`md`, `py`, `ts`/`tsx`, `js`/`jsx`, `go`, `rs`, `rb`, `java`, `kt`,
  `swift`, `c`/`cpp`/`h`/`hpp`, `sh`, `toml`, `yaml`/`yml`, `json`)
  and blocklists build / cache / VCS paths (`node_modules`, `dist`,
  `build`, `target`, `.next`, `.nuxt`, `__pycache__`, `.git`,
  `.venv`/`venv`, `coverage`, `.cache`) inline — `case` statements,
  no external script. Adjust the patterns in `hooks.json` for
  project-specific needs. Other hooks (`UserPromptSubmit`,
  `PostToolUse activity log`, `Stop`) are unchanged. Docs at
  [`docs/guides/integrations/claude-code.md`](docs/guides/integrations/claude-code.md)
  Hooks Automation section synced to match. A
  ``test_plugin_hooks_command_matches_docs_snippet`` parity test
  locks ``hooks.json`` and the docs snippet against silent drift on
  future edits. Existing installs that copy-pasted the previous
  snippet into ``~/.claude/settings.json`` continue with the old
  unfiltered behavior until the user re-pulls the snippet. **Documented
  gap**: rapid consecutive writes still re-index the same file; native
  debounce support is tracked separately.

## [0.1.32] — 2026-04-26

### Changed (BREAKING)

- **Caller-supplied `namespace=` / `target=` overrides on every
  session-start entry point and `mem_agent_share` now reject malformed
  shapes via a new `validate_namespace` gate.** PR #491 / #494 / #498
  closed the bypass on `agent_id` itself, but each session-start surface
  also accepted an explicit `namespace=` argument that landed verbatim
  in storage — a Python / MCP / CLI caller could write
  `"agent-runtime:foo:bar"` even though `agent_id` was clean (the
  bypass shape PR #495 review flagged as Concern 3). The new validator
  now runs at `mem_session_start(namespace=)` (MCP),
  `mm session start --namespace` (CLI),
  `MemtomemStore.start_agent_session(namespace=)` and
  `MemtomemStore.start_session(namespace=)` (Python adapter),
  `mem_agent_share(target=)` (MCP), and `mm agent share --target`
  (CLI — closes #497, the kin gap PR #499 review flagged on the share
  surface). `agent-runtime:<seg>` overrides re-route the trailing
  segment through `validate_agent_id` so the override path can't widen
  the contract that the direct `agent_id=` path enforces.

  **Migration:** callers passing structured-but-unsupported namespace
  shapes (anything containing slashes, whitespace, comma, control
  characters, leading dash, or more than one colon under the
  `agent-runtime:` prefix) will now see
  `Error: invalid namespace 'X': ...` instead of the prior silent
  store. Existing in-tree shapes (`default`, `shared`,
  `archive:summary`, `claude-memory:project-x`,
  `agent-runtime:planner`, `custom:scope`) are unchanged. The bare
  single-segment `"agent-runtime"` (no trailing colon) is also rejected
  now — it shadows the multi-agent prefix and the strict-arity rule
  requires exactly one trailing segment after the prefix. Anyone
  holding such a namespace should rename it via `mem_ns_rename` before
  running session-start with the override. Closes #496 and #497.

- **`mem_agent_register`, `mem_agent_search`, and `mm agent register`
  now reject malformed `agent_id` values loudly instead of silently
  rewriting them.** PR #491 had wired `validate_agent_id` into the
  three session-start surfaces (`mem_session_start` / `mm session
  start` / `mm session wrap`) so a hostile shape like `"foo:bar"` or
  `"../x"` raised
  `Error: invalid agent-id 'foo:bar': must match [A-Za-z0-9._-]+ ...`.
  The matching multi-agent registration / search surfaces still ran
  `sanitize_namespace_segment`, so the same input produced two UX
  outcomes depending on which tool the caller hit first — registering
  an agent rewrote the id in place under `agent-runtime:foo_bar`,
  while starting a session for that same id rejected it. The read /
  write contract is now symmetric: an `agent_id` either works on
  every surface or fails on every surface, with the same
  `invalid agent-id 'X'` error fragment regardless of entry point.

  **Migration:** callers passing values that were previously rewritten
  (anything containing `:`, `/`, `..`, whitespace, or characters
  outside `[A-Za-z0-9._-]`) will now see a hard error. Those callers
  were already storing memories under unexpected namespaces — pick a
  canonical id matching the documented charset and re-register. The
  LangGraph adapter parity gap remains tracked separately. Closes #493.

- **`MemtomemStore.start_agent_session` (LangGraph adapter) now
  rejects the same hostile `agent_id` shapes via `validate_agent_id`
  instead of the prior `if not agent_id` empty-only check.** Closes
  the last in-process surface where a Python caller could land
  `"agent-runtime:foo:bar"` in storage even though the MCP / CLI
  surfaces refuse the same shape. The raised exception narrows from
  the generic `ValueError("agent_id must be a non-empty string")` to
  `InvalidNameError("invalid agent-id 'X': ...")` — `InvalidNameError`
  is a `ValueError` subclass, so `except ValueError:` callers keep
  working, but any code substring-matching the old message text will
  break and should switch to matching `"invalid agent-id"`. Closes
  #492.

### Internal

- Renamed the legacy storage-layer helper
  `memtomem.storage.sqlite_namespace.validate_namespace(name) -> bool`
  to `_is_valid_ns_chars` and removed it from
  `memtomem.storage.sqlite_backend.__all__`. Disambiguates from the
  strict caller-input validator
  `memtomem.constants.validate_namespace(value) -> str` introduced in
  PR #491–#503, which raises `InvalidNameError`. The two had the same
  name but different signatures and contracts (charset-only bool vs.
  shape-aware raises), which made debugging harder when a stack trace
  pointed at "validate_namespace". The storage helper has no in-tree
  callers outside its sibling `_ensure_valid_namespace` wrapper, so
  the rename is internal-only — it was nominally exported via
  `__all__`, but a repo-wide grep showed zero external imports.

## [0.1.31] — 2026-04-26

### Added

- **Web UI Context Gateway tabs (Artifact Sync, Skills, Commands,
  Agents) graduate from `--dev` to the polished prod surface.** The
  hardening that landed in #482 (round-trip `## *-Specific`
  preservation), #483 (project-scope `codex_agents` default), and
  #484 (settings-sync host-write confirm) closed the rough edges that
  justified hiding these from `mm web` users by default. The four
  `/api/context/*` routers and matching settings nav buttons now
  mount in `prod`; dev-only stays dev-only for `Namespaces`,
  `Sessions`, `Working Memory`, `Procedures`, `Health Report`, and
  `Hook Files`. Trust boundary unchanged — the loopback + single-user
  Tier 1 deployment shape from `feedback_tier2_web_gating_deferred.md`
  is still the only supported one, and these are the first
  mutator-heavy endpoints to ship in the prod surface, so a future
  Tier 2 hardening pass starts here.
- **`mm context {generate,sync} --include=settings` now confirms before
  writing settings files outside the project root** (today only
  `~/.claude/settings.json`); pass `--yes` / `-y` to skip the prompt.
  The same gate is enforced inside `generate_all_settings`, so callers
  that bypass the CLI — `mem_context_generate` / `mem_context_sync`
  (MCP) and `POST /settings-sync` (Web) — also refuse host writes
  unless they pass `allow_host_writes=true`. Closes one of the
  2026-04-26 audit P0 items: a stray sync from a worktree no longer
  silently edits the real home directory regardless of which front-end
  drove it. Symlink-aware (`Path.resolve()` both sides) so a
  symlinked `.claude` cannot smuggle a host write past the gate.

### Changed (BREAKING)

- **`mm context sync` now writes Codex sub-agents to project-scope
  `.codex/agents/<name>.toml` instead of user-scope
  `~/.codex/agents/<name>.toml`.** Previously every `mm context sync`
  wrote into the user's home directory regardless of which project the
  command ran from, so two projects with same-named canonical agents
  silently overwrote each other in `~/.codex/agents/`, and any
  `mm context sync` from a worktree leaked onto the real host home —
  defeating worktree isolation. The OpenAI Codex CLI [supports both
  scopes](https://developers.openai.com/codex/subagents) ("standalone
  TOML files under `~/.codex/agents/` for personal agents or
  `.codex/agents/` for project-scoped agents"), and project scope is
  the right default for a per-repo canonical source. This makes
  `codex_agents` symmetric with `claude_agents` (`.claude/agents/`)
  and `gemini_agents` (`.gemini/agents/`).

  **Migration:** if you relied on the old user-scope output, copy any
  agents you want to keep with
  `cp ~/.codex/agents/*.toml <project>/.codex/agents/`, or restore
  user-scope behavior on a future `mm context sync` once `--scope=user`
  ships (tracked separately). Existing `~/.codex/agents/` files are
  left in place; memtomem just stops writing there. After copying the
  ones you want to keep, delete the originals (`rm ~/.codex/agents/*.toml`)
  — Codex CLI still loads them, but memtomem no longer manages them, so
  any future edits in the canonical source won't reach the user-scope
  copy and the two will drift silently.

  `detect_agent_dirs` now also picks up `.codex/agents/<name>.toml`
  alongside Claude/Gemini, so `mm context detect` and the Web UI
  Context Gateway list project-scope Codex agents in their inventory.

- **`mem_add`, `mem_batch_add`, `mem_index`, and `mem_fetch` now
  inherit the agent scope from `mem_session_start(agent_id="...")`.**
  Following gap G1 of the 2026-04-26 multi-agent surface review, the
  session-aware MCP write tools join the same priority chain
  `mem_agent_search` already uses:

  1. explicit `namespace=` argument (escape hatch — wins everything),
  2. `app.current_agent_id` → `agent-runtime:<id>` (set by
     `mem_session_start`),
  3. `app.current_namespace` (legacy `mem_ns_set` path),
  4. config default.

  Previously these write tools consulted only step 3, so calling
  `mem_session_start(agent_id="planner")` followed by `mem_add` /
  `mem_index` / `mem_fetch` silently wrote to `default` instead of
  `agent-runtime:planner`, contradicting the multi-agent contract the
  public page advertises. The change is now symmetric with the
  LangGraph adapter (which has had this semantic since PR #460) and
  with `mem_agent_search`'s read-side resolution.

  `mem_consolidate_apply` already routed through `_mem_add_core`, so
  it picks up the new contract automatically.

  **Who is affected:**
  - **Pre-multi-agent users** (no `mem_session_start` call): no
    change. `current_agent_id` stays `None`; resolution falls
    through to step 3 / step 4 exactly as before.
  - **Multi-agent users using only `mem_session_start`**: writes
    now land in the agent's namespace — the documented behavior,
    finally honored. The 940-line e2e guide's workaround (passing
    `namespace="agent-runtime:<id>"` on every write) is no longer
    required.
  - **Rare combination — both `mem_ns_set` and `mem_session_start`
    set in the same session**: write target changes from the
    `mem_ns_set` namespace to the agent's namespace. Pass
    `namespace="<old-ns>"` explicitly on the call to keep the old
    destination, or call `mem_session_end()` before writing.

  `mem_add` now echoes the resolved `Namespace:` line in its
  confirmation output (`mem_batch_add` / `mem_fetch` already did),
  so the resolution is observable on the first call.

  Out of scope (tracked separately):
  - `mem_search` and `mem_recall` (single-agent reads) — `mem_search`
    keeps its `current_namespace` semantic; use `mem_agent_search` to
    read inside the agent scope.
  - `mem_import_notion` / `mem_import_obsidian` — these intentionally
    fall back to source-tagged namespaces (`notion` / `obsidian`)
    rather than the session scope, since their value is tagging
    ingested content by source.

### Fixed

- **`mm context` round-trip no longer silently drops `## <Agent>-Specific`
  sections.** `extract_sections_from_agent_file` mapped agent-override
  headings (`## Claude-Specific`, `## Cursor-Specific`,
  `## Gemini-Specific`, `## Codex-Specific`, `## Copilot-Specific`) to
  their literal heading strings, but the generators look for canonical
  keys (`Claude`, `Cursor`, …). On `mm context init` (extract-existing)
  followed by `mm context generate`, the override section was discarded
  and `mm context diff` showed `[in sync]` because both sides had the
  same loss — masking the data loss. Added the five aliases to the
  reverse-extract map so override sections survive the round-trip.
  Separately, `CopilotGenerator` emitted the override content without
  any `##` heading, so a second round-trip absorbed it into the
  preceding section; it now writes `## Copilot-Specific` like the
  other generators.

- **`mm session start` and `mm session wrap` now derive
  `agent-runtime:<id>` namespace from `--agent-id`, mirroring the MCP
  `mem_session_start` behavior shipped in PR #475.** Previously both
  CLI surfaces silently lost `--agent-id` for namespace derivation,
  leaving sessions in the `default` namespace despite the multi-agent
  contract the public page advertises (Persona-A trap from the
  2026-04-26 surface review, gap G2). `mm session wrap` was the more
  consequential half — its default `--agent-id headless` meant every
  `mm session wrap -- claude -p ...` invocation also landed in
  `default` regardless of the wrapped command. Both surfaces now use a
  shared `_derive_session_namespace` helper with the same priority
  chain as MCP: explicit `--namespace` wins; otherwise
  `agent-runtime:<agent-id>` for non-default agents; otherwise
  `default`. `mm session start` also echoes the resolved namespace so
  users can verify before continuing.

- **`mm web` Sync All button no longer toasts "Settings sync failed"
  in `prod` mode.** Follow-up to the #488 Context Gateway tabs prod
  graduation. The "Sync All" overview button fanned out to
  `/api/context/settings/sync` (the settings-hook merge), but that
  router intentionally stays dev-only; in `prod` the hop returned 404
  and the whole button toasted failure even though the artifact
  fanout (skills / commands / agents) succeeded — making the prod
  surface look broken. The same shape hit the overview's 4th
  "Settings" card, which deep-linked into the dev-only `hooks-sync`
  section, so clicking the card landed on a dead tab. Both code
  paths now self-gate on `STATE.uiMode === 'dev'` (matching the Home
  dashboard pattern); prod gets a clean 3-card overview and a Sync
  All that completes silently for the artifacts it actually has a UI
  for. JS pin tightened to count both gate sites so a future
  refactor can't silently drop one. (PR #489)

## [0.1.30] — 2026-04-26

### Fixed

- **`mm uninstall` no longer reports `pid None` after a contended
  server start.** The runtime pid file was opened with
  `open(pid_file, "w")`, which truncates at open *before* the
  `fcntl.flock(LOCK_EX | LOCK_NB)` probe decides ownership. When a
  second `memtomem-server` started while the first was still
  running (multiple Claude Code / Codex / Gemini MCP clients
  spawning in parallel, or any restart race), the second process
  zeroed out the live server's pid file, then bailed on the flock
  probe — leaving `mm uninstall` with no recorded pid to surface.
  Fixed by switching to `"a+"` (no open-time truncate) and
  performing `seek(0); truncate(); write(pid)` only after the
  flock is held. `mm uninstall` now distinguishes a `pid unknown`
  branch (truncate-race fingerprint or a partial-write startup
  crash) from `pid None` and points at `lsof <pidfile>` for
  diagnosis. Latent since the pid lock was introduced; predates
  the #412 runtime-dir relocation. (PR #476)

- **`mem_agent_share` example in server `instructions=` had wrong
  parameter name.** The recipe shipped in #477 showed
  `mem_agent_share(memory_id=...)` but the real signature is
  `mem_agent_share(chunk_id=..., target=...)`. Weak LLMs (reproduced
  on Claude Haiku 4.5) followed the example verbatim, hit a
  parameter-name rejection, then `mem_do(action="help")`-recovered to
  the correct args — burning an extra round trip per multi-agent
  share. Stronger LLMs (Sonnet 4.5+) inferred past the typo. Fixed
  the example and tightened `tests/test_server_instructions.py` with
  a signature-parity check: any `tool(arg=...)` form in INSTRUCTIONS
  is now cross-checked against `inspect.signature(...)`, so future
  parameter renames force the example to follow.

### Added

- **MCP `initialize` response now carries server-level `instructions`.**
  The MCP server passes a workflow-recipe string to
  `FastMCP(instructions=...)`, which clients auto-inject into every
  LLM session alongside the tool list. The string covers the
  single-agent quickstart (`mem_add` / `mem_search`), the multi-agent
  recipe (`mem_agent_register` → `mem_session_start` →
  `mem_agent_search` / `mem_agent_share` → `mem_session_end`),
  namespace conventions (`default` / `agent-runtime:<id>` / `shared:`),
  and common pitfalls (e.g. `mem_add` without `namespace=` consults
  `current_namespace`, not the session's `agent-runtime:*` scope).
  Source of truth: `memtomem/server/instructions.py`; pinned by
  `tests/test_server_instructions.py` so renames or convention drift
  fail loud. Motivation: tool docstrings alone left LLMs guessing
  which tool to call when users asked for "agent isolation" — clients
  were silently falling back to `mem_add` instead of
  `mem_agent_share`. The instructions field gives the LLM a workflow
  hint without anyone having to paste a system snippet.

### Changed

- **`mem_session_start(agent_id="<id>")` now derives the session
  namespace from `agent_id` when the caller doesn't pass an
  explicit `namespace=`.** Previously the session record's
  namespace fell back to `app.current_namespace` or `"default"`,
  ignoring the supplied `agent_id` even though the LangGraph
  adapter `MemtomemStore.start_agent_session` already auto-derived
  `agent-runtime:<id>`. The MCP tool now matches that semantic.
  Priority order: explicit `namespace=` > `agent-runtime:<id>`
  (when `agent_id != "default"`) > `app.current_namespace` >
  `"default"`. Only the **session record's** namespace field
  changes; `app.current_namespace` is untouched, so subsequent
  `mem_add` / `mem_search` without explicit `namespace=` keep the
  legacy fallback. Backward compat: callers passing explicit
  `namespace=` are unaffected (priority 1); callers not passing
  `agent_id` (defaults to `"default"`) are unaffected (priority 3
  fallback). Caught while walking the multi-agent test scenarios
  — `mem_session_start(agent_id="planner")` reported
  `Namespace: default` even though `current_agent_id` was set to
  `"planner"`, which the tester reasonably expected to flow into
  the session row.

## [0.1.29] — 2026-04-25

Hotfix release. `mm agent register <id>` now surfaces in
`mm agent list` immediately, even before any chunks land in the
agent's namespace. Caught during the first external walkthrough
of the v0.1.28 multi-agent test scenarios — exactly the gap the
walkthrough was designed to expose.

### Fixed

- **`mm agent list` no longer hides registered-but-empty agents.**
  `list_namespace_meta` previously sourced rows from
  `chunks LEFT JOIN namespace_metadata`, so a namespace with a
  metadata row but zero chunks was filtered out by `GROUP BY
  c.namespace`. After `mm agent register planner` the agent
  stayed invisible until someone wrote into
  `agent-runtime:planner`. The Web UI's `GET /namespaces`
  response had the same blind spot through the same storage
  method. Fixed by sourcing from the union of
  `namespace_metadata.namespace` and `chunks.namespace`, then
  joining for chunk count + description/color. Three states now
  surface correctly: metadata only (registered, 0 chunks),
  chunks only (legacy / un-registered), and both. Return shape
  unchanged so callers (CLI, Web route) pick up the fix
  transparently. (PR #473)

### Docs

- **ADR-0002 graduates to public.** The blockquote-tags
  reader/writer contract shipped in v0.1.28 is now documented at
  `docs/adr/0002-mem-add-blockquote-tags.md` with file:line
  references against the v0.1.28 layout, an inbound link from
  the `mem_add` section of the reference guide, and a reverse
  cross-link from the v0.1.28 changelog entry. (PR #472)

## [0.1.28] — 2026-04-25

Multi-agent share provenance + per-entry tag round-trip release.
Largest behavior change: the markdown chunker now promotes per-entry
``> tags:`` blockquotes into ``ChunkMetadata.tags`` and strips the
header from chunk content, so ``mem_add(tags=...)`` finally
round-trips through ``mem_search(tag_filter=...)``. This regenerates
chunk UUIDs on first reindex — a one-time discontinuity; the new
``chunk_links`` table closes the structural gap for future shares,
and a one-shot back-fill populates link rows from existing
``shared-from=<uuid>`` audit tags so older audit chains resolve.

See [ADR-0002](docs/adr/0002-mem-add-blockquote-tags.md) for the
full reader/writer contract and the reasoning behind the on-disk
blockquote format.

Also flips ``agent-runtime:`` into ``system_namespace_prefixes`` by
default (restores the isolation guarantee from the multi-agent
guide), and defaults ``mem_import(on_conflict)`` to ``"skip"`` so
re-imports become idempotent.

### Fixed

- **`mem_batch_add` no longer over-applies tags across entries or onto
  pre-existing chunks.** Pre-fix the tool collected the union of every
  entry's tags after indexing and broadcast that union onto every chunk
  the file produced — including chunks added in earlier sessions and
  unrelated entries inside the same batch. With the chunker now
  promoting per-entry blockquote tags directly (PR #463), the
  post-index broadcast is removed; per-entry tags now stay attached to
  the entry that declared them, and pre-existing chunks in the same
  file are no longer retagged on a subsequent batch.
- **Oversized sections with a per-entry blockquote header no longer
  drift sub-chunks' file line numbers.** When the chunker stripped the
  ``> created:`` / ``> tags:`` blockquote from section text before
  paragraph-splitting, ``_split_section`` still seeded its line
  counter from the heading line — so sub-chunks 2..N reported
  ``start_line`` 3–5 lines earlier than the body they actually
  covered. ``mem_edit`` of a non-first sub-chunk would then pull in
  real body lines from the previous sub-chunk and silently drop them
  on save. The chunker now tracks how many lines the strip consumed
  and seeds the line counter accordingly; the first sub-chunk's
  ``start_line`` still anchors at the heading (preserving
  ``mem_edit``'s header-preservation contract for it). Single-chunk
  sections and sections without a blockquote header are unchanged.

### Changed

- **`mem_add(tags=...)` now writes a canonical blockquote header.**
  ``append_entry`` emits ``> tags: ["a", "b"]`` (explicit ``> `` prefix
  on every line, JSON / double-quoted list) instead of the legacy
  lazy-continuation ``tags: ['a', 'b']`` (Python ``repr()``). Old
  files still parse — the chunker's section-leading parser accepts
  both shapes — but a fresh ``mem_add`` no longer relies on
  CommonMark lazy continuation for the metadata block.
- **`mem_edit` and Web UI chunk edit preserve the per-entry header.**
  New ``replace_chunk_body`` helper keeps the heading line and the
  section-leading ``> created:`` / ``> tags:`` blockquote intact when
  the caller passes body-only ``new_content``. Both ``mem_edit`` and
  the Web UI ``PATCH /api/chunks/{id}`` route use it. The Web UI
  editor surfaces ``chunk.content`` (header-stripped by the chunker),
  so this matters for Save-from-browser. To override the heading
  explicitly, prefix ``new_content`` with ``## `` and the call
  reverts to a full replacement (preserving the pre-RFC semantic).

- **`mem_add(tags=...)` now round-trips through `mem_search(tag_filter=...)`.**
  The markdown chunker promotes the per-entry blockquote header
  (``> created: ...`` / ``> tags: [...]`` / legacy lazy-continuation
  ``tags: [...]``) to first-class `ChunkMetadata.tags`, and strips it
  from chunk content so it no longer leaks into BM25 / embedding
  inputs. File-level YAML frontmatter tags compose with per-section
  blockquote tags via union. Mid-section blockquotes (a quoted
  paragraph in body prose) are untouched. Multi-agent integration
  test asserts on `metadata.tags` for `shared-from=<src>`. Reindex
  to backfill tags onto memories added by older `mem_add` calls.

  **Note: re-indexed chunks regenerate UUIDs.** Stripping the
  blockquote header changes `content`, which changes
  `content_hash = sha256(content)` (`models.py:97`), which the differ
  treats as a new chunk and assigns a fresh `uuid4()`. After
  reindex: any external pinning of `chunk_id` (notebooks, scripts,
  cross-LTM references) will miss, and existing
  `shared-from=<old-uuid>` audit-tag chains will reference UUIDs
  that no longer exist. This is a one-time discontinuity; the new
  `chunk_links` table (see below) closes the structural gap for
  future shares, and a one-shot back-fill in this release
  best-effort recovers pre-existing chains from the
  `shared-from=` tags.

### Added

- **LangGraph adapter (`MemtomemStore`) gains multi-agent helpers.** New
  `start_agent_session(agent_id)` derives the namespace from
  `agent-runtime:<id>` and binds `_current_agent_id`; subsequent
  `search()` / `add()` calls inherit the agent scope without the caller
  passing `namespace=` on every call. `search(include_shared=...)` is a
  3-state toggle: `None` (auto: include shared if an agent is bound),
  `True` (force include — raises `ValueError` if no agent is bound,
  surfacing programming errors instead of degrading to a silent
  un-pinned search), `False` (private only). `add()` defaults
  `namespace=None` to the bound agent's private bucket; pass an
  explicit `namespace="shared"` to publish across agents. Existing
  `start_session(agent_id, namespace)` is preserved as a low-level
  escape hatch. The adapter still does not implement LangGraph's
  `BaseStore` (`aput` / `aget` / `alist_namespaces`); that surface
  remains a follow-up.

- **`mm agent` CLI: `register`, `list`, `share`, plus hidden `debug-resolve`.**
  Mirrors the `mem_agent_*` MCP tools so operators don't have to spin up
  an MCP client for one-off agent setup. `mm agent list [--json]` groups
  registered agents (`agent-runtime:` namespaces) with the cross-agent
  `shared` namespace; `mm agent share <chunk-id> [--target ...]`
  performs the same content copy + `shared-from=<src>` audit tag as the
  MCP tool. The hidden `mm agent debug-resolve` dumps the namespace
  filter `mem_agent_search` would resolve given simulated
  `current_agent_id` / `current_namespace` / `--include-shared` inputs,
  as JSON — for use in multi-agent integration scripts so they can
  assert namespace resolution without standing up an MCP client.

- **Structured share lineage via new `chunk_links` table.**
  `mem_agent_share` now records a source→destination row in
  `chunk_links` (indexed FK, `ON DELETE SET NULL` on `source_id`,
  `ON DELETE CASCADE` on `target_id`) alongside the
  `shared-from=<src>` audit tag it already writes into chunk
  content. Tag-only provenance was a full-table
  `LIKE '%shared-from=%'` scan with no index and broke on reindex
  UUID churn; the structured link is an `O(fanout)` indexed lookup
  and stays correct across source delete. New `Storage` Python API:
  `add_chunk_link`, `get_chunk_link`, `get_chunks_shared_from`,
  `walk_share_chain` (cycle defence + `max_depth`). Public MCP
  surface is unchanged — `mem_agent_share`'s signature and
  copy-on-share semantics are identical; the link is a storage
  invariant. A one-shot back-fill on first 0.1.28 startup scans
  pre-existing `shared-from=<uuid>` tags and populates link rows
  so audit chains authored on older versions resolve
  structurally; unresolvable source UUIDs store `source_id=NULL`.
  (PR #469, PR #470)

- **`mem_import` gains `on_conflict` and `preserve_ids` (bundle schema v2).**
  Bundles now carry per-chunk `chunk_id` + `content_hash` so importers can
  dedup by content across instances. `on_conflict` accepts `"skip"`
  (default, idempotent re-import and cross-PC merge), `"update"` (overwrite
  existing row's metadata while preserving UUID), or `"duplicate"` (pre-v2
  row-duplication, kept for back-compat). `preserve_ids=True` reuses the
  bundle's original UUIDs for new inserts in v2 bundles. v1 bundles still
  import; hashes are derived from content on the fly. Exposed on the
  `mem_import` MCP tool, `import_chunks()`, and `POST /api/export/import`
  (`on_conflict` / `preserve_ids` multipart fields). (PR #451 follow-up)

### Changed

- **`agent-runtime:` is now hidden from default `mem_search` (behavior
  change).** ``search.system_namespace_prefixes`` default extended from
  ``["archive:"]`` to ``["archive:", "agent-runtime:"]`` so per-agent
  private chunks (created by ``mem_agent_register`` / ``mem_agent_search``)
  no longer leak into ``namespace=None`` search results — restoring the
  isolation guarantee advertised on the multi-agent guide. The hidden
  count surfaces through the existing ``hidden_system_ns`` hint, and
  ``mem_agent_search`` is unaffected because it pins ``namespace=``
  explicitly. To restore the pre-change behaviour, override
  ``search.system_namespace_prefixes: []`` in ``config.json`` (or drop
  ``agent-runtime:`` from the list while keeping ``archive:``). New
  ``memtomem.constants`` module exports ``AGENT_NAMESPACE_PREFIX``,
  ``SHARED_NAMESPACE``, and ``default_system_prefixes`` so callers
  derive the literal from a single source.

- **Default import behaviour flipped to `on_conflict="skip"`.** Previously
  every record got a fresh UUID, so re-importing the same bundle doubled
  row counts and merging bundles with overlapping content produced silent
  duplicates. Callers that relied on the old row-duplication behaviour
  must pass `on_conflict="duplicate"` explicitly.

### Fixed

- **`mem_agent_share` now stamps a `shared-from=<source-uuid>` audit
  tag on the copy, with chain dedup.** The docstring previously
  contradicted itself ("Creates a copy" / "cross-reference link instead
  of copying" / actual implementation: a brand-new chunk via
  `mem_add`). The new behaviour is a content **copy** with provenance
  recorded only via the new tag — the function name is preserved for
  API stability and true cross-reference / link semantics are tracked
  as a follow-up RFC. Re-sharing strips any inherited
  `shared-from=...` tag before appending so audit chains do not grow
  unbounded.

- **`mem_session_start` now records `agent_id` so `mem_agent_search`
  inherits it via session context.** Previously `mem_session_start`
  only set `current_session_id`, while `mem_agent_search(agent_id=None)`
  fell back to `current_namespace` — a different axis — so the
  multi-agent guide's "agent_id is not auto-detected, but inherited via
  session context" promise silently broke. New `current_agent_id` field
  on `AppContext`, guarded by a dedicated `_session_lock` (kept distinct
  from `_config_lock` so config writes can't block session updates and
  vice versa). `_resolve_agent_namespace` helper documents the priority:
  explicit `agent_id` arg > `current_agent_id` > `current_namespace`.
  Calling `mem_session_start` while a session is already active now
  auto-ends the previous session (warning log + inline notice in the
  return string) instead of silently overwriting the pointer; the
  storage row for the auto-ended session is closed with an
  `auto_ended: true` metadata flag for audit. `mem_session_end` resets
  both fields.

## [0.1.27] — 2026-04-24

Hotfix release. Closes the multi-instance bug (#444): running
`memtomem-server` in one Claude Code session used to block every
other session from connecting, because the legacy-flock probe took
`LOCK_EX` and `sys.exit(1)`'d on contention. Multiple sessions across
different projects can now coexist.

### Changed

- **Legacy flock downgraded to `LOCK_SH`; two 0.1.26+ servers can now
  coexist.** Previously `_try_hold_legacy_flock` took `LOCK_EX` and
  called `sys.exit(1)` on contention, which was intended as a
  cross-version mutex against pre-0.1.25 servers (#412 B1) but also
  blocked two 0.1.26+ instances from running at the same time — e.g.
  one MCP server per Claude Code session across multiple projects.
  Shared locks compose with other shared locks but still conflict with
  exclusive, so pre-0.1.25's `LOCK_EX` still blocks us (and vice
  versa) — cross-version protection is preserved. On contention we
  now log a warning and fall through to the XDG path rather than
  aborting. (#444, PR #445)

## [0.1.26] — 2026-04-24

Hotfix release. Closes the stale-legacy-pid-file race that caused
`memtomem-server` to intermittently refuse to start after an earlier
shutdown, producing the misleading "pre-0.1.25 install" error in
Claude Code / `claude mcp list`. The live-orphan-holder axis of the
same user symptom (server alive, handshake failed, lock legitimately
held) is tracked as a separate follow-up (#440).

### Fixed

- **Legacy `.server.pid` teardown: `~/.memtomem/.server.pid` is now
  unlinked on both normal exit and SIGTERM.** Previously the legacy
  path was only `flock`-released by kernel cleanup but the file itself
  survived, so the next server spawn could race against the stale file
  under parallel MCP health probes (e.g. `claude mcp list`) and abort
  with the misleading "pre-0.1.25 install" message.
  `_install_sigterm_handler` is now variadic and tracks both the new
  `$XDG_RUNTIME_DIR` pid file and the legacy one (when held), matching
  the `atexit` cleanup. (#437, PR #439)

## [0.1.25] — 2026-04-23

Feature + UX release. Adds `mm status` CLI as the terminal mirror of
`mem_status`. Cuts idle-handshake disk writes: MCP handshake no longer
creates `~/.memtomem/` — the DB opens on the first tool call, and the
server pid file moved to `$XDG_RUNTIME_DIR/memtomem/`. Plus several
`mm init` back-navigation and flag-precedence fixes (#371, #420, #421),
a `serverInfo.version` pin (#383), a `mem_embedding_reset` recovery-path
fix (#409), `-y` refuses to write config for missing extras (#396, #402),
and a docs troubleshooting cleanup (#398).

### Added

- **`mm status` CLI command — terminal mirror of the MCP `mem_status` tool.**
  Closes the gap noted in #382: the README and Quick Start told first-time
  users to "call the `mem_status` tool" to confirm a healthy install, but
  there was no equivalent CLI command — only `mm config show` (config, not
  runtime state) and `mm watchdog status` (periodic-check results). Users
  with no editor open, or running a scripted post-install check, had no
  one-liner that surfaced "is the DB reachable, what's indexed, is the
  embedding config in sync." `mm status` is a thin wrapper that builds an
  `AppContext` from `cli_components` and calls the same `format_status_report`
  helper now shared by `mem_status`, so the CLI and MCP outputs are
  identical. (#382)

### Changed

- **`memtomem-server` pid / flock file moved to `$XDG_RUNTIME_DIR/memtomem/server.pid`.**
  Previously the server wrote `~/.memtomem/.server.pid` at startup, which
  forced `~/.memtomem/` into existence on every MCP handshake (even for
  clients that connect but never call a tool). Runtime state now lives
  on `$XDG_RUNTIME_DIR/memtomem/` when the platform provides it
  (Linux w/ systemd), or `$TMPDIR/memtomem-$UID/` otherwise (macOS, BSD,
  Linux without systemd). Combined with #399 Phase 3's lazy DB creation,
  an idle MCP handshake against a fresh machine now leaves `~/.memtomem/`
  untouched entirely. `mm uninstall` probes both the new and legacy
  locations during the transition window, so a mixed-version upgrade
  (pre-#412 server still running + new uninstall CLI) still refuses
  correctly. (#412)

- **MCP handshake no longer creates `~/.memtomem/memtomem.db`.** Previously,
  every MCP client that connected to memtomem (Claude Code's `claude mcp list`,
  Cursor, Windsurf, Gemini CLI) instantiated the SQLite database on handshake —
  even before the user ran `mm init`, and even for short-lived health-check
  spawns. The DB now opens on the first tool call (`mem_search`, `mem_add`,
  `mem_status`, …) instead of on the lifespan startup, so a client that
  connects but never calls a tool leaves the storage path alone. Two
  follow-on behavior changes are accepted as part of the trade-off:

  - The `"Embedding dimension mismatch detected at startup — entering
    degraded mode"` warning for issue #349 no longer fires on
    `memtomem-server` boot stderr. It now surfaces inside the first tool
    call that triggers initialization. Recovery tools
    (`mem_embedding_reset`, `mem_status`, `mem_stats`, `mem_list`,
    `mem_read`) remain callable; users learn about the mismatch from
    the first tool response instead of the boot log.
  - An idle server (no tool calls) no longer runs background
    maintenance. Consolidation, policy, and health-watchdog schedulers
    start on the first tool call rather than the lifespan handshake.
    A client that connects but never calls a tool will not see
    scheduler ticks — consistent with "no DB to maintain" but worth
    flagging for any maintenance schedule that assumed
    background-without-tool-calls semantics.

  Paired with #412 (runtime pid file relocation), an idle MCP handshake
  now leaves `~/.memtomem/` untouched altogether — the advisory-lock
  write that forced its creation moved to `$XDG_RUNTIME_DIR`. (#399,
  #411; builds on #400 plumbing and #410 handler migration.)

- **Docs: troubleshooting dropped the misleading `memtomem-server` TTY
  verify command.** `uvx --from memtomem memtomem-server` launched bare in
  a terminal paints the terminal with JSON-RPC parse errors on a healthy
  install (the server expects JSON-RPC on stdin, so TTY noise triggers
  `ERROR` lines) and silently provisions `~/.memtomem/`. "Tools don't
  appear in my editor" in `docs/guides/getting-started.md` and
  `docs/guides/mcp-clients.md` now point at side-effect-free checks
  instead: `mm --version` (or `uvx --from memtomem mm --version` for
  uvx-only setups), then a `mem_status` call from inside the editor.
  A blockquote explains why the old command is not a healthy-install
  indicator. (#398, closes #381)

### Fixed

- **`mm init`: step headers show the correct position in every flow —
  not a hardcoded number correct only for `--advanced`.** Previously every
  `_step_*` function called `step_header(N, title)` with a hardcoded `N`
  that matched the step's index in the 10-step advanced sequence. Preset
  flows re-use the same step functions in shorter sequences, so
  `_step_memory_dir` advertised `3. Memory Directory` even when it was
  the first prompt the user saw (`--preset korean`) or the second (default
  interactive after a preset pick). `wizard.run_steps` now seeds
  `state["_wizard_position"] = (current_index, total)` before each
  invocation, and `step_header(state, title)` reads that instead of
  taking the integer as an argument. Numbers: advanced 1–10 (unchanged),
  `--preset X` 1–3, default-interactive 1–4 (picker counts as step 1, the
  silent `_step_provider_dirs_auto` counts as step 3 but prints no header
  of its own). (#420)

- **`mm init`: "b" (back) at the MCP step now reaches the memory-directory
  prompt instead of stalling on a silent banner.** The wizard's
  `_step_provider_dirs_auto` sits between `_step_memory_dir` and `_step_mcp`
  but only prints a detection banner — no prompt. Pre-fix, "b" at the MCP
  step decremented the step index onto that silent step, which re-emitted
  its banner and advanced straight back to `_step_mcp` — so "b" appeared to
  do nothing until the user hit it twice. `wizard.run_steps` now skips past
  `@silent_step`-marked functions when handling `StepBack`, so "b" lands on
  the previous *interactive* step (memory_dir). Banner does not re-fire on
  the back-pass (it only runs forward, once after each memory_dir
  confirmation). Unit coverage for the skip mechanism lives in
  `test_wizard.py`; end-to-end CliRunner regression is in
  `test_init_cmd.py::TestBackNavThroughSilentStep`. (#421)

- **`mm init`: "b" (back) now works at step 3 in the default interactive
  path.** The preset picker and its follow-up steps (memory dir, provider
  dirs auto-detect, MCP config) used to run in two separate `run_steps`
  calls, which made `_step_memory_dir` index 0 of the second call — so
  hitting "b" at the "Memory Directory" prompt showed `(already at first
  step)` and re-prompted the same step instead of returning to the preset
  picker. The two calls are now combined into one: `_step_preset_picker`
  applies the chosen preset inline and raises a new `_AdvancedSelected`
  exception when the user picks Advanced, which the caller catches to
  dispatch the full 10-step wizard. "b" from memory-dir decrements back
  into the picker, and re-picking a different preset overwrites the
  previously applied state cleanly (via the idempotent `_apply_preset`).
  The explicit `--preset <name>` flag path is unchanged — there's no
  picker to return to there. (#371)

- **`mm init` default-interactive CLI flag precedence is now "flag wins
  over prompt", matching the documented behavior of
  `_override_from_flags`.** The default path used to run the override
  pass before the memory-dir / MCP prompts, so prompts silently
  clobbered explicit flags: `mm init --memory-dir /x` (no `--preset`,
  no `-y`) would apply `/x`, then immediately ask for a directory and
  overwrite with whatever the user typed (usually the default
  `~/memories`). The override pass now runs after the combined
  `run_steps` on the preset branch, so flag values win — same rule that
  already held for `--preset <name>` (path 2) and `-y` (non-interactive).
  User-visible effect: `mm init --memory-dir /x` now respects `/x`
  without requiring `--preset`. Advanced (`--advanced`) is unchanged —
  advanced prompts per-field and has no baseline to override. (#371)

- **MCP `serverInfo.version` now reports the memtomem package version.**
  Previously `FastMCP.__init__` left the underlying `Server.version`
  at `None`, so the lowlevel server fell back to
  `importlib.metadata.version("mcp")` and every `initialize` response
  carried the MCP SDK version (e.g. `1.27.0`) as `serverInfo.version`
  instead of `mm --version` (e.g. `0.1.24`). Monitoring probes, client
  telemetry, and any "which version are we both on" comparison saw a
  misleading field. The value is now pinned at module construction
  time via a direct write to `mcp._mcp_server.version`. (#383)

- **`mem_embedding_reset(mode="revert_to_stored")` no longer raises
  `AttributeError` on the recovery path.** #399 Phase 1 made `embedder`,
  `search_pipeline`, and `index_engine` read-only `@property`s on
  `AppContext`, but `_revert_to_stored` kept writing to them directly.
  Any user hitting the degraded-mode "revert to stored" recovery flow
  would see `AttributeError: property 'embedder' of 'AppContext' object
  has no setter` — the exact scenario the tool exists to handle. The
  writes now mutate `app._components` fields directly (the underlying
  dataclass is still mutable by design), and a new end-to-end test
  pins all three runtime slots as swapped, not just `embedder`. (#409)

- **`mm init -y` refuses to write `config.json` when required extras are missing.**
  Previously `-y` accepted `--provider onnx|ollama|openai` and
  `--tokenizer kiwipiepy` without checking whether the corresponding Python
  extras were importable, then wrote the user-specified choice to
  `config.json`. The mismatch surfaced only at runtime as a stderr warning
  from `component_factory` (embedder falls back to 0d) or `fts_tokenizer`
  (kiwipiepy falls back to unicode61) — a scripted install would appear to
  succeed and silently degrade. `-y` now exits non-zero with an actionable
  error listing the missing extras (and collapses multiple misses into a
  `memtomem[all]` hint when all four are missing). The interactive wizard's
  warn-and-continue path is unchanged; the wizard wording was aligned with
  the new `-y` refuse semantics in #405 (closes #403). (#396, #402)

## [0.1.24] — 2026-04-23

Bug-fix release closing a first-run UX gap in `mm init --preset <name>`
and a matching prod-mode log-noise bug in the web UI. Wizard installs
that auto-discover provider memory folders (Claude Code projects,
Claude Desktop plans, Codex memories) now actually consider those
folders in the seed decision, instead of silently skipping the seed
because the primary `~/memories` happens to be empty.

### Fixed

- **Wizard auto-seed now scans `memory_dir + provider_dirs` union.**
  `mm init --preset korean` / `--preset english` / `--preset minimal`
  registers the primary memory dir plus every auto-discovered provider
  folder into `indexing.memory_dirs`, but the opt-in inline seed only
  inspected the primary dir. Since the primary is typically the empty
  `~/memories` on a fresh install, the seed silently returned `False`
  and the Next-steps hint pointed at `mm index ~/memories` — which
  indexed zero files and left the 28 provider dirs invisible to search
  until the user found Sources → Reindex All in the web UI.

  The seed now sums file count and bytes across the union (deduped
  preserving order, mirrors the `combined_dirs` construction that
  feeds `indexing.memory_dirs`) and prompts with "across N memory
  dirs" phrasing when more than one dir is in scope. `_seed_with_progress`
  streams paths serially with a single progress bar. Multi-path Ctrl-C
  / failure hints point at `mm web` → Sources → Reindex All instead of
  a misleading single-path `mm index <dir>` (the `mm index` CLI is
  single-path only as of v0.1.23). Next-steps step 1 in
  `_write_config_and_summary` gets the same split — declined seed with
  multi-dir registration now reads "uv run mm web  (Sources → Reindex
  All to index N memory_dirs)" instead of `mm index ~/memories`.
  PR #295 constraints remain intact: every seed action is
  confirmation-gated (default No), progress-bar instrumented, and
  Ctrl-C resumable. No silent startup scan is reintroduced. (#388)

- **Web UI: `loadNamespaceDropdowns` gated on dev mode.** `/api/namespaces`
  is mounted only in `_DEV_ONLY_ROUTERS` by design (test pin in
  `test_web_mode.py::test_dev_only_routes_blocked_in_prod_but_exposed_in_dev`),
  and the `loadDashboard` caller already had the matching gate. The
  dropdown loader in `settings-namespaces.js` was the orphan: every
  prod session fired a guaranteed 404 at page load (bare module-level
  call) and another on each switch to the search or timeline tab.
  Gate is now internal to `loadNamespaceDropdowns` (prod → early
  return, filter dropdowns keep the static "All Namespaces" option);
  boot-time populate moves into `_applyUiModeFilter` which runs after
  `STATE.uiMode` resolves, so dev still gets a single deterministic
  fetch instead of the previous race-dependent bare call. (#385)

## [0.1.23] — 2026-04-22

Feature release adding a first-class `mm uninstall` command so users can
clean up local state (DB, config, fragments) with confirmation and
install-context awareness, instead of guessing at `rm -rf ~/.memtomem/`.

### Added

- **`mm uninstall` CLI** — categorised inventory + confirmation prompt +
  per-install-context binary-uninstall hint. Flags: `--keep-config`
  (preserve `config.json` + `config.d/*` + backups), `--keep-data`
  (preserve DB + WAL/SHM/journal + `memories/`), `--force` (bypass the
  running-server safety check), `-y/--yes` (skip confirmation).

  The command solves three concrete problems that `uv tool uninstall`
  alone can't: (1) it tells you the right binary-removal command for
  your detected install context (uv-tool / uvx / venv-relative / system
  / unknown, reusing the existing `RuntimeProfile` from
  `cli/init_cmd.py`), (2) it refuses to delete while the MCP server is
  alive (open WAL handle during deletion risks corruption), and (3) it
  honours custom `storage.sqlite_path` so DBs outside `~/.memtomem/` are
  included in the cleanup. It falls back to default paths when
  `config.json` can't be loaded (uninstall is itself a recovery
  scenario), and detects external editor MCP entries (`~/.claude.json`,
  `~/.codex/config.toml`, etc.) as inventory-only — those must still be
  cleaned manually, tracked for a follow-up PR. Deletion runs in
  low→high value order (pid/session → fragments → backups → config →
  memories → DB) with per-group success logging, so a mid-flight
  failure leaves a recoverable trail. (#379)

### Docs

- **`docs/guides/uninstall.md`** — adds a "Recommended: `mm uninstall`"
  section at the top; the manual `rm -rf` flow remains below as a
  fallback for environments without the CLI installed. (#379)

## [0.1.22] — 2026-04-22

Bug-fix release closing the second root-cause path for "no such table:
chunks_vec". v0.1.21 (#305) added the startup gate for legacy DBs whose
stored `dim=0` mismatched a real configured provider; this release fixes
the symmetric writer-side gap that surfaced on **fresh** `mm init
--provider none` installs (no provider switch, just intentional BM25-only
mode). Also bundles two follow-ups from the v0.1.21 wizard work that
wouldn't have made sense to ship separately.

### Fixed

- **`mm index` now succeeds on fresh `--provider none` installs** —
  NoopEmbedder reports `dimension=0`, so the schema gate at
  `sqlite_schema.py` correctly skipped creating the `chunks_vec` virtual
  table. But the writer never honored that skip: `upsert_chunks`,
  `delete_chunks`, `delete_by_source`, and `delete_by_namespace` issued
  unconditional SQL against `chunks_vec`, rolling back the whole
  transaction with `no such table: chunks_vec`. Every file indexed as 0
  chunks, and BM25 search returned empty because the FTS write shared
  the same rolled-back transaction. Fix gates every chunks_vec touch on
  `SqliteBackend._has_vec_table` (cached boolean, primed from a one-time
  `sqlite_master` probe at `initialize()` and refreshed by
  `reset_embedding_meta` / `reset_all`). `NamespaceOps` receives the
  flag as a required live callable so dim=0 → real-provider recovery
  flips cleanly without reconstruction. Existing dim=0 DBs need no
  migration; `mm embedding-reset` recovery path is unchanged. (#377)

### Added

- **Wizard auto-seeds the initial index for small memory dirs** — when
  the discovered `memory_dirs` are below the size threshold and stdin is
  a TTY, `mm init` offers an opt-in confirmation (default No) to run the
  initial `mm index` inline so users see search results immediately
  without a separate command. (#375)
- **Wizard shows progress bar + final summary for large memory dirs** —
  same opt-in seed extended to large dirs with a streaming progress bar
  (files-done / files-total) and a defensive "0 chunks indexed" yellow
  warning if the stream lands zero counters. (#376)

### Docs

- **Indexing model clarified** — `memory_dirs` are auto-watched (file
  changes are picked up live) while `mm index` is the one-shot
  manual-seed entry; previous wording conflated the two. (#374)

## [0.1.21] — 2026-04-22

Phase 3 of the `mm init` install-context series (#360 → v0.1.20 → this
release). The v0.1.18 axis-mismatch bug exposed two independent axes
(cwd filesystem vs runtime interpreter) being re-derived at five
different call sites with no shared contract; this release collapses
them into a single `RuntimeProfile` struct so the next install-context
judgment has exactly one place to land. Also adds first-class `uvx`
detection — pre-Phase-3 the wizard bucketed ephemeral-env invocations
into the generic "PyPI" label and the ephemeral-install hint branch
was dead code.

### Changed

- **`mm init` now classifies `uvx memtomem init` as ephemeral** — when the
  wizard detects it's running under `uvx` (sys.prefix points at
  `~/.cache/uv/archive-v0/…` or `builds-v0/…`), the summary labels the
  install as `uvx (ephemeral)` and `Next steps` adds a one-line note
  explaining the env is destroyed on exit and pointing at
  `uv tool install "memtomem[all]"` for repeat use. Pre-Phase-3 the wizard
  bucketed uvx into the generic "PyPI" label and the existing
  `_install_extras` uvx hint branch was dead code (the call site never
  routed to it). (#363)

### Internal

- **`RuntimeProfile` dataclass + single-source-of-truth refactor** — the
  cwd filesystem axis (source / project / pypi) and runtime interpreter
  axis (`sys.executable`, workspace `.venv` match, `mm` binary origin) are
  now built once at `mm init` entry into a frozen `RuntimeProfile` struct
  threaded through state; downstream call sites
  (`_collect_missing_extras`, `_extra_install_hint`, the cwd-vs-runtime
  mismatch banner, `Next steps` `run_prefix`, summary `Install:` label)
  all read from it. Replaces 4 near-identical 5-ancestor walks
  (`_is_source_install` / `_detect_source_dir` / `_is_project_install` /
  `_detect_project_dir`) with 2 helpers returning `Path | None`. Also
  unifies the in-process module-presence check on `importlib.util.find_spec`
  across `mm init` and `mm web` (was split between `find_spec` and
  `__import__`). 20 new tests cover RuntimeProfile fields, the 5-way
  `mm_binary_origin` heuristic, and the project-install path E2E. (#363)
- **Legacy `state["source_install"] / source_dir / project_install /
  project_dir` keys dropped from `mm init`** — after the Phase 3
  `RuntimeProfile` refactor landed, the parallel legacy state keys were
  intentionally left in place so #367 could ship as a structural-only
  change. This release removes them: `init()` entry now writes only
  `state["_profile"]`, and every downstream reader (the MCP server
  command builder, the missing-extras workspace dir, the "Detected:
  install" echo paths) reads from `profile.cwd_install_type` /
  `profile.cwd_install_dir` directly. Test fixtures migrate to a new
  `_make_test_profile(tmp_path, kind=...)` helper that builds a
  `RuntimeProfile` without invoking the live `_runtime_profile()`. (#368,
  #369)
- **`_get_or_build_profile` back-compat shim removed** — the shim existed
  to let test fixtures build state directly without populating
  `_profile`. With the legacy state keys gone (#369) the reconstruction
  path was dead for production; this release deletes the function
  entirely (~65 lines) and inlines `state["_profile"]` at the 6 call
  sites. `_extra_install_hint` and `_collect_missing_extras` now tolerate
  missing `_profile` via `state.get("_profile")` + `None`-check, treating
  it as PyPI install — preserves the documented "no state / PyPI default"
  contract that `_extra_install_hint(extras, state=None)` already
  advertised. (#370)

## [0.1.20] — 2026-04-22

Phase 2 of the `mm init` install-context UX series (#360 → #361 → this
release). Phase 1 (v0.1.19) fixed the install-hint text for the
cwd-vs-runtime mismatch; this release closes the UX loop by actually
explaining the mismatch and offering to install the missing extras.

### Changed

- **`mm init` now offers to auto-install missing python extras** — the
  summary still surfaces the same Phase 1 install-type-aware hint, but
  first prompts `Install memtomem[all] now?` (defaulting to No — Enter
  skips, preserving the previous hint-only behavior). When the user
  confirms, the wizard shells out to `uv sync --extra …`
  (source/project installs) or `uv tool install --reinstall
  "memtomem[…]"` (tool installs) and prints a single `Installed missing
  extras: …` line on success. Subprocess failures (missing binary,
  timeout, non-zero rc) fall back to the hint. Also adds a one-line
  info banner when the wizard is run from a source/project checkout
  with a global (non-workspace-venv) interpreter, explaining that
  `Next steps` assume `uv run mm` — Phase 1 silenced the false warning
  for this combination but didn't explain why. (#360 Phase 2, #362,
  #364)
- **Non-interactive `mm init -y` runs skip the new prompt** —
  non-TTY contexts (e.g. `mm init -y </dev/null`, CI jobs, Docker
  build steps) short-circuit the `Install memtomem[all] now?` prompt
  entirely and fall back to the Phase 1 hint output; TTY runs show
  the prompt with a **No** default. The non-TTY gate is required
  because `click.prompt` raises `Abort!` on stdin EOF rather than
  returning the `default=` — without it, every scripted pipeline that
  passed on v0.1.19 would hard-exit in v0.1.20. (#365)

## [0.1.19] — 2026-04-22

First-UX follow-up to 0.1.18. The new missing-extras warning misfired on
the "source repo cwd + global `mm`" combination, sending users into an
unnecessary tool-env reinstall when their workspace `.venv` already had
the extras `uv run mm` would use.

### Fixed

- **`mm init` reconciles install-type detection with runtime interpreter**
  — when source-install or project-install is detected, the missing-extras
  probe now spawns `<dir>/.venv/bin/python` (the interpreter `uv run mm`
  will use) instead of running `find_spec` in the wizard's own process,
  and the install hint branches by install type: workspace installs get
  `uv sync --extra <name>` / `--extra all`, while PyPI / `uv tool install`
  paths keep the existing `uv tool install --reinstall "memtomem[…]"`
  hint. Fresh worktrees without a workspace `.venv` see a single
  `Workspace .venv not found — run \`uv sync --extra all\` first.` line
  instead of a noisy warning that probes the wrong interpreter. The
  interactive `_step_embedding` advanced path also shares the install-type
  hint and marks the warning as surfaced so the summary doesn't double-
  print. Subprocess probe failures (timeout / missing binary / bad JSON)
  fall back to the in-process probe so the wizard never silently swallows
  a real gap. (#360 Phase 1)

## [0.1.18] — 2026-04-22

First-UX patch on top of 0.1.17. New users following `mm init` →
`mm web` with a base install hit two silent-failure modes that this
release closes.

### Fixed

- **`mm init` surfaces missing `[onnx]`/`[web]` extras in the summary** —
  preset paths (minimal/english/korean) skipped `_step_embedding`'s
  inline fastembed check, and the `[web]` hint was gated out for
  source-install users. The wizard summary now lists any missing extras
  with a single `memtomem[all]` install hint (or narrower
  `memtomem[onnx]` / `memtomem[web]` when only one is missing) before
  "Next steps", so `mm index` and `mm web` don't fail with opaque
  `fastembed is required` / `Web UI requires the [web] extra` errors
  after following the printed commands. Interpreter-local
  `importlib.util.find_spec` check matches the `mm` binary the wizard
  is running under. (#353, #356)
- **Web UI surfaces `/api/index` embedding failures** — the backend
  route already propagated `IndexingStats.errors` in the response body,
  but the main-tab index handler in `app.js` ignored the field and
  always fired a green "Indexed N chunks" toast. A run where every file
  failed to embed (base install missing `fastembed`) looked like a
  clean success despite hundreds of `Embedding failed … fastembed is
  required` entries on stderr. The handler now flips to a red
  `toast.index_partial` toast and unhides an `Errors` row in the result
  card with up to 5 entries plus a `"…and N more"` trailer. i18n
  `toast.index_partial` / `index.result.errors` added for en + ko.
  (#354, #357)

## [0.1.17] — 2026-04-22

memtomem remains in **alpha**. This release closes the embedding-mismatch
recovery loop end-to-end. The MCP server used to fail-fast crash at
lifespan startup whenever a legacy `provider=none` DB (dim=0) was paired
with a real-provider config, leaving MCP clients with no in-protocol path
to fix it. The server now stays up in degraded mode, `mem_embedding_reset`
drives recovery over MCP, and the `mm web` banner + one-click reset
makes the same flow clickable instead of terminal-only.

### Added

- **`mm version` subcommand** — prints `memtomem <version>`, identical output to
  `mm --version`. Adds parity with `mms version` (memtomem-stm) so users
  switching between the two CLIs get consistent behavior. (#347)
- **`mm init` detects reinstall-path embedding mismatch** — when the new
  preset's provider / dimension differs from what an existing
  `~/.memtomem/memtomem.db` has stored (classically a previous
  `provider=none` install → `chunks_vec` at dim=0 vs a new `onnx/bge-m3`
  1024d config), the wizard now prints the mismatch and offers to reset
  the vector index in place. Without this prompt the next MCP server
  startup would fail with `EmbeddingDimensionMismatchError`. Under
  non-interactive `-y`, the wizard prints a loud recovery hint
  (`mm embedding-reset --mode apply-current`) instead of prompting. The
  chunks table itself is preserved; only the vector index is rebuilt,
  so a re-index is required afterwards. (#348)

### Changed

- **MCP server degraded mode on embedding mismatch (behavior change)** —
  `memtomem-server` no longer fail-fast crashes with
  `EmbeddingDimensionMismatchError` when it encounters a `chunks_vec` /
  provider mismatch at startup. It re-opens storage with
  `strict_dim_check=False` (the same seam `mm embedding-reset` uses),
  exposes the structured mismatch info on `AppContext.embedding_broken`,
  and stays callable. Vector-dependent writes (`mem_add`, `mem_batch_add`,
  `mem_edit`) return an actionable error instead of crashing on
  `upsert_chunks`; `mem_status` / `mem_stats` surface a DEGRADED line;
  `mem_embedding_reset` drives recovery entirely over MCP. File watcher
  and background schedulers (consolidation, policy, health_watchdog) are
  skipped in degraded mode — restart after recovery to bring them back.
  (#349, #350)
- **`mm web` banner fires on degraded mode** — the web lifespan used to
  auto-sync runtime config to the DB-stored embedding info and clear the
  mismatch flag, which for the dim=0 case silently downgraded the user's
  onnx/bge-m3 config to BM25-only and suppressed the banner. The auto-sync
  now skips when `embedding_broken` is set, so the existing
  `#embedding-mismatch-banner` + "Reset vector index" button reach the
  user. (#349 follow-up, #351)
- **`docs/guides/uninstall.md` documents the reinstall-from-scratch path** —
  new section explaining that `mm init` only rewrites config/MCP and
  leaves the DB in place, with a `rm -rf ~/.memtomem && mm init` recipe
  for users who want a fully blank slate. (#348)

## [0.1.16] — 2026-04-21

memtomem remains in **alpha**. This release narrows the default `mm web`
surface to the polished page set so first-time users see a coherent
dashboard instead of the full maintainer toolbox. The full surface
remains available via `mm web --dev` (or `MEMTOMEM_WEB__MODE=dev`), and
every UI-level hide is backed by a matching 404 on the API so
scripting callers fail loudly rather than against a half-built page.

### Added

- **`mm web --mode` / `--dev` flags + `MEMTOMEM_WEB__MODE` env var** — select
  the Web UI surface. `prod` (default) shows the polished page set; `dev`
  extends it with opt-in maintainer pages (Namespaces, Sessions, Working
  Memory, Procedures, Health Report, Artifact Sync, Hook Files,
  Skills/Commands/Agents). `--mode` and `--dev` are mutually exclusive; an
  invalid env value fails fast instead of silently falling back. (#343, #344)
- **`GET /api/system/ui-mode`** — localhost-guarded endpoint returning the
  resolved mode so the SPA can filter tabs on boot. (#343)

### Changed

- **`mm web` default surface shrunk to the polished page set.** `Home`,
  `Search`, `Sources`, `Index`, `Tags`, `Timeline`, and Settings (`Config`,
  `Dedup`, `Age-out`, `Export/Import`, `Reset Database`) are always on. The
  10 remaining Settings sections are opt-in via `mm web --dev` (or
  `MEMTOMEM_WEB__MODE=dev`). Their API routes (`/api/namespaces`,
  `/api/sessions`, `/api/scratch`, `/api/procedures`, `/api/context/*`,
  `/api/settings-sync`, `/api/watchdog/*`, `/api/eval/*`) now return
  **404** in `prod` mode — scripts that rely on them need `dev` mode. (#344)

## [0.1.15] — 2026-04-21

memtomem remains in **alpha**. APIs, defaults, and on-disk config surfaces
may still shift between 0.1.x releases — external feedback and issue
reports are especially welcome at
[github.com/memtomem/memtomem/issues](https://github.com/memtomem/memtomem/issues).

This release focuses on **CLI scriptability**. All read/write session
commands now expose `--json` output for hook consumers and scripting
pipelines, `mm --version` answers the obvious first question newcomers
ask, and the contributor docs lock the `--json` error-shape convention
(read vs write) so future flags don't re-litigate the shape per command.

### Added

- **`mm --version` flag** — Click's idiomatic entry point for version output,
  added via `click.version_option` at the group level. Emits
  `memtomem X.Y.Z`. (#330)
- **`mm config show --json`** — alias of `--format json`, added to align with
  the documented CLI output convention (binary human/machine scenario uses
  `--json`). Both flag forms emit identical output. (#332)
- **`mm session list --json`** — scriptable JSON output for the session list,
  emitting `{sessions: [...], count: N}`. Closes the parity gap where
  `watchdog` and `config show` were already scriptable but `session list`
  required parsing the text table. (#331)
- **`mm session events --json`** — same treatment for per-session event
  output, emitting `{session_id, events: [...], count: N}`. When invoked
  without a session argument and no active session, returns
  `{error: "no_session"}` on stdout (exit 0) so scripts can parse the
  failure instead of getting a Click exit-1 + stderr line. Text path is
  unchanged. (#331)
- **`mm activity log --json`** — scriptable ack output for hook-driven
  event writes. Success: `{ok: true, session_id, event_type}`. No active
  session: `{ok: false, reason: "no_active_session"}`. Write failure:
  `{ok: false, reason: "write_failed"}`. Exit code is always 0 (the
  silent-by-default hook contract is preserved without the flag). The
  ok-flag shape intentionally differs from `session events --json`'s
  `{error: ...}` — write acks have no natural disambiguator, so an
  explicit `ok` discriminator is clearer for consumers. (#335)
- **`mm activity log --json` invalid-meta ack** — malformed `--meta`
  JSON now emits `{ok: false, reason: "invalid_meta"}` under `--json`
  (exit 0) so scripts can distinguish bad input from a write failure.
  Without `--json` the `json.JSONDecodeError` still bubbles to Click
  (traceback + exit 1) so a hook author mistyping meta sees why. (#338)

### Changed

- **Documented CLI output convention** — `CONTRIBUTING.md` now spells out
  when to use `--json` (binary scenario) vs `--format [table|json|...]`
  (genuine non-JSON modes like `plain` / `context` / `smart`), with a
  forward-compatibility guidance to prefer `--format` when a command might
  grow additional modes later. (#332)
- **Documented JSON error shape convention** — `CONTRIBUTING.md` now
  specifies the `--json` error shape by command kind: read commands
  (`list`, `get`, `show`, `events`, `status`) emit `{"error": "<reason>"}`
  since their success payloads self-disambiguate; write commands (`log`,
  `add`, `set`, `run`) emit `{"ok": false, "reason": "<reason>"}` with an
  explicit discriminator since write acks have no natural key-based
  disambiguator. Both shapes exit 0 so `--json` pipelines don't break
  on handled failures. Anchors the shape choice made in #336 / #337 so
  future `--json` flags don't re-litigate per-command. (#339)

## [0.1.14] — 2026-04-21

memtomem remains in **alpha**. APIs, defaults, and on-disk config surfaces
may still shift between 0.1.x releases — external feedback and issue
reports are especially welcome at
[github.com/memtomem/memtomem/issues](https://github.com/memtomem/memtomem/issues).

### Added
- **`mm init` preset picker**: interactive `mm init` now opens with a preset
  picker (`Minimal` / `English (Recommended)` / `Korean-optimized`) plus an
  `Advanced` entry that runs the full 10-step wizard. Preset paths only
  prompt for the memory directory and MCP registration; embedding /
  reranker / tokenizer / namespace defaults come from the preset bundle.
  New CLI flags `--preset <name>` and `--advanced` expose the same choices
  non-interactively; `--preset` and `--advanced` are mutually exclusive.
  (#326)
- **Non-TTY guard for `mm init`**: running the default interactive path
  with piped stdin (no `--preset`, no `--advanced`, no `-y`) now exits
  cleanly with a usage error pointing at those flags, instead of hanging
  on a closed prompt. (#326)

### Changed
- **`mm init -y` behavior**: scripted `mm init -y` (with no other flags)
  is now equivalent to `mm init --preset minimal -y` — same defaults as
  before this release (provider=none, BM25-only, unicode61 tokenizer), so
  existing CI / automation calls continue to work unchanged. Existing
  explicit flags (`--provider`, `--model`, `--tokenizer`, ...) override
  the preset baseline in both interactive and non-interactive paths.
  (#326)

## [0.1.13] — 2026-04-20

memtomem remains in **alpha**. APIs, defaults, and on-disk config surfaces
may still shift between 0.1.x releases — external feedback and issue
reports are especially welcome at
[github.com/memtomem/memtomem/issues](https://github.com/memtomem/memtomem/issues).

### Added
- **`mm agent migrate` CLI**: renames legacy `agent/{id}` namespaces to
  `agent-runtime:{id}` (see `### Changed` below). Pass `--dry-run` to preview
  without applying. Safe to re-run — namespaces already in the new format
  are skipped (#318).
- **Wizard preset namespace rules**: `mm init` now appends matching
  `NamespacePolicyRule` entries to `namespace.rules` when you accept a
  provider category, so auto-discovered Claude-projects memory dirs route
  to a meaningful namespace instead of collapsing to `default` (#296).
  `claude-memory` → `claude:{ancestor:1}` (picks the project-id folder
  above the generic `memory` basename); `claude-plans` → `claude-plans`;
  `codex` → `codex`. Rules are deduplicated by `path_glob` (with `~`
  expansion on both sides) so re-running `mm init` is idempotent, and
  user-authored rules with the same `path_glob` but a different namespace
  are preserved rather than overwritten. The flag-driven non-interactive
  path (`--include-provider`) matches the interactive behavior. Labels are
  deliberately flat pending RFC #304 (`{provider, product}` hierarchy). The
  four-entry vocabulary (`user`, `claude-memory`, `claude-plans`, `codex`)
  is locked against silent expansion via an import-time assertion (#313).
- **Reranker candidate-pool scaling**: `rerank.oversample` (default `2.0`),
  `rerank.min_pool` (default `20`), and `rerank.max_pool` (default `200`).
  The cross-encoder now sees
  `max(min_pool, min(max_pool, int(oversample * top_k)))` candidates, so the
  classic 2× oversample holds at `top_k=10` (pool=20) and scales with
  larger requests (`top_k=50` → pool=100, `top_k=150` → pool=200). All
  three knobs plus `rerank.enabled` are runtime-tunable via `mm config set`
  and the Web UI — no restart required. `provider`/`model`/`api_key` still
  need a restart (reranker instance is cached).
- **Vendor → product grouping in the Memory Dirs panel**: the Sources tab
  now groups memory directories by vendor (`User`, `Claude`, `OpenAI`)
  with multi-product vendors (currently `Claude` → `Claude projects` +
  `Claude plans`) rendering products as nested sections. Single-product
  vendors keep the previous one-row layout with the product label
  (`User`, `Codex`). Driven by a new `provider` field on
  `GET /api/memory-dirs/status` so the client doesn't duplicate the
  category → vendor map. RFC #304 Phase 1–2 (#321 + #322). New i18n
  keys: `sources.memory_dirs.provider.{user,claude,openai}` (en + ko).

### Changed
- **Multi-agent namespace format**: `mem_agent_register` / `mem_agent_search`
  now generate `agent-runtime:{agent_id}` instead of the legacy
  `agent/{agent_id}`, aligning with the `{bucket}-{kind}:` convention used by
  `claude-memory:` and `codex-memory:` (#318). `/` is dropped from
  `_NS_NAME_RE` (reverting the temporary widening in #319) since no live
  caller needs it, and the duplicated `_NS_SAFE_RE` (ingest) +
  `_AGENT_ID_SAFE_RE` (multi-agent) sanitizers are consolidated into
  `sanitize_namespace_segment` in `storage/sqlite_namespace.py` (no allowlist
  change). Existing `agent/{id}` namespaces can be migrated with
  `mm agent migrate`.
- **Memory Dirs panel: per-child collapse removed** (behavior change):
  expanding the `Claude` vendor now reveals both `Claude projects` and
  `Claude plans` together — they no longer collapse independently. Old
  per-category collapse state was not persisted, so no migration is
  needed. First-load defaults unchanged (`User` open, vendor groups
  closed). No vendor-level bulk-reindex button; per-product reindex
  buttons remain on each product section. RFC #304 Q4/Q5 (#322).

### Fixed
- **Reranker candidate pool is now actually wired**: `RerankConfig.top_k`
  was declared but never read, so the cross-encoder only ever saw the
  response `top_k` and could not rescue items RRF ranked just outside it
  (#307).
- **Reranker-failure fallback now honors response size**: when the
  cross-encoder raises, `fused` is trimmed to the caller's `top_k`
  instead of leaking the wider pool size through the remaining pipeline
  stages (#309).

### Deprecated
- `rerank.top_k` (env var `MEMTOMEM_RERANK__TOP_K`) is superseded by
  `rerank.oversample` + `rerank.min_pool` + `rerank.max_pool`. Legacy
  configs are migrated to `rerank.min_pool` with a `DeprecationWarning`.
  Slated for removal in 0.3.

## [0.1.12] — 2026-04-19

memtomem remains in **alpha**. APIs, defaults, and on-disk config surfaces
may still shift between 0.1.x releases — external feedback and issue
reports are especially welcome at
[github.com/memtomem/memtomem/issues](https://github.com/memtomem/memtomem/issues).

### Changed
- **Provider memory directories are now opt-in via `mm init`.** The wizard
  has a new "Provider memory folders" step (Step 4 of 10) that detects
  Claude Code per-project memory (`~/.claude/projects/<project>/memory/`),
  Claude plans (`~/.claude/plans/`), and Codex memories
  (`~/.codex/memories/`) and lets you accept each category. Accepted paths
  land in `indexing.memory_dirs` directly, replacing the previous silent
  runtime auto-discovery. Non-interactive mode supports the new repeatable
  `--include-provider {claude-memory,claude-plans,codex}` flag.
- **Auto-discovery scope narrowed** to canonical memory surfaces per each
  provider's official documentation:
  - Claude Code: only the `*/memory/` subdirectories with at least one
    `.md` file (previously the entire `~/.claude/projects/` tree
    including session JSONL transcripts and `staging/`).
  - Codex: `~/.codex/memories/` (unchanged).
- **Gemini CLI removed from auto-discovery.** Its memory is the single file
  `~/.gemini/GEMINI.md` (incompatible with the directory-based
  `memory_dirs` abstraction), and the parent dir contains secrets like
  `oauth_creds.json`. Use `mm ingest gemini-memory` for one-shot manual
  import — that command is unchanged.

### Deprecated
- `indexing.auto_discover` is now a one-shot migration trigger only, not a
  runtime auto-discovery flag. Existing installs with the legacy default
  (`true`) get migrated transparently on the next CLI/server startup —
  canonical provider dirs that exist on the machine are appended to
  `indexing.memory_dirs` and the flag flips to `false`. The field will be
  removed in a future release.

### Migration notes
- After upgrading, run `mm index --rebuild` to clean up index entries left
  over from the previous wider scan (session transcripts, staging dirs,
  Gemini configs). The migration narrows `memory_dirs` but doesn't
  retroactively prune already-indexed content.
- New Claude Code projects created after running `mm init` are not
  auto-indexed — re-run `mm init` or use
  `mm config set indexing.memory_dirs` to add them when needed.

## [0.1.11] — 2026-04-19

memtomem remains in **alpha**. APIs, defaults, and on-disk config surfaces
may still shift between 0.1.x releases — external feedback and issue
reports are especially welcome at
[github.com/memtomem/memtomem/issues](https://github.com/memtomem/memtomem/issues).

### Added
- **FastEmbed reranker provider**: new `rerank.provider="fastembed"` routes
  reranking through `fastembed.rerank.cross_encoder.TextCrossEncoder` —
  local ONNX, no external service, no PyTorch dependency. Reuses the
  existing `memtomem[onnx]` extra so enabling reranking adds no new
  packages. Supports the built-in fastembed catalog (e.g.
  `Xenova/ms-marco-MiniLM-L-6-v2`,
  `jinaai/jina-reranker-v2-base-multilingual`) plus custom ONNX exports
  via `TextCrossEncoder.add_custom_model()`.
- **Chunking semantic pack**: new `indexing.target_chunk_tokens` (default
  384) drives a greedy Pass 2 that packs short hierarchy-compatible
  siblings/ancestor-descendants up to the target, plus a Pass 3 tail
  backward sweep for final-chunk orphans. Short orphans in Pass 1 are now
  rescued across sub-heading divergence as long as they share a top-level
  root (mem_add entries with distinct roots still stay separate). Set
  `target_chunk_tokens=0` to restore the pre-PR merge behaviour.
- **ReStructuredText chunker**: `.rst` section-header-aware splitting.
- **Web UI `--open` flag**: opt-in browser launch with configurable timeout
  (replaces the old always-open default).
- Numeric validation errors now include the offending value in MCP tool
  responses.
- **Namespace policy rules** (#253): new `NamespacePolicyRule` config list
  provides path-glob → namespace mappings, so users can auto-tag files at
  index time instead of passing `namespace=` on every `mem_index` call.
  Resolution order: explicit param → rules (first match) → `enable_auto_ns`
  → `default_namespace`. Uses `pathspec.GitIgnoreSpec` patterns
  (case-insensitive, same syntax as `indexing.exclude_patterns`) with a
  `{parent}` placeholder that expands to the matched file's immediate
  parent folder name. Contributes via `config.d/*.json` (APPEND merge).
  Default `[]` — existing users see no behavior change until they opt in.
  See `docs/guides/configuration.md`.
- **Wizard "Preserved" summary** (#254): `mm init` now lists non-default
  keys inherited from a previous config that the wizard didn't write this
  run, using a built-in-default diff (not a bool heuristic, so non-bool
  leftovers like `search.rrf_k=120` surface too). Malformed `config.json`
  is backed up to `config.json.bak-<unix-ts>` instead of silently
  overwritten. Transparency-only — write behavior unchanged.
- **`mm init --fresh`** (#255): opt-in flag that drops wizard-untouched
  canonical config keys whose values differ from built-in defaults, then
  runs the normal wizard. Complements PR #254's surfacing with bulk
  cleanup. Default behavior unchanged.
- **`mm config unset <key>`** (#259): targeted removal of a single
  override. Distinct from `mm init --fresh` (single-key vs bulk; no backup
  vs backup; idempotent scripting vs interactive wizard). Useful for stale
  cross-machine paths in `memory_dirs` or a single field shadowing a
  `config.d/` fragment.
- **Web UI per-field reset-to-default (↺) button** (#272): every Config
  field in the Web UI now has a ↺ action that restores the built-in
  default in place. Schema and choice metadata served via new
  `GET /api/config/defaults` + `/api/config/schema`; the frontend reads
  both and overlays a per-field reset affordance.
- **Web UI i18n coverage** (#281): remaining `showConfirm`/`showToast`
  dialog strings now route through `t()`, completing the Korean UI
  translation (closes #29).
- **`indexing.auto_discover` flag** (#282): opt-out of auto-discovery
  of `~/.claude/projects`, `~/.gemini`, `~/.codex/memories` from
  `memory_dirs`. Defaults to `True` — no behavior change for existing
  users. Use `mm config set indexing.auto_discover false` to pin
  `memory_dirs` to the explicit list in `config.json` + `config.d/*.json`.

### Fixed
- **Web UI config hot-reload** (#267, #269, #274):
  `~/.memtomem/config.json` and `config.d/*.json` are re-read on every
  `GET /api/config` and at the top of every config-writing endpoint
  (`PATCH /api/config`, `POST /api/config/save`,
  `POST /api/memory-dirs/add|remove`). Previously, external edits
  (`mm config set`, manual editor) were invisible to the running server
  and got silently clobbered on the next UI save. The writer lock was
  extended from PATCH to all four write handlers (closing a pre-existing
  gap). If `config.json` becomes invalid on disk, the UI keeps the last
  known-good config, surfaces `config_reload_error` in the response, and
  refuses writes with HTTP 409 until the file is fixed. Follow-up #269
  closes a GET-path signature overwrite race under concurrent requests;
  #274 mirrors the CAS guard onto the `reload_if_stale` error branch.
- **ONNX `bge-m3`**: fastembed 0.8.0 dropped `BAAI/bge-m3` from its built-in
  `TextEmbedding` catalog — re-registered via `add_custom_model` against the
  official HF ONNX export (1024-dim, CLS pooling, normalized). Existing
  `mm init` users keep working with no config change.
- **Async I/O**: 6 blocking file read/write calls in MCP tool handlers
  (`mem_add`, `mem_edit`, `mem_delete`, `mem_context_*`) wrapped with
  `asyncio.to_thread` to prevent event loop starvation.
- **Security**: parameterized query for namespace in `execute_auto_tag`
  (#155); exception class names removed from web error responses and
  `/health` endpoint (#80, #81).
- **Logging**: silenced errors surfaced in health watchdog, search
  pipeline, and consolidation engine (#164); silent `except: pass`
  blocks in `sqlite_backend` and `status_config` now log warnings (#78).
- **Auto-tag**: `namespace_filter` passed to `auto_tag_storage` to fix
  silent failure of namespace-scoped policies (#114).
- **CLI**: pass real `index_engine`/`config` to `FileWatcher` in
  `watchdog run` (#111); warn when code chunkers unavailable due to
  missing optional deps (#142).
- File handle leak on `flock` failure; stale `--include` help text;
  `response.ok` checks in `context-gateway.js` (#77).
- **Namespace rules via mutation path** (#257): `coerce_and_validate` now
  handles `list[BaseSettings]`, so `PATCH /api/config`, `mm config set
  namespace.rules '[...]'`, and the init wizard correctly coerce dict
  entries into `NamespacePolicyRule` instances. Previously the load path
  correctly coerced dict entries but the mutation path silently passed
  raw dicts through; downstream `rule.path_glob` access then raised
  `AttributeError`.
- **Fragment / env drag-in on save** (#258): in-process save paths (Web UI
  `PATCH /api/config`, `/memory-dirs/*`, MCP `mem_config`) now persist
  only fields whose values differ from a fresh comparand built from
  defaults + env + `config.d/` fragments. Previously, PATCH-ing one field
  silently copied fragment and env values into `config.json`'s REPLACE
  layer, freezing subsequent fragment edits. Extends #256's class-level
  default drop by broadening the comparand to include fragments and env.
- **Atomic config.json writes everywhere** (#262): `save_config_overrides`
  (every `mm config set` / Web UI PATCH / MCP `mem_config` /
  `/memory-dirs/add|remove`) and `_write_config_and_summary` (normal `init`
  + `init --fresh`) now use `_atomic_write_json` (tempfile + `os.replace`,
  tmp cleanup on failure). Prevents mid-write failure from corrupting
  `config.json` — the `--fresh` path's `shutil.copy2` backup + direct
  write could previously leave a half-written file next to a valid `.bak`
  on partial failure.
- **Indexing exclude guard coverage** (#271): moved the entry-point
  exclude guard from `index_file` into the innermost common seam
  `_index_file`, so sibling public entry `index_path_stream(single_file)`
  is also covered. Follow-up to the 0.1.10 security fix (#252).
- **Wizard `.mcp.json` scope clarification** (#280): wizard output now
  prints per-editor scope hints (Claude Code user vs project `.mcp.json`,
  Cursor global vs project) so users don't paste the same block in both
  scopes.
- **Context atomic writes + name validation** (#283): all 6 context
  fan-out sites (profile/skill/prompt read-modify-write paths) now use a
  shared `atomic_write_{bytes,text}` helper (`0o600` default, `fsync` +
  `os.replace`). Profile/skill/prompt names are validated through
  `validate_name` at parse + extract, rejecting `..`, path separators,
  control characters, and names longer than 64 bytes.
- **Context CRLF tolerance + TOML escape** (#285): CRLF line endings are
  now accepted everywhere, unknown keys warn instead of failing the
  whole parse, and full TOML-style escape sequences (`\n`, `\t`, `\"`,
  `\\`, `\uXXXX`) are honored in string values.
- **Context-gateway write serialization** (#286): dedicated `asyncio`
  lock prevents interleaved writes when multiple endpoints (context
  gateway, skill editor, profile editor) mutate the same file in quick
  succession.
- **Auto-discover regression in `build_comparand`** (#284):
  `ensure_auto_discovered_dirs` now also runs during the comparand
  build, so an unrelated save doesn't drag auto-discovered directories
  into `config.json`'s REPLACE layer. Post-merge follow-up to #282.
- **FTS rebuild singleton + thread offload** (#287): Web UI FTS rebuild
  now coalesces concurrent rebuild triggers into a singleton task and
  runs on `asyncio.to_thread` with a dedicated writer connection, so
  large indexes don't stall the event loop or serialize redundant
  rebuilds.

### Changed
- **Single-source version** via `importlib.metadata` + Python 3.13
  classifier (#76).
- **Typing overhaul**: `CtxType` widened to `Optional` (drops 82
  `type: ignore`, #90); `policy_engine` storage narrowed to
  `SqliteBackend` (#89); 12 `union-attr` ignores eliminated (#100);
  RST chunker annotations corrected (#126); `llm_provider` tightened
  from `object` to `LLMProvider` (#130); 4 list/dict element types
  tightened (#145).
- Dead config sections removed (`conflict`, `entity_extraction`,
  `timezone`) — `extra="ignore"` ensures old config files still load.
- **MiniLM pooling upstream change**: fastembed 0.8.0 switched
  `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` from CLS to
  mean pooling. Users who indexed with fastembed <0.5.1 and this model
  should re-index for consistent dense-search quality; new installs are
  unaffected.
- **Reranker default**: `rerank.provider` now defaults to `fastembed`
  (local ONNX, ~80 MB download on first use) instead of `cohere`
  (external API); `rerank.model` default is now
  `Xenova/ms-marco-MiniLM-L-6-v2`. Installs that had
  `rerank.enabled=true` with the implicit `cohere` default must now set
  `provider: "cohere"` (and `api_key`) explicitly to keep prior behavior.
  `rerank.enabled=false` installs (the shipped default) are unaffected.
  Non-English content should set
  `model="jinaai/jina-reranker-v2-base-multilingual"` — the English
  default degrades non-English quality.
- Refactored truncation magic numbers in consolidation engine;
  watcher queue maxsize extracted to constant.
- CI: ruff lint/format scope extended to `tests/`; notebooks CI job
  and branch protection added.
- **Silent-leftover prevention on save** (#256): every mutable-field save
  path (`mm config set`, `PATCH /api/config?persist=true`,
  `POST /api/config/save`, `/memory-dirs/add|remove`, `mem_config` MCP)
  now drops fields whose values equal the class-level default and prunes
  matching historical leftovers on next save. Stops Web UI section-saves
  from pinning default-False `mmr.enabled` into `config.json` and
  permanently shadowing `config.d/` fragments.

### Docs
- Webhook config section and `indexing.supported_extensions` added to
  configuration reference (#170 high tier).
- MCP tool error response contract documented (#167).
- **Beginner-surface restructure** (#288): move WIP / internal /
  power-user-only docs into a private `memtomem/memtomem-docs` repo.
  The public surface is now intentionally small:
  `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, `CLA.md`,
  `SECURITY.md`, `docs/adr/`, `docs/guides/` (4 intro + 4 power-user
  guides), and `packages/memtomem/README.md` (PyPI page).
- **Notebooks slim** (#289): public `examples/notebooks/` now contains
  `01_hello_memory.ipynb` (5-minute Python-API quick-start) only.
  Notebooks 02–08 (filters, agent patterns, search tuning, LangGraph,
  lifecycle, embedding providers, LLM features) were moved to the same
  private `memtomem-docs` repo as internal reference material.

## [0.1.10] — 2026-04-19

### Security

- Fix credential-file indexing on filesystem-watch events.

  **Affected versions**: 0.1.0 through 0.1.9.

  In these versions, the fs watcher's per-file re-index path
  (`IndexEngine.index_file`) did not apply the directory-exclude
  filter. Any supported-extension file (`.json`, `.yaml`, `.py`, ...)
  inside an auto-discovered memory directory (`~/.claude/projects`,
  `~/.gemini`, `~/.codex/memories`) was indexed on each modify event.
  For users running memtomem alongside Gemini CLI, the ~hourly OAuth
  token refresh drove continuous re-indexing of `~/.gemini/oauth_creds.json`.

  The fix combines changes across PRs #225 / #226 (built-in denylist +
  config + cleanup CLI), #252 (entry-point guard), and #251
  (documentation):

  - **PRs #225 / #226** — built-in credential/secret denylist
    (`oauth_creds.json`, `credentials*`, `id_rsa*`, `*.pem`, `*.key`,
    `.ssh/**`, Claude Code subagent metadata
    `.claude/**/*.meta.json`); directory denylist extended with `.aws`,
    `.ssh`, `.gnupg`; user-configurable `indexing.exclude_patterns`
    config field (`.gitignore` syntax, case-insensitive via `pathspec`;
    user `!negation` cannot override built-in secret patterns); and
    `mm purge --matching-excluded` cleanup CLI.
  - **PR #252** — entry-point guard at `IndexEngine.index_file` matching
    both absolute paths and memory-dir-relative paths.
  - **PR #251** — documentation of `exclude_patterns`, cloud-sync
    watcher edge cases, and related configuration surface in
    `docs/guides/configuration.md` and `docs/guides/google-drive.md`.

  **Upgrade action**: `pip install -U memtomem` to 0.1.10. No config
  migration required — any existing user `exclude_patterns` stack on
  top of the built-in denylist.

  **Post-upgrade recommended**:
  1. Dry-run the cleanup: `mm purge --matching-excluded` (prints what
     would be deleted).
  2. Apply it: `mm purge --matching-excluded --apply` to remove
     pre-existing chunks whose source paths match the denylist.
  3. Rotate any credentials that may have been indexed during the
     affected period. Gemini CLI refreshes OAuth tokens on an ~hourly
     schedule, so any v0.1.x server running alongside Gemini CLI
     should be treated as having refreshed copies of those tokens in
     the index. Also review any sensitive content under
     `~/.claude/projects` (Claude Code session/conversation data) and
     `~/.codex/memories` that may have been indexed, and handle per
     your usual data-handling policy.

  **Follow-up tracking** (defense-in-depth concerns surfaced during
  this work, not required for the security fix):
  - #260 — auto-discover unconditional override (design RFC)
  - #261 — post-#252 watcher residual investigation

## [0.1.9] — 2026-04-13

### Fixed
- **Config robustness**: invalid `config.json` values now warn and fall
  back to defaults instead of crashing on startup; `mm init` preserves
  non-init config fields on re-run; 4 cross-path sync gaps closed
  (CLI/Web/MCP all converge on the same read-merge-write logic);
  save-path data-loss edge case eliminated.
- **Embedding**: Ollama `base_url` defaults to `localhost:11434` when
  the env var is set but empty.

### Changed
- Default LLM models updated to latest releases.

### Docs
- Official website link added to README.
- Docs sweep: stale numbers, env vars, and notebook setup corrected
  across 14 doc files; missing onnx extra and CLI commands added to
  getting-started; Gemini CLI setup and tool categories added to
  reference sections; core MCP tool docstrings corrected.

## [0.1.8] — 2026-04-13

### Added
- **Structured search output**: `mem_search(output_format="structured")`
  returns JSON with `chunk_id`, `namespace`, `score`, `source`,
  `hierarchy`, and full `content` per result. Enables STM proxy to use
  real UUIDs for `increment_access` feedback instead of sha256 fallbacks.
- **Version negotiation**: `mem_do(action="version")` returns server
  version and capabilities JSON (e.g. `search_formats`). Used by STM
  proxy to discover supported features before switching parsers.
- **Auto-discover AI tool memory directories**: `memory_dirs` now
  automatically includes `~/.claude/projects`, `~/.gemini`, and
  `~/.codex/memories` when they exist, so `mem_index` and the file
  watcher accept paths under these directories without manual
  `MEMORY_DIRS` configuration. Auto-discovered directories are appended
  after `config.json` overrides via `ensure_auto_discovered_dirs()`.
- **Database reset**: `mm reset` CLI command, `mem_reset` MCP tool (advanced
  category, routed via `mem_do`), `POST /api/reset` web endpoint, and
  Settings > Maintenance > Reset tab in Web UI. Deletes all data (chunks,
  sessions, history, relations, entities, policies, health snapshots) and
  reinitializes the DB; embedding configuration is preserved.
- **Namespace prefix grouping**: Web UI Settings tab and namespace
  dropdowns group namespaces by colon prefix (collapsible sections).
- **Improved web extra messaging**: `mm web` and `mm init` now explain that
  the `[web]` extra is not included in the base install, reducing confusion
  when `uv tool install memtomem` registers the `memtomem-web` entry point
  but FastAPI is missing.

### Fixed
- **Hooks record format**: migrated context gateway hooks from array to
  record format for Claude Code ≥ 2.1.104 compatibility.
- **Home activity graph**: rebuilt as responsive GitHub-style contribution
  grid.
- **Codex commands**: removed phantom Codex commands generator (feature
  doesn't exist upstream).

### Changed
- **Web JS refactor**: split `app.js` (3554 lines) into core + 8 domain
  modules. No build step; global function dependencies preserved.

### Docs
- Uninstall instructions in user guide and getting started.
- Consolidated scope section in Agent Context Management.
- Hooks references updated to record format.
- Stale tool counts, wizard step count, and tags parameter type fixed.

## [0.1.7] — 2026-04-12

### Added
- **PolicyScheduler**: background loop that periodically runs all enabled
  memory lifecycle policies (`auto_archive`, `auto_consolidate`,
  `auto_expire`, `auto_tag`). Controlled by `MEMTOMEM_POLICY__ENABLED`
  and `MEMTOMEM_POLICY__SCHEDULER_INTERVAL_MINUTES`. Follows the existing
  `ConsolidationScheduler` / `HealthWatchdog` lifecycle pattern.
  - `run_all_enabled()` gains a `max_actions` parameter — cumulative
    action cap checked between policies (individual handlers run
    atomically). Configurable via `MEMTOMEM_POLICY__MAX_ACTIONS_PER_RUN`.
  - Consecutive failure counter: escalates to WARNING after 3 failures.
  - Cache invalidation only when mutations actually occur.
- **`auto_promote` policy handler** — inverse of `auto_archive`. Moves
  archived chunks back to an active namespace when access patterns
  indicate continued relevance (`min_access_count`, `recency_days`,
  `min_importance_score`). Ping-pong prevention: promotion resets
  `last_accessed_at` to now.
- **Gemini / Codex memory ingest**: `mm ingest gemini-memory` indexes
  `GEMINI.md` files (namespace `gemini-memory:<slug>`);
  `mm ingest codex-memory` indexes Codex `~/.codex/memories/` directories
  (namespace `codex-memory:<slug>`). Shared infrastructure via
  `_build_namespace(prefix=)` and `tag_fn` parameters.
- **Multi-slug Claude ingest**: `mm ingest claude-memory --source
  ~/.claude/projects/` auto-discovers all `<slug>/memory/` subdirectories
  and ingests them in a single run with per-slug + aggregate output.
- **MCP `mem_ingest` tool**: `mem_do(action="ingest")` exposes all three
  ingest commands (Claude, Gemini, Codex) via MCP, including multi-slug
  discovery for `source_type="claude"`.
- **Web UI: Hooks Sync** — new Settings subsection for comparing and
  resolving conflicts between memtomem's canonical hooks and Claude's
  `~/.claude/settings.json`. Per-conflict resolution with mtime guard.
- **Web UI: Korean i18n** — language toggle (EN/한) in the header.
  Auto-detects browser locale; persists choice in `localStorage`. All
  static labels translated via `data-i18n` attributes and `t()` function.

### Fixed
- **MCP tool registration audit** — 9 issues resolved: orphaned
  `mem_ask` import, incomplete `ns_assign`/`cleanup_orphans` registration,
  missing `ingest`/`search`/`context` categories in `mem_do` docstring,
  shutdown isolation, missing `@tool_handler` on `mem_increment_access`,
  and atexit ordering.

### Docs
- User guide Section 8: Memory Policies (5 types, 4 MCP tools,
  scheduler, combining patterns).
- Configuration reference: `auto_promote`, `auto_consolidate` config keys.
- Web UI guide: Hooks Sync and i18n sections.

## [0.1.6] — 2026-04-12

### Added
- **Phase D: Claude `settings.json` integration** — new `SettingsGenerator`
  protocol and `ClaudeSettingsGenerator` implementation for merging memtomem
  hooks into `~/.claude/settings.json`. Completes the LTM Manager roadmap
  (Phases A → A.5 → B → C → D) and absorbs context-gateway Phase 4.
  - New `--include=settings` flag for `mm context {generate,sync,diff,detect}`
    (CLI and MCP `mem_context_*` tools).
  - New `mm init` wizard step (Step 8) prompts for Claude Code hooks setup.
  - Canonical source: `.memtomem/settings.json` with a `hooks` record
    (keyed by event name, e.g. `PostToolUse`).
  - Additive-only merge: rules are matched by `(event, matcher)`; on
    collision the user's existing rule wins and a guided warning is emitted.
  - Formatting: `json.dumps(indent=2)` normalization — byte-for-byte
    preservation of hand-edited formatting is explicitly not guaranteed.
  - Malformed `~/.claude/settings.json` is skipped with an error message
    (not silently overwritten).
  - If Claude Code is not installed (`~/.claude/` missing), the settings
    runtime is silently skipped — memtomem never creates `~/.claude/`.
  - Basic concurrent-write guard via mtime comparison between read and write.

## [0.1.5] — 2026-04-12

### Added
- Phase 3.5: canonical slash commands now fan out to Codex as well
  (`~/.codex/prompts/<name>.md`, user-scope). Codex's custom-prompts
  format is a Claude-compatible Markdown + YAML superset — `description`,
  `argument-hint`, and the `$ARGUMENTS` / `$1..$9` / `$NAME` / `$$`
  placeholders are all passed through verbatim; only `allowed-tools`
  and `model` are dropped (reported via the standard `dropped` channel).
  Codex custom prompts are upstream-deprecated — OpenAI recommends
  migrating to skills, which memtomem already fans out to Codex via
  `.agents/skills/` in Phase 1 — but fan-out is provided for parity
  with the existing Claude + Gemini pipeline. The `mem_context_*` MCP
  tools and the `mm context {generate,sync,diff} --include=commands`
  CLI pick up the new `codex_commands` runtime automatically via the
  registry (no new tools or flags). `extract_commands_to_canonical`
  intentionally still skips Codex — user-scope paths span projects,
  matching the Phase 2 Codex sub-agent policy.

## [0.1.4] — 2026-04-11

### Added
- `examples/notebooks/` — six scenario-based Jupyter notebooks that walk
  through the Python API (`create_components()`, `search_pipeline.search()`,
  `index_engine.index_path()`, storage mixins, and `MemtomemStore` for
  LangGraph). Covers hello-memory, bulk indexing + filters, session /
  scratch / recall, search tuning, a two-node LangGraph agent, and the
  full memory lifecycle (hash-diff incremental re-index on edit,
  single-chunk delete via `storage.delete_chunks`, orphan cleanup via
  `delete_by_source`, and `force=True` full re-embed). Each notebook
  runs against a throwaway temp directory so it cannot touch the user's
  real `~/.memtomem/` setup.
- Notebook 02 includes a "Korean with the kiwipiepy tokenizer" section
  that prints the token stream produced by `unicode61` vs. `kiwipiepy`
  side by side and runs the same query under each configuration.
- `examples/notebooks/README.md` now has a "How memories are stored"
  section that explains the file-backed (`index_file` path used by
  notebooks 01/02/04/05/06) vs DB-only (`create_session`, `scratch_set`,
  … used by notebook 03) storage paths and the shared temp directory
  layout every notebook relies on.
- `docs/guides/hands-on-tutorial.md` gained steps 3.6 / 3.7 covering the
  file lifecycle from the MCP side: reading `mem_index` `Indexed` /
  `Skipped (unchanged)` / `Deleted (stale)` stats after a file edit,
  `mem_index force=true` full re-embed for model swaps, and
  `mem_do action="orphans"` (dry-run → apply) to clean up chunks whose
  source file was deleted. Step 1.2 now also documents the
  `MEMTOMEM_TOOL_MODE` env var and which tutorial steps use the `mem_do`
  routing vs top-level calls.

### Changed
- `SqliteBackend.clear_embedding_mismatch()` is now a public method
  (refactor 15136a0). The `needs_reindex_ids` and `needs_embed_ids`
  tracking sets were previously reset via direct attribute mutation
  through the protected `_backend` accessor, which leaked internal
  state across module boundaries. Four writers (`_finalize_write`,
  `_reset_all_state`, `web/app.py`'s force-reindex handler, and the
  FTS rebuild path) now go through the public method, and the
  protected-attribute touch is no longer needed outside storage.
- STM decoupling CSS sweep — removed ~164 lines of orphan dashboard
  CSS from `packages/memtomem/src/memtomem/web/static/style.css`.
  The `.stm-*` block (59 lines, #15) and the parallel `.proxy-*`
  plus `.trend-*` block (105 lines, #16, covering Proxy Settings,
  Proxy Diff View, and Compression Trend Chart) had no HTML/JS
  consumers — any rendering path for these selectors had already
  moved to the external `memtomem-stm` package when STM was split
  out. The six `--bg-*` / `--text-*` CSS aliases they previously
  shared are retained since `.harness-*` sections still consume
  them; the comment on `style.css` line 24 that documented their
  purpose was rewritten to match the current consumers. The
  `.health-*` rules are kept intact — `app.js` still uses them for
  the generic system-health summary, which is unrelated to proxy.
- `app_lifespan(server: FastMCP)` → `app_lifespan(_server: FastMCP)`
  in `packages/memtomem/src/memtomem/server/lifespan.py`. The MCP
  framework requires the parameter in the callback signature but
  memtomem's lifespan never reads it; the underscore prefix makes
  the "intentionally unused by framework contract" nature explicit
  and silences dead-code detectors.

### Fixed
- `docs/guides/user-guide.md` tab-overview table listed an **STM**
  row (`Proxy monitoring — compression metrics, server status, call
  history (only when STM installed)`) that described the dashboard
  UI removed with the STM decoupling. The actual
  `packages/memtomem/src/memtomem/web/static/index.html` has seven
  tabs (Home, Search, Sources, Index, Tags, Timeline, More) — no
  STM tab, and the styles backing the removed row were already
  gone after #15 and #16. Dropped the stale row from the table.
  The separate "STM: Proactive Memory Surfacing (Optional)" section
  further down the same file is intentionally kept since it
  correctly documents the external `memtomem-stm` package as a
  cross-reference, not a core UI feature.
- `MemtomemStore.index()` (LangGraph adapter) and the `mm` shell `index`
  command called a nonexistent `IndexEngine.index_directory()` method and
  would crash at runtime. Routed both to `index_path()` and added
  regression tests in `tests/test_langgraph.py`.
- `docs/guides/hands-on-tutorial.md` steps 3.2 / 3.3 / 3.4 used to call
  `mem_batch_add` / `mem_edit` / `mem_delete` as top-level tools, but
  those are non-core actions — readers following the tutorial with the
  default MCP config (`MEMTOMEM_TOOL_MODE=core`) would hit "tool not
  found" errors. All three call sites now go through
  `mem_do(action="...", params={...})`, matching the default tool set.
- `docs/guides/hands-on-tutorial.md` `mem_status` / `mem_stats` example
  outputs had drifted from the real formats in
  `server/tools/status_config.py`. Step 1.3 showed a one-line
  `Chunks: 0 | Sources: 0` form that the code has never produced;
  step 3.5 showed `Chunks: 12 | Sources: 4 | Storage: sqlite` as the
  `mem_stats` response. Both now show the actual multi-line output
  (`memtomem Status` header with `Storage` / `DB path` / `Embedding` /
  `Dimension` / `Top-K` / `RRF k` and an `Index stats` section for
  `mem_status`; `Memory index statistics:` header plus bullet list
  for `mem_stats`).
- `docs/guides/user-guide.md` `mem_index` examples likewise did not
  match the real "`Indexing complete: ...`" block — the Index-a-directory
  response was `"Indexed 47 files (312 chunks)"` and the Incremental
  re-indexing response used a `"3 new, 2 updated, 1 deleted"` phrasing
  that does not correspond to any code path. Both now use the real
  multi-line format (`Files scanned` / `Total chunks` / `Indexed` /
  `Skipped (unchanged)` / `Deleted (stale)` / `Duration`) and the
  section now explains that an edited section contributes to **both**
  `Indexed` (new hash) and `Deleted (stale)` (old hash) because the
  diff is hash-based.
- Broad docs-vs-source audit (commit after 75d7146) found the same
  class of drift in several more places. Fixed:
  - `docs/guides/agent-memory-guide.md` — every non-core tool call
    (`mem_scratch_set/get/promote`, `mem_session_start/end`,
    `mem_procedure_save`, `mem_consolidate(_apply)`, `mem_reflect(_save)`,
    `mem_eval`, `mem_agent_register/share/search`, `mem_fetch`) was
    shown as a top-level call, which fails in the default
    `MEMTOMEM_TOOL_MODE=core`. Every call is now routed through
    `mem_do(action="...", params={...})`, with a tool-mode note at the
    top of Scenario 1 pointing at the existing Tool Mode Configuration
    section. The companion example outputs were also rewritten to
    match the real return strings from `session.py`, `scratch.py`,
    `procedure.py`, `consolidation.py`, `reflection.py`,
    `evaluation.py`, `multi_agent.py`, and `url_index.py` (e.g. the
    `- ` dash prefixes on `Session started`/`Agent registered`
    outputs, the extra "Use namespace='...' for ..." two-line hint in
    `agent_register`, the real `Memory added to ... / - Chunks
    indexed / - File` shape from `mem_add` including in the template
    scenarios).
  - `docs/guides/user-guide.md` Google Drive section had another
    `"Indexed 47 files (312 chunks)"` one-liner alongside the one
    already fixed in section 1. Now uses the canonical
    `Indexing complete:` block.
  - `docs/guides/use-cases.md` Coding Tools section showed
    `mem_stats() > "Total chunks: 0, Storage backend: sqlite"` and
    `mem_index(path="...") > "Indexed 47 files, 1284 chunks"`. Both
    replaced with the real multi-line responses.
  - `docs/guides/integrations/claude-code.md` and
    `docs/guides/integrations/claude-desktop.md` First-Indexing
    examples both showed `→ "Indexed 47 files, 1284 chunks in 3.2s"`
    — the `in 3.2s` suffix never existed in the code. Replaced with
    the real `Indexing complete:` block (`Duration: 3200ms`).
  - `docs/guides/integrations/claude-code.md` UserPromptSubmit and
    PostToolUse hook examples called `memtomem search` / `memtomem
    index` as shell commands, but the installed CLI binary is `mm`
    (the `memtomem` entry point is for the MCP server). Copying the
    config as-is would have produced `command not found`. Changed
    both the `command:` values and the Hook Event Summary table to
    use `mm search` / `mm index`.
  - `docs/guides/hands-on-tutorial.md` Step 3.1 `mem_add` example
    showed `"Added 1 chunk (saved to ...)\nTags: python, typing"`
    which also does not match the real `memory_crud.py:116` return
    (`Memory added to ... / - Chunks indexed / - File`). Updated.

## [0.1.3] — 2026-04-10

Quality & security audit: 79+ fixes across nine audit rounds.

### Security
- Path traversal guard on source validation and symlink resolution.
- Webhook SSRF protection (private IP / internal host blocking).
- Recursion depth limit for structured-data (JSON/YAML/TOML) chunking.
- Binary file detection so non-text files are skipped during indexing.
- Namespace validation and shell crash guard.
- File size limit enforcement during ingestion.

### Fixed
- Cache race conditions and invalidation gaps in the search pipeline.
- Index lock handling and rollback consistency on partial failures.
- WAL checkpoint handling to prevent DB growth.
- Retention policy correctness and persistence reliability.
- Batch query correctness under concurrent access.
- Resource leaks (file handles, DB connections, embedder clients).
- Float epsilon handling in scoring; overlap cap enforcement in chunking.
- Cache TTL snapshot and lock-timeout races.

## [0.1.2] — 2026-04-10

### Added
- Session and activity tracking CLI: `mm session start/end/list/events`,
  `mm activity log`, and `mm session wrap -- CMD` to wrap headless
  processes with a session lifecycle.
- PostToolUse and Stop hooks for automatic activity logging.
- Timezone config: `MEMTOMEM_TIMEZONE=Asia/Seoul` (display only, storage
  stays UTC).
- Web UI sessions panel with event type badges, expandable metadata, and
  client-side filtering.
- `parent_context` and `file_context` metadata on chunks for better
  retrieval context.

### Changed
- Sibling heading sections (same parent) merge when short to reduce chunk
  fragmentation. Top-level `mem_add` entries stay independent of sibling
  merge.
- Token estimation uses a dynamic ratio: 4 for English, 2 for Korean.

### Fixed
- SQLite `busy_timeout=10` prevents "database is locked" when the CLI and
  MCP server access storage concurrently.
- MCP server PID lock warns about duplicate instances instead of silently
  racing on writes.

## [0.1.1] — 2026-04-10

### Added
- `mm init --non-interactive` mode for CI and automation.
- Project-scoped install support via `uv add memtomem`.

### Changed
- README optimized as a GitHub profile landing page (163 → 115 lines);
  PyPI badge and ecosystem section added.
- `mm init` docs clarified to drop the unneeded `uv run` prefix after
  `uv tool install`; README Quick Start leads with explicit install +
  wizard.

### Fixed
- `mem_add` produced duplicate chunks because `index_entry` and
  `index_file` were two separate indexing paths. Removed `index_entry`
  and routed all ingestion through `index_file`.
- `mm init` wrote `MEMORY_DIRS` as a plain string into `.mcp.json`,
  which crashed the server on startup. The wizard now serialises list
  env vars as JSON (#13).
- `mm web` surfaces an actionable error when the `[web]` extra is
  missing instead of failing with a bare `ModuleNotFoundError` (#14).

## [0.1.0.post1] — 2026-04-10

Metadata-only re-release; no code changes.

### Changed
- Corporate ownership recorded as DAPADA Inc. alongside the memtomem
  contributors in package authors and `LICENSE`.
- `Issues` URL added to PyPI project metadata (#12).

## [0.1.0] — 2026-04-08

Initial open-source release.

### Core (memtomem)
- MCP server with 72 tools + `mem_do` meta-tool (65 actions, aliases)
- CLI (`memtomem` / `mm`): init, search, add, recall, index, config, context, shell, web, watchdog
- Web UI dashboard: search, sources, tags, sessions, health report
- Hybrid search pipeline: BM25 (FTS5) + dense vectors (sqlite-vec) + RRF fusion
- Multi-stage pipeline: query expansion → parallel retrieval → RRF → time-decay → reranking → MMR → access boost → context-window expansion
- Context-window search (small-to-big retrieval): `search(context_window=N)` + `mem_expand` action
- Tool modes: `core` (9 tools), `standard` (~32), `full` (72)

### Storage
- SQLite with FTS5, sqlite-vec, WAL mode, read pool (3 connections)
- Mixin architecture: Session, Scratch, Relation, Analytic, History, Entity, Policy
- Incremental indexing with SHA-256 content hashing

### Chunking
- Markdown: heading-aware sections with frontmatter/wikilink support
- Python: AST-based splitting at function/class boundaries
- JavaScript/TypeScript: tree-sitter parsing
- JSON/YAML/TOML: structure-aware splitting

### Embedding
- Ollama (local, default `nomic-embed-text` 768-dim)
- OpenAI (cloud)
- `bge-m3` recommended for multilingual (KR/EN/JP/CN)

### Agent Memory
- Episodic (sessions), working (scratchpad with TTL), procedural (workflows)
- Multi-agent namespaces, cross-references, entity extraction
- Memory policies (auto-archive/expire/tag), consolidation/reflection

### Integrations
- LangGraph adapter (`MemtomemStore`)
- Claude Code plugin (experimental)
- OpenClaw plugin (experimental)

### Security
- XSS: DOMPurify sanitization
- SSRF: private IP/internal host blocking
- Path traversal: source validation, symlink rejection
- SQL injection: all queries parameterized

### Testing
- 886 automated tests
- CI: GitHub Actions (lint, typecheck, test)

### Related projects
- [**memtomem-stm**](https://github.com/memtomem/memtomem-stm) — Short-Term Memory proxy gateway with proactive memory surfacing. Distributed as a separate package; communicates with memtomem core entirely through the MCP protocol.
