Skip to content

semantic_memory.enabled is honored only at startup: search, add and list still build and query the semantic stack whenever a request names the semantic type #1600

Description

@edwinyyyu

What happened

semantic_memory.enabled: false only skips the semantic service at startup. Every request path that can name the semantic memory type still resolves the semantic stack lazily and uses it: add_memories, search_memories and list_memories on the REST API, and both MCP tools unconditionally. The flag does not make the memory type unavailable; it only changes who pays to bring it up.

Line numbers as of 21105d6e (speedkick), packages/server/src/memmachine_server/main/memmachine.py unless stated:

  • start (line 417) gates semantic service startup on self._conf.semantic_memory.enabled and logs "Semantic memory is disabled; skipping semantic service startup." (line 422). That is the only place the flag is consulted on the request-serving side.
  • add_memory (line 777), query_search (line 1052) and list_search (line 1129) each branch on MemoryType.Semantic in target_memories and then call self._resources.get_semantic_session_manager(). None of them reads the flag. query_search's docstring says "Search across enabled memory types", which is not what it does.
  • SemanticManager.get_semantic_session_manager (common/resource_manager/semantic_manager.py:255) constructs get_semantic_service (line 218) on first use, which constructs get_semantic_storage (line 83) and get_semantic_config_storage (line 175) and resolves the semantic embedder and language model. Neither getter reads enabled. get_semantic_storage calls storage.startup(), which for the pgvector backend runs Alembic migrations (semantic_memory/storage/sqlalchemy_pgvector_semantic.py:209-212). So the first request that names the semantic type against a "disabled" semantic memory brings up the whole stack, migrations included, on the request path.
  • The REST router (server/api_v2/router.py) defaults types to [MemoryType.Episodic] for add (line 297), search (line 315) and list (line 339) since Change default memory type to episodic #1555. That keeps an untyped REST request off the semantic stack, but an explicit types: ["semantic"] or ["episodic", "semantic"] goes straight through.
  • The MCP tools mcp_add_memory (server/api_v2/mcp.py:468) and mcp_search_memory (line 528) hardcode target_memories=ALL_MEMORY_TYPES, so every MCP add and search runs the semantic branch regardless of config.

What a caller sees depends on how complete the semantic config is, and both outcomes are wrong:

  1. Semantic fully configured (database, embedding model, LLM present) but enabled: false. The request succeeds and the semantic branch does real work: the session manager's search (semantic_memory/semantic_session_manager.py:154) resolves set ids through the config store, which registers set types (line 367 onward, a write), embeds the query, and searches the semantic tables; the response carries a populated semantic_memory field. Measured on 2026-08-27 at 231ce171 with this configuration: a search naming both types cost 2.00 embedder calls per request at steady state over 50 requests, and 4 on the first request after a server start (semantic embedder initialization). The gating code above is unchanged between 231ce171 and 21105d6e (checked by diff), so this still holds. At the time the REST default was all types, so an untyped search paid this too; since Change default memory type to episodic #1555 it takes an explicit types or the MCP tool.
  2. Semantic incomplete (no semantic database), enabled: false. This is the shape feat(config): default to no short-term or semantic memory, and infer the backend #1580 now produces by default, and the shape _auto_disable_when_incomplete forces whenever a field is missing. get_semantic_storage raises ResourceNotReadyError("No database configured for semantic storage.") from inside the request, so a request naming the semantic type fails, and an MCP add or search fails on every call because the tools always name it.

Episodic memory is handled differently: with episodic_memory.enabled: false, _with_default_episodic_memory_conf raises ResourceNotReadyError("Episodic memory is disabled") (line 529), so a request naming it fails with the flag as the stated reason. Semantic memory gets neither that error nor a skip.

#1575 is the same missing gate on the delete seam: _cleanup_semantic_history (line 1221) calls get_semantic_service (line 1231) unconditionally, so project deletion never completes with semantic disabled.

Expected

A disabled memory type is not served, whichever route names it. query_search's own docstring states the contract: search across enabled memory types. The cleanest fix is one gate in one place plus a filter at the entry:

  • SemanticManager.get_semantic_session_manager, get_semantic_service and get_semantic_storage refuse when self._conf.enabled is false, with an error that says so (as the episodic path already does), instead of lazily building a stack the operator turned off.
  • MemMachine.add_memory, query_search and list_search intersect target_memories with the enabled types, so a request that names a disabled type gets None for that field, exactly as when the type was not requested. (Rejecting such a request with a 4xx is the alternative; either is fine, but it should be one or the other, and the same for episodic and semantic.)
  • The MCP tools pass the enabled types rather than ALL_MEMORY_TYPES.

With the getters gated, #1575 closes as a side effect, since _cleanup_semantic_history would be skipped or fail fast at the same point.

Notes

Found while tracing double embedder calls in a deployment that had disabled semantic memory by config and believed the flag was effective. Configuration-level disablement is a no-op for any request that names the type, which is easy to miss because typed episodic-only requests behave correctly.

Investigated and written by Claude (Claude Code), filed from the account of the user who commissioned the investigation.

Activity

  1. added theissue type on Sep 9, 2026
  2. added 2 commits that reference this issue on Sep 9, 2026
    07f8b13
    304619e
  3. edwinyyyu commented on Sep 9, 2026

    @edwinyyyu
    ContributorAuthor

    Two corrections to the Expected section, now that #1601 is up.

    The refusal does not belong in SemanticResourceManager's getters. A component should not read whether it is enabled; the server routes. #1601 puts the refusal in ResourceManagerImpl.get_semantic_manager (the composition root, which already refuses to hand out anything the deployment does not have) and the routing in MemMachine (filter target_memories by the enabled types; guard the delete seams at the call site). SemanticResourceManager and everything below it never see the flag.

    The last sentence is wrong: gating the getters alone would not close #1575. A fail-fast inside _cleanup_semantic_history still raises in the deletion worker, which drops the job. The delete path needs the call-site skip, which #1601 adds, alongside the removal of the shadowed duplicate definition of _cleanup_semantic_history that made the original graceful handling dead code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions