fix(config): drop default-valued fields on save to prevent silent leftover pins#256
Merged
fix(config): drop default-valued fields on save to prevent silent leftover pins#256
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
save_config_overridesblindly wrote every mutable field each call. A Web UI "save section" with default-False MMR thus pinnedmmr.enabled=falseinto~/.memtomem/config.json, permanently shadowing anyconfig.d/fragment that set it True. Memory-dirs add/remove andmm config setleaked 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
model_fields[key].get_default(call_default_factory=True).{}survives.mm config set,PATCH /api/config?persist=true,POST /api/config/save,/memory-dirs/add|remove,mem_configMCP.What this does NOT do
GET /api/config/defaults+↺buttons insettings-config.js, including custom widgetsrrf_weights/exclude_patterns)._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_FIELDSindexing.memory_dirsis excluded from drop-default. Its factory_default_memory_dirs()auto-discovers AI tool dirs viais_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_factoryreads 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=falseis the default, but it's now pinned and permanently overrides anyconfig.d/fragment setting it True.After (clean)
{ "indexing": {"memory_dirs": [...]} }mmrsection omitted — fragment precedence restored. Existing leaks from prior installs get pruned on the next save.Testing
TestSaveConfigOverrides:test_default_valued_field_not_persisted— fresh save omits default-onlymmrsectiontest_existing_default_entry_pruned_on_save— pinnedmmr.enabled=falseleftover is prunedtest_non_default_value_still_persists— explicit non-defaults survivetest_memory_dirs_equal_to_default_still_persisted— locks in the exemption (test name reflects intentional asymmetry)test_section_with_only_defaults_dropped_entirely— empty section removed, no orphan{}Internal consumers
grepconfirmed only internal readers touchconfig.json(init_cmdwizard read-merge-write,policy.pyparses 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:GET /api/config/defaultsendpoint.config.json(mtime watcher invalidates cache)._FRESH_PRESERVE_KEYSand_EXTRA_PERSIST_FIELDS.--freshwrite-after-backup atomicity (tempfile + atomic rename).Test plan
uv run pytest -m "not ollama"green (1680 passed)uv run ruff check+ruff format --checkcleanuv run mypy packages/memtomem/srccleanmmr.enabled=truepersisted; uncheck → save → confirmmmrsection droppedmemory_dirs = _default_memory_dirs()→ save → confirm still persisted (exemption)