fix(cli): defer FastAPI and app imports out of CLI startup#988
Merged
Conversation
Every basic-memory CLI invocation paid roughly 2 seconds of module-import cost before any work started, which blew the Claude Code plugin's SessionStart hook budget on cold machines (#886). The cost came from module-level imports that pulled the entire server stack into CLI startup: - mcp/async_client.py imported FastAPI at module level, so every consumer of get_client() loaded FastAPI even for cloud-routed or help-only paths. - mcp/clients/*.py imported call_* helpers from basic_memory.mcp.tools.utils, which executes the whole tools package __init__ — every MCP tool module plus fastmcp and the mcp SDK. - mcp/project_context.py imported fastmcp.Context and ToolError eagerly. - CLI command modules (tool, ci, schema) imported MCP tool functions at module level; db and the import_* commands pulled SQLAlchemy/Alembic and the markdown/file-service stack; status/doctor/orphans/command_utils imported ToolError (the mcp SDK) and basic_memory.db. - schemas/base.py imported dateparser (~0.13s) for one helper function. The fix only defers imports to the point of use (no behavior changes): FastAPI now loads inside _resolve_local_asgi_database alongside the existing lazy api.app import, so it is only paid when a request actually routes through the in-process ASGI transport; the typed clients import call_* per method; project_context uses PEP 563 annotations with Context under TYPE_CHECKING; the CLI command modules import their heavy dependencies inside the command bodies. Tests that patched the old module-level aliases now patch the source modules instead. Measured on a warm cache (python -X importtime / wall time): - import basic_memory.cli.main: 1.92s -> 0.45s - bm --help: 2.40s -> 0.52s - bm tool search-notes --help: 2.40s -> 0.86s A regression test asserts that importing the CLI entry module with full command registration leaves fastapi, sqlalchemy, alembic, fastmcp, mcp, basic_memory.api.app, basic_memory.db, basic_memory.markdown, basic_memory.mcp.tools, and basic_memory.services out of sys.modules. Fixes #886 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Every
basic-memoryCLI invocation paid ~2s of module-import cost before any work started, which blew the Claude Code plugin's SessionStart hook budget on cold machines (symptom: "Couldn't read from — it may be misnamed or unreachable").Cost breakdown (warm cache,
python -X importtimeonbasic_memory.cli.main)import basic_memory.cli.main(total)bm --help(wall)bm tool search-notes --help(wall)Where the time went, and what was deferred:
mcp/async_client.pyimported FastAPI at module level (~0.1s, and the gateway to the app-deps chain).Depends/Requestnow import inside_resolve_local_asgi_database, right next to the existing lazybasic_memory.api.appimport from perf(cli): defer local ASGI app import #828 — so FastAPI and the API app load only when a request actually routes through the in-process ASGI transport. Cloud-routed tool calls never load them.mcp/clients/*.pyimportedcall_*frombasic_memory.mcp.tools.utils, which executes the whole tools package__init__— every MCP tool module plus fastmcp and the mcp SDK (~0.46s). Each client method now imports itscall_*helper at call time (same deferred-import precedent already used inproject_context.py).mcp/project_context.pyimportedfastmcp.ContextandToolErroreagerly (~0.5s standalone). Now PEP 563 lazy annotations withContextunderTYPE_CHECKING;ToolErrorimports inside the two functions that catch it.tool.py/ci.py/schema.pyimported MCP tool functions at module level;db.pypulled SQLAlchemy/Alembic + repositories + sync; the fourimport_*commands pulled the markdown/file-service stack (which imports SQLAlchemy models);status/doctor/orphans/command_utilsimportedToolError(mcp SDK) andbasic_memory.db. All moved into the command/function bodies with comments explaining the startup-cost constraint.schemas/base.pyimported dateparser (~0.13s) for the singleparse_timeframehelper — now imported per call.Why this is safe
bm mcpserver registration is unaffected — it already importsbasic_memory.mcp.toolsexplicitly inside the command body.basic_memory.mcp.tools.<tool>,basic_memory.mcp.tools.utils.call_*,basic_memory.db.*, etc.) — same coverage, same assertions.bm doctorin a fresh HOME passes all file<->DB checks, exercising the deferred FastAPI -> api.app -> db path at runtime.Regression guard
New
tests/cli/test_cli_exit.py::test_bm_cli_import_does_not_load_heavy_stack(extends the #828 subprocess pattern) asserts that importing the CLI entry module with full command registration leavesfastapi,sqlalchemy,alembic,fastmcp,mcp,basic_memory.api.app,basic_memory.db,basic_memory.markdown,basic_memory.mcp.tools, andbasic_memory.servicesout ofsys.modules. Verified to fail on main without this fix (it lists exactly those modules) and pass with it.Remaining floor (not removable without architectural change)
~0.45s remains: pydantic + pydantic-settings via
ConfigManager(~0.2s), typer/rich/loguru/logfire, and the pydantic schemas pulled by the typed clients and project_context (~0.1s). A realbm tool search-notesagainst a local project still loads the tool stack + API app at call time by design; against a cloud project it now skips FastAPI/api.app/SQLAlchemy entirely.Fixes #886
🤖 Generated with Claude Code