Skip to content

fix(cli): defer FastAPI and app imports out of CLI startup#988

Merged
phernandez merged 1 commit into
mainfrom
fix/886-defer-cli-heavy-imports
Jun 12, 2026
Merged

fix(cli): defer FastAPI and app imports out of CLI startup#988
phernandez merged 1 commit into
mainfrom
fix/886-defer-cli-heavy-imports

Conversation

@phernandez

Copy link
Copy Markdown
Member

Every basic-memory CLI 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 importtime on basic_memory.cli.main)

chain before after
import basic_memory.cli.main (total) 1.92s 0.45s
bm --help (wall) 2.40s 0.52s
bm tool search-notes --help (wall) 2.40s 0.86s

Where the time went, and what was deferred:

  • mcp/async_client.py imported FastAPI at module level (~0.1s, and the gateway to the app-deps chain). Depends/Request now import inside _resolve_local_asgi_database, right next to the existing lazy basic_memory.api.app import 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/*.py imported call_* from basic_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 its call_* helper at call time (same deferred-import precedent already used in project_context.py).
  • mcp/project_context.py imported fastmcp.Context and ToolError eagerly (~0.5s standalone). Now PEP 563 lazy annotations with Context under TYPE_CHECKING; ToolError imports inside the two functions that catch it.
  • CLI command modules pulled the rest in at registration time: tool.py/ci.py/schema.py imported MCP tool functions at module level; db.py pulled SQLAlchemy/Alembic + repositories + sync; the four import_* commands pulled the markdown/file-service stack (which imports SQLAlchemy models); status/doctor/orphans/command_utils imported ToolError (mcp SDK) and basic_memory.db. All moved into the command/function bodies with comments explaining the startup-cost constraint.
  • schemas/base.py imported dateparser (~0.13s) for the single parse_timeframe helper — now imported per call.

Why this is safe

  • No behavior changes: only import placement moved; every deferred import is at the point of use and fails just as fast if the dependency is broken.
  • bm mcp server registration is unaffected — it already imports basic_memory.mcp.tools explicitly inside the command body.
  • Tests that monkeypatched the old module-level aliases now patch the source modules (basic_memory.mcp.tools.<tool>, basic_memory.mcp.tools.utils.call_*, basic_memory.db.*, etc.) — same coverage, same assertions.
  • End-to-end verified: bm doctor in 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 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. 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 real bm tool search-notes against 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

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>
@phernandez phernandez merged commit 0247ef0 into main Jun 12, 2026
37 of 38 checks passed
@phernandez phernandez deleted the fix/886-defer-cli-heavy-imports branch June 12, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI eagerly imports embedding stack (onnxruntime/fastembed) for filter-only search-notes → slow SessionStart brief

1 participant