superlocalmemory 3.8.8 → 3.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/CLAUDE.md +3 -3
  5. package/plugin/agents/slm-governance-advisor.md +1 -1
  6. package/plugin/agents/slm-loop-runner.md +1 -1
  7. package/plugin/agents/slm-memory-advisor.md +1 -1
  8. package/plugin/agents/slm-optimize-advisor.md +1 -1
  9. package/plugin/requirements.txt +1 -1
  10. package/plugin/skills/slm-cache/SKILL.md +1 -1
  11. package/plugin/skills/slm-compress/SKILL.md +1 -1
  12. package/plugin/skills/slm-governance/SKILL.md +1 -1
  13. package/plugin/skills/slm-graph/SKILL.md +1 -1
  14. package/plugin/skills/slm-loop/SKILL.md +1 -1
  15. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  16. package/plugin/skills/slm-profile/SKILL.md +1 -1
  17. package/plugin/skills/slm-recall/SKILL.md +1 -1
  18. package/plugin/skills/slm-remember/SKILL.md +1 -1
  19. package/plugin/skills/slm-scope/SKILL.md +1 -1
  20. package/plugin/skills/slm-session/SKILL.md +1 -1
  21. package/plugin/skills/slm-status/SKILL.md +1 -1
  22. package/pyproject.toml +1 -1
  23. package/src/superlocalmemory/__init__.py +1 -1
  24. package/src/superlocalmemory/core/embeddings.py +55 -2
  25. package/src/superlocalmemory/core/engine_ingestion.py +5 -0
  26. package/src/superlocalmemory/core/ingestion_command.py +36 -0
  27. package/src/superlocalmemory/core/materialization_control.py +20 -0
  28. package/src/superlocalmemory/core/recall_gate.py +27 -3
  29. package/src/superlocalmemory/core/store_pipeline.py +10 -0
  30. package/src/superlocalmemory/mcp/tools_core.py +16 -7
  31. package/src/superlocalmemory/server/unified_daemon.py +3 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.8.9] - 2026-07-27 — Reliable MCP recall and background processing
9
+
10
+ ### Fixed
11
+ - MCP recall is reliable through both supported connection methods: local
12
+ stdio and Streamable HTTP. HTTP tool calls no longer hang the daemon.
13
+ - Changing dashboard memory or model settings no longer leaves background
14
+ processing stuck. Your saved memories stay available while processing resumes
15
+ safely with the updated settings.
16
+ - Newly saved memories remain immediately searchable while their richer
17
+ background indexing completes.
18
+
19
+ ### Notes
20
+ - Existing memories and configuration are preserved during upgrade. No manual
21
+ migration is required.
22
+
8
23
  ## [3.8.8] - 2026-07-27 — Live-database recall and vector integrity
9
24
 
10
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.8.8",
3
+ "version": "3.8.9",
4
4
  "description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -15,5 +15,5 @@
15
15
  "mcpServers": "./.mcp.json",
16
16
  "name": "superlocalmemory",
17
17
  "repository": "https://github.com/qualixar/superlocalmemory",
18
- "version": "3.8.8"
18
+ "version": "3.8.9"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.8.8 -->
1
+ <!-- BEGIN SuperLocalMemory v3.8.9 -->
2
2
 
3
3
  ## SuperLocalMemory (SLM) — Agent Rules
4
4
 
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
39
39
  ### Subagents
40
40
  slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
41
41
 
42
- <!-- END SuperLocalMemory v3.8.8 -->
42
+ <!-- END SuperLocalMemory v3.8.9 -->
43
43
 
44
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
77
77
  # What NOT to do
78
78
  Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
79
79
 
80
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
80
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -68,4 +68,4 @@ assessment. The gate is the authority.
68
68
 
69
69
  ---
70
70
 
71
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
71
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
46
46
  # What NOT to do
47
47
  Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
48
48
 
49
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
49
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
41
41
  # What NOT to do
42
42
  Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
43
43
 
44
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==3.8.8
1
+ superlocalmemory==3.8.9
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -245,4 +245,4 @@ Before running any destructive operation (`forget`, `compact_memories`):
245
245
 
246
246
  ---
247
247
 
248
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -311,4 +311,4 @@ profile. See `slm-profile` for the full profile switching workflow.
311
311
 
312
312
  ---
313
313
 
314
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -96,4 +96,4 @@ paused, name the approval needed; when errored, quote the short detail.
96
96
 
97
97
  ---
98
98
 
99
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -145,4 +145,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
145
145
 
146
146
  ---
147
147
 
148
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
148
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -236,4 +236,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
236
236
 
237
237
  ---
238
238
 
239
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -238,4 +238,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
238
238
 
239
239
  ---
240
240
 
241
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -173,4 +173,4 @@ to review the impact. See `slm-remember` for the full deletion discipline.
173
173
 
174
174
  ---
175
175
 
176
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -227,4 +227,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
227
227
 
228
228
  ---
229
229
 
230
- *SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.8.8"
3
+ version = "3.8.9"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
32
32
  os.environ["OMP_NUM_THREADS"] = "2"
33
33
  # ---------------------------------------------------------------------------
34
34
 
35
- __version__ = "3.8.8"
35
+ __version__ = "3.8.9"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -800,9 +800,32 @@ class EmbeddingService:
800
800
  client = self._get_http_client()
801
801
  last_error: Exception | None = None
802
802
  for attempt in range(max_retries):
803
+ from superlocalmemory.core.materialization_control import (
804
+ MaterializationDeferred,
805
+ )
806
+ from superlocalmemory.core.recall_gate import (
807
+ background_preempt_requested,
808
+ is_background_work,
809
+ )
810
+ if background_preempt_requested():
811
+ raise MaterializationDeferred(
812
+ "background embedding yielded to runtime transition"
813
+ )
803
814
  try:
804
- resp = client.post(endpoint, headers=headers, json=body)
815
+ request_kwargs = {"headers": headers, "json": body}
816
+ if is_background_work():
817
+ # Runtime reconfigure drains admitted operations in five
818
+ # seconds. A background remote read must leave enough
819
+ # scheduling margin to observe that transition and release
820
+ # its lease, while interactive recall keeps the provider's
821
+ # normal timeout budget.
822
+ request_kwargs["timeout"] = 3.5
823
+ resp = client.post(endpoint, **request_kwargs)
805
824
  resp.raise_for_status()
825
+ if background_preempt_requested():
826
+ raise MaterializationDeferred(
827
+ "background embedding yielded to runtime transition"
828
+ )
806
829
  data = resp.json()
807
830
  if "data" not in data or not isinstance(data["data"], list):
808
831
  raise ValueError(
@@ -818,6 +841,12 @@ class EmbeddingService:
818
841
  )
819
842
  return results
820
843
  except Exception as exc:
844
+ if isinstance(exc, MaterializationDeferred):
845
+ raise
846
+ if background_preempt_requested():
847
+ raise MaterializationDeferred(
848
+ "background embedding yielded to runtime transition"
849
+ ) from exc
821
850
  last_error = exc
822
851
  if attempt < max_retries - 1:
823
852
  time.sleep(2 ** attempt)
@@ -857,15 +886,39 @@ class EmbeddingService:
857
886
  client = self._get_http_client()
858
887
  last_error: Exception | None = None
859
888
  for attempt in range(max_retries):
889
+ from superlocalmemory.core.materialization_control import (
890
+ MaterializationDeferred,
891
+ )
892
+ from superlocalmemory.core.recall_gate import (
893
+ background_preempt_requested,
894
+ is_background_work,
895
+ )
896
+ if background_preempt_requested():
897
+ raise MaterializationDeferred(
898
+ "background embedding yielded to runtime transition"
899
+ )
860
900
  try:
861
- resp = client.post(url, headers=headers, json=body)
901
+ request_kwargs = {"headers": headers, "json": body}
902
+ if is_background_work():
903
+ request_kwargs["timeout"] = 3.5
904
+ resp = client.post(url, **request_kwargs)
862
905
  resp.raise_for_status()
906
+ if background_preempt_requested():
907
+ raise MaterializationDeferred(
908
+ "background embedding yielded to runtime transition"
909
+ )
863
910
  data = resp.json()
864
911
  results = []
865
912
  for item in sorted(data["data"], key=lambda d: d["index"]):
866
913
  results.append(item["embedding"])
867
914
  return results
868
915
  except Exception as exc:
916
+ if isinstance(exc, MaterializationDeferred):
917
+ raise
918
+ if background_preempt_requested():
919
+ raise MaterializationDeferred(
920
+ "background embedding yielded to runtime transition"
921
+ ) from exc
869
922
  last_error = exc
870
923
  if attempt < max_retries - 1:
871
924
  time.sleep(2 ** attempt)
@@ -736,6 +736,11 @@ def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
736
736
  materialization_checkpoint=checkpoint_materialization,
737
737
  )
738
738
  except Exception as exc:
739
+ from superlocalmemory.core.materialization_control import (
740
+ MaterializationDeferred,
741
+ )
742
+ if isinstance(exc, MaterializationDeferred):
743
+ raise
739
744
  # ``run_store`` checkpoints the completed relational pipeline
740
745
  # immediately before post-hooks run. Prefer that durable ledger
741
746
  # over rebuilding state from local variables: a one-time hook
@@ -22,6 +22,7 @@ from dataclasses import dataclass, field
22
22
  from enum import Enum
23
23
  from typing import Any, Callable
24
24
 
25
+ from superlocalmemory.core.materialization_control import MaterializationDeferred
25
26
  from superlocalmemory.storage.database import DatabaseManager
26
27
 
27
28
  logger = logging.getLogger("superlocalmemory.ingestion_command")
@@ -606,6 +607,31 @@ class IngestionOperationRepository:
606
607
  ) from exc
607
608
  return self._from_row(rows[0])
608
609
 
610
+ def defer_enriching(
611
+ self,
612
+ operation_id: str,
613
+ *,
614
+ owner: str,
615
+ ) -> IngestionOperation:
616
+ """Release a transition-preempted lease without consuming a retry.
617
+
618
+ Queryable evidence remains durable. The compare-and-swap owner check
619
+ prevents a stale worker from requeueing work that another process has
620
+ already reclaimed.
621
+ """
622
+ rows = self.db.execute(
623
+ "UPDATE ingestion_operations SET state='queryable', "
624
+ "lease_owner='', lease_expires_at=0, next_retry_at=0, "
625
+ "attempt_count=CASE WHEN attempt_count > 0 THEN attempt_count - 1 ELSE 0 END, "
626
+ "last_error='', updated_at=strftime('%Y-%m-%dT%H:%M:%fZ', 'now') "
627
+ "WHERE operation_id=? AND state='enriching' AND lease_owner=? "
628
+ "RETURNING *",
629
+ (operation_id, owner),
630
+ )
631
+ if not rows:
632
+ raise InvalidStateTransition("enriching lease ownership was lost")
633
+ return self._from_row(rows[0])
634
+
609
635
  def reap_stuck_enriching(
610
636
  self,
611
637
  *,
@@ -936,6 +962,11 @@ class IngestionCommand:
936
962
  )
937
963
  except LeaseLost:
938
964
  raise
965
+ except MaterializationDeferred:
966
+ return self.repository.defer_enriching(
967
+ operation_id,
968
+ owner=self._owner,
969
+ )
939
970
  except Exception as exc:
940
971
  return self.repository.finish_enriching(
941
972
  operation_id,
@@ -983,6 +1014,11 @@ class IngestionCommand:
983
1014
  )
984
1015
  except LeaseLost:
985
1016
  raise
1017
+ except MaterializationDeferred:
1018
+ return self.repository.defer_enriching(
1019
+ operation.operation_id,
1020
+ owner=self._owner,
1021
+ )
986
1022
  except Exception as exc:
987
1023
  return self.repository.finish_enriching(
988
1024
  operation.operation_id,
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3
4
+
5
+ """Control-flow signals for best-effort materialization work.
6
+
7
+ These exceptions deliberately live outside the ingestion state machine and
8
+ embedding implementation so either layer can request a durable deferral
9
+ without introducing an import cycle.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+
15
+ class MaterializationDeferred(RuntimeError):
16
+ """Best-effort enrichment yielded to a runtime transition.
17
+
18
+ The queryable projection is already durable. This signal must be handled
19
+ by :class:`IngestionCommand` as a requeue, never as a failed attempt.
20
+ """
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import threading
17
17
  from contextlib import contextmanager
18
- from typing import Iterator
18
+ from typing import Callable, Iterator
19
19
 
20
20
  _condition = threading.Condition(threading.Lock())
21
21
  _active = 0
@@ -42,19 +42,30 @@ def in_flight() -> int:
42
42
 
43
43
 
44
44
  @contextmanager
45
- def background_work() -> Iterator[None]:
45
+ def background_work(
46
+ *,
47
+ preempt_requested: Callable[[], bool] | None = None,
48
+ ) -> Iterator[None]:
46
49
  """Mark best-effort work that must yield shared inference to recall.
47
50
 
48
51
  The marker is thread-local because materialization, health probes, and
49
52
  interactive handlers all share one resident engine and one embedder.
50
- Nested callers restore the previous marker on exit.
53
+ Nested callers restore the previous marker on exit. A daemon-owned
54
+ materializer may also provide a preemption callback for a profile/runtime
55
+ reconfigure. Inference clients use that callback to cut a bounded
56
+ background request short instead of holding the transition drain lease.
51
57
  """
52
58
  previous = bool(getattr(_work_context, "background", False))
59
+ previous_preempt = getattr(_work_context, "preempt_requested", None)
53
60
  _work_context.background = True
61
+ _work_context.preempt_requested = (
62
+ preempt_requested if preempt_requested is not None else previous_preempt
63
+ )
54
64
  try:
55
65
  yield
56
66
  finally:
57
67
  _work_context.background = previous
68
+ _work_context.preempt_requested = previous_preempt
58
69
 
59
70
 
60
71
  def is_background_work() -> bool:
@@ -62,6 +73,19 @@ def is_background_work() -> bool:
62
73
  return bool(getattr(_work_context, "background", False))
63
74
 
64
75
 
76
+ def background_preempt_requested() -> bool:
77
+ """Return whether daemon-owned background work must release its lease."""
78
+ callback = getattr(_work_context, "preempt_requested", None)
79
+ if not callable(callback):
80
+ return False
81
+ try:
82
+ return bool(callback())
83
+ except Exception:
84
+ # A status read is advisory. It must not crash a materializer or turn
85
+ # a valid recall into an ingestion failure.
86
+ return False
87
+
88
+
65
89
  def wait_for_foreground_idle() -> None:
66
90
  """Block background inference while an interactive recall is active."""
67
91
  if not is_background_work():
@@ -34,6 +34,14 @@ logger = logging.getLogger(__name__)
34
34
  _INIT_LANGEVIN_RADIUS = 0.05
35
35
 
36
36
 
37
+ def _reraise_materialization_deferral(exc: Exception) -> None:
38
+ """Keep explicit runtime preemption out of best-effort fallbacks."""
39
+ from superlocalmemory.core.materialization_control import MaterializationDeferred
40
+
41
+ if isinstance(exc, MaterializationDeferred):
42
+ raise exc
43
+
44
+
37
45
  def _ingestion_effect_id(operation_id: str, *parts: object) -> str:
38
46
  """Return a stable ID for a relational effect owned by one ingestion."""
39
47
  if not operation_id:
@@ -247,6 +255,7 @@ def _upsert_fact_vectors(fact, profile_id, ann_index, vector_store, embedder=Non
247
255
  try:
248
256
  fact.embedding = embedder.embed(fact.content)
249
257
  except Exception as _emb_exc: # pragma: no cover - defensive
258
+ _reraise_materialization_deferral(_emb_exc)
250
259
  logger.debug("on-demand embed failed for %s: %s", fact.fact_id, _emb_exc)
251
260
  return
252
261
  if not getattr(fact, "embedding", None):
@@ -429,6 +438,7 @@ def run_store(
429
438
  )
430
439
  extraction_complete = facts is not None
431
440
  except Exception as _extract_exc:
441
+ _reraise_materialization_deferral(_extract_exc)
432
442
  # P0-1 (remember-write-04): an extractor EXCEPTION (transient LLM/embed
433
443
  # backend error) must NOT orphan the already-committed memory. The None
434
444
  # guard below only handled a None *return*, not a raise. Treat a raise
@@ -282,7 +282,6 @@ def register_core_tools(server, get_engine: Callable) -> None:
282
282
  import asyncio
283
283
  try:
284
284
  from superlocalmemory.mcp._daemon_proxy import choose_pool
285
- pool = choose_pool()
286
285
  # S9-DASH-10: priority for session_id, so engagement
287
286
  # signals land on the right pending_outcome:
288
287
  # 1. Explicit ``session_id`` tool-call argument.
@@ -322,15 +321,25 @@ def register_core_tools(server, get_engine: Callable) -> None:
322
321
  pass
323
322
  if not effective_sid:
324
323
  effective_sid = f"mcp:{agent_id}"
325
- # V3.3.19: Run in thread pool to avoid blocking MCP event loop.
324
+ # Resolve the daemon proxy inside the worker too. ``choose_pool``
325
+ # verifies daemon ownership through a synchronous /health request;
326
+ # when this tool is served by the daemon's mounted HTTP MCP app,
327
+ # resolving it on Uvicorn's event-loop thread makes that loop wait
328
+ # on its own health response forever. Stdio did not exhibit this
329
+ # because its MCP process is external to the daemon.
330
+ #
326
331
  # V3.4.26: WorkerPool now concurrent — parallel calls no longer
327
332
  # block behind a single threading.Lock. See worker_pool.py.
333
+ def _recall_via_daemon_pool():
334
+ pool = choose_pool()
335
+ return pool.recall(
336
+ query, limit=limit, session_id=effective_sid,
337
+ fast=fast, include_global=include_global,
338
+ include_shared=include_shared, window=window or None,
339
+ )
340
+
328
341
  result = await asyncio.to_thread(
329
- pool.recall, query, limit=limit, session_id=effective_sid,
330
- fast=fast,
331
- include_global=include_global,
332
- include_shared=include_shared,
333
- window=window or None,
342
+ _recall_via_daemon_pool,
334
343
  )
335
344
  if result.get("ok"):
336
345
  return {
@@ -4060,7 +4060,9 @@ def _run_materializer_operation(
4060
4060
  "resident engine does not match pending profile"
4061
4061
  )
4062
4062
  from superlocalmemory.core.recall_gate import background_work
4063
- with background_work():
4063
+ with background_work(
4064
+ preempt_requested=lambda: bool(runtime is not None and runtime.transitioning),
4065
+ ):
4064
4066
  return operation(engine)
4065
4067
 
4066
4068