superlocalmemory 3.8.0 → 3.8.2

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 (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  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 +2 -2
  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 +3 -5
  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 +2 -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 +3 -5
  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 +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -0,0 +1,144 @@
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
+ """Component healer — the repair actions behind the registry (v3.8.2).
6
+
7
+ Detection lives in :mod:`core.component_registry` (read-only probes). This
8
+ module performs the *actions* for components the registry marked
9
+ ``auto_fixable``: re-download a HuggingFace model, or pip-install a small
10
+ pure-python dependency. Both the background self-heal thread
11
+ (``server.unified_daemon._self_heal`` Step 0/0.5) and the foreground
12
+ ``slm doctor --fix`` command call :func:`heal_missing`, so the repair logic
13
+ exists in exactly one place.
14
+
15
+ Safety (Varun's mandate, unchanged):
16
+ * NEVER ``sudo``; NEVER auto ``ollama pull`` (surprise network/disk);
17
+ NEVER auto-install multi-GB deps (torch) — those are manual fix commands.
18
+ * pip only into a user-writable interpreter (no PEP-668 marker).
19
+ * Bounded retries; every action is fail-open — a repair failure never
20
+ raises into the caller and never wedges recall.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ from typing import Any, Callable
29
+
30
+ from superlocalmemory.core import component_registry as cr
31
+
32
+ # Progress sink: (component_key, human_message) -> None. Defaults to no-op.
33
+ ProgressFn = Callable[[str, str], None]
34
+
35
+
36
+ def _noop(_key: str, _msg: str) -> None:
37
+ pass
38
+
39
+
40
+ def _pip_install(package: str, timeout: int = 300) -> tuple[bool, str]:
41
+ """pip-install one package into the current interpreter. Fail-open."""
42
+ try:
43
+ result = subprocess.run(
44
+ [sys.executable, "-m", "pip", "install", "--no-input", package],
45
+ timeout=timeout, capture_output=True, text=True,
46
+ )
47
+ if result.returncode == 0:
48
+ return True, f"installed {package}"
49
+ tail = (result.stderr or result.stdout or "").strip().splitlines()
50
+ return False, (tail[-1] if tail else f"pip exit {result.returncode}")
51
+ except subprocess.TimeoutExpired:
52
+ return False, f"pip install {package} timed out ({timeout}s)"
53
+ except Exception as exc: # never propagate — heal is fail-open
54
+ return False, f"{type(exc).__name__}: {exc}"
55
+
56
+
57
+ def _heal_embedder() -> tuple[bool, str]:
58
+ from superlocalmemory.cli.setup_wizard import _EMBED_MODEL, _download_model
59
+
60
+ ok = _download_model(_EMBED_MODEL, "Embedding model")
61
+ return ok, "embedding model ready" if ok else "download failed"
62
+
63
+
64
+ def _heal_reranker() -> tuple[bool, str]:
65
+ from superlocalmemory.cli.setup_wizard import _RERANKER_MODEL, _download_reranker
66
+
67
+ ok = _download_reranker(_RERANKER_MODEL)
68
+ return ok, "reranker ready" if ok else "download failed"
69
+
70
+
71
+ def _heal_sqlite_vec() -> tuple[bool, str]:
72
+ if not cr.pip_is_user_writable():
73
+ return False, "interpreter externally managed (PEP 668) — install manually"
74
+ return _pip_install("sqlite-vec")
75
+
76
+
77
+ # key -> action. Only keys the registry can mark auto_fixable appear here.
78
+ _ACTIONS: dict[str, Callable[[], tuple[bool, str]]] = {
79
+ "embedder_model": _heal_embedder,
80
+ "reranker_model": _heal_reranker,
81
+ "sqlite_vec": _heal_sqlite_vec,
82
+ }
83
+
84
+
85
+ def heal_missing(
86
+ config: Any = None,
87
+ keys: list[str] | None = None,
88
+ on_progress: ProgressFn | None = None,
89
+ max_retries: int = 2,
90
+ ) -> dict[str, Any]:
91
+ """Repair every auto-fixable missing component (optionally filtered to ``keys``).
92
+
93
+ Returns ``{attempted, healed, failed, results}`` where ``results`` is a
94
+ list of ``{key, success, detail}``. Marks each component ``retrying`` in
95
+ the registry's transient overlay while its repair is in flight so the
96
+ dashboard shows live progress; clears the marker on success (a fresh
97
+ probe then confirms ``ok``) or on give-up (probe reports ``missing`` again).
98
+ """
99
+ progress = on_progress or _noop
100
+ targets = cr.auto_fixable_missing(config)
101
+ if keys is not None:
102
+ wanted = set(keys)
103
+ targets = [c for c in targets if c.key in wanted]
104
+
105
+ results: list[dict[str, Any]] = []
106
+ healed = failed = 0
107
+
108
+ for comp in targets:
109
+ action = _ACTIONS.get(comp.key)
110
+ if action is None:
111
+ # auto_fixable but no registered action — record, do not pretend.
112
+ results.append({"key": comp.key, "success": False,
113
+ "detail": "no repair action registered"})
114
+ failed += 1
115
+ continue
116
+
117
+ cr.mark_transient(comp.key, cr.STATUS_RETRYING, f"repairing {comp.label}…")
118
+ progress(comp.key, f"repairing {comp.label}…")
119
+
120
+ success, detail = False, "not attempted"
121
+ for attempt in range(1, max_retries + 1):
122
+ success, detail = action()
123
+ if success:
124
+ break
125
+ progress(comp.key,
126
+ f"attempt {attempt}/{max_retries} failed: {detail}")
127
+ if attempt < max_retries:
128
+ time.sleep(2)
129
+
130
+ cr.clear_transient(comp.key)
131
+ results.append({"key": comp.key, "success": success, "detail": detail})
132
+ if success:
133
+ healed += 1
134
+ progress(comp.key, f"✓ {detail}")
135
+ else:
136
+ failed += 1
137
+ progress(comp.key, f"✗ {detail} (fix manually: {comp.fix_cmd})")
138
+
139
+ return {
140
+ "attempted": len(targets),
141
+ "healed": healed,
142
+ "failed": failed,
143
+ "results": results,
144
+ }
@@ -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