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.
- package/CHANGELOG.md +221 -1
- package/README.md +6 -6
- 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 +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/ensure-venv.sh +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 +1 -1
- 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 +3 -3
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +2 -2
- package/pyproject.toml +1 -1
- package/scripts/postinstall.js +4 -0
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +1 -1
- package/src/superlocalmemory/cli/commands.py +164 -11
- package/src/superlocalmemory/cli/db_migrate.py +0 -2
- package/src/superlocalmemory/cli/gdpr_io.py +1 -1
- package/src/superlocalmemory/cli/main.py +7 -7
- package/src/superlocalmemory/cli/service_installer.py +2 -1
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/core/config.py +41 -7
- package/src/superlocalmemory/core/context_cache.py +0 -2
- package/src/superlocalmemory/core/engine.py +371 -63
- package/src/superlocalmemory/core/evidence_bundle.py +3 -1
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/install_detector.py +131 -0
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
- package/src/superlocalmemory/core/security_primitives.py +3 -6
- package/src/superlocalmemory/core/store_pipeline.py +172 -29
- package/src/superlocalmemory/core/topic_signature.py +0 -2
- package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
- package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +15 -1
- package/src/superlocalmemory/encoding/graph_builder.py +2 -2
- package/src/superlocalmemory/encoding/scene_builder.py +8 -2
- package/src/superlocalmemory/hooks/adapter_base.py +0 -2
- package/src/superlocalmemory/hooks/context_payload.py +0 -2
- package/src/superlocalmemory/hooks/portable_kit.py +8 -8
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
- package/src/superlocalmemory/infra/backup.py +44 -8
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
- package/src/superlocalmemory/learning/arm_catalog.py +0 -2
- package/src/superlocalmemory/learning/bandit.py +0 -2
- package/src/superlocalmemory/learning/bandit_cache.py +0 -2
- package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
- package/src/superlocalmemory/learning/ensemble.py +0 -2
- package/src/superlocalmemory/learning/labeler.py +0 -2
- package/src/superlocalmemory/learning/legacy_migration.py +0 -2
- package/src/superlocalmemory/learning/model_cache.py +0 -2
- package/src/superlocalmemory/learning/ranker.py +0 -2
- package/src/superlocalmemory/learning/reward_archive.py +6 -1
- package/src/superlocalmemory/learning/reward_proxy.py +0 -2
- package/src/superlocalmemory/learning/signal_worker.py +0 -2
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +4 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +7 -3
- package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
- package/src/superlocalmemory/mcp/profiles.py +1 -1
- package/src/superlocalmemory/mcp/server.py +2 -2
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_active.py +56 -0
- package/src/superlocalmemory/mcp/tools_core.py +41 -40
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/optimize/cache/manager.py +2 -2
- package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
- package/src/superlocalmemory/optimize/compress/router.py +1 -1
- package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
- package/src/superlocalmemory/optimize/proxy/server.py +1 -1
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +2 -2
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +9 -2
- package/src/superlocalmemory/retrieval/bm25_channel.py +6 -10
- package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +272 -43
- package/src/superlocalmemory/retrieval/entity_channel.py +8 -2
- package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
- package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -3
- package/src/superlocalmemory/retrieval/scope_policy.py +22 -1
- package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
- package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
- package/src/superlocalmemory/retrieval/strategy.py +94 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +179 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
- package/src/superlocalmemory/retrieval/vector_store.py +151 -10
- package/src/superlocalmemory/server/api.py +6 -1
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/recall_serializer.py +10 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/brain.py +2 -2
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/prewarm.py +4 -4
- package/src/superlocalmemory/server/routes/v3_api.py +2 -2
- package/src/superlocalmemory/server/ui.py +8 -1
- package/src/superlocalmemory/server/unified_daemon.py +570 -16
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +46 -3
- package/src/superlocalmemory/storage/backup.py +531 -0
- package/src/superlocalmemory/storage/database.py +139 -34
- package/src/superlocalmemory/storage/embedding_codec.py +129 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
- package/src/superlocalmemory/storage/migration_runner.py +153 -2
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
- package/src/superlocalmemory/storage/migrations.py +15 -1
- package/src/superlocalmemory/storage/models.py +7 -0
- package/src/superlocalmemory/storage/quantized_store.py +4 -2
- package/src/superlocalmemory/storage/schema.py +98 -0
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/summaries/session_summary.py +1 -1
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
- package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/core.js +1 -1
- package/src/superlocalmemory/ui/js/od-brain.js +1 -1
- package/src/superlocalmemory/ui/js/od-memories.js +190 -1
- 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
|
-
#
|
|
642
|
-
#
|
|
643
|
-
#
|
|
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
|
-
|
|
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
|
|
699
|
+
"Fact consolidation: %d display summaries over %d facts "
|
|
700
|
+
"(%d clusters refused)",
|
|
666
701
|
fc_stats.get("consolidated", 0),
|
|
667
|
-
fc_stats.get("
|
|
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 (
|
|
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
|
|
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 (
|
|
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 |
|
|
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
|
|
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
|
|
159
|
-
#
|
|
160
|
-
#
|
|
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
|
|