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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/core/embeddings.py +55 -2
- package/src/superlocalmemory/core/engine_ingestion.py +5 -0
- package/src/superlocalmemory/core/ingestion_command.py +36 -0
- package/src/superlocalmemory/core/materialization_control.py +20 -0
- package/src/superlocalmemory/core/recall_gate.py +27 -3
- package/src/superlocalmemory/core/store_pipeline.py +10 -0
- package/src/superlocalmemory/mcp/tools_core.py +16 -7
- 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
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v3.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.
|
|
42
|
+
<!-- END SuperLocalMemory v3.8.9 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.8.
|
|
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.
|
|
80
|
+
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.
|
|
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.
|
|
44
|
+
SuperLocalMemory v3.8.9 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.8.
|
|
1
|
+
superlocalmemory==3.8.9
|
package/pyproject.toml
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|