Skip to content

fix(config): drop default-valued fields on save to prevent silent leftover pins#256

Merged
memtomem merged 1 commit intomainfrom
fix/save-drops-defaults
Apr 18, 2026
Merged

fix(config): drop default-valued fields on save to prevent silent leftover pins#256
memtomem merged 1 commit intomainfrom
fix/save-drops-defaults

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

save_config_overrides blindly wrote every mutable field each call. A Web UI "save section" with default-False MMR thus pinned mmr.enabled=false into ~/.memtomem/config.json, permanently shadowing any config.d/ fragment that set it True. Memory-dirs add/remove and mm config set leaked the same way because all persist paths funnel through this one function.

Closes two follow-ups from #255: the memory-dirs dump leak (follow-up #1) and the Web UI section-save symmetric case (follow-up #2). The stated fix in both follow-ups was "Drop-equal-to-default-on-write" — same code change.

What this does

  • Compare each mutable field to its class-level default via model_fields[key].get_default(call_default_factory=True).
  • If equal, drop from the write dict and prune any matching existing entry (auto-cleans historical leftovers on next save).
  • Empty sections are removed — no orphan {} survives.
  • Covers every persist path: mm config set, PATCH /api/config?persist=true, POST /api/config/save, /memory-dirs/add|remove, mem_config MCP.

What this does NOT do

  • No UX affordance. Users still have no explicit "reset this field to default" button in the Web UI. Server auto-prunes defaults on save, but users need to know the current default to type it in. Tracked as a separate follow-up (new endpoint GET /api/config/defaults + buttons in settings-config.js, including custom widgets rrf_weights/exclude_patterns).
  • No schema-driven rewrite. Two hardcoded sets still exist: _FRESH_PRESERVE_KEYS (12 keys) and _EXTRA_PERSIST_FIELDS (1 key). Unifying them via Pydantic field metadata is a follow-up, deferred until either list grows or a drift incident.

Critical exemption: _EXTRA_PERSIST_FIELDS

indexing.memory_dirs is excluded from drop-default. Its factory _default_memory_dirs() auto-discovers AI tool dirs via is_dir() fs checks — environment-dependent. "Equal to default" on machine A does not hold on machine B. Dropping env-dependent defaults would silently lose user-curated dir lists across environments.

Criterion documented in the comment so future additions have an objective test: if a field's default_factory reads filesystem, network, hostname, or other runtime state, add it to _EXTRA_PERSIST_FIELDS.

Before / after

Before (leak)

User unchecks MMR in Web UI → section save:

{
  "mmr": {"enabled": false},
  "indexing": {"memory_dirs": [...]}
}

mmr.enabled=false is the default, but it's now pinned and permanently overrides any config.d/ fragment setting it True.

After (clean)

{
  "indexing": {"memory_dirs": [...]}
}

mmr section omitted — fragment precedence restored. Existing leaks from prior installs get pruned on the next save.

Testing

  • 1680 CI tests pass, ruff + format + mypy clean.
  • 5 new tests in TestSaveConfigOverrides:
    • test_default_valued_field_not_persisted — fresh save omits default-only mmr section
    • test_existing_default_entry_pruned_on_save — pinned mmr.enabled=false leftover is pruned
    • test_non_default_value_still_persists — explicit non-defaults survive
    • test_memory_dirs_equal_to_default_still_persistedlocks in the exemption (test name reflects intentional asymmetry)
    • test_section_with_only_defaults_dropped_entirely — empty section removed, no orphan {}
  • Manual smoke (check → uncheck → verify drop, memory_dirs stays) passed.

Internal consumers

grep confirmed only internal readers touch config.json (init_cmd wizard read-merge-write, policy.py parses a separate config string, tests). No external parsers, no breaking change for library users.

Follow-ups (after this PR)

Updated list in project_wizard_leftovers_cleanup.md:

  1. Web UI per-field reset button + GET /api/config/defaults endpoint.
  2. Web UI hot-reload of config.json (mtime watcher invalidates cache).
  3. Schema-driven rewrite of _FRESH_PRESERVE_KEYS and _EXTRA_PERSIST_FIELDS.
  4. --fresh write-after-backup atomicity (tempfile + atomic rename).

Test plan

  • uv run pytest -m "not ollama" green (1680 passed)
  • uv run ruff check + ruff format --check clean
  • uv run mypy packages/memtomem/src clean
  • Manual smoke: check MMR → save → confirm mmr.enabled=true persisted; uncheck → save → confirm mmr section dropped
  • Manual smoke: memory_dirs = _default_memory_dirs() → save → confirm still persisted (exemption)

save_config_overrides blindly wrote every mutable field each call. A Web
UI "save section" with default-False MMR thus pinned mmr.enabled=false
into ~/.memtomem/config.json, permanently shadowing any config.d/ fragment
that set it True. Memory-dirs add/remove and `mm config set` leaked the
same way because all persist paths funnel through this one function.

Fix: compare each mutable field to its class-level default via
model_fields[key].get_default(call_default_factory=True). If equal, drop
from the write dict AND prune any matching existing entry. Empty sections
are removed so no orphan `{}` survives. The same operation quietly
cleans historical leftovers the next time a user saves anything.

_EXTRA_PERSIST_FIELDS (currently indexing.memory_dirs) is exempt: its
default_factory auto-discovers AI tool dirs via is_dir() fs checks, so
"equals default" on one machine does not hold on another. Dropping
env-dependent defaults would silently lose user-curated dir lists across
environments. Criterion documented in the comment so future additions
have an objective test.

Subsumes two follow-ups from #255: the memory-dirs dump leak and the
Web UI section-save symmetric case. UX complement (explicit per-field
reset buttons + GET /api/config/defaults) remains as a separate
follow-up; server auto-prunes defaults now, but users still need a way
to trigger that reset without knowing the default value.

Internal config.json readers (init_cmd wizard read-merge-write, tests)
unaffected. No external parsers touch the file.
@memtomem memtomem merged commit ab6820c into main Apr 18, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 18, 2026
@memtomem memtomem deleted the fix/save-drops-defaults branch April 20, 2026 14:56
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants