superlocalmemory 3.8.6 → 3.8.8

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 (45) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +7 -4
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/core/backend_orchestrator.py +7 -1
  34. package/src/superlocalmemory/core/embeddings.py +73 -11
  35. package/src/superlocalmemory/core/engine.py +6 -1
  36. package/src/superlocalmemory/core/ollama_embedder.py +5 -0
  37. package/src/superlocalmemory/core/recall_gate.py +39 -4
  38. package/src/superlocalmemory/core/recall_pipeline.py +40 -0
  39. package/src/superlocalmemory/encoding/scene_builder.py +105 -15
  40. package/src/superlocalmemory/graph/cozo_backend.py +5 -5
  41. package/src/superlocalmemory/retrieval/entity_channel.py +201 -56
  42. package/src/superlocalmemory/retrieval/vector_store.py +238 -123
  43. package/src/superlocalmemory/server/recall_health.py +3 -1
  44. package/src/superlocalmemory/server/unified_daemon.py +104 -16
  45. package/src/superlocalmemory/storage/embedding_migrator.py +88 -60
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ 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.8] - 2026-07-27 — Live-database recall and vector integrity
9
+
10
+ ### Fixed
11
+ - Foreground recall now retains priority while startup repair and canonical
12
+ remember enrichment are active. Background embedding work yields between
13
+ items and cannot cold-start the shared local model ahead of a user query.
14
+ - Daemon readiness now verifies that the local embedding subprocess is
15
+ actually alive and has served a request instead of trusting a stale startup
16
+ flag.
17
+ - Scene clustering reuses durable fact embeddings and ignores legacy scene
18
+ rows whose facts were already consolidated away. Mature databases no longer
19
+ issue thousands of redundant model requests or repeatedly recycle the
20
+ embedding worker after restart.
21
+ - sqlite-vec row allocation is serialized across processes, and every metadata
22
+ pointer is validated against the vector payload's profile. Cross-profile
23
+ row-id collisions are repaired without exposing another profile's memory.
24
+ - Vector writes roll back abandoned transactions, missing vectors are repaired
25
+ exactly, and semantic search expands past legacy orphan payloads.
26
+ - Entity-cache startup reads are bounded and load only the columns used by the
27
+ entity channel.
28
+ - Exact lexical evidence remains protected after learned ranking so a freshly
29
+ remembered exact marker cannot be displaced by a weaker semantic match.
30
+
31
+ ### Notes
32
+ - Recall remains physically query-only; remember owns durable writes and
33
+ continues full canonical enrichment asynchronously after immediate queryable
34
+ admission.
35
+ - This candidate must pass the installed wheel, installed npm tarball,
36
+ dashboard, MCP, CLI, and mature live-database gates before publication.
37
+
38
+ ## [3.8.7] - 2026-07-27 — Existing graph-store compatibility
39
+
40
+ ### Fixed
41
+ - Upgrades now retain the PyCozo 0.3.0 native binding used to create existing
42
+ Cozo graph stores. Version 3.8.6 selected PyCozo 0.7.6, whose incompatible
43
+ on-disk format could stop the daemon at startup with
44
+ `Unknown storage version 1`.
45
+ - A native Cozo panic is now contained at the optional projection boundary.
46
+ The daemon remains available on canonical SQLite and preserves the graph
47
+ files unchanged instead of losing access to memory.
48
+
49
+ ### Notes
50
+ - Existing PyCozo 0.3.0 graphs require no rebuild or database migration.
51
+ - All serialized-write, read-only recall, admission-journal, and dead-letter
52
+ fixes from 3.8.6 are unchanged.
53
+
8
54
  ## [3.8.6] - 2026-07-27 — Serialized writes and read-only recall
9
55
 
10
56
  ### Fixed
package/README.md CHANGED
@@ -5,15 +5,15 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- <h1 align="center">SuperLocalMemory V3.8.6</h1>
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.6</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/>
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> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; 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.6-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v3.8.6 — Current Release"/></a>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.8.6",
3
+ "version": "3.8.8",
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.6"
18
+ "version": "3.8.8"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.8.6 -->
1
+ <!-- BEGIN SuperLocalMemory v3.8.8 -->
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.6 -->
42
+ <!-- END SuperLocalMemory v3.8.8 -->
43
43
 
44
- SuperLocalMemory v3.8.6 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
80
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
71
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
49
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==3.8.6
1
+ superlocalmemory==3.8.8
@@ -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.6 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v3.8.6 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
148
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
@@ -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.6 · Qualixar · AGPL-3.0-or-later
131
+ SuperLocalMemory v3.8.8 · Qualixar · AGPL-3.0-or-later
@@ -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.6 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.8 · 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.6 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.8 · 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.6"
3
+ version = "3.8.8"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -82,7 +82,10 @@ dependencies = [
82
82
  # until `slm db scale prepare -> verify -> promote` proves parity with
83
83
  # canonical SQLite; dependency installation never migrates user data.
84
84
  "lancedb==0.30.2",
85
- "pycozo[embedded]==0.7.6",
85
+ # Cozo's on-disk format is not forward-compatible across these native
86
+ # bindings. Existing SLM graph stores were created by 0.3.0; upgrading the
87
+ # wheel without an export/import migration makes daemon startup panic.
88
+ "pycozo[embedded]==0.3.0",
86
89
  # v3.6.10: LLMLingua-2 prose compression (aggressive mode, opt-in at runtime).
87
90
  # Hard dependency so `pip install superlocalmemory` ships compression-ready;
88
91
  # the ~560MB model downloads on first setup/warmup (fail-open). Verified the
@@ -121,13 +124,13 @@ lancedb = [
121
124
  "lancedb==0.30.2",
122
125
  ]
123
126
  cozo = [
124
- "pycozo[embedded]==0.7.6",
127
+ "pycozo[embedded]==0.3.0",
125
128
  ]
126
129
  # Backwards-compatible explicit install target for automation and docs. The
127
130
  # packages are also base dependencies from v3.7 onward.
128
131
  scale = [
129
132
  "lancedb==0.30.2",
130
- "pycozo[embedded]==0.7.6",
133
+ "pycozo[embedded]==0.3.0",
131
134
  ]
132
135
  ingestion = [
133
136
  "keyring>=25.0.0",
@@ -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.6"
35
+ __version__ = "3.8.8"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -438,7 +438,13 @@ class BackendOrchestrator:
438
438
  self._cozo = CozoDBGraphBackend(str(cozo_path / "graph"))
439
439
  self._update_status("cozo", "not_initialized")
440
440
  logger.info("CozoDB initialized at %s", cozo_path)
441
- except Exception as exc:
441
+ except BaseException as exc:
442
+ # PyO3 exposes Rust panics as PanicException(BaseException), not
443
+ # Exception. An incompatible optional projection must never abort
444
+ # daemon startup or hide canonical SQLite memory. Re-raise genuine
445
+ # process-control exceptions; preserve the graph and degrade Cozo.
446
+ if not isinstance(exc, Exception) and type(exc).__name__ != "PanicException":
447
+ raise
442
448
  logger.warning("CozoDB init failed: %s", exc)
443
449
  self._cozo = None
444
450
 
@@ -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
- vecs = self._openai_compatible_embed_batch([text])
296
- vec = vecs[0]
297
- self._validate_dimension(np.asarray(vec))
298
- return vec
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
- return self._cloud_embed_single(text)
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
- results = self._openai_compatible_embed_batch(texts)
314
- for vec in results:
315
- if vec is not None:
316
- self._validate_dimension(np.asarray(vec))
317
- return results
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
- return self._cloud_embed_batch(texts)
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"
@@ -637,7 +637,12 @@ class MemoryEngine:
637
637
  except (ValueError, TypeError):
638
638
  _timeout_s = 0.5 # default 500 ms
639
639
  try:
640
- _future = self._store_fast_embed_pool.submit(_embedder_ref.embed, fact_text)
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:
@@ -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,58 @@ recall.
14
14
  from __future__ import annotations
15
15
 
16
16
  import threading
17
+ from contextlib import contextmanager
18
+ from typing import Iterator
17
19
 
18
- _lock = threading.Lock()
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 _lock:
27
+ with _condition:
25
28
  _active += 1
26
29
 
27
30
 
28
31
  def end_recall() -> None:
29
32
  global _active
30
- with _lock:
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 _lock:
40
+ with _condition:
36
41
  return _active
42
+
43
+
44
+ @contextmanager
45
+ def background_work() -> Iterator[None]:
46
+ """Mark best-effort work that must yield shared inference to recall.
47
+
48
+ The marker is thread-local because materialization, health probes, and
49
+ interactive handlers all share one resident engine and one embedder.
50
+ Nested callers restore the previous marker on exit.
51
+ """
52
+ previous = bool(getattr(_work_context, "background", False))
53
+ _work_context.background = True
54
+ try:
55
+ yield
56
+ finally:
57
+ _work_context.background = previous
58
+
59
+
60
+ def is_background_work() -> bool:
61
+ """Return whether the current thread is running best-effort work."""
62
+ return bool(getattr(_work_context, "background", False))
63
+
64
+
65
+ def wait_for_foreground_idle() -> None:
66
+ """Block background inference while an interactive recall is active."""
67
+ if not is_background_work():
68
+ return
69
+ with _condition:
70
+ while _active > 0:
71
+ _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