superlocalmemory 3.8.1 → 3.8.3
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 +67 -0
- package/README.md +2 -2
- 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 +2 -2
- 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 +3 -5
- 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 +2 -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 +3 -5
- 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/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +360 -2
- package/src/superlocalmemory/cli/main.py +62 -3
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embeddings.py +14 -1
- package/src/superlocalmemory/core/engine.py +9 -5
- package/src/superlocalmemory/core/ingestion_command.py +36 -16
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +28 -0
- package/src/superlocalmemory/core/recall_pipeline.py +39 -3
- package/src/superlocalmemory/core/store_pipeline.py +42 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +17 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- package/src/superlocalmemory/server/routes/behavioral.py +6 -2
- package/src/superlocalmemory/server/routes/learning.py +13 -3
- package/src/superlocalmemory/server/routes/memories.py +80 -26
- package/src/superlocalmemory/server/routes/v3_api.py +120 -0
- package/src/superlocalmemory/server/unified_daemon.py +349 -10
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- package/src/superlocalmemory/ui/index.html +3 -2
- package/src/superlocalmemory/ui/js/core.js +6 -1
- package/src/superlocalmemory/ui/js/od-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +43 -0
- package/src/superlocalmemory/ui/js/od-graph.js +35 -0
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +37 -0
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +72 -3
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Central component / dependency capability registry (v3.8.2).
|
|
6
|
+
|
|
7
|
+
Single source of truth for "what SLM needs, and whether it is present" —
|
|
8
|
+
consumed by the self-heal thread (``server.unified_daemon._self_heal``),
|
|
9
|
+
``slm doctor``, the ``GET /api/v3/components`` route, and the dashboard
|
|
10
|
+
System-Health panel.
|
|
11
|
+
|
|
12
|
+
Design principles
|
|
13
|
+
-----------------
|
|
14
|
+
* **Cheap, side-effect-free probes.** Probing NEVER loads torch into this
|
|
15
|
+
process and NEVER downloads anything: it uses import-spec checks, a
|
|
16
|
+
HuggingFace-cache lookup, and a short Ollama HTTP ping. Repair (model
|
|
17
|
+
download / pip install) is the self-heal thread's job — see
|
|
18
|
+
``core.component_healer`` and ``unified_daemon._self_heal`` Step 0/0.5.
|
|
19
|
+
* **Honest, actionable reporting.** Every component carries a status plus,
|
|
20
|
+
when missing, a plain-language fix command and whether SLM can auto-fix
|
|
21
|
+
it (``auto_fixable``). A non-technical user reads the report; the daemon
|
|
22
|
+
acts on ``auto_fixable`` items on their behalf.
|
|
23
|
+
* **Live repair visibility.** A tiny thread-safe transient overlay lets the
|
|
24
|
+
self-heal thread mark a component ``retrying`` while a download/install is
|
|
25
|
+
in flight, so the dashboard shows progress. Overlays are advisory and are
|
|
26
|
+
cleared as soon as a fresh probe confirms the component is present.
|
|
27
|
+
* **Immutability.** :class:`Component` is a frozen dataclass; snapshots
|
|
28
|
+
return freshly-built lists/dicts and are never mutated in place.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import importlib.util
|
|
34
|
+
import threading
|
|
35
|
+
import time
|
|
36
|
+
from dataclasses import dataclass, replace
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------
|
|
41
|
+
# Status + category vocabulary
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
STATUS_OK = "ok" # present and usable
|
|
45
|
+
STATUS_MISSING = "missing" # absent — recall/feature degraded until fixed
|
|
46
|
+
STATUS_DEGRADED = "degraded" # present but not fully functional / unverifiable
|
|
47
|
+
STATUS_RETRYING = "retrying" # self-heal is actively downloading/installing
|
|
48
|
+
|
|
49
|
+
CATEGORY_REQUIRED = "required" # core recall breaks without it
|
|
50
|
+
CATEGORY_RECOMMENDED = "recommended" # semantic quality degrades without it
|
|
51
|
+
CATEGORY_OPTIONAL = "optional" # only used by an opt-in feature
|
|
52
|
+
|
|
53
|
+
# Model repo ids — kept in sync with cli.setup_wizard (single definition here
|
|
54
|
+
# would be ideal, but importing the wizard pulls its CLI surface; these three
|
|
55
|
+
# strings are stable and asserted equal by tests).
|
|
56
|
+
_EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5"
|
|
57
|
+
_RERANKER_MODEL = "cross-encoder/ms-marco-MiniLM-L-12-v2"
|
|
58
|
+
_COMPRESSOR_MODEL = "microsoft/llmlingua-2-xlm-roberta-large-meetingbank"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Component:
|
|
63
|
+
"""One capability SLM depends on, and its current health.
|
|
64
|
+
|
|
65
|
+
Frozen: probes build new instances; the transient overlay uses
|
|
66
|
+
:func:`dataclasses.replace` to derive a modified copy rather than
|
|
67
|
+
mutating shared state.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
key: str
|
|
71
|
+
label: str
|
|
72
|
+
category: str
|
|
73
|
+
status: str
|
|
74
|
+
detail: str = ""
|
|
75
|
+
fix_cmd: str = ""
|
|
76
|
+
auto_fixable: bool = False
|
|
77
|
+
last_checked: float = 0.0
|
|
78
|
+
|
|
79
|
+
def as_dict(self) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"key": self.key,
|
|
82
|
+
"label": self.label,
|
|
83
|
+
"category": self.category,
|
|
84
|
+
"status": self.status,
|
|
85
|
+
"detail": self.detail,
|
|
86
|
+
"fix_cmd": self.fix_cmd,
|
|
87
|
+
"auto_fixable": self.auto_fixable,
|
|
88
|
+
"last_checked": self.last_checked,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------------
|
|
93
|
+
# Environment helpers
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def pip_is_user_writable() -> bool:
|
|
97
|
+
"""True when a plain ``pip install`` into this interpreter is expected to
|
|
98
|
+
succeed — i.e. the interpreter is NOT PEP-668 externally-managed.
|
|
99
|
+
|
|
100
|
+
Mirrors the doctor's PEP-668 check. Used to decide whether SLM may
|
|
101
|
+
auto-``pip install`` a pure-python component (sqlite-vec) on the user's
|
|
102
|
+
behalf, or must instead hand them a pipx/uv fix command.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
import sysconfig
|
|
106
|
+
|
|
107
|
+
stdlib = sysconfig.get_path("stdlib")
|
|
108
|
+
if not stdlib:
|
|
109
|
+
return True
|
|
110
|
+
return not (Path(stdlib) / "EXTERNALLY-MANAGED").exists()
|
|
111
|
+
except Exception:
|
|
112
|
+
# Unknown → be conservative and do NOT auto-install.
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _module_present(module: str) -> bool:
|
|
117
|
+
"""True if ``module`` can be imported, WITHOUT importing it.
|
|
118
|
+
|
|
119
|
+
``find_spec`` only resolves the loader; it never executes the module, so
|
|
120
|
+
this stays cheap and never pulls torch/lancedb/etc. into the daemon.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
return importlib.util.find_spec(module) is not None
|
|
124
|
+
except Exception:
|
|
125
|
+
# A broken/partial install can raise inside find_spec — treat as absent.
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _hf_model_cached(repo_id: str) -> bool | None:
|
|
130
|
+
"""Heuristic: is ``repo_id`` already in the local HuggingFace cache?
|
|
131
|
+
|
|
132
|
+
Returns True/False, or None when it cannot be determined (huggingface_hub
|
|
133
|
+
not installed). Checks for the model's ``config.json`` — present for every
|
|
134
|
+
HF model — without loading torch/sentence-transformers. This is a fast
|
|
135
|
+
presence heuristic; the authoritative load-test lives in the healer's real
|
|
136
|
+
download call, which no-ops when the model is genuinely present.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
from huggingface_hub import try_to_load_from_cache
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
hit = try_to_load_from_cache(repo_id, "config.json")
|
|
144
|
+
except Exception:
|
|
145
|
+
return None
|
|
146
|
+
# try_to_load_from_cache returns a filesystem path (str) only when the file
|
|
147
|
+
# is cached; None when unknown, or a _CACHED_NO_EXIST sentinel object when
|
|
148
|
+
# known-absent. Testing for str is version-proof across huggingface_hub
|
|
149
|
+
# releases (the sentinel's import location has moved between versions).
|
|
150
|
+
return isinstance(hit, str)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _embedding_is_remote(config: Any) -> bool:
|
|
154
|
+
"""True when embeddings come from a remote endpoint (no local model needed).
|
|
155
|
+
|
|
156
|
+
Delegates to the wizard's canonical detector; conservative False on any
|
|
157
|
+
import/attribute error so a local embedder is still probed.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
from superlocalmemory.cli.setup_wizard import _embedding_is_remote as _r
|
|
161
|
+
|
|
162
|
+
return bool(_r(config))
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------------------------------
|
|
168
|
+
# Individual probes — each returns a Component (never raises)
|
|
169
|
+
# --------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def probe_python() -> Component:
|
|
172
|
+
import sys
|
|
173
|
+
|
|
174
|
+
v = sys.version_info
|
|
175
|
+
ok = v >= (3, 11)
|
|
176
|
+
return Component(
|
|
177
|
+
key="python",
|
|
178
|
+
label="Python runtime",
|
|
179
|
+
category=CATEGORY_REQUIRED,
|
|
180
|
+
status=STATUS_OK if ok else STATUS_MISSING,
|
|
181
|
+
detail=f"{v.major}.{v.minor}.{v.micro}" + ("" if ok else " (need >= 3.11)"),
|
|
182
|
+
fix_cmd="" if ok else "Install Python 3.11+ from https://python.org/downloads/",
|
|
183
|
+
auto_fixable=False,
|
|
184
|
+
last_checked=time.time(),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def probe_search_deps() -> Component:
|
|
189
|
+
"""sentence-transformers / torch / scikit-learn — semantic recall channel."""
|
|
190
|
+
missing = [m for m in ("sentence_transformers", "torch", "sklearn")
|
|
191
|
+
if not _module_present(m)]
|
|
192
|
+
ok = not missing
|
|
193
|
+
return Component(
|
|
194
|
+
key="search_deps",
|
|
195
|
+
label="Semantic search dependencies",
|
|
196
|
+
category=CATEGORY_RECOMMENDED,
|
|
197
|
+
status=STATUS_OK if ok else STATUS_MISSING,
|
|
198
|
+
detail="sentence-transformers, torch, scikit-learn"
|
|
199
|
+
if ok else "missing: " + ", ".join(missing),
|
|
200
|
+
fix_cmd="" if ok else "pip install 'superlocalmemory[search]'",
|
|
201
|
+
# NOT background-auto-installed: torch is ~2GB — a silent multi-GB
|
|
202
|
+
# download is a surprise the user must consent to. Fix command guides
|
|
203
|
+
# them; the daemon does not pull it unasked.
|
|
204
|
+
auto_fixable=False,
|
|
205
|
+
last_checked=time.time(),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _probe_hf_model(key: str, label: str, repo_id: str, *,
|
|
210
|
+
category: str, auto_fixable: bool,
|
|
211
|
+
fix_cmd: str) -> Component:
|
|
212
|
+
cached = _hf_model_cached(repo_id)
|
|
213
|
+
if cached is True:
|
|
214
|
+
status, detail, fix = STATUS_OK, repo_id, ""
|
|
215
|
+
elif cached is False:
|
|
216
|
+
status, detail, fix = STATUS_MISSING, f"{repo_id} not in local cache", fix_cmd
|
|
217
|
+
else:
|
|
218
|
+
# huggingface_hub unavailable → can't verify; report degraded, not a
|
|
219
|
+
# false "missing" alarm.
|
|
220
|
+
status, detail, fix = (
|
|
221
|
+
STATUS_DEGRADED, "cannot verify (huggingface_hub unavailable)", "",
|
|
222
|
+
)
|
|
223
|
+
# Downloading model weights only helps when the runtime that loads them
|
|
224
|
+
# (sentence-transformers/torch) is already present. If search deps are
|
|
225
|
+
# missing, the fix is to install those first — not to pull weights.
|
|
226
|
+
can_auto = (
|
|
227
|
+
auto_fixable
|
|
228
|
+
and status == STATUS_MISSING
|
|
229
|
+
and _module_present("sentence_transformers")
|
|
230
|
+
)
|
|
231
|
+
return Component(
|
|
232
|
+
key=key, label=label, category=category,
|
|
233
|
+
status=status, detail=detail, fix_cmd=fix,
|
|
234
|
+
auto_fixable=can_auto,
|
|
235
|
+
last_checked=time.time(),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def probe_embedder_model(config: Any = None) -> Component:
|
|
240
|
+
if config is not None and _embedding_is_remote(config):
|
|
241
|
+
return Component(
|
|
242
|
+
key="embedder_model", label="Embedding model",
|
|
243
|
+
category=CATEGORY_REQUIRED, status=STATUS_OK,
|
|
244
|
+
detail="remote embedding endpoint (no local model required)",
|
|
245
|
+
last_checked=time.time(),
|
|
246
|
+
)
|
|
247
|
+
return _probe_hf_model(
|
|
248
|
+
"embedder_model", "Embedding model", _EMBED_MODEL,
|
|
249
|
+
category=CATEGORY_REQUIRED, auto_fixable=True,
|
|
250
|
+
fix_cmd="slm doctor --fix (or: slm warmup)",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def probe_reranker_model(config: Any = None) -> Component:
|
|
255
|
+
# Only auto-heal / flag the reranker when the user actually has it enabled.
|
|
256
|
+
enabled = True
|
|
257
|
+
try:
|
|
258
|
+
enabled = bool(getattr(config.retrieval, "use_cross_encoder", True)) \
|
|
259
|
+
if config is not None else True
|
|
260
|
+
except Exception:
|
|
261
|
+
enabled = True
|
|
262
|
+
comp = _probe_hf_model(
|
|
263
|
+
"reranker_model", "Reranker model", _RERANKER_MODEL,
|
|
264
|
+
category=CATEGORY_RECOMMENDED if enabled else CATEGORY_OPTIONAL,
|
|
265
|
+
auto_fixable=enabled,
|
|
266
|
+
fix_cmd="slm doctor --fix",
|
|
267
|
+
)
|
|
268
|
+
if not enabled and comp.status == STATUS_MISSING:
|
|
269
|
+
# Not enabled → absent is expected, not a problem.
|
|
270
|
+
return replace(comp, status=STATUS_OK,
|
|
271
|
+
detail="disabled (retrieval.use_cross_encoder=false)",
|
|
272
|
+
fix_cmd="", auto_fixable=False)
|
|
273
|
+
return comp
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def probe_compressor_model() -> Component:
|
|
277
|
+
"""LLMLingua-2 prose compressor (~560MB).
|
|
278
|
+
|
|
279
|
+
NEVER auto-downloaded by self-heal: it is large and only used by opt-in
|
|
280
|
+
aggressive optimize compression, where it lazy-downloads on first real
|
|
281
|
+
use. Reported as optional so the dashboard shows it without alarm.
|
|
282
|
+
"""
|
|
283
|
+
comp = _probe_hf_model(
|
|
284
|
+
"compressor_model", "Prose compression model", _COMPRESSOR_MODEL,
|
|
285
|
+
category=CATEGORY_OPTIONAL, auto_fixable=False,
|
|
286
|
+
fix_cmd="Downloads automatically on first use of aggressive compression",
|
|
287
|
+
)
|
|
288
|
+
if comp.status == STATUS_MISSING:
|
|
289
|
+
return replace(comp, detail=comp.detail + " (lazy — downloads on first use)")
|
|
290
|
+
return comp
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def probe_sqlite_vec() -> Component:
|
|
294
|
+
present = _module_present("sqlite_vec")
|
|
295
|
+
writable = pip_is_user_writable()
|
|
296
|
+
if present:
|
|
297
|
+
status, detail, fix, auto = STATUS_OK, "sqlite-vec installed", "", False
|
|
298
|
+
elif writable:
|
|
299
|
+
status, detail, fix, auto = (
|
|
300
|
+
STATUS_MISSING, "sqlite-vec not installed (vector index)",
|
|
301
|
+
"slm doctor --fix (or: pip install sqlite-vec)", True,
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
status, detail, fix, auto = (
|
|
305
|
+
STATUS_MISSING, "sqlite-vec not installed; interpreter externally managed",
|
|
306
|
+
"pipx inject superlocalmemory sqlite-vec (or use a uv/venv install)",
|
|
307
|
+
False,
|
|
308
|
+
)
|
|
309
|
+
return Component(
|
|
310
|
+
key="sqlite_vec", label="Vector index (sqlite-vec)",
|
|
311
|
+
category=CATEGORY_RECOMMENDED, status=status, detail=detail,
|
|
312
|
+
fix_cmd=fix, auto_fixable=auto, last_checked=time.time(),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _probe_optional_pkg(key: str, label: str, module: str, pip_name: str,
|
|
317
|
+
note: str) -> Component:
|
|
318
|
+
present = _module_present(module)
|
|
319
|
+
return Component(
|
|
320
|
+
key=key, label=label, category=CATEGORY_OPTIONAL,
|
|
321
|
+
status=STATUS_OK if present else STATUS_MISSING,
|
|
322
|
+
detail=f"{pip_name} installed" if present else f"{note}",
|
|
323
|
+
fix_cmd="" if present else f"pip install {pip_name}",
|
|
324
|
+
auto_fixable=False, # opt-in scale/feature backends — staged consent.
|
|
325
|
+
last_checked=time.time(),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def probe_lancedb() -> Component:
|
|
330
|
+
return _probe_optional_pkg(
|
|
331
|
+
"lancedb", "LanceDB scale backend", "lancedb", "lancedb",
|
|
332
|
+
"not installed (optional large-scale vector backend)",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def probe_cozo() -> Component:
|
|
337
|
+
return _probe_optional_pkg(
|
|
338
|
+
"cozo", "CozoDB graph backend", "pycozo", "pycozo",
|
|
339
|
+
"not installed (optional large-scale graph backend)",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def probe_llmlingua() -> Component:
|
|
344
|
+
return _probe_optional_pkg(
|
|
345
|
+
"llmlingua", "LLMLingua compressor lib", "llmlingua", "llmlingua",
|
|
346
|
+
"not installed (optional prose compression)",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def probe_ollama(config: Any = None) -> Component:
|
|
351
|
+
"""Ollama reachability — only meaningful in Mode B.
|
|
352
|
+
|
|
353
|
+
Never auto-fixed: installing the Ollama binary or pulling a model is a
|
|
354
|
+
surprise network/disk action, so it is always a manual fix command.
|
|
355
|
+
"""
|
|
356
|
+
mode = ""
|
|
357
|
+
api_base = "http://localhost:11434"
|
|
358
|
+
model = ""
|
|
359
|
+
try:
|
|
360
|
+
if config is not None:
|
|
361
|
+
mode = getattr(getattr(config, "mode", None), "value", "") or ""
|
|
362
|
+
api_base = getattr(getattr(config, "llm", None), "api_base", api_base) or api_base
|
|
363
|
+
model = getattr(getattr(config, "llm", None), "model", "") or ""
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
if mode != "b":
|
|
367
|
+
return Component(
|
|
368
|
+
key="ollama", label="Ollama (local LLM)",
|
|
369
|
+
category=CATEGORY_OPTIONAL, status=STATUS_OK,
|
|
370
|
+
detail="not in use (Mode B only)", last_checked=time.time(),
|
|
371
|
+
)
|
|
372
|
+
try:
|
|
373
|
+
import httpx
|
|
374
|
+
|
|
375
|
+
resp = httpx.get(f"{api_base}/api/tags", timeout=5.0)
|
|
376
|
+
if resp.status_code == 200:
|
|
377
|
+
names = [m.get("name", "").split(":")[0]
|
|
378
|
+
for m in resp.json().get("models", [])]
|
|
379
|
+
if model and model.split(":")[0] not in names:
|
|
380
|
+
return Component(
|
|
381
|
+
key="ollama", label="Ollama (local LLM)",
|
|
382
|
+
category=CATEGORY_RECOMMENDED, status=STATUS_DEGRADED,
|
|
383
|
+
detail=f"running but '{model}' not pulled",
|
|
384
|
+
fix_cmd=f"ollama pull {model}", auto_fixable=False,
|
|
385
|
+
last_checked=time.time(),
|
|
386
|
+
)
|
|
387
|
+
return Component(
|
|
388
|
+
key="ollama", label="Ollama (local LLM)",
|
|
389
|
+
category=CATEGORY_RECOMMENDED, status=STATUS_OK,
|
|
390
|
+
detail=f"running, {len(names)} models", last_checked=time.time(),
|
|
391
|
+
)
|
|
392
|
+
status_detail = f"HTTP {resp.status_code}"
|
|
393
|
+
except Exception:
|
|
394
|
+
status_detail = f"not reachable at {api_base}"
|
|
395
|
+
return Component(
|
|
396
|
+
key="ollama", label="Ollama (local LLM)",
|
|
397
|
+
category=CATEGORY_RECOMMENDED, status=STATUS_MISSING,
|
|
398
|
+
detail=status_detail,
|
|
399
|
+
fix_cmd="Install/start Ollama: https://ollama.com then: brew services start ollama",
|
|
400
|
+
auto_fixable=False, last_checked=time.time(),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# --------------------------------------------------------------------------
|
|
405
|
+
# Transient overlay — live self-heal progress
|
|
406
|
+
# --------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
_transient_lock = threading.Lock()
|
|
409
|
+
_transient: dict[str, tuple[str, str]] = {} # key -> (status, detail)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def mark_transient(key: str, status: str, detail: str = "") -> None:
|
|
413
|
+
"""Mark a component's live repair state (e.g. RETRYING while downloading)."""
|
|
414
|
+
with _transient_lock:
|
|
415
|
+
_transient[key] = (status, detail)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def clear_transient(key: str) -> None:
|
|
419
|
+
with _transient_lock:
|
|
420
|
+
_transient.pop(key, None)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _apply_transient(comp: Component) -> Component:
|
|
424
|
+
with _transient_lock:
|
|
425
|
+
override = _transient.get(comp.key)
|
|
426
|
+
if override is None:
|
|
427
|
+
return comp
|
|
428
|
+
status, detail = override
|
|
429
|
+
# A confirmed-present component supersedes a stale "retrying" marker.
|
|
430
|
+
if comp.status == STATUS_OK:
|
|
431
|
+
return comp
|
|
432
|
+
return replace(comp, status=status, detail=detail or comp.detail)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# --------------------------------------------------------------------------
|
|
436
|
+
# Aggregation
|
|
437
|
+
# --------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def probe_all(config: Any = None) -> list[Component]:
|
|
440
|
+
"""Probe every component once and return a fresh list (transient overlaid)."""
|
|
441
|
+
probes = [
|
|
442
|
+
probe_python(),
|
|
443
|
+
probe_search_deps(),
|
|
444
|
+
probe_embedder_model(config),
|
|
445
|
+
probe_reranker_model(config),
|
|
446
|
+
probe_compressor_model(),
|
|
447
|
+
probe_sqlite_vec(),
|
|
448
|
+
probe_llmlingua(),
|
|
449
|
+
probe_lancedb(),
|
|
450
|
+
probe_cozo(),
|
|
451
|
+
probe_ollama(config),
|
|
452
|
+
]
|
|
453
|
+
return [_apply_transient(c) for c in probes]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def auto_fixable_missing(config: Any = None) -> list[Component]:
|
|
457
|
+
"""The subset SLM may repair without asking — drives self-heal + doctor --fix."""
|
|
458
|
+
return [c for c in probe_all(config)
|
|
459
|
+
if c.status == STATUS_MISSING and c.auto_fixable]
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def snapshot(config: Any = None) -> dict[str, Any]:
|
|
463
|
+
"""Full registry snapshot for /api/v3/components, doctor --json, dashboard."""
|
|
464
|
+
comps = probe_all(config)
|
|
465
|
+
counts = {STATUS_OK: 0, STATUS_MISSING: 0,
|
|
466
|
+
STATUS_DEGRADED: 0, STATUS_RETRYING: 0}
|
|
467
|
+
for c in comps:
|
|
468
|
+
counts[c.status] = counts.get(c.status, 0) + 1
|
|
469
|
+
missing_needed = [c for c in comps
|
|
470
|
+
if c.status == STATUS_MISSING
|
|
471
|
+
and c.category in (CATEGORY_REQUIRED, CATEGORY_RECOMMENDED)]
|
|
472
|
+
healthy = not missing_needed and counts[STATUS_RETRYING] == 0
|
|
473
|
+
return {
|
|
474
|
+
"components": [c.as_dict() for c in comps],
|
|
475
|
+
"summary": {
|
|
476
|
+
"ok": counts[STATUS_OK],
|
|
477
|
+
"missing": counts[STATUS_MISSING],
|
|
478
|
+
"degraded": counts[STATUS_DEGRADED],
|
|
479
|
+
"retrying": counts[STATUS_RETRYING],
|
|
480
|
+
"auto_fixable_missing": sum(
|
|
481
|
+
1 for c in comps
|
|
482
|
+
if c.status == STATUS_MISSING and c.auto_fixable
|
|
483
|
+
),
|
|
484
|
+
"healthy": healthy,
|
|
485
|
+
},
|
|
486
|
+
"generated_at": time.time(),
|
|
487
|
+
}
|
|
@@ -273,6 +273,27 @@ class RetrievalConfig:
|
|
|
273
273
|
agentic_max_rounds: int = 3
|
|
274
274
|
agentic_confidence_threshold: float = 0.3
|
|
275
275
|
|
|
276
|
+
# v3.8.2: Client-driven agentic (the recall spine's global flag).
|
|
277
|
+
# The agent hot path (CLI / MCP / plugins) is consumed by a frontier
|
|
278
|
+
# LLM (Claude Code, Copilot, Codex, …) that reformulates multi-hop /
|
|
279
|
+
# low-confidence queries far better than the local Ollama model. So the
|
|
280
|
+
# hot path DELEGATES the agentic reformulation loop to that calling LLM:
|
|
281
|
+
# it returns fast local retrieval (all six channels + reranker) plus the
|
|
282
|
+
# confidence signals (no_confident_match / answer_confidence / abstained),
|
|
283
|
+
# and never spends an internal LLM round unless a caller asks for it.
|
|
284
|
+
# True (default) → hot path skips the internal agentic round
|
|
285
|
+
# (equivalent to fast=True) and the client drives
|
|
286
|
+
# refinement. Consistent ~1.5-2s recall, no LLM tail.
|
|
287
|
+
# False → hot path always runs the internal agentic round
|
|
288
|
+
# when a query looks multi-hop / low-confidence
|
|
289
|
+
# (equivalent to fast=False) — for deployments with
|
|
290
|
+
# no smart client in front of SLM.
|
|
291
|
+
# This ONLY sets the default when a caller does not pass ``fast``
|
|
292
|
+
# explicitly. The dashboard human path (search-all → cluster summary)
|
|
293
|
+
# passes fast=False directly and always keeps internal synthesis.
|
|
294
|
+
# Env kill-switch: SLM_HOT_PATH_INTERNAL_AGENTIC=1 forces internal-on.
|
|
295
|
+
client_driven_agentic: bool = True
|
|
296
|
+
|
|
276
297
|
# Spreading activation
|
|
277
298
|
spreading_activation_decay: float = 0.7
|
|
278
299
|
spreading_activation_threshold: float = 0.1
|
|
@@ -652,9 +652,22 @@ class EmbeddingService:
|
|
|
652
652
|
release_embedding_lock()
|
|
653
653
|
|
|
654
654
|
def _reset_idle_timer(self) -> None:
|
|
655
|
-
"""Reset the configurable worker-idle timer.
|
|
655
|
+
"""Reset the configurable worker-idle timer.
|
|
656
|
+
|
|
657
|
+
F5 fix: opportunistically evict the worker mid-window when memory
|
|
658
|
+
pressure is detected (``_check_memory_pressure`` returns False).
|
|
659
|
+
This prevents the idle-timeout window from holding a worker alive
|
|
660
|
+
while system memory is exhausted.
|
|
661
|
+
"""
|
|
656
662
|
if self._idle_timer is not None:
|
|
657
663
|
self._idle_timer.cancel()
|
|
664
|
+
if not self._check_memory_pressure():
|
|
665
|
+
# Pressure detected — kill worker immediately; do not schedule a
|
|
666
|
+
# new idle timer so no further embedding work is attempted until
|
|
667
|
+
# the next explicit request reloads the worker.
|
|
668
|
+
self._kill_worker()
|
|
669
|
+
self._idle_timer = None
|
|
670
|
+
return
|
|
658
671
|
self._idle_timer = threading.Timer(
|
|
659
672
|
_IDLE_TIMEOUT_SECONDS, self.unload,
|
|
660
673
|
)
|
|
@@ -369,6 +369,7 @@ class MemoryEngine:
|
|
|
369
369
|
from superlocalmemory.core.maintenance_scheduler import MaintenanceScheduler
|
|
370
370
|
self._maintenance_scheduler = MaintenanceScheduler(
|
|
371
371
|
self._db, self._config, self._profile_id,
|
|
372
|
+
embedder=self._embedder, # v3.8.2: periodic NULL-embedding self-heal
|
|
372
373
|
)
|
|
373
374
|
self._maintenance_scheduler.start()
|
|
374
375
|
except Exception as exc:
|
|
@@ -612,7 +613,7 @@ class MemoryEngine:
|
|
|
612
613
|
mode: Mode | None = None, limit: int = CANONICAL_RECALL_LIMIT,
|
|
613
614
|
agent_id: str = "unknown",
|
|
614
615
|
session_id: str | None = None,
|
|
615
|
-
fast: bool =
|
|
616
|
+
fast: bool | None = None,
|
|
616
617
|
*,
|
|
617
618
|
include_global: bool | None = None,
|
|
618
619
|
include_shared: bool | None = None,
|
|
@@ -627,10 +628,13 @@ class MemoryEngine:
|
|
|
627
628
|
``put_nowait`` and the actual ``pending_outcomes`` INSERT runs
|
|
628
629
|
on a background worker.
|
|
629
630
|
|
|
630
|
-
``fast
|
|
631
|
-
retrieval channels
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
``fast`` controls only the internal agentic verification round; all six
|
|
632
|
+
local retrieval channels + reranker run regardless. ``fast=None`` (the
|
|
633
|
+
default) resolves to the client-driven-agentic policy: the agent hot
|
|
634
|
+
path skips the internal round (equivalent to ``fast=True``) and lets the
|
|
635
|
+
calling LLM drive query refinement using the returned confidence signals.
|
|
636
|
+
Pass ``fast=False`` to force the internal agentic round when no smart
|
|
637
|
+
client sits in front of SLM. See ``recall_pipeline.resolve_hot_path_fast``.
|
|
634
638
|
|
|
635
639
|
Multi-scope: ``include_global`` / ``include_shared`` control which
|
|
636
640
|
scopes participate in retrieval. ``None`` (the default) means "use the
|
|
@@ -21,8 +21,12 @@ from dataclasses import dataclass, field
|
|
|
21
21
|
from enum import Enum
|
|
22
22
|
from typing import Any, Callable
|
|
23
23
|
|
|
24
|
+
import logging
|
|
25
|
+
|
|
24
26
|
from superlocalmemory.storage.database import DatabaseManager
|
|
25
27
|
|
|
28
|
+
logger = logging.getLogger("superlocalmemory.ingestion_command")
|
|
29
|
+
|
|
26
30
|
_MATERIALIZATION_LOCKS = tuple(threading.RLock() for _ in range(64))
|
|
27
31
|
_MAX_AUTOMATIC_MATERIALIZATION_ATTEMPTS = 10
|
|
28
32
|
|
|
@@ -545,18 +549,31 @@ class IngestionCommand:
|
|
|
545
549
|
interval = min(30.0, max(0.1, self._lease_seconds / 3.0))
|
|
546
550
|
|
|
547
551
|
def heartbeat() -> None:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
552
|
+
# F9 fix: outer broad catch ensures lost.set() is always called
|
|
553
|
+
# when the heartbeat thread dies for any reason (not just sqlite3.Error).
|
|
554
|
+
# Without this, an AttributeError or unexpected exception would kill
|
|
555
|
+
# the thread silently, leaving lost=False while the lease has expired.
|
|
556
|
+
try:
|
|
557
|
+
while not stop.wait(interval):
|
|
558
|
+
try:
|
|
559
|
+
renewed = self.repository.renew_enriching_lease(
|
|
560
|
+
operation_id,
|
|
561
|
+
owner=self._owner,
|
|
562
|
+
lease_seconds=self._lease_seconds,
|
|
563
|
+
)
|
|
564
|
+
except sqlite3.Error:
|
|
565
|
+
continue
|
|
566
|
+
if not renewed:
|
|
567
|
+
lost.set()
|
|
568
|
+
return
|
|
569
|
+
except Exception:
|
|
570
|
+
logger.warning(
|
|
571
|
+
"heartbeat thread died unexpectedly for operation %s — "
|
|
572
|
+
"signalling lease lost",
|
|
573
|
+
operation_id,
|
|
574
|
+
exc_info=True,
|
|
575
|
+
)
|
|
576
|
+
lost.set()
|
|
560
577
|
|
|
561
578
|
thread = threading.Thread(
|
|
562
579
|
target=heartbeat,
|
|
@@ -569,10 +586,13 @@ class IngestionCommand:
|
|
|
569
586
|
finally:
|
|
570
587
|
stop.set()
|
|
571
588
|
thread.join(timeout=max(1.0, interval * 2))
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
)
|
|
589
|
+
# F7 fix: check inside finally so LeaseLost wins even when callback
|
|
590
|
+
# raised. Without this, the original exception would propagate and
|
|
591
|
+
# the LeaseLost signal would be masked.
|
|
592
|
+
if lost.is_set():
|
|
593
|
+
raise LeaseLost(
|
|
594
|
+
f"ingestion lease lost for operation {operation_id}"
|
|
595
|
+
)
|
|
576
596
|
return result
|
|
577
597
|
|
|
578
598
|
def submit(self, request: IngestionRequest) -> IngestionOperation:
|