superlocalmemory 4.0.8 → 4.0.10

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 (147) hide show
  1. package/CHANGELOG.md +221 -1
  2. package/README.md +6 -6
  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/scripts/ensure-venv.sh +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +1 -1
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +1 -1
  19. package/plugin/skills/slm-recall/SKILL.md +1 -1
  20. package/plugin/skills/slm-remember/SKILL.md +1 -1
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +1 -1
  23. package/plugin/skills/slm-status/SKILL.md +3 -3
  24. package/plugin-src/rules/AGENTS.md +1 -1
  25. package/plugin-src/skills/slm-status/SKILL.md +2 -2
  26. package/pyproject.toml +1 -1
  27. package/scripts/postinstall.js +4 -0
  28. package/src/superlocalmemory/__init__.py +1 -1
  29. package/src/superlocalmemory/cli/_lazy_init.py +1 -1
  30. package/src/superlocalmemory/cli/commands.py +164 -11
  31. package/src/superlocalmemory/cli/db_migrate.py +0 -2
  32. package/src/superlocalmemory/cli/gdpr_io.py +1 -1
  33. package/src/superlocalmemory/cli/main.py +7 -7
  34. package/src/superlocalmemory/cli/service_installer.py +2 -1
  35. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  36. package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
  37. package/src/superlocalmemory/core/config.py +41 -7
  38. package/src/superlocalmemory/core/context_cache.py +0 -2
  39. package/src/superlocalmemory/core/engine.py +371 -63
  40. package/src/superlocalmemory/core/evidence_bundle.py +3 -1
  41. package/src/superlocalmemory/core/fact_consolidator.py +316 -125
  42. package/src/superlocalmemory/core/install_detector.py +131 -0
  43. package/src/superlocalmemory/core/maintenance.py +44 -6
  44. package/src/superlocalmemory/core/memory_health.py +266 -0
  45. package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
  46. package/src/superlocalmemory/core/operation_request.py +1 -1
  47. package/src/superlocalmemory/core/ops_remediation.py +2 -2
  48. package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
  49. package/src/superlocalmemory/core/security_primitives.py +3 -6
  50. package/src/superlocalmemory/core/store_pipeline.py +172 -29
  51. package/src/superlocalmemory/core/topic_signature.py +0 -2
  52. package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
  53. package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
  54. package/src/superlocalmemory/encoding/cognitive_consolidator.py +15 -1
  55. package/src/superlocalmemory/encoding/graph_builder.py +2 -2
  56. package/src/superlocalmemory/encoding/scene_builder.py +8 -2
  57. package/src/superlocalmemory/hooks/adapter_base.py +0 -2
  58. package/src/superlocalmemory/hooks/context_payload.py +0 -2
  59. package/src/superlocalmemory/hooks/portable_kit.py +8 -8
  60. package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
  61. package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
  62. package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
  63. package/src/superlocalmemory/infra/backup.py +44 -8
  64. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
  65. package/src/superlocalmemory/learning/arm_catalog.py +0 -2
  66. package/src/superlocalmemory/learning/bandit.py +0 -2
  67. package/src/superlocalmemory/learning/bandit_cache.py +0 -2
  68. package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
  69. package/src/superlocalmemory/learning/ensemble.py +0 -2
  70. package/src/superlocalmemory/learning/labeler.py +0 -2
  71. package/src/superlocalmemory/learning/legacy_migration.py +0 -2
  72. package/src/superlocalmemory/learning/model_cache.py +0 -2
  73. package/src/superlocalmemory/learning/ranker.py +0 -2
  74. package/src/superlocalmemory/learning/reward_archive.py +6 -1
  75. package/src/superlocalmemory/learning/reward_proxy.py +0 -2
  76. package/src/superlocalmemory/learning/signal_worker.py +0 -2
  77. package/src/superlocalmemory/math/fisher.py +1 -1
  78. package/src/superlocalmemory/math/hopfield.py +4 -1
  79. package/src/superlocalmemory/math/langevin.py +1 -1
  80. package/src/superlocalmemory/math/sheaf.py +7 -3
  81. package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
  82. package/src/superlocalmemory/mcp/profiles.py +1 -1
  83. package/src/superlocalmemory/mcp/server.py +2 -2
  84. package/src/superlocalmemory/mcp/session_binding.py +92 -0
  85. package/src/superlocalmemory/mcp/tools_active.py +56 -0
  86. package/src/superlocalmemory/mcp/tools_core.py +41 -40
  87. package/src/superlocalmemory/mcp/tools_ops.py +2 -2
  88. package/src/superlocalmemory/optimize/cache/manager.py +2 -2
  89. package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
  90. package/src/superlocalmemory/optimize/compress/router.py +1 -1
  91. package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
  92. package/src/superlocalmemory/optimize/proxy/server.py +1 -1
  93. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
  94. package/src/superlocalmemory/optimize/storage/db.py +2 -2
  95. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  96. package/src/superlocalmemory/retrieval/ann_index.py +9 -2
  97. package/src/superlocalmemory/retrieval/bm25_channel.py +6 -10
  98. package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
  99. package/src/superlocalmemory/retrieval/engine.py +272 -43
  100. package/src/superlocalmemory/retrieval/entity_channel.py +8 -2
  101. package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
  102. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  103. package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
  104. package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
  105. package/src/superlocalmemory/retrieval/reranker.py +3 -3
  106. package/src/superlocalmemory/retrieval/scope_policy.py +22 -1
  107. package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
  108. package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
  109. package/src/superlocalmemory/retrieval/strategy.py +94 -0
  110. package/src/superlocalmemory/retrieval/temporal_channel.py +179 -10
  111. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
  112. package/src/superlocalmemory/retrieval/vector_store.py +151 -10
  113. package/src/superlocalmemory/server/api.py +6 -1
  114. package/src/superlocalmemory/server/asset_versions.py +171 -0
  115. package/src/superlocalmemory/server/recall_serializer.py +10 -0
  116. package/src/superlocalmemory/server/routes/abstraction.py +201 -0
  117. package/src/superlocalmemory/server/routes/brain.py +2 -2
  118. package/src/superlocalmemory/server/routes/data_io.py +29 -1
  119. package/src/superlocalmemory/server/routes/entity.py +13 -1
  120. package/src/superlocalmemory/server/routes/mesh.py +1 -1
  121. package/src/superlocalmemory/server/routes/prewarm.py +4 -4
  122. package/src/superlocalmemory/server/routes/v3_api.py +2 -2
  123. package/src/superlocalmemory/server/ui.py +8 -1
  124. package/src/superlocalmemory/server/unified_daemon.py +570 -16
  125. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  126. package/src/superlocalmemory/storage/_schema_version.py +46 -3
  127. package/src/superlocalmemory/storage/backup.py +531 -0
  128. package/src/superlocalmemory/storage/database.py +139 -34
  129. package/src/superlocalmemory/storage/embedding_codec.py +129 -0
  130. package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
  131. package/src/superlocalmemory/storage/migration_runner.py +153 -2
  132. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
  133. package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
  134. package/src/superlocalmemory/storage/migrations.py +15 -1
  135. package/src/superlocalmemory/storage/models.py +7 -0
  136. package/src/superlocalmemory/storage/quantized_store.py +4 -2
  137. package/src/superlocalmemory/storage/schema.py +98 -0
  138. package/src/superlocalmemory/summaries/base.py +1 -1
  139. package/src/superlocalmemory/summaries/non_answer.py +223 -0
  140. package/src/superlocalmemory/summaries/session_summary.py +1 -1
  141. package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
  142. package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
  143. package/src/superlocalmemory/ui/index.html +1 -1
  144. package/src/superlocalmemory/ui/js/core.js +1 -1
  145. package/src/superlocalmemory/ui/js/od-brain.js +1 -1
  146. package/src/superlocalmemory/ui/js/od-memories.js +190 -1
  147. package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
@@ -0,0 +1,131 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Detect all SuperLocalMemory installations present on this machine.
5
+
6
+ Supports three install types: pipx, venv (~/.slm-venv), and npm global.
7
+ All detection is read-only and fast (< 200 ms). No writes are ever performed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import glob
13
+ import json
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ # Roots for the two Python install types. Patched in tests.
20
+ _VENV_ROOT: Path = Path.home() / ".slm-venv"
21
+ _PIPX_ROOT: Path = Path.home() / ".local" / "pipx" / "venvs" / "superlocalmemory"
22
+
23
+
24
+ def _npm_global_root() -> Optional[Path]:
25
+ """Return the npm global node_modules root, or None on any failure."""
26
+ try:
27
+ result = subprocess.run(
28
+ ["npm", "root", "-g"],
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=5,
32
+ )
33
+ if result.returncode == 0:
34
+ root = result.stdout.strip()
35
+ if root:
36
+ return Path(root)
37
+ except Exception: # npm absent, timeout, etc.
38
+ pass
39
+ return None
40
+
41
+
42
+ def _read_python_version(base: Path) -> Optional[str]:
43
+ """Read __version__ from the first matching site-packages layout under base.
44
+
45
+ Both layouts are searched. POSIX virtualenvs use
46
+ ``lib/python3.13/site-packages``; Windows uses ``Lib/site-packages`` with no
47
+ version component and a capitalised directory. Searching only the POSIX
48
+ shape made detection silently return None on Windows — which is precisely
49
+ where multi-install divergence between pip and npm is most likely, and where
50
+ the version-mismatch error would then name no installations at all.
51
+ """
52
+ patterns = [
53
+ str(base / "lib" / "python*" / "site-packages" / "superlocalmemory" / "__init__.py"),
54
+ str(base / "Lib" / "site-packages" / "superlocalmemory" / "__init__.py"),
55
+ str(base / "lib" / "site-packages" / "superlocalmemory" / "__init__.py"),
56
+ ]
57
+ matches: list[str] = []
58
+ for pattern in patterns:
59
+ matches.extend(glob.glob(pattern))
60
+ for init_path in sorted(set(matches)):
61
+ try:
62
+ text = Path(init_path).read_text(encoding="utf-8", errors="replace")
63
+ for line in text.splitlines():
64
+ line = line.strip()
65
+ if line.startswith("__version__"):
66
+ # __version__ = "4.1.0" or __version__ = '4.1.0'
67
+ parts = line.split("=", 1)
68
+ if len(parts) == 2:
69
+ return parts[1].strip().strip("\"'")
70
+ except OSError:
71
+ continue
72
+ return None
73
+
74
+
75
+ def _read_npm_version(npm_root: Path) -> Optional[str]:
76
+ """Read version from npm global package.json."""
77
+ pkg_json = npm_root / "superlocalmemory" / "package.json"
78
+ try:
79
+ data = json.loads(pkg_json.read_text(encoding="utf-8"))
80
+ return str(data.get("version", "")).strip() or None
81
+ except (OSError, json.JSONDecodeError):
82
+ return None
83
+
84
+
85
+ def _detect_all_installs() -> list[dict]:
86
+ """Return all SuperLocalMemory installs detected on this machine.
87
+
88
+ Each entry is a dict with keys:
89
+ - ``path`` (str) — directory of the install
90
+ - ``version`` (str) — version string read from package metadata
91
+ - ``type`` (str) — one of "pipx", "venv", "npm"
92
+
93
+ Detection is read-only and best-effort. A missing or unreadable install
94
+ produces no entry rather than an error. Subprocess calls are bounded to
95
+ 5 seconds total.
96
+ """
97
+ results: list[dict] = []
98
+
99
+ # --- pipx ---
100
+ pipx_version = _read_python_version(_PIPX_ROOT)
101
+ if pipx_version is not None:
102
+ results.append({
103
+ "path": str(_PIPX_ROOT) + "/",
104
+ "version": pipx_version,
105
+ "type": "pipx",
106
+ })
107
+
108
+ # --- ~/.slm-venv ---
109
+ venv_version = _read_python_version(_VENV_ROOT)
110
+ if venv_version is not None:
111
+ results.append({
112
+ "path": str(_VENV_ROOT) + "/",
113
+ "version": venv_version,
114
+ "type": "venv",
115
+ })
116
+
117
+ # --- npm global ---
118
+ npm_root = _npm_global_root()
119
+ if npm_root is not None:
120
+ npm_version = _read_npm_version(npm_root)
121
+ if npm_version is not None:
122
+ results.append({
123
+ "path": str(npm_root / "superlocalmemory") + "/",
124
+ "version": npm_version,
125
+ "type": "npm",
126
+ })
127
+
128
+ return results
129
+
130
+
131
+ __all__ = ["_detect_all_installs"]
@@ -31,6 +31,16 @@ if TYPE_CHECKING:
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
+
35
+ class _ConsolidationDisabled(Exception):
36
+ """Internal signal: consolidation is switched off, so skip its block.
37
+
38
+ A private exception rather than restructuring the surrounding try/except:
39
+ the block's job is to keep one optional maintenance step from taking the
40
+ whole pass down with it, and that guarantee should not be weakened to
41
+ express "deliberately skipped". Caught immediately below, never propagated.
42
+ """
43
+
34
44
  # Backfill constants
35
45
  _BACKFILL_BURN_IN_STEPS = 50
36
46
  _LANGEVIN_DIM = 8
@@ -638,9 +648,12 @@ def run_maintenance(
638
648
  logger.warning("Entity summary consolidation failed: %s", exc)
639
649
 
640
650
  # 4. Fact consolidation (v3.8.4 concurrency-safe path via DatabaseManager).
641
- # Merges clusters of warm/cold atomic facts about the same entity into a
642
- # single consolidated fact, archives the originals (NEVER deletes them),
643
- # and records provenance in fact_consolidations.
651
+ # Groups warm/cold atomic facts that share an entity and writes ONE
652
+ # DISPLAY summary per cluster into consolidated_summaries, with provenance
653
+ # in fact_consolidations. It does not write to atomic_facts and does not
654
+ # archive the source facts — until 4.0.10 it did both, which put 1,195
655
+ # model-written rows into the retrieval corpus and left 528 genuine
656
+ # memories archived out of normal recall.
644
657
  #
645
658
  # Uses the DatabaseManager path so LLM calls happen OUTSIDE the write lock:
646
659
  # - Discover clusters in a short memory_read() (no write lock held).
@@ -652,20 +665,45 @@ def run_maintenance(
652
665
  try:
653
666
  from superlocalmemory.core.fact_consolidator import consolidate_facts
654
667
 
668
+ # The documented off-switch has to actually switch something off.
669
+ # ConsolidationConfig.enabled has existed since Phase 5 and this call
670
+ # site never read it, so a user who ran `slm config` to turn
671
+ # consolidation off got consolidation anyway — for four months, on
672
+ # every maintenance pass. -2 is a third distinguishable value, kept
673
+ # apart from 0 (nothing to merge) and -1 (the step failed), so a
674
+ # deliberately disabled step is never mistaken for either.
675
+ _consolidation = getattr(config, "consolidation", None)
676
+ if _consolidation is not None and not getattr(_consolidation, "enabled", True):
677
+ counts["facts_consolidated"] = -2
678
+ logger.debug("Fact consolidation disabled by configuration")
679
+ raise _ConsolidationDisabled
680
+
655
681
  fc_stats = consolidate_facts(
656
682
  db,
657
683
  profile_id=profile_id,
658
- max_clusters=getattr(config, "max_consolidation_clusters", 20),
684
+ # Read from ConsolidationConfig, with the old SLMConfig-level name
685
+ # as the fallback. `getattr(config, "max_consolidation_clusters")`
686
+ # alone never resolved — SLMConfig has no such attribute — so the
687
+ # default was the only value this had ever used.
688
+ max_clusters=int(
689
+ getattr(_consolidation, "max_consolidation_clusters", None)
690
+ or getattr(config, "max_consolidation_clusters", None)
691
+ or 20
692
+ ),
659
693
  dry_run=False,
660
694
  config=config,
661
695
  )
662
696
  counts["facts_consolidated"] = fc_stats.get("consolidated", 0)
663
697
  if fc_stats.get("consolidated", 0) > 0:
664
698
  logger.info(
665
- "Fact consolidation: %d clusters merged, %d facts archived",
699
+ "Fact consolidation: %d display summaries over %d facts "
700
+ "(%d clusters refused)",
666
701
  fc_stats.get("consolidated", 0),
667
- fc_stats.get("facts_archived", 0),
702
+ fc_stats.get("facts_summarized", 0),
703
+ fc_stats.get("rejected", 0),
668
704
  )
705
+ except _ConsolidationDisabled:
706
+ pass
669
707
  except Exception as exc:
670
708
  # WARNING, not debug, and a distinguishable count. Leaving this at debug
671
709
  # with facts_consolidated=0 made a failing consolidation report exactly
@@ -0,0 +1,266 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Tell the owner, in their own words, whether their memory works.
5
+
6
+ Until now the only way to learn that 43.7% of a store could not be found by
7
+ asking a question was to write the SQL yourself. One machine sat in exactly
8
+ that state for months while every status line it showed said the system was
9
+ healthy, because nothing measured reachability and nothing reported it.
10
+
11
+ So this module answers four questions a non-engineer can act on:
12
+
13
+ * How many memories do I have?
14
+ * How many can actually be found by asking a question?
15
+ * How many were withheld because a model wrote them, not me?
16
+ * Is anything still being repaired?
17
+
18
+ Read-only, and every query is bounded. Fail-soft by construction: a missing
19
+ table or column yields ``None`` for that line rather than an exception, because
20
+ a health report that crashes on an old store is worse than one that says "not
21
+ known yet".
22
+
23
+ Consumed by ``slm doctor``, ``GET /api/v3/memory-health``, and the dashboard.
24
+ One implementation so the three cannot disagree with each other.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import re
31
+ import sqlite3
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ __all__ = ["MemoryHealth", "measure", "describe"]
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class MemoryHealth:
42
+ """A store's answer-ability, counted rather than assumed."""
43
+
44
+ #: Memories that recall is allowed to return.
45
+ live_facts: int = 0
46
+ #: Of those, how many have a vector projection, i.e. can be found by
47
+ #: meaning rather than only by matching words.
48
+ findable_by_meaning: int = 0
49
+ #: Memories with no vector at all. These are reachable by keyword only.
50
+ missing_vector: int = 0
51
+ #: Machine-written summaries withheld from recall and kept for display.
52
+ withheld_summaries: int = 0
53
+ #: Summaries preserved in the display table.
54
+ display_summaries: int = 0
55
+ #: Memories hidden by the retention system, excluding the withheld ones.
56
+ hidden_by_forgetting: int = 0
57
+ #: Rows whose retention zone contradicts their retention score, i.e. hidden
58
+ #: while the maths says to keep them. Should be zero after repair.
59
+ inconsistently_hidden: int = 0
60
+ #: Present only when a table or column was absent.
61
+ unavailable: tuple[str, ...] = field(default_factory=tuple)
62
+
63
+ @property
64
+ def reachability(self) -> float:
65
+ """Share of live memories findable by meaning, 0.0-1.0."""
66
+ if self.live_facts <= 0:
67
+ return 1.0
68
+ return self.findable_by_meaning / self.live_facts
69
+
70
+ @property
71
+ def healthy(self) -> bool:
72
+ """Whether anything here warrants telling the owner about."""
73
+ return (
74
+ self.reachability >= 0.99
75
+ and self.missing_vector == 0
76
+ and self.inconsistently_hidden == 0
77
+ )
78
+
79
+
80
+ def measure(db_path: str | Path) -> MemoryHealth:
81
+ """Count the store's answer-ability. Read-only; never raises."""
82
+ unavailable: list[str] = []
83
+ try:
84
+ conn = sqlite3.connect(f"file:{Path(db_path)}?mode=ro", uri=True)
85
+ except sqlite3.Error as exc:
86
+ logger.debug("memory health: cannot open %s: %s", db_path, exc)
87
+ return MemoryHealth(unavailable=("database",))
88
+
89
+ try:
90
+ if not _table_exists(conn, "atomic_facts"):
91
+ return MemoryHealth(unavailable=("atomic_facts",))
92
+
93
+ # Quarantine came in 4.0.10. On an older store every fact is "live",
94
+ # which is the honest reading of a store that has no withheld rows.
95
+ has_q = _has_column(conn, "atomic_facts", "quarantined")
96
+ if not has_q:
97
+ unavailable.append("quarantined")
98
+ live_clause = "COALESCE(quarantined, 0) = 0" if has_q else "1=1"
99
+
100
+ live = _count(conn, f"SELECT COUNT(*) FROM atomic_facts WHERE {live_clause}")
101
+ withheld = (
102
+ _count(conn, "SELECT COUNT(*) FROM atomic_facts WHERE quarantined = 1")
103
+ if has_q else 0
104
+ )
105
+ missing_vec = _count(
106
+ conn,
107
+ f"SELECT COUNT(*) FROM atomic_facts "
108
+ f"WHERE embedding IS NULL AND {live_clause}",
109
+ )
110
+
111
+ if _table_exists(conn, "embedding_metadata"):
112
+ findable = _count(
113
+ conn,
114
+ "SELECT COUNT(*) FROM embedding_metadata em "
115
+ "JOIN atomic_facts af ON af.fact_id = em.fact_id "
116
+ f"WHERE {_prefixed(live_clause, 'af')}",
117
+ )
118
+ else:
119
+ unavailable.append("embedding_metadata")
120
+ findable = 0
121
+
122
+ display = (
123
+ _count(conn, "SELECT COUNT(*) FROM consolidated_summaries")
124
+ if _table_exists(conn, "consolidated_summaries") else 0
125
+ )
126
+ if not _table_exists(conn, "consolidated_summaries"):
127
+ unavailable.append("consolidated_summaries")
128
+
129
+ hidden = inconsistent = 0
130
+ if _table_exists(conn, "fact_retention"):
131
+ hidden = _count(
132
+ conn,
133
+ "SELECT COUNT(*) FROM fact_retention r "
134
+ "JOIN atomic_facts af ON af.fact_id = r.fact_id "
135
+ "WHERE r.lifecycle_zone IN ('archive', 'forgotten') "
136
+ f" AND {_prefixed(live_clause, 'af')}",
137
+ )
138
+ # The contradiction M043 repairs: hidden, yet scored to keep.
139
+ inconsistent = _count(
140
+ conn,
141
+ "SELECT COUNT(*) FROM fact_retention r "
142
+ "JOIN atomic_facts af ON af.fact_id = r.fact_id "
143
+ "WHERE r.lifecycle_zone IN ('archive', 'forgotten') "
144
+ " AND r.retention_score > 0.8 "
145
+ f" AND {_prefixed(live_clause, 'af')}",
146
+ )
147
+ else:
148
+ unavailable.append("fact_retention")
149
+
150
+ return MemoryHealth(
151
+ live_facts=live,
152
+ findable_by_meaning=findable,
153
+ missing_vector=missing_vec,
154
+ withheld_summaries=withheld,
155
+ display_summaries=display,
156
+ hidden_by_forgetting=hidden,
157
+ inconsistently_hidden=inconsistent,
158
+ unavailable=tuple(unavailable),
159
+ )
160
+ except sqlite3.Error as exc:
161
+ logger.debug("memory health measurement failed: %s", exc)
162
+ return MemoryHealth(unavailable=(*unavailable, "query_failed"))
163
+ finally:
164
+ conn.close()
165
+
166
+
167
+ def describe(health: MemoryHealth) -> list[str]:
168
+ """Plain-language lines for a reader who does not write SQL.
169
+
170
+ No percentages without the counts behind them, and no jargon: "findable by
171
+ asking a question" rather than "vector coverage", because the person who
172
+ needs this line is the one who would not know what a vector is.
173
+ """
174
+ lines: list[str] = []
175
+ if "atomic_facts" in health.unavailable or "database" in health.unavailable:
176
+ return ["Memory store not readable yet."]
177
+
178
+ lines.append(f"You have {health.live_facts:,} memories.")
179
+
180
+ if "embedding_metadata" in health.unavailable:
181
+ lines.append(
182
+ "Whether they can be found by asking a question is not known yet — "
183
+ "the search index has not been built."
184
+ )
185
+ elif health.live_facts:
186
+ pct = 100.0 * health.reachability
187
+ if health.findable_by_meaning >= health.live_facts:
188
+ # "All" only when the counts actually agree. The threshold used to
189
+ # be reachability >= 0.99, which printed "All of them can be found
190
+ # by asking a question (5,199 indexed)" on a store of 5,205 — a
191
+ # claim of all, contradicted by the number beside it. This module
192
+ # exists to be believed; it cannot round in its own favour.
193
+ lines.append(
194
+ f"All {health.live_facts:,} of them can be found by asking a "
195
+ f"question."
196
+ )
197
+ elif health.reachability >= 0.99:
198
+ gap = health.live_facts - health.findable_by_meaning
199
+ lines.append(
200
+ f"{health.findable_by_meaning:,} of them can be found by asking "
201
+ f"a question. The other {gap:,} can only be found by matching "
202
+ f"words. That is a small enough share to be normal — a memory "
203
+ f"written moments ago, or one the model could not read."
204
+ )
205
+ else:
206
+ gap = health.live_facts - health.findable_by_meaning
207
+ lines.append(
208
+ f"{health.findable_by_meaning:,} of them ({pct:.0f}%) can be "
209
+ f"found by asking a question. The other {gap:,} can only be "
210
+ f"found by matching words, so a question phrased differently "
211
+ f"will miss them. This repairs itself as the service runs; if "
212
+ f"it does not, the embedding model is unavailable."
213
+ )
214
+
215
+ if health.withheld_summaries:
216
+ lines.append(
217
+ f"{health.withheld_summaries:,} machine-written summaries are kept "
218
+ f"out of your answers and shown on the dashboard instead. They were "
219
+ f"written by the summarizer, not by you, and they used to be "
220
+ f"returned as if they were your own notes."
221
+ )
222
+
223
+ if health.inconsistently_hidden:
224
+ lines.append(
225
+ f"{health.inconsistently_hidden:,} memories are hidden even though "
226
+ f"they are marked worth keeping. This is a fault and it is repaired "
227
+ f"automatically the next time the service starts."
228
+ )
229
+
230
+ if health.hidden_by_forgetting:
231
+ lines.append(
232
+ f"{health.hidden_by_forgetting:,} older memories are set aside by "
233
+ f"the forgetting curve. They are not deleted and a deep search "
234
+ f"still reaches them."
235
+ )
236
+
237
+ return lines
238
+
239
+
240
+ def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
241
+ return conn.execute(
242
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,),
243
+ ).fetchone() is not None
244
+
245
+
246
+ def _has_column(conn: sqlite3.Connection, table: str, column: str) -> bool:
247
+ return any(
248
+ row[1] == column for row in conn.execute(f"PRAGMA table_info({table})")
249
+ )
250
+
251
+
252
+ def _prefixed(clause: str, prefix: str) -> str:
253
+ """Qualify a bare column reference for use in a joined query.
254
+
255
+ Word-bounded, so a future column named ``quarantined_at`` is not silently
256
+ rewritten to ``af.quarantined_at`` by a substring match. No such column
257
+ exists today; the point is that the failure would be a wrong count rather
258
+ than an error, and a wrong count in a health report is the one thing this
259
+ module must not produce.
260
+ """
261
+ return re.sub(r"\bquarantined\b", f"{prefix}.quarantined", clause)
262
+
263
+
264
+ def _count(conn: sqlite3.Connection, sql: str) -> int:
265
+ row = conn.execute(sql).fetchone()
266
+ return int(row[0]) if row else 0
@@ -281,7 +281,7 @@ class OperationPolicyRegistry:
281
281
  allowed_transports=_ALL_TRANSPORTS,
282
282
  audit_level="full",
283
283
  ),
284
- # Operational recovery & admin remediation (Wave-3 resilience slice)
284
+ # Operational recovery & admin remediation (resilience slice)
285
285
  # OPS_INSPECT: read-only listing of failed/stuck/degraded ops.
286
286
  # Allowed over all transports so dashboard, MCP, and CLI all work.
287
287
  OperationKind.OPS_INSPECT: OperationPolicy(
@@ -51,7 +51,7 @@ class OperationKind(str, Enum):
51
51
  SCHEMA_MIGRATE = "schema_migrate"
52
52
  VECTOR_MIGRATE = "vector_migrate"
53
53
  EVOLVE_SKILL = "evolve_skill"
54
- # Operational recovery & admin remediation (V4 Wave-3 resilience slice)
54
+ # Operational recovery & admin remediation (V4 resilience slice)
55
55
  OPS_INSPECT = "ops_inspect" # List failed/stuck/degraded operations (OWNER/ADMIN)
56
56
  OPS_RESOLVE = "ops_resolve" # Retry/force-reconcile/cancel an operation (OWNER/ADMIN)
57
57
 
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
3
 
4
- """Operational Recovery & Admin Remediation helpers (Wave-3 resilience slice).
4
+ """Operational Recovery & Admin Remediation helpers (resilience slice).
5
5
 
6
6
  Provides two primary functions used by HTTP endpoints, MCP tools, and CLI:
7
7
 
@@ -22,7 +22,7 @@ Design constraints (NON-NEGOTIABLE):
22
22
  - Additive & backward-compatible — healthy-path unaffected
23
23
  - Immutable return dicts; explicit error handling; no silent swallowing
24
24
 
25
- Part of SuperLocalMemory V4 | Wave-3: Operational Recovery
25
+ Part of SuperLocalMemory V4 | Operational Recovery
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -16,7 +16,7 @@ The persona is one bounded roll-up per profile that consumes the top community
16
16
  summaries. It is recall-GATED (never auto-injected into the hot recall path —
17
17
  avoids the V3.4.40 summary-pollution regression) and SIZE-bounded. Drill-down
18
18
  (``get_sources``) walks the hierarchy back down to the source atoms, matching
19
- the market bar for summary->source provenance (Zep-style).
19
+ the design goal for summary->source provenance (Zep-style attribution).
20
20
 
21
21
  Runs in the background consolidation lane after community summaries.
22
22
  Fail-open throughout; recompute replaces a profile's row.
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Shared security primitives for SLM v3.4.22.
6
6
 
7
- LLD reference: `.backup/active-brain/lld/LLD-07-schema-migrations-and-security-primitives.md`
8
- Section: 6.1 through 6.10.
9
7
 
10
8
  Every file write, subprocess spawn, and secret-bearing string across SLM
11
9
  daemon, adapters, hooks, and binary installer routes through this module.
@@ -155,10 +153,9 @@ def safe_resolve(base: Path, rel: str | Path) -> Path:
155
153
  # enforces the LLD-00 regex AND the base-containment check. Callers in
156
154
  # LLD-09 (session state files) and LLD-11 (evolution.lock) MUST use this.
157
155
  #
158
- # Naming deviation from IMPLEMENTATION-MANIFEST P0.2: the manifest reused
159
- # the name ``safe_resolve`` but the existing path-style helper is used in
160
- # 9+ call sites. A separate name avoids breakage. See
161
- # ``.backup/active-brain/MANIFEST-DEVIATION.md`` P0.2 entry.
156
+ # Naming deviation note: the name ``safe_resolve`` was already used by an
157
+ # existing path-style helper that has 9+ call sites. A separate name
158
+ # (``safe_resolve_identifier``) avoids silent breakage at those call sites.
162
159
 
163
160
  _SAFE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,128}$")
164
161