Skip to content

feat(server): migrate handlers to _get_app_initialized (#399 phase 2)#410

Merged
memtomem merged 1 commit intomainfrom
feat/lazy-init-phase2
Apr 23, 2026
Merged

feat(server): migrate handlers to _get_app_initialized (#399 phase 2)#410
memtomem merged 1 commit intomainfrom
feat/lazy-init-phase2

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

  • Lazy DB init: defer ~/.memtomem/memtomem.db creation until first tool call #399 Phase 2 handler migration: every @mcp.tool / @mcp.resource handler now fetches the AppContext via await _get_app_initialized(ctx) instead of the bare _get_app(ctx).
  • Unblocks Phase 3 (dropping the eager ensure_initialized from app_lifespan). Without this migration, a handler still reaching through _get_app would hit the _require_initialized guard the moment Phase 3 flips — handshake-only MCP sessions still need that flip to actually stop creating ~/.memtomem/memtomem.db, but handlers have to be ready first.
  • Runtime behavior unchanged: Phase 1 kept app_lifespan calling ensure_initialized eagerly, so every handler still sees populated components. The migration is purely a call-site swap.

What changed

File surface Change
server/context.py New async _get_app_initialized(ctx) -> AppContext — one-line wrapper that calls _get_app and awaits ensure_initialized (idempotent via _init_lock, so repeated awaits across 100+ handler call sites have no cost after the first).
server/__init__.py Re-export the helper.
server/resources.py 5 @mcp.resource handlers migrated.
server/tools/*.py (30 files) 99 handler call sites switched from app = _get_app(ctx) to app = await _get_app_initialized(ctx). Import swap + single-line swap per handler, no logic changes.
tests/test_context_window.py 7 monkeypatches on memtomem.server.tools.search._get_app retargeted to _get_app_initialized via AsyncMock(return_value=app) (the helper is async; sync lambda _: app no longer works).
tests/test_tools_logic.py + tests/test_trust_ux.py _fake_ctx / _recall_ctx / _search_ctx SimpleNamespace stubs gain ensure_initialized=AsyncMock() so the await inside the helper resolves without touching real storage.
tests/test_tool_registration.py New regression guard test_handlers_use_get_app_initialized — AST-checks that no file under server/tools/ or server/resources.py imports bare _get_app, so new handlers can't silently drift back.

Deliberately out of scope

Test plan

  • uv run ruff check packages/memtomem/src packages/memtomem/tests — pass
  • uv run ruff format --check packages/memtomem/src packages/memtomem/tests — pass
  • uv run pytest -m "not ollama"2172 passed (2171 pre-existing + 1 new guard). No regressions.
  • Spot-checked test_server_app_context.py (Phase 1 lock-semantics tests) still pass — the helper builds on the same ensure_initialized path those tests cover.
  • uv run mypy packages/memtomem/src/memtomem/server — no new errors introduced by this PR (3 pre-existing read-only-property writes in status_config._revert_to_stored tracked in bug(server): _revert_to_stored writes to read-only AppContext properties (#399 phase 1 regression) #409).

🤖 Generated with Claude Code

Every @mcp.tool / @mcp.resource / @register handler now fetches the
AppContext through ``await _get_app_initialized(ctx)`` instead of the
bare ``_get_app(ctx)``. Phase 1 (#399) turned storage/embedder/
index_engine/search_pipeline into properties that read from a lazy
``_components`` slot; phase 3 will drop the eager init call from
``app_lifespan`` so handshake-only MCP sessions stop creating
``~/.memtomem/memtomem.db``. Phase 2 migrates the call sites so that
flip is safe — a handler that still used ``_get_app`` would hit the
``_require_initialized`` guard on the first storage read.

Behavior is unchanged today because ``app_lifespan`` still calls
``ensure_initialized`` eagerly. The helper is a thin wrapper: it calls
``_get_app`` and awaits ``ensure_initialized`` once (idempotent via
``_init_lock``), so the repeated awaits in the 100+ handler call sites
have no cost after the first one.

Scope:
- server/context.py: new ``async _get_app_initialized`` helper
- server/__init__.py: re-export
- server/resources.py + 30 files under server/tools/: swap import +
  change ``app = _get_app(ctx)`` to ``app = await _get_app_initialized(ctx)``
- tests/test_context_window.py: update 7 monkeypatches to target the
  new helper via ``AsyncMock(return_value=app)``
- tests/test_tools_logic.py + tests/test_trust_ux.py: add
  ``ensure_initialized=AsyncMock()`` to the SimpleNamespace fakes so
  the await inside the helper resolves
- tests/test_tool_registration.py: new regression guard
  ``test_handlers_use_get_app_initialized`` — AST-checks that no file
  under server/tools/ or server/resources.py imports bare ``_get_app``,
  preventing silent drift on new handlers

Out of scope (filed separately):
- #409 — ``_revert_to_stored`` writes to the read-only properties the
  phase 1 conversion introduced. Pre-existing latent bug surfaced by
  this PR's mypy run; recovery path with no test coverage. Fixing it
  here would mix concerns.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 4f33a43 into main Apr 23, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 2026
@memtomem memtomem deleted the feat/lazy-init-phase2 branch April 23, 2026 05:23
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