Problem
_revert_to_stored in packages/memtomem/src/memtomem/server/tools/status_config.py:254,256,266 writes to app.embedder, app.search_pipeline, app.index_engine:
def _revert_to_stored(app: AppContext) -> str:
...
new_embedder = create_embedder(config.embedding)
app.embedder = new_embedder # line 254
app.search_pipeline = SearchPipeline(...) # line 256
...
app.index_engine = IndexEngine(...) # line 266
...
After #399 Phase 1 (7855042), these three names on AppContext are read-only properties that proxy to self._components.<name>. Assigning to a property without a setter raises AttributeError: property 'embedder' of 'AppContext' object has no setter at runtime.
mypy flags it:
status_config.py:254: error: Property "embedder" defined in "AppContext" is read-only [misc]
status_config.py:256: error: Property "search_pipeline" defined in "AppContext" is read-only [misc]
status_config.py:266: error: Property "index_engine" defined in "AppContext" is read-only [misc]
Because mypy is advisory in this repo (CLAUDE.md) and there is no test that calls mem_embedding_reset(mode="revert_to_stored") end-to-end (grep: only _maybe_offer_embedding_reset in tests/test_init_cmd.py, which hits a different code path), the regression shipped with Phase 1.
Impact
mem_embedding_reset(mode="revert_to_stored") is a recovery path — uncommon, but the whole point is that it stays callable when the DB is in degraded mode (#349). An AttributeError in the recovery tool defeats that. The user's fallback is re-starting the server after manually editing config, which is exactly the friction mem_embedding_reset exists to remove.
Fix sketch
Components is a mutable @dataclass (server/component_factory.py:30-44), so the natural fix is to mutate the inner container:
comp = app._components
assert comp is not None # handler already awaited ensure_initialized
comp.embedder = new_embedder
comp.search_pipeline = SearchPipeline(...)
comp.index_engine = IndexEngine(...)
After this, app.embedder (the property) reads back the new value — no divergence.
Alternative: add explicit mutator methods on AppContext (replace_embedder / replace_search_pipeline / replace_index_engine) so the _components access stays behind the class's public surface. Probably overkill for a recovery-only path.
Test gap to close in the same fix
Add an integration test that calls mem_embedding_reset(mode="revert_to_stored") on a fixture in degraded mode and asserts:
- The tool returns the success message without raising.
app.embedder / app.search_pipeline / app.index_engine reflect the stored (not configured) embedder dimension after the call.
- A follow-up
mem_search (or any vector-dependent tool) succeeds — demonstrating the three slots were actually swapped, not just embedder.
Context
Problem
_revert_to_storedinpackages/memtomem/src/memtomem/server/tools/status_config.py:254,256,266writes toapp.embedder,app.search_pipeline,app.index_engine:After #399 Phase 1 (
7855042), these three names onAppContextare read-only properties that proxy toself._components.<name>. Assigning to apropertywithout a setter raisesAttributeError: property 'embedder' of 'AppContext' object has no setterat runtime.mypyflags it:Because
mypyis advisory in this repo (CLAUDE.md) and there is no test that callsmem_embedding_reset(mode="revert_to_stored")end-to-end (grep: only_maybe_offer_embedding_resetintests/test_init_cmd.py, which hits a different code path), the regression shipped with Phase 1.Impact
mem_embedding_reset(mode="revert_to_stored")is a recovery path — uncommon, but the whole point is that it stays callable when the DB is in degraded mode (#349). AnAttributeErrorin the recovery tool defeats that. The user's fallback is re-starting the server after manually editing config, which is exactly the frictionmem_embedding_resetexists to remove.Fix sketch
Componentsis a mutable@dataclass(server/component_factory.py:30-44), so the natural fix is to mutate the inner container:After this,
app.embedder(the property) reads back the new value — no divergence.Alternative: add explicit mutator methods on
AppContext(replace_embedder/replace_search_pipeline/replace_index_engine) so the_componentsaccess stays behind the class's public surface. Probably overkill for a recovery-only path.Test gap to close in the same fix
Add an integration test that calls
mem_embedding_reset(mode="revert_to_stored")on a fixture in degraded mode and asserts:app.embedder/app.search_pipeline/app.index_enginereflect the stored (not configured) embedder dimension after the call.mem_search(or any vector-dependent tool) succeeds — demonstrating the three slots were actually swapped, not justembedder.Context
7855042 feat(server): add AppContext.ensure_initialized plumbing) — the property conversion ofstorage/embedder/index_engine/search_pipeline. Those properties exist soAppContextcan defer component creation until first tool call; the read-only shape is deliberate but this one write-path wasn't migrated.mypy packages/memtomem/src/memtomem/serveroutput during the PR's lint pass.feedback_one_change_per_pr).