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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ 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
|
+
|
|
23
|
+
## [3.8.8] - 2026-07-27 — Live-database recall and vector integrity
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Foreground recall now retains priority while startup repair and canonical
|
|
27
|
+
remember enrichment are active. Background embedding work yields between
|
|
28
|
+
items and cannot cold-start the shared local model ahead of a user query.
|
|
29
|
+
- Daemon readiness now verifies that the local embedding subprocess is
|
|
30
|
+
actually alive and has served a request instead of trusting a stale startup
|
|
31
|
+
flag.
|
|
32
|
+
- Scene clustering reuses durable fact embeddings and ignores legacy scene
|
|
33
|
+
rows whose facts were already consolidated away. Mature databases no longer
|
|
34
|
+
issue thousands of redundant model requests or repeatedly recycle the
|
|
35
|
+
embedding worker after restart.
|
|
36
|
+
- sqlite-vec row allocation is serialized across processes, and every metadata
|
|
37
|
+
pointer is validated against the vector payload's profile. Cross-profile
|
|
38
|
+
row-id collisions are repaired without exposing another profile's memory.
|
|
39
|
+
- Vector writes roll back abandoned transactions, missing vectors are repaired
|
|
40
|
+
exactly, and semantic search expands past legacy orphan payloads.
|
|
41
|
+
- Entity-cache startup reads are bounded and load only the columns used by the
|
|
42
|
+
entity channel.
|
|
43
|
+
- Exact lexical evidence remains protected after learned ranking so a freshly
|
|
44
|
+
remembered exact marker cannot be displaced by a weaker semantic match.
|
|
45
|
+
|
|
46
|
+
### Notes
|
|
47
|
+
- Recall remains physically query-only; remember owns durable writes and
|
|
48
|
+
continues full canonical enrichment asynchronously after immediate queryable
|
|
49
|
+
admission.
|
|
50
|
+
- This candidate must pass the installed wheel, installed npm tarball,
|
|
51
|
+
dashboard, MCP, CLI, and mature live-database gates before publication.
|
|
52
|
+
|
|
8
53
|
## [3.8.7] - 2026-07-27 — Existing graph-store compatibility
|
|
9
54
|
|
|
10
55
|
### Fixed
|
|
@@ -12,9 +57,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
57
|
Cozo graph stores. Version 3.8.6 selected PyCozo 0.7.6, whose incompatible
|
|
13
58
|
on-disk format could stop the daemon at startup with
|
|
14
59
|
`Unknown storage version 1`.
|
|
60
|
+
- A native Cozo panic is now contained at the optional projection boundary.
|
|
61
|
+
The daemon remains available on canonical SQLite and preserves the graph
|
|
62
|
+
files unchanged instead of losing access to memory.
|
|
15
63
|
|
|
16
64
|
### Notes
|
|
17
|
-
-
|
|
65
|
+
- Existing PyCozo 0.3.0 graphs require no rebuild or database migration.
|
|
18
66
|
- All serialized-write, read-only recall, admission-journal, and dead-letter
|
|
19
67
|
fixes from 3.8.6 are unchanged.
|
|
20
68
|
|
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V3.8.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V3.8.8</h1>
|
|
9
9
|
<p align="center"><strong>Enterprise-grade, local-first memory for AI agents and teams.</strong><br/>
|
|
10
10
|
<em>A persistent, auditable long-term brain for your agents that runs on your own infrastructure — with multi-workspace isolation, role-based access, and GDPR + EU AI Act governance controls built in.</em></p>
|
|
11
|
-
<p align="center"><code>v3.8.
|
|
11
|
+
<p align="center"><code>v3.8.8</code> — one control plane: auditable retrieval · multi-scope memory (personal / shared / global) · Cache · Compress · trusted-peer Mesh · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
12
12
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
13
13
|
<p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v3.8.
|
|
16
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v3.8.8-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v3.8.8 — Current Release"/></a>
|
|
17
17
|
<a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
|
|
18
18
|
<a href="#three-surfaces-proxy--mcp-tools--skill"><img src="https://img.shields.io/badge/Proxy_|_MCP_|_Skill-22c55e?style=for-the-badge" alt="Three Surfaces: Proxy, MCP Tools, Skill"/></a>
|
|
19
19
|
<a href="https://pypi.org/project/superlocalmemory/"><img src="https://img.shields.io/pypi/v/superlocalmemory?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"/></a>
|
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
|
|
@@ -128,4 +128,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
128
128
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
129
129
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
130
130
|
|
|
131
|
-
SuperLocalMemory v3.8.
|
|
131
|
+
SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
|
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",
|
|
@@ -220,6 +220,7 @@ class EmbeddingService:
|
|
|
220
220
|
self._owns_worker_lock = False
|
|
221
221
|
self._request_count: int = 0
|
|
222
222
|
self._http_client: object | None = None
|
|
223
|
+
self._remote_ready = False
|
|
223
224
|
|
|
224
225
|
# Register for atexit cleanup (prevent orphaned workers)
|
|
225
226
|
ref = weakref.ref(self, _live_embedding_services.discard)
|
|
@@ -246,6 +247,31 @@ class EmbeddingService:
|
|
|
246
247
|
return bool(self._config.api_endpoint and self._config.api_key)
|
|
247
248
|
return self._available
|
|
248
249
|
|
|
250
|
+
@property
|
|
251
|
+
def is_warm(self) -> bool:
|
|
252
|
+
"""Return whether the configured backend has served a request.
|
|
253
|
+
|
|
254
|
+
``is_available`` only means that the backend may be started. A local
|
|
255
|
+
sentence-transformers cold start can take minutes on Apple Silicon, so
|
|
256
|
+
background enrichment must not mistake availability for readiness and
|
|
257
|
+
occupy the only worker ahead of an interactive recall.
|
|
258
|
+
"""
|
|
259
|
+
config = getattr(self, "_config", None)
|
|
260
|
+
if config is not None and (
|
|
261
|
+
config.is_openai_compatible or config.is_cloud
|
|
262
|
+
):
|
|
263
|
+
return bool(
|
|
264
|
+
getattr(self, "_remote_ready", False)
|
|
265
|
+
and self.is_available
|
|
266
|
+
)
|
|
267
|
+
proc = getattr(self, "_worker_proc", None)
|
|
268
|
+
if proc is None or getattr(self, "_request_count", 0) <= 0:
|
|
269
|
+
return False
|
|
270
|
+
try:
|
|
271
|
+
return proc.poll() is None
|
|
272
|
+
except Exception:
|
|
273
|
+
return False
|
|
274
|
+
|
|
249
275
|
@property
|
|
250
276
|
def dimension(self) -> int:
|
|
251
277
|
return self._config.dimension
|
|
@@ -291,13 +317,27 @@ class EmbeddingService:
|
|
|
291
317
|
"""Embed a single text string. Returns list of floats or None."""
|
|
292
318
|
if not text or not text.strip():
|
|
293
319
|
raise ValueError("Cannot embed empty text")
|
|
320
|
+
from superlocalmemory.core.recall_gate import wait_for_foreground_idle
|
|
321
|
+
wait_for_foreground_idle()
|
|
294
322
|
if self._config.is_openai_compatible:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
323
|
+
try:
|
|
324
|
+
vecs = self._openai_compatible_embed_batch([text])
|
|
325
|
+
vec = vecs[0]
|
|
326
|
+
self._validate_dimension(np.asarray(vec))
|
|
327
|
+
self._remote_ready = True
|
|
328
|
+
return vec
|
|
329
|
+
except Exception:
|
|
330
|
+
self._remote_ready = False
|
|
331
|
+
raise
|
|
299
332
|
if self._config.is_cloud:
|
|
300
|
-
|
|
333
|
+
try:
|
|
334
|
+
vec = self._cloud_embed_single(text)
|
|
335
|
+
self._validate_dimension(np.asarray(vec))
|
|
336
|
+
self._remote_ready = True
|
|
337
|
+
return vec
|
|
338
|
+
except Exception:
|
|
339
|
+
self._remote_ready = False
|
|
340
|
+
raise
|
|
301
341
|
result = self._subprocess_embed([text])
|
|
302
342
|
if result is None:
|
|
303
343
|
return None
|
|
@@ -309,14 +349,34 @@ class EmbeddingService:
|
|
|
309
349
|
"""Embed a batch of texts."""
|
|
310
350
|
if not texts:
|
|
311
351
|
raise ValueError("Cannot embed empty batch")
|
|
352
|
+
from superlocalmemory.core.recall_gate import is_background_work
|
|
353
|
+
if is_background_work():
|
|
354
|
+
# A single large background batch can own the only local inference
|
|
355
|
+
# worker for tens of seconds. Slice it so a recall arriving after
|
|
356
|
+
# this call started gets priority before the next text.
|
|
357
|
+
return [self.embed(text) for text in texts]
|
|
312
358
|
if self._config.is_openai_compatible:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
359
|
+
try:
|
|
360
|
+
results = self._openai_compatible_embed_batch(texts)
|
|
361
|
+
for vec in results:
|
|
362
|
+
if vec is not None:
|
|
363
|
+
self._validate_dimension(np.asarray(vec))
|
|
364
|
+
self._remote_ready = any(vec is not None for vec in results)
|
|
365
|
+
return results
|
|
366
|
+
except Exception:
|
|
367
|
+
self._remote_ready = False
|
|
368
|
+
raise
|
|
318
369
|
if self._config.is_cloud:
|
|
319
|
-
|
|
370
|
+
try:
|
|
371
|
+
results = self._cloud_embed_batch(texts)
|
|
372
|
+
for vec in results:
|
|
373
|
+
if vec is not None:
|
|
374
|
+
self._validate_dimension(np.asarray(vec))
|
|
375
|
+
self._remote_ready = any(vec is not None for vec in results)
|
|
376
|
+
return results
|
|
377
|
+
except Exception:
|
|
378
|
+
self._remote_ready = False
|
|
379
|
+
raise
|
|
320
380
|
result = self._subprocess_embed(texts)
|
|
321
381
|
if result is None:
|
|
322
382
|
return [None] * len(texts)
|
|
@@ -353,6 +413,8 @@ class EmbeddingService:
|
|
|
353
413
|
Includes a timeout (_SUBPROCESS_RESPONSE_TIMEOUT seconds) so the CLI
|
|
354
414
|
never hangs indefinitely on cold model loads or network issues.
|
|
355
415
|
"""
|
|
416
|
+
from superlocalmemory.core.recall_gate import wait_for_foreground_idle
|
|
417
|
+
wait_for_foreground_idle()
|
|
356
418
|
with self._lock:
|
|
357
419
|
# Only an explicit terminal disable (``False``) short-circuits. A
|
|
358
420
|
# ``None`` availability is the recall-health self-heal's "re-probe"
|
|
@@ -738,9 +800,32 @@ class EmbeddingService:
|
|
|
738
800
|
client = self._get_http_client()
|
|
739
801
|
last_error: Exception | None = None
|
|
740
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
|
+
)
|
|
741
814
|
try:
|
|
742
|
-
|
|
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)
|
|
743
824
|
resp.raise_for_status()
|
|
825
|
+
if background_preempt_requested():
|
|
826
|
+
raise MaterializationDeferred(
|
|
827
|
+
"background embedding yielded to runtime transition"
|
|
828
|
+
)
|
|
744
829
|
data = resp.json()
|
|
745
830
|
if "data" not in data or not isinstance(data["data"], list):
|
|
746
831
|
raise ValueError(
|
|
@@ -756,6 +841,12 @@ class EmbeddingService:
|
|
|
756
841
|
)
|
|
757
842
|
return results
|
|
758
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
|
|
759
850
|
last_error = exc
|
|
760
851
|
if attempt < max_retries - 1:
|
|
761
852
|
time.sleep(2 ** attempt)
|
|
@@ -795,15 +886,39 @@ class EmbeddingService:
|
|
|
795
886
|
client = self._get_http_client()
|
|
796
887
|
last_error: Exception | None = None
|
|
797
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
|
+
)
|
|
798
900
|
try:
|
|
799
|
-
|
|
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)
|
|
800
905
|
resp.raise_for_status()
|
|
906
|
+
if background_preempt_requested():
|
|
907
|
+
raise MaterializationDeferred(
|
|
908
|
+
"background embedding yielded to runtime transition"
|
|
909
|
+
)
|
|
801
910
|
data = resp.json()
|
|
802
911
|
results = []
|
|
803
912
|
for item in sorted(data["data"], key=lambda d: d["index"]):
|
|
804
913
|
results.append(item["embedding"])
|
|
805
914
|
return results
|
|
806
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
|
|
807
922
|
last_error = exc
|
|
808
923
|
if attempt < max_retries - 1:
|
|
809
924
|
time.sleep(2 ** attempt)
|
|
@@ -637,7 +637,12 @@ class MemoryEngine:
|
|
|
637
637
|
except (ValueError, TypeError):
|
|
638
638
|
_timeout_s = 0.5 # default 500 ms
|
|
639
639
|
try:
|
|
640
|
-
|
|
640
|
+
def _best_effort_embed():
|
|
641
|
+
from superlocalmemory.core.recall_gate import background_work
|
|
642
|
+
with background_work():
|
|
643
|
+
return _embedder_ref.embed(fact_text)
|
|
644
|
+
|
|
645
|
+
_future = self._store_fast_embed_pool.submit(_best_effort_embed)
|
|
641
646
|
try:
|
|
642
647
|
emb = _future.result(timeout=_timeout_s)
|
|
643
648
|
if emb:
|
|
@@ -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
|
+
"""
|