Skip to content

feat(cli): add mm config unset for targeted override removal#259

Merged
memtomem merged 1 commit intomainfrom
feat/config-unset
Apr 18, 2026
Merged

feat(cli): add mm config unset for targeted override removal#259
memtomem merged 1 commit intomainfrom
feat/config-unset

Conversation

@memtomem
Copy link
Copy Markdown
Owner

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.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. Distinct from
mm init --fresh (single-key vs bulk; no backup vs backup;
idempotent scripting vs wizard re-run). Both commands stay.

Behaviour (6 locked decisions)

  1. Ambient cleanup: no. Unset touches exactly the named keys.
    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.
  2. Valid-key set = MUTABLE_FIELDS_EXTRA_MUTATION_FIELDS.
    indexing.memory_dirs is accepted (removal isn't a mutation, and
    the migration case is exactly what motivates this command) even
    though mm config set memory_dirs … stays rejected.
  3. Already-unset canonical key → exit 0 + (already at default).
    Idempotent re-runs are safe.
  4. Unknown key → exit 1 with a difflib suggestion (cutoff 0.7)
    when a canonical key is nearby, otherwise just Skipped.
  5. Malformed config.json → exit 1, no auto-recovery, message
    points at mm init --fresh. Unset is raw-edit semantic; hiding
    malformed state behind a backup would confuse.
  6. Empty config.json after removal is deleted with a note line
    (load_config_overrides short-circuits on missing file).

_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.

Output matrix

Case Message Exit
Canonical + pinned → removed Removed: mmr.enabled 0
Canonical + already unset Unset: mmr.enabled (already at default) 0
Non-canonical + similar canonical exists Skipped mmr.enabld: not set (did you mean 'mmr.enabled'?) 1
Non-canonical + no close match Skipped foo.bar: not set 1

Multi-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_json

Introduced in config.py next to _json_default — tempfile in
path.parent + os.replace, with tmp cleanup on failure. Used by
unset as initial consumer. save_config_overrides and
cli/init_cmd.py's --fresh write path still call write_text
directly; migrating them is the remaining wizard-leftovers follow-up
on "--fresh write-after-backup atomicity" and reduces to swapping
those two call sites once this PR lands.

Tests (13 in TestConfigUnset)

Full output matrix, 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

  • docs/guides/configuration.md: new "Removing individual
    overrides" subsection; the existing machine-migration section
    now lists mm config unset indexing.memory_dirs as Option 1.
  • docs/guides/getting-started.md + docs/guides/user-guide.md:
    cheatsheets updated.

Test plan

  • uv run pytest -m "not ollama" -q — 1712 passed
  • uv run ruff check packages/memtomem/src packages/memtomem/tests
  • uv run ruff format --check packages/memtomem/src packages/memtomem/tests
  • uv run mypy packages/memtomem/src — success, no issues
  • 13 new TestConfigUnset tests all green

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.
@memtomem memtomem merged commit a78c255 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 feat/config-unset branch April 18, 2026 22:40
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