superlocalmemory 3.8.7 → 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 +49 -1
- package/README.md +3 -3
- 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/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/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 +128 -13
- package/src/superlocalmemory/core/engine.py +6 -1
- 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/ollama_embedder.py +5 -0
- package/src/superlocalmemory/core/recall_gate.py +63 -4
- package/src/superlocalmemory/core/recall_pipeline.py +40 -0
- package/src/superlocalmemory/core/store_pipeline.py +10 -0
- package/src/superlocalmemory/encoding/scene_builder.py +105 -15
- package/src/superlocalmemory/mcp/tools_core.py +16 -7
- package/src/superlocalmemory/retrieval/entity_channel.py +201 -56
- package/src/superlocalmemory/retrieval/vector_store.py +238 -123
- package/src/superlocalmemory/server/recall_health.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +106 -16
- package/src/superlocalmemory/storage/embedding_migrator.py +88 -60
|
@@ -90,6 +90,8 @@ class OllamaEmbedder:
|
|
|
90
90
|
"""
|
|
91
91
|
if not text or not text.strip():
|
|
92
92
|
raise ValueError("Cannot embed empty text")
|
|
93
|
+
from superlocalmemory.core.recall_gate import wait_for_foreground_idle
|
|
94
|
+
wait_for_foreground_idle()
|
|
93
95
|
|
|
94
96
|
# V3.3.27: Check cache first
|
|
95
97
|
cache_key = text.strip()
|
|
@@ -119,6 +121,9 @@ class OllamaEmbedder:
|
|
|
119
121
|
"""
|
|
120
122
|
if not texts:
|
|
121
123
|
raise ValueError("Cannot embed empty batch")
|
|
124
|
+
from superlocalmemory.core.recall_gate import is_background_work
|
|
125
|
+
if is_background_work():
|
|
126
|
+
return [self.embed(text) for text in texts]
|
|
122
127
|
|
|
123
128
|
# V3.3.27: Split into cached and uncached
|
|
124
129
|
results: list[list[float] | None] = [None] * len(texts)
|
|
@@ -14,23 +14,82 @@ recall.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import threading
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from typing import Callable, Iterator
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
_condition = threading.Condition(threading.Lock())
|
|
19
21
|
_active = 0
|
|
22
|
+
_work_context = threading.local()
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def begin_recall() -> None:
|
|
23
26
|
global _active
|
|
24
|
-
with
|
|
27
|
+
with _condition:
|
|
25
28
|
_active += 1
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
def end_recall() -> None:
|
|
29
32
|
global _active
|
|
30
|
-
with
|
|
33
|
+
with _condition:
|
|
31
34
|
_active = max(0, _active - 1)
|
|
35
|
+
if _active == 0:
|
|
36
|
+
_condition.notify_all()
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
def in_flight() -> int:
|
|
35
|
-
with
|
|
40
|
+
with _condition:
|
|
36
41
|
return _active
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def background_work(
|
|
46
|
+
*,
|
|
47
|
+
preempt_requested: Callable[[], bool] | None = None,
|
|
48
|
+
) -> Iterator[None]:
|
|
49
|
+
"""Mark best-effort work that must yield shared inference to recall.
|
|
50
|
+
|
|
51
|
+
The marker is thread-local because materialization, health probes, and
|
|
52
|
+
interactive handlers all share one resident engine and one embedder.
|
|
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.
|
|
57
|
+
"""
|
|
58
|
+
previous = bool(getattr(_work_context, "background", False))
|
|
59
|
+
previous_preempt = getattr(_work_context, "preempt_requested", None)
|
|
60
|
+
_work_context.background = True
|
|
61
|
+
_work_context.preempt_requested = (
|
|
62
|
+
preempt_requested if preempt_requested is not None else previous_preempt
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
yield
|
|
66
|
+
finally:
|
|
67
|
+
_work_context.background = previous
|
|
68
|
+
_work_context.preempt_requested = previous_preempt
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_background_work() -> bool:
|
|
72
|
+
"""Return whether the current thread is running best-effort work."""
|
|
73
|
+
return bool(getattr(_work_context, "background", False))
|
|
74
|
+
|
|
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
|
+
|
|
89
|
+
def wait_for_foreground_idle() -> None:
|
|
90
|
+
"""Block background inference while an interactive recall is active."""
|
|
91
|
+
if not is_background_work():
|
|
92
|
+
return
|
|
93
|
+
with _condition:
|
|
94
|
+
while _active > 0:
|
|
95
|
+
_condition.wait(timeout=0.1)
|
|
@@ -100,6 +100,45 @@ def _apply_markers_to_response(response: RecallResponse) -> None:
|
|
|
100
100
|
r.marker = _emit_marker(r.fact.fact_id)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _preserve_exact_lexical_evidence(
|
|
104
|
+
response: RecallResponse,
|
|
105
|
+
query: str,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Keep a deterministic exact BM25 hit ahead of learned refinements.
|
|
108
|
+
|
|
109
|
+
Adaptive and bandit ranking are valuable for ambiguous candidates, but
|
|
110
|
+
they must not demote a fact containing the caller's exact query behind
|
|
111
|
+
semantically similar noise. This guard runs after every learned layer and
|
|
112
|
+
changes only ordering; it does not introduce or bypass evidence.
|
|
113
|
+
"""
|
|
114
|
+
normalized_query = " ".join(query.casefold().split())
|
|
115
|
+
if len(normalized_query) < 3 or len(response.results) < 2:
|
|
116
|
+
return
|
|
117
|
+
exact = [
|
|
118
|
+
result
|
|
119
|
+
for result in response.results
|
|
120
|
+
if (
|
|
121
|
+
float((result.channel_scores or {}).get("bm25", 0.0) or 0.0) > 0.0
|
|
122
|
+
and normalized_query
|
|
123
|
+
in " ".join(result.fact.content.casefold().split())
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
if not exact:
|
|
127
|
+
return
|
|
128
|
+
strongest = max(
|
|
129
|
+
exact,
|
|
130
|
+
key=lambda result: float(
|
|
131
|
+
(result.channel_scores or {}).get("bm25", 0.0) or 0.0,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
if response.results[0] is strongest:
|
|
135
|
+
return
|
|
136
|
+
response.results = [
|
|
137
|
+
strongest,
|
|
138
|
+
*(result for result in response.results if result is not strongest),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
103
142
|
# ---------------------------------------------------------------------------
|
|
104
143
|
# Stage 8 SB-1 — feed shadow_router from recall-settled signals.
|
|
105
144
|
#
|
|
@@ -854,6 +893,7 @@ def run_recall(
|
|
|
854
893
|
except Exception as exc:
|
|
855
894
|
logger.debug("Ranking pipeline skipped: %s", exc)
|
|
856
895
|
|
|
896
|
+
_preserve_exact_lexical_evidence(response, query)
|
|
857
897
|
_mark("learning+ranking")
|
|
858
898
|
# Deliberately no trust, Fisher, retention, lifecycle, popularity, or graph
|
|
859
899
|
# mutation here. Those state transitions require a separately authenticated
|
|
@@ -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
|
|
@@ -38,6 +38,8 @@ class SceneBuilder:
|
|
|
38
38
|
def __init__(self, db, embedder=None) -> None:
|
|
39
39
|
self._db = db
|
|
40
40
|
self._embedder = embedder
|
|
41
|
+
# Key by scene ID, never theme. Themes are deliberately non-unique,
|
|
42
|
+
# while eligibility and durable anchor membership are scene-specific.
|
|
41
43
|
self._scene_embeddings_cache: dict[str, list[float]] = {}
|
|
42
44
|
|
|
43
45
|
def assign_to_scene(
|
|
@@ -53,8 +55,12 @@ class SceneBuilder:
|
|
|
53
55
|
if self._embedder is None:
|
|
54
56
|
return self._create_scene(new_fact, profile_id)
|
|
55
57
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
+
# Canonical ingestion already embeds the fact before scene assignment.
|
|
59
|
+
# Reuse that vector so scene clustering does not issue a duplicate model
|
|
60
|
+
# request for every remembered fact.
|
|
61
|
+
fact_emb = new_fact.embedding
|
|
62
|
+
if fact_emb is None:
|
|
63
|
+
fact_emb = self._embedder.embed(new_fact.content)
|
|
58
64
|
|
|
59
65
|
# v3.4.38: Defensive None guard. embedder.embed() returns None when
|
|
60
66
|
# the embedding worker is unavailable (timeout, crash). Without this
|
|
@@ -69,30 +75,58 @@ class SceneBuilder:
|
|
|
69
75
|
if not scenes:
|
|
70
76
|
return self._create_scene(new_fact, profile_id)
|
|
71
77
|
|
|
78
|
+
live_scene_embeddings = self._load_live_scene_embeddings(profile_id)
|
|
79
|
+
live_scene_ids = set(live_scene_embeddings)
|
|
80
|
+
self._scene_embeddings_cache.update({
|
|
81
|
+
scene_id: embedding
|
|
82
|
+
for scene_id, embedding in live_scene_embeddings.items()
|
|
83
|
+
if embedding is not None
|
|
84
|
+
})
|
|
85
|
+
# Old consolidation/deletion paths left scene rows whose fact IDs no
|
|
86
|
+
# longer exist. They are not evidence and must not trigger thousands of
|
|
87
|
+
# replacement model calls after restart. A cache hit cannot prove that
|
|
88
|
+
# a scene still has a surviving fact, so eligibility is always derived
|
|
89
|
+
# from the current database state.
|
|
90
|
+
scenes = [
|
|
91
|
+
scene for scene in scenes if scene.scene_id in live_scene_ids
|
|
92
|
+
]
|
|
93
|
+
if not scenes:
|
|
94
|
+
return self._create_scene(new_fact, profile_id)
|
|
95
|
+
|
|
72
96
|
# Find best matching scene
|
|
73
97
|
best_scene: MemoryScene | None = None
|
|
74
98
|
best_sim = -1.0
|
|
75
99
|
|
|
76
|
-
#
|
|
100
|
+
# A scene's theme is derived from its first (anchor) fact. On daemon
|
|
101
|
+
# restart the in-memory cache is empty, but the anchor embeddings remain
|
|
102
|
+
# durable in atomic_facts. Prime from those vectors before calling the
|
|
103
|
+
# model; otherwise a mature database re-embeds thousands of themes and
|
|
104
|
+
# repeatedly recycles the shared foreground worker.
|
|
105
|
+
# V3.3.27: Batch-embed all still-uncached scene themes in ONE call.
|
|
77
106
|
# Previously: 200+ individual embed() calls per fact (30s on Mode B).
|
|
78
107
|
# Now: 1 batch call for all uncached themes, then cache hits for the rest.
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
uncached_scenes = [
|
|
109
|
+
scene for scene in scenes
|
|
110
|
+
if scene.scene_id not in self._scene_embeddings_cache
|
|
111
|
+
]
|
|
112
|
+
if uncached_scenes and hasattr(self._embedder, 'embed_batch'):
|
|
81
113
|
try:
|
|
82
|
-
batch_embs = self._embedder.embed_batch(
|
|
83
|
-
|
|
114
|
+
batch_embs = self._embedder.embed_batch(
|
|
115
|
+
[scene.theme for scene in uncached_scenes]
|
|
116
|
+
)
|
|
117
|
+
for scene, emb in zip(uncached_scenes, batch_embs):
|
|
84
118
|
if emb is not None:
|
|
85
|
-
self._scene_embeddings_cache[
|
|
119
|
+
self._scene_embeddings_cache[scene.scene_id] = emb
|
|
86
120
|
except Exception:
|
|
87
121
|
pass # Fall through to individual embeds below
|
|
88
122
|
|
|
89
123
|
for scene in scenes:
|
|
90
|
-
if scene.
|
|
91
|
-
theme_emb = self._scene_embeddings_cache[scene.
|
|
124
|
+
if scene.scene_id in self._scene_embeddings_cache:
|
|
125
|
+
theme_emb = self._scene_embeddings_cache[scene.scene_id]
|
|
92
126
|
else:
|
|
93
127
|
theme_emb = self._embedder.embed(scene.theme)
|
|
94
128
|
if theme_emb is not None:
|
|
95
|
-
self._scene_embeddings_cache[scene.
|
|
129
|
+
self._scene_embeddings_cache[scene.scene_id] = theme_emb
|
|
96
130
|
if theme_emb is None:
|
|
97
131
|
continue
|
|
98
132
|
sim = _cosine(fact_emb, theme_emb)
|
|
@@ -130,10 +164,6 @@ class SceneBuilder:
|
|
|
130
164
|
comparisons in assign_to_scene.
|
|
131
165
|
"""
|
|
132
166
|
theme = fact.content[:200]
|
|
133
|
-
# Pre-compute theme embedding for future comparisons
|
|
134
|
-
if self._embedder is not None:
|
|
135
|
-
self._scene_embeddings_cache[theme] = self._embedder.embed(theme)
|
|
136
|
-
|
|
137
167
|
scene = MemoryScene(
|
|
138
168
|
profile_id=profile_id,
|
|
139
169
|
theme=theme,
|
|
@@ -142,6 +172,14 @@ class SceneBuilder:
|
|
|
142
172
|
created_at=datetime.now(UTC).isoformat(),
|
|
143
173
|
last_updated=datetime.now(UTC).isoformat(),
|
|
144
174
|
)
|
|
175
|
+
# Pre-compute theme embedding for future comparisons. The canonical fact
|
|
176
|
+
# vector represents this exact theme and avoids a duplicate model call.
|
|
177
|
+
if self._embedder is not None:
|
|
178
|
+
theme_embedding = fact.embedding
|
|
179
|
+
if theme_embedding is None:
|
|
180
|
+
theme_embedding = self._embedder.embed(theme)
|
|
181
|
+
if theme_embedding is not None:
|
|
182
|
+
self._scene_embeddings_cache[scene.scene_id] = theme_embedding
|
|
145
183
|
self._save_scene(scene)
|
|
146
184
|
return scene
|
|
147
185
|
|
|
@@ -171,6 +209,58 @@ class SceneBuilder:
|
|
|
171
209
|
)
|
|
172
210
|
return [self._row_to_scene(dict(r)) for r in rows]
|
|
173
211
|
|
|
212
|
+
def _load_live_scene_embeddings(
|
|
213
|
+
self,
|
|
214
|
+
profile_id: str,
|
|
215
|
+
) -> dict[str, list[float] | None]:
|
|
216
|
+
"""Load one durable anchor embedding for every live scene.
|
|
217
|
+
|
|
218
|
+
``json_each`` resolves the first still-existing fact in each scene, so
|
|
219
|
+
scenes whose original anchor was consolidated away can still reuse a
|
|
220
|
+
surviving member. The result also identifies fully stale scene rows,
|
|
221
|
+
which are ignored by assignment instead of being re-embedded.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
rows = self._db.execute(
|
|
225
|
+
"""
|
|
226
|
+
WITH live_scene_facts AS (
|
|
227
|
+
SELECT
|
|
228
|
+
ms.scene_id,
|
|
229
|
+
ms.theme,
|
|
230
|
+
af.embedding,
|
|
231
|
+
ROW_NUMBER() OVER (
|
|
232
|
+
PARTITION BY ms.scene_id
|
|
233
|
+
ORDER BY CAST(member.key AS INTEGER)
|
|
234
|
+
) AS member_rank
|
|
235
|
+
FROM memory_scenes AS ms
|
|
236
|
+
JOIN json_each(ms.fact_ids_json) AS member
|
|
237
|
+
JOIN atomic_facts AS af
|
|
238
|
+
ON af.fact_id = member.value
|
|
239
|
+
AND af.profile_id = ms.profile_id
|
|
240
|
+
WHERE ms.profile_id = ?
|
|
241
|
+
)
|
|
242
|
+
SELECT scene_id, embedding
|
|
243
|
+
FROM live_scene_facts
|
|
244
|
+
WHERE member_rank = 1
|
|
245
|
+
""",
|
|
246
|
+
(profile_id,),
|
|
247
|
+
)
|
|
248
|
+
except Exception:
|
|
249
|
+
return {}
|
|
250
|
+
|
|
251
|
+
result: dict[str, list[float] | None] = {}
|
|
252
|
+
for row in rows:
|
|
253
|
+
data = dict(row)
|
|
254
|
+
raw_embedding = data.get("embedding")
|
|
255
|
+
embedding = None
|
|
256
|
+
if raw_embedding:
|
|
257
|
+
try:
|
|
258
|
+
embedding = json.loads(raw_embedding)
|
|
259
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
260
|
+
embedding = None
|
|
261
|
+
result[str(data["scene_id"])] = embedding
|
|
262
|
+
return result
|
|
263
|
+
|
|
174
264
|
def _save_scene(self, scene: MemoryScene) -> None:
|
|
175
265
|
"""Upsert scene to DB."""
|
|
176
266
|
self._db.execute(
|
|
@@ -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 {
|