feat(cli): add mm config unset for targeted override removal#259
Merged
feat(cli): add mm config unset for targeted override removal#259
Conversation
Closes wizard-leftovers follow-up: CLI mm config unset/reset. PR #258 closed the fragment/env/factory drag-in for new saves, but left a known limitation: historical config.json entries that don't match the local comparand (e.g., machine-A paths in memory_dirs carried to machine-B, or mmr.enabled=false shadowing a config.d fragment that sets it true) stay pinned until actively reset. The existing escape hatches were hand-edit or mm init --fresh — both too heavy for a single stale key. mm config unset <key> is the targeted affordance. It's distinct from mm init --fresh (single-key vs bulk, no backup vs backup, idempotent scripting pattern vs wizard re-run) and both commands stay. Behavior (6 locked decisions): - Scope: exactly the named keys. Ambient default-equal leftovers in other fields are not auto-cleaned; they get pruned on the next normal save via PR #258's delta-only write. - Valid-key set: MUTABLE_FIELDS ∪ _EXTRA_MUTATION_FIELDS. memory_dirs is accepted (removal isn't a mutation and is precisely the migration case this command unblocks) even though mm config set still rejects it. - Already-unset canonical key exits 0 with "(already at default)". Idempotent re-runs. - Unknown key exits 1 with a difflib suggestion (cutoff 0.7) when a canonical key is nearby. - Malformed config.json errors + exit 1 with no auto-recovery; points to mm init --fresh. Unset is raw-edit semantic — hiding malformed state behind a backup would confuse. - Empty config.json after removal is deleted, with a note line. _EXTRA_MUTATION_FIELDS (currently just indexing.memory_dirs) triggers a domain warning pointing at dedicated endpoints (mm memory-dirs list / mm index) after successful removal. Also introduces _atomic_write_json in config.py — tempfile in the same directory + os.replace — used by unset. save_config_overrides and cli/init_cmd.py's --fresh write remain on the direct write_text path; migrating them is the remaining wizard-leftovers follow-up on "--fresh write-after-backup atomicity". Tests (13 in TestConfigUnset): full output matrix (removed / already-unset / skipped with and without suggestion), empty-file deletion, _EXTRA_MUTATION_FIELDS allowance + domain warning, malformed JSON handling, multi-key best-effort, atomic write preserves original on os.replace failure + cleans up tmp on success, plus a fragment-reappearance end-to-end regression guard (fragment sets mmr.enabled=true, config.json pins false, unset → reload → fragment layer wins). Docs: configuration.md gets a "Removing individual overrides" subsection; machine-migration section lists unset as Option 1; getting-started.md and user-guide.md cheatsheets updated.
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
Closes wizard-leftovers follow-up: CLI
mm config unset/reset.PR #258 closed the fragment/env/factory drag-in for new saves, but
left a known limitation — historical
config.jsonentries that don'tmatch the local comparand (e.g., machine-A paths in
memory_dirscarried to machine-B, or
mmr.enabled=falseshadowing aconfig.dfragment that sets it
true) stay pinned until actively reset. Theexisting escape hatches were hand-edit or
mm init --fresh, both tooheavy for a single stale key.
mm config unset <key>is the targeted affordance. Distinct frommm init --fresh(single-key vs bulk; no backup vs backup;idempotent scripting vs wizard re-run). Both commands stay.
Behaviour (6 locked decisions)
Default-equal leftovers in other fields get pruned on the next
normal save via fix(config): delta-only save against comparand to close fragment/env drag-in #258's delta-only write.
MUTABLE_FIELDS∪_EXTRA_MUTATION_FIELDS.indexing.memory_dirsis accepted (removal isn't a mutation, andthe migration case is exactly what motivates this command) even
though
mm config set memory_dirs …stays rejected.(already at default).Idempotent re-runs are safe.
difflibsuggestion (cutoff 0.7)when a canonical key is nearby, otherwise just
Skipped.config.json→ exit 1, no auto-recovery, messagepoints at
mm init --fresh. Unset is raw-edit semantic; hidingmalformed state behind a backup would confuse.
config.jsonafter removal is deleted with a note line(
load_config_overridesshort-circuits on missing file)._EXTRA_MUTATION_FIELDS(currently justindexing.memory_dirs)triggers a domain warning pointing at dedicated endpoints
(
mm memory-dirs list/mm index) after successful removal.Output matrix
Removed: mmr.enabledUnset: mmr.enabled (already at default)Skipped mmr.enabld: not set (did you mean 'mmr.enabled'?)Skipped foo.bar: not setMulti-key input is best-effort: iterate, collect Removed /
Already-unset / Skipped buckets, emit per-key lines. Exit 0 if no
skips, 1 otherwise.
_atomic_write_jsonIntroduced in
config.pynext to_json_default— tempfile inpath.parent+os.replace, with tmp cleanup on failure. Used byunsetas initial consumer.save_config_overridesandcli/init_cmd.py's--freshwrite path still callwrite_textdirectly; migrating them is the remaining wizard-leftovers follow-up
on "
--freshwrite-after-backup atomicity" and reduces to swappingthose two call sites once this PR lands.
Tests (13 in
TestConfigUnset)Full output matrix, empty-file deletion,
_EXTRA_MUTATION_FIELDSallowance + domain warning, malformed JSON handling, multi-key
best-effort, atomic-write-preserves-original-on-
os.replace-failureregression guard (fragment sets
mmr.enabled=true,config.jsonpins
false, unset → reload → fragment layer wins).Docs
docs/guides/configuration.md: new "Removing individualoverrides" subsection; the existing machine-migration section
now lists
mm config unset indexing.memory_dirsas Option 1.docs/guides/getting-started.md+docs/guides/user-guide.md:cheatsheets updated.
Test plan
uv run pytest -m "not ollama" -q— 1712 passeduv run ruff check packages/memtomem/src packages/memtomem/testsuv run ruff format --check packages/memtomem/src packages/memtomem/testsuv run mypy packages/memtomem/src— success, no issuesTestConfigUnsettests all green