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
|
@@ -100,6 +100,25 @@ def _get_ram_gb() -> float:
|
|
|
100
100
|
return 0.0
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _ollama_available() -> bool:
|
|
104
|
+
"""True if a local Ollama server is reachable (or its binary is installed).
|
|
105
|
+
|
|
106
|
+
v3.8.2: used to RECOMMEND Mode B at setup with a single keypress. Bounded
|
|
107
|
+
and fail-safe — a slow/absent Ollama never blocks or slows the wizard.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
import httpx
|
|
111
|
+
|
|
112
|
+
if httpx.get("http://localhost:11434/api/tags", timeout=1.5).status_code == 200:
|
|
113
|
+
return True
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
try:
|
|
117
|
+
return shutil.which("ollama") is not None
|
|
118
|
+
except Exception:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
103
122
|
# ---------------------------------------------------------------------------
|
|
104
123
|
# Model download
|
|
105
124
|
# ---------------------------------------------------------------------------
|
|
@@ -388,14 +407,23 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
388
407
|
return
|
|
389
408
|
|
|
390
409
|
# -- Step 2: Mode selection --
|
|
410
|
+
# v3.8.2: pre-detect Ollama so we can recommend Mode B with one keypress.
|
|
411
|
+
# No detection → zero-config Mode A. (Non-interactive always stays A: Mode B
|
|
412
|
+
# needs a pulled model, so we never assume it without the user confirming.)
|
|
413
|
+
ollama_present = _ollama_available()
|
|
414
|
+
default_mode = "b" if ollama_present else "a"
|
|
391
415
|
print()
|
|
392
416
|
print("─── Step 2/10: Choose Operating Mode ───")
|
|
393
417
|
print()
|
|
394
|
-
|
|
418
|
+
if ollama_present:
|
|
419
|
+
print(" ✓ Ollama detected on this machine — Mode B recommended.")
|
|
420
|
+
print(" Press Enter to accept, or pick another mode.")
|
|
421
|
+
print()
|
|
422
|
+
print(" [A] Local Guardian" + ("" if ollama_present else " (recommended)"))
|
|
395
423
|
print(" No model-provider call in the core memory path.")
|
|
396
424
|
print(" Review optional integrations and network policy for your deployment.")
|
|
397
425
|
print()
|
|
398
|
-
print(" [B] Smart Local")
|
|
426
|
+
print(" [B] Smart Local" + (" (recommended — Ollama detected)" if ollama_present else ""))
|
|
399
427
|
print(" Local LLM via Ollama for enrichment.")
|
|
400
428
|
print(" Data stays on your machine.")
|
|
401
429
|
print()
|
|
@@ -405,10 +433,13 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
405
433
|
print()
|
|
406
434
|
|
|
407
435
|
if interactive:
|
|
408
|
-
choice = _prompt(
|
|
436
|
+
choice = _prompt(
|
|
437
|
+
f" Select mode [A/B/C] (default: {default_mode.upper()}): ",
|
|
438
|
+
default_mode,
|
|
439
|
+
).lower()
|
|
409
440
|
else:
|
|
410
441
|
choice = "a"
|
|
411
|
-
print(" Auto-selecting Mode A (non-interactive)")
|
|
442
|
+
print(" Auto-selecting Mode A (non-interactive, zero-config)")
|
|
412
443
|
|
|
413
444
|
if choice not in ("a", "b", "c"):
|
|
414
445
|
print(f" Invalid choice '{choice}', using Mode A.")
|
|
@@ -496,6 +527,15 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
496
527
|
else:
|
|
497
528
|
print(f"\n ✓ CodeGraph disabled (enable later in {cg_config_path})")
|
|
498
529
|
|
|
530
|
+
# v3.8.2: up-front download summary so a large pull is never a surprise on
|
|
531
|
+
# a metered/slow link. Sizes approximate, one-time, cached locally.
|
|
532
|
+
if st_ok and not _embedding_is_remote(config):
|
|
533
|
+
print()
|
|
534
|
+
print(" Models to download (one-time, cached locally):")
|
|
535
|
+
print(" • Embedding model ~500 MB — required for semantic recall")
|
|
536
|
+
print(" • Reranker model ~130 MB — result quality")
|
|
537
|
+
print(" • Compression model ~560 MB — optional (you'll be asked)")
|
|
538
|
+
|
|
499
539
|
# -- Step 4: Download models --
|
|
500
540
|
print()
|
|
501
541
|
# H-06 (CVE-2025-14926): a malicious HuggingFace checkpoint can execute code
|
|
@@ -532,8 +572,25 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
532
572
|
_download_reranker(_RERANKER_MODEL)
|
|
533
573
|
|
|
534
574
|
print()
|
|
535
|
-
print("─── Step 4c/10:
|
|
536
|
-
|
|
575
|
+
print("─── Step 4c/10: Compression Model (LLMLingua-2, ~560MB) ───")
|
|
576
|
+
print(" Only used for aggressive prompt compression. It also downloads")
|
|
577
|
+
print(" automatically the first time you enable that feature — so skipping")
|
|
578
|
+
print(" here is safe.")
|
|
579
|
+
# v3.8.2: consent-gate the 560MB compressor (was unconditional). Skip by
|
|
580
|
+
# default; it lazy-downloads on first real use of compression.
|
|
581
|
+
if not st_ok:
|
|
582
|
+
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
583
|
+
elif interactive:
|
|
584
|
+
comp_choice = _prompt(
|
|
585
|
+
" Download the ~560MB compression model now? [y/N] (default: N): ",
|
|
586
|
+
"n",
|
|
587
|
+
).lower()
|
|
588
|
+
if comp_choice in ("y", "yes"):
|
|
589
|
+
_download_compressor(_COMPRESSOR_MODEL)
|
|
590
|
+
else:
|
|
591
|
+
print(" ✓ Skipped — downloads automatically when you enable compression.")
|
|
592
|
+
else:
|
|
593
|
+
print(" ✓ Skipped (non-interactive) — downloads on first use.")
|
|
537
594
|
|
|
538
595
|
# -- Step 5: Daemon Configuration (v3.4.3) --
|
|
539
596
|
print()
|
|
@@ -655,17 +712,21 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
655
712
|
print(" It detects degradation, generates improvements, and verifies them blindly.")
|
|
656
713
|
print(" Requires an LLM backend (Claude CLI, Ollama, or API key).")
|
|
657
714
|
print()
|
|
715
|
+
print(" OFF by default — when enabled it makes background LLM calls.")
|
|
658
716
|
print(" [Y] Enable Skill Evolution")
|
|
659
|
-
print(" [N]
|
|
717
|
+
print(" [N] Keep disabled (enable later: slm config set evolution.enabled true)")
|
|
660
718
|
print()
|
|
661
719
|
|
|
720
|
+
# v3.8.2: default OFF (was Y) — consistent with the npm profiles and no
|
|
721
|
+
# surprise outbound LLM calls for a fresh user. Opt-in, not opt-out.
|
|
662
722
|
if interactive:
|
|
663
|
-
evo_choice = _prompt(" Enable Skill Evolution? [
|
|
723
|
+
evo_choice = _prompt(" Enable Skill Evolution? [y/N] (default: N): ", "n").lower()
|
|
664
724
|
else:
|
|
665
|
-
evo_choice = "
|
|
666
|
-
print("
|
|
725
|
+
evo_choice = "n"
|
|
726
|
+
print(" Skill Evolution stays OFF (non-interactive) — enable later with:")
|
|
727
|
+
print(" slm config set evolution.enabled true")
|
|
667
728
|
|
|
668
|
-
evolution_enabled = evo_choice in ("
|
|
729
|
+
evolution_enabled = evo_choice in ("y", "yes")
|
|
669
730
|
|
|
670
731
|
# Write evolution config to config.json directly
|
|
671
732
|
# (SLMConfig.save() doesn't serialize evolution)
|
|
@@ -753,6 +814,21 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
753
814
|
else:
|
|
754
815
|
print(" Adapters: none (enable via: slm adapters enable gmail)")
|
|
755
816
|
print()
|
|
817
|
+
# v3.8.2: surface component self-heal status so the user leaves setup
|
|
818
|
+
# KNOWING everything they need is present — or exactly what to run if not.
|
|
819
|
+
try:
|
|
820
|
+
from superlocalmemory.core import component_registry as _cr
|
|
821
|
+
|
|
822
|
+
_sum = _cr.snapshot(config)["summary"]
|
|
823
|
+
if _sum.get("healthy"):
|
|
824
|
+
print(" ✓ All components ready — models and dependencies present.")
|
|
825
|
+
else:
|
|
826
|
+
print(f" ⚠ {_sum.get('missing', 0)} component(s) need attention.")
|
|
827
|
+
print(" SLM auto-repairs fixable ones on daemon start. To check/fix now:")
|
|
828
|
+
print(" slm doctor (auto-fix: slm doctor --fix)")
|
|
829
|
+
except Exception:
|
|
830
|
+
pass
|
|
831
|
+
print()
|
|
756
832
|
print(" Quick start:")
|
|
757
833
|
print(' slm remember "your first memory"')
|
|
758
834
|
print(' slm recall "search query"')
|
|
@@ -760,8 +836,8 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
760
836
|
print(" slm adapters enable gmail → start Gmail ingestion")
|
|
761
837
|
print()
|
|
762
838
|
print(" Need help?")
|
|
763
|
-
print(" slm
|
|
764
|
-
print(" slm
|
|
839
|
+
print(" slm help — every command, grouped (try: slm help self-heal)")
|
|
840
|
+
print(" slm doctor — diagnose issues (auto-fix: slm doctor --fix)")
|
|
765
841
|
print(" slm serve install — install auto-start service")
|
|
766
842
|
print(" https://github.com/qualixar/superlocalmemory")
|
|
767
843
|
print()
|
|
@@ -816,20 +892,70 @@ def check_first_use(command: str) -> None:
|
|
|
816
892
|
|
|
817
893
|
|
|
818
894
|
def _configure_external_integrations(*, interactive: bool) -> bool:
|
|
819
|
-
"""Request consent before editing Claude Code configuration.
|
|
895
|
+
"""Request consent before editing Claude Code configuration.
|
|
896
|
+
|
|
897
|
+
v3.8.2: after the Claude Code step, a pip user can also wire every OTHER
|
|
898
|
+
detected IDE in one step (previously only Claude Code was offered here;
|
|
899
|
+
other IDEs required a manual `slm connect <ide>` each).
|
|
900
|
+
"""
|
|
820
901
|
print()
|
|
821
902
|
print(" Optional Claude Code integration can install the SLM plugin and hooks.")
|
|
822
903
|
if not interactive:
|
|
823
904
|
print(" Skipped in non-interactive setup. Run: slm connect claude-code")
|
|
824
905
|
return False
|
|
906
|
+
changed = False
|
|
825
907
|
choice = _prompt(
|
|
826
908
|
" Install Claude Code plugin and hooks now? [y/N] (default: N): ",
|
|
827
909
|
"n",
|
|
828
910
|
).lower()
|
|
829
|
-
if choice
|
|
911
|
+
if choice in ("y", "yes"):
|
|
912
|
+
changed = _install_external_integrations()
|
|
913
|
+
else:
|
|
830
914
|
print(" Skipped. Run `slm connect claude-code` when ready.")
|
|
915
|
+
# v3.8.2: offer to connect all other detected IDEs (Cursor, VS Code, …).
|
|
916
|
+
changed = _configure_other_ides(interactive=interactive) or changed
|
|
917
|
+
return changed
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _configure_other_ides(*, interactive: bool) -> bool:
|
|
921
|
+
"""Consent-gated: connect all OTHER detected IDEs (Cursor, VS Code, …).
|
|
922
|
+
|
|
923
|
+
Uses the same IDEConnector the dashboard uses. Best-effort — a failure to
|
|
924
|
+
detect or connect any single IDE never blocks setup.
|
|
925
|
+
"""
|
|
926
|
+
if not interactive:
|
|
927
|
+
return False
|
|
928
|
+
try:
|
|
929
|
+
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
930
|
+
|
|
931
|
+
connector = IDEConnector()
|
|
932
|
+
installed = [s for s in connector.get_status() if s.get("installed")]
|
|
933
|
+
except Exception:
|
|
831
934
|
return False
|
|
832
|
-
|
|
935
|
+
others = [s for s in installed if s.get("id") not in ("claude-code", "claude")]
|
|
936
|
+
if not others:
|
|
937
|
+
return False
|
|
938
|
+
names = ", ".join(s.get("name", s.get("id")) for s in others)
|
|
939
|
+
print()
|
|
940
|
+
print(f" Detected other IDEs: {names}")
|
|
941
|
+
print(" SLM can wire them all so memory works across every editor.")
|
|
942
|
+
choice = _prompt(" Connect them now? [Y/n] (default: Y): ", "y").lower()
|
|
943
|
+
if choice not in ("", "y", "yes"):
|
|
944
|
+
print(" Skipped. Connect any later with: slm connect <ide>")
|
|
945
|
+
return False
|
|
946
|
+
changed = False
|
|
947
|
+
for s in others:
|
|
948
|
+
ide_id = s.get("id")
|
|
949
|
+
label = s.get("name", ide_id)
|
|
950
|
+
try:
|
|
951
|
+
if connector.connect(ide_id):
|
|
952
|
+
print(f" ✓ {label}")
|
|
953
|
+
changed = True
|
|
954
|
+
else:
|
|
955
|
+
print(f" ⚠ {label} — connect later: slm connect {ide_id}")
|
|
956
|
+
except Exception:
|
|
957
|
+
print(f" ⚠ {label} — connect later: slm connect {ide_id}")
|
|
958
|
+
return changed
|
|
833
959
|
|
|
834
960
|
|
|
835
961
|
def _install_external_integrations() -> bool:
|
|
@@ -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
|
+
}
|