fix(config): coerce list[BaseSettings] on mutation path#257
Merged
Conversation
PR #253 introduced NamespacePolicyRule as the first list[BaseSettings] field. Load path handled it correctly via _list_item_type + model_validate, but mutation path (coerce_and_validate, PATCH /api/config, mm config set, init wizard) silently passed raw dicts through, causing AttributeError on rule.path_glob in engine.py:121. This PR mirrors the load-path pattern on mutation, unifies _json_default across all config-write sites (fixing a latent repr-string round-trip), and adds regression guards for both the PR #253 failure mode and legacy config.json recovery. Refs: PR #253, #256 (adjacent save-path fix)
memtomem
pushed a commit
that referenced
this pull request
Apr 18, 2026
Backfills 8 PRs (#253, #254, #255, #256, #257, #258, #259, #262) merged to main between v0.1.9 and 0.1.10 release prep (#263) without CHANGELOG entries. Entries land in the next release, not 0.1.10. - Added: #253 (NamespacePolicyRule), #254 (wizard Preserved summary), #255 (mm init --fresh), #259 (mm config unset) - Changed: #256 (save drops default-valued fields) - Fixed: #257 (list[BaseSettings] mutation coercion), #258 (fragment/ env drag-in on save), #262 (atomic config.json writes)
memtomem
added a commit
that referenced
this pull request
Apr 18, 2026
Backfills 8 PRs (#253, #254, #255, #256, #257, #258, #259, #262) merged to main between v0.1.9 and 0.1.10 release prep (#263) without CHANGELOG entries. Entries land in the next release, not 0.1.10. - Added: #253 (NamespacePolicyRule), #254 (wizard Preserved summary), #255 (mm init --fresh), #259 (mm config unset) - Changed: #256 (save drops default-valued fields) - Fixed: #257 (list[BaseSettings] mutation coercion), #258 (fragment/ env drag-in on save), #262 (atomic config.json writes) Co-authored-by: pandas-studio <[email protected]>
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
PR #253 introduced
NamespacePolicyRuleas memtomem's firstlist[BaseSettings]config field. The load path was wired to handle it correctly (via_list_item_type+model_validateinload_config_dand_dedup_key), but the mutation path was left asymmetric —coerce_and_validate,PATCH /api/config,mm config set, and the init wizard all silently passed raw dicts through. Downstream code (indexing/engine.py:121) accessesrule.path_globon each entry and wouldAttributeErroron a dict.Adjacent to PR #256 (save-path drop-default); together #253 → #256 → this PR complete the config-layer round-trip.
Root cause
PR #253 added the new
list[BaseSettings]shape but only fixed the read side of the config pipeline. The write side — the three canonical mutation entry points plus the init wizard — kept validating only primitive lists. A user PATCH-ingnamespace.rulesor runningmm config set namespace.rules '[...]'saw no validation error, but the runtime config held dicts where model instances were expected.What this does
1.
coerce_and_validatehandleslist[BaseSettings]New branch inside the existing
expected_type is listpath, gated by:The
isinstance(item_type, type)guard is deliberate — it protects againstUnion, generic aliases, or other non-class item types that wouldTypeErrorinsideissubclass. Existing primitive-list callers (search.rrf_weights,importance.weights) take theelsepath unchanged; all their existing tests still pass.Inside the branch: JSON strings are parsed (CLI path), lists of dicts are
model_validate-ed (Web UI path), already-instance entries pass through, scalars/malformed shapes raiseValueErrorthat propagates as arejectedentry in PATCH or a non-zero CLI exit.2.
MUTABLE_FIELDS["namespace"]+FIELD_CONSTRAINTS["namespace.rules"]Registers the field at both gates so
PATCH /api/configno longer reportsnamespace.rules: read-only fieldandmm config setno longer printsnot a mutable field. Frontend impact check:grep FIELD_CONSTRAINTS packages/memtomem/src/memtomem/web/static/→ zero hits. UI rendering is unaffected.3. Unified
_json_defaultJSON serializersave_config_overridespreviously usedjson.dumps(..., default=str), which would emitBaseSettings.__repr__for any nested model — a latent round-trip bug. Fixed with_json_defaultthat returnsmodel_dump(mode="json")forBaseSettingsandstr()forPath. Scope audit viagrep "default=str\|default=_json_default" packages/memtomem/src/confirmed:config.pysave_config_overrides→ fixed (uses_json_default)cli/init_cmd.py:706wizard write → also fixed (imports + uses_json_default) for consistency across all config-write pathsdefault=strcall sites (chunking/structured.py,cli/watchdog_cmd.py,cli/config_cmd.pyshow,server/scheduler.py,server/health_store.py,server/webhooks.py,server/tools/consolidation.py) all serialize non-config payloads (scheduler state, webhook bodies, health snapshots, etc.) — unrelated toBaseSettingsconfig models, intentionally left alone.Though
namespace.rulesis currently the onlylist[BaseSettings]field in play, unifying the helper prevents a future nested model from silently re-introducing the same latent bug.4. Migration safety for pre-fix installations
Any
config.jsonwritten by a pre-fix wizard may contain repr-string leftovers fornamespace.rules. On load:coerce_and_validateviaFIELD_CONSTRAINTS["namespace.rules"].ValueError.load_config_overridescatches theValueError, logs a warning, skips the key.[]. No crash, no data loss beyond the already-corrupt entry.Test:
test_legacy_repr_string_in_config_handled_gracefullycovers both scalar-value and list-of-strings cases.Testing
test_cli.py::TestCoerceAndValidate:test_namespace_rules_from_json_string— CLI pathtest_namespace_rules_from_list_of_dicts_pr253_regression— Web PATCH path; docstring pins theengine.py:121failure mode that originally motivated this fixtest_namespace_rules_passthrough_for_model_instancestest_namespace_rules_empty_listtest_namespace_rules_rejects_malformed_jsontest_namespace_rules_rejects_non_list_jsontest_namespace_rules_rejects_scalar_entrytest_namespace_rules_propagates_model_validation_errortest_cli.py::TestSaveConfigOverrides:test_legacy_repr_string_in_config_handled_gracefully— migration safetytest_namespace_rules_round_trip— save → load returns validated instances (not dicts)test_cli.py::TestConfigCLI:test_config_set_namespace_rules_json— end-to-end CLI persistence + reloadtest_config_set_namespace_rules_rejects_malformedtest_web_routes_extended.py::TestConfigPatch:test_patch_namespace_rules— PATCH apply + runtime type checktest_patch_namespace_rules_validation_error— bad rule lands inrejectedmm config set namespace.rules '[...]'→ reload producesNamespacePolicyRuleinstances →_FRESH_PRESERVE_KEYScontainsnamespace.rules(PR cli(init): add --fresh to drop wizard-untouched config leftovers #255 preserve list) → subsequentsave_config_overrideskeeps the rules (non-default, so not dropped by PR fix(config): drop default-valued fields on save to prevent silent leftover pins #256 logic).Unblocks
project_list_basesettings_config_gap.md)mm initwizard rule presetslist[BaseSettings]config fieldTest plan
uv run pytest -m "not ollama"— 1694 passeduv run ruff check+ruff format --check— cleanuv run mypy packages/memtomem/src— 0 issuesmm config set namespace.rules '[...]'round-trip, reload produces model instances_FRESH_PRESERVE_KEYScontainsnamespace.rules(interop with PR cli(init): add --fresh to drop wizard-untouched config leftovers #255)