Skip to content

Honor semantic_memory.enabled on every request path, not only at startup (speedkick) - #1601

Closed
edwinyyyu wants to merge 1 commit into
MemMachine:speedkickfrom
edwinyyyu:fix/semantic-enabled-gate-speedkick
Closed

edwinyyyu wants to merge 1 commit into
MemMachine:speedkickfrom
edwinyyyu:fix/semantic-enabled-gate-speedkick

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1600 and #1575. Both are the same missing gate: semantic_memory.enabled was consulted once, in start(), and nowhere on the request-serving side.

Purpose of the change

With semantic_memory.enabled: false, every request path that could name the semantic type still resolved the semantic stack lazily and used it. add_episodes, query_search and list_search branched on target_memories alone, and the SemanticResourceManager getters built storage, config store, embedder and language model on first use without reading the flag, running Alembic migrations on the request path for pgvector. The flag changed who paid to bring the stack up, not whether it came up:

  • Complete semantic config but enabled: false: the request succeeded and did real work. Measured on 2026-08-27 at 231ce171: a search naming both types cost 2.00 embedder calls per request at steady state, 4 on the first request after a start.
  • Incomplete config with enabled: false, which is what feat(config): default to no short-term or semantic memory, and infer the backend #1580 now produces by default: get_semantic_storage raised ResourceNotReadyError("No database configured for semantic storage.") from inside the request.

The REST router has defaulted types to episodic since #1555, so an untyped REST call was shielded. An explicit types, the Python client (which sends both types on every add and search), and both MCP tools (which hardcode ALL_MEMORY_TYPES) were not.

Deletion had the same gap with a twist. memmachine.py defined _cleanup_semantic_history twice; both arrived in #1151. The later definition, which requests the semantic service unconditionally, shadowed the earlier one that caught ResourceNotReadyError. So with semantic memory disabled, every project deletion raised in the worker, dropped the job, and left the session in delete status until a restart re-queued it and failed the same way (#1575). delete_episodes requested the service unconditionally too.

Description

ResourceManagerImpl.get_semantic_manager (common/resource_manager/resource_manager.py): raises ResourceNotReadyError("Semantic memory is disabled.", "semantic_memory") when the flag is off, so nothing semantic is constructed. get_semantic_service and get_semantic_session_manager go through it. The semantic-only MemMachine operations (features, set types, and so on) now fail with that message instead of building a stack the operator turned off; their routes already map unexpected exceptions to 500, and I have left the status mapping alone.

Where the flag is read. Two places, both on the server side: MemMachine routes requests (the filter below, and the delete guards), and the composition root refuses to build a component the deployment does not have, the same contract it has for an embedder name that is not configured. SemanticResourceManager and everything below it never see the flag. The first revision of this PR had the semantic manager's own getters read it; that was the wrong layer, and the episodic side already shows the right one, with "Episodic memory is disabled" raised in MemMachine.

MemMachine (main/memmachine.py):

  • New enabled_memory_types property, read from episodic_memory.enabled and semantic_memory.enabled at call time.
  • add_episodes, query_search and list_search filter target_memories through it. A request naming a disabled type gets None for that field, exactly as when it did not name the type. The docstring of query_search already promised "Search across enabled memory types"; now it does.
  • The shadowed _cleanup_semantic_history is removed. The surviving one is called only when semantic memory is enabled, matching the guard _delete_queued_session already applied to the semantic delete itself. delete_episodes resolves the semantic service under the same guard, in the same position as before so an enabled-but-broken semantic store still fails before anything is deleted.

Two more mixed paths. With the composition root refusing, anything that reached the semantic stack unconditionally on a non-semantic request would have started failing on a disabled deployment. Two did. The MCP delete_memory tool goes through _delete_memories in server/api_v2/service.py, which called delete_features even when no semantic uids were named, so an episodic-only delete over MCP would have failed. It now calls delete_features only when semantic uids were named. MemMachine.delete_all asked for the semantic manager before touching the episode store; it now touches the semantic stores only when semantic memory is enabled. Both are routing decisions in the server layer, and both have tests.

Skip, not reject. #1600 left the choice open. The client settles it: memmachine_client sends [Episodic, Semantic] on every add and search, so rejecting a request that names a disabled type would fail every client call against the default configuration. Episodic gets the same treatment, so a semantic-only deployment no longer stores the episode and then raises "Episodic memory is disabled" on the same request. The MCP tools are unchanged; ALL_MEMORY_TYPES now means every type the server has.

Tests. The main/conftest.py integration configuration built a complete SemanticMemoryConf without saying enabled=True and relied on the lazy build; it now says it. The batched-deletion test flips semantic memory on so its per-batch cleanup assertion keeps meaning something.

Not in scope: the second half of #1575, that a failed deletion leaves a session no API call can revisit. That is #1577.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Behavior change worth stating: a request that names a disabled memory type used to either do hidden work or fail; it now returns None for that type. The semantic-only endpoints now fail with "Semantic memory is disabled." where they previously failed with a storage error or silently built the stack.

How Has This Been Tested?

  • Unit Test

Thirteen new tests. The seven MemMachine ones from the first revision were run against the unfixed code first; all failed there, and the deletion one failed with the exact log line from #1575 (Failed to delete session s1 ... No database configured for semantic storage.).

test_memmachine_mock.py: enabled_memory_types follows the flags; query_search skips semantic when disabled (episodic still searched, semantic session manager never requested, semantic_memory is None); query_search skips episodic when disabled; add_episodes skips semantic; list_search skips semantic; delete_episodes skips semantic history; and delete_session completes with semantic disabled (episodes deleted, session row deleted, semantic service never requested, with the getter mocked to raise the #1575 error).

test_resource_manager.py: get_semantic_manager, get_semantic_service and get_semantic_session_manager on a ResourceManagerImpl whose config has semantic memory disabled raise ResourceNotReadyError matching "disabled". No other code path produces that message, so these fail without the gate.

test_service.py: _delete_memories never calls delete_features when no semantic uids are named, and calls it with the uids when they are. test_memmachine_mock.py: delete_all with semantic memory disabled deletes the episode store and never requests the semantic manager (mocked to raise the disabled error).

Test Results:

targeted suites (main, server/api_v2, common/resource_manager, semantic history cleanup):
349 passed, 9 skipped, 69 deselected

integration-marked batched-deletion test, run explicitly:
1 passed, 3 deselected

full server suite (one module skipped at collection for an optional extra absent locally, hnswlib):
1 failed, 1926 passed, 16 skipped, 1509 deselected, 1 error
  the failure is test_version.py::test_get_version, which rejects the setuptools-scm
  version string an untagged checkout produces (a .devN+g<sha> string);
  nothing in this diff touches versioning

ruff check: All checks passed!
ruff format: 1 file reformatted (this branch's own edit)
ty check packages: 23 diagnostics, all unresolved-import for optional extras absent from this venv, none in touched files

Checklist

  • I have signed the commit(s) within this pull request
  • My code follows the style guidelines of this project (See STYLE_GUIDE.md)
  • I have added unit tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

🤖 Generated with Claude Code

https://claude.ai/code/session_018nSC7JKXByD1WADJukvupg

@edwinyyyu
edwinyyyu force-pushed the fix/semantic-enabled-gate-speedkick branch from 4f5cb71 to 07f8b13 Compare September 9, 2026 21:35
…tup (speedkick)

`semantic_memory.enabled: false` only skipped the semantic service in
start(). Every request path that could name the semantic type still
resolved the semantic stack lazily and used it: add_episodes, query_search
and list_search branched on target_memories alone, and the SemanticManager
getters built storage, config store, embedder and language model on first
use without reading the flag (running Alembic migrations on the request
path for pgvector). The flag changed who paid to bring the stack up, not
whether it came up. With a complete config the request succeeded and did
real work; with the incomplete config that MemMachine#1580 now produces by default,
get_semantic_storage raised from inside the request.

Deletion had the same gap with a twist. memmachine.py defined
_cleanup_semantic_history twice; both arrived in MemMachine#1151. The later
definition, which requests the semantic service unconditionally, shadowed
the earlier one that caught ResourceNotReadyError, so with semantic memory
disabled every project deletion raised in the worker, dropped the job, and
left the session in `delete` status for good (MemMachine#1575). delete_episodes
requested the service unconditionally too.

Three changes:

- ResourceManagerImpl.get_semantic_manager raises
  ResourceNotReadyError("Semantic memory is disabled.") when the flag is
  off, so nothing semantic is constructed; get_semantic_service and
  get_semantic_session_manager go through it. The flag is read only on the
  server side: MemMachine routes requests, and the composition root refuses
  to build a component the deployment does not have. SemanticResourceManager
  and everything below it never see it. The semantic-only MemMachine
  operations (features, set types, ...) now fail with that message instead
  of building a stack the operator turned off.

- MemMachine gains enabled_memory_types, read from the two configuration
  flags, and add_episodes, query_search and list_search filter
  target_memories through it. A request naming a disabled type gets None
  for that field, exactly as when it did not name the type. Skipping
  rather than rejecting is forced by the client: memmachine_client sends
  both types on every add and search, so rejecting would fail every client
  call against the default configuration. Episodic gets the same
  treatment, so a semantic-only deployment no longer stores the episode
  and then raises "Episodic memory is disabled" on the same request. The
  MCP tools keep passing ALL_MEMORY_TYPES, which now means every type the
  server has.

- The shadowed _cleanup_semantic_history is removed; the surviving one is
  called only when semantic memory is enabled, matching the guard
  _delete_queued_session already applied to the semantic delete itself,
  and delete_episodes resolves the semantic service under the same guard.

- Two more mixed paths reached the semantic stack unconditionally and
  would have started failing on a disabled deployment once the composition
  root refused: the MCP delete tool's _delete_memories called
  delete_features even with no semantic uids named, and delete_all asked
  for the semantic manager before touching the episode store. The first
  now calls delete_features only when semantic uids were named; the second
  touches the semantic stores only when semantic memory is enabled.

The integration conftest built a complete SemanticMemoryConf without
saying enabled=True and relied on the lazy build; it now says it. The
batched deletion test flips semantic memory on so its per-batch cleanup
assertion keeps meaning something.

Fixes MemMachine#1575
Fixes MemMachine#1600

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_018nSC7JKXByD1WADJukvupg
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.

1 participant