superlocalmemory 3.7.5 → 3.7.7
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 +27 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-recall/SKILL.md +4 -3
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +4 -3
- package/pyproject.toml +6 -6
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +169 -9
- package/src/superlocalmemory/cli/setup_wizard.py +53 -2
- package/src/superlocalmemory/core/backend_orchestrator.py +6 -1
- package/src/superlocalmemory/core/config.py +1 -1
- package/src/superlocalmemory/core/engine.py +3 -2
- package/src/superlocalmemory/core/engine_wiring.py +3 -1
- package/src/superlocalmemory/core/scale_engine.py +9 -1
- package/src/superlocalmemory/core/store_pipeline.py +1 -1
- package/src/superlocalmemory/hooks/before_web_hook.py +1 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +1 -1
- package/src/superlocalmemory/infra/auth_middleware.py +5 -5
- package/src/superlocalmemory/mcp/_daemon_proxy.py +8 -10
- package/src/superlocalmemory/mcp/server.py +1 -0
- package/src/superlocalmemory/mcp/tools_active.py +11 -7
- package/src/superlocalmemory/mcp/tools_core.py +178 -20
- package/src/superlocalmemory/optimize/cache/centroid_store.py +21 -3
- package/src/superlocalmemory/optimize/cache/manager.py +7 -0
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -10
- package/src/superlocalmemory/retrieval/engine.py +16 -8
- package/src/superlocalmemory/server/profile_runtime.py +384 -0
- package/src/superlocalmemory/server/recall_health.py +13 -7
- package/src/superlocalmemory/server/routes/chat.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +9 -16
- package/src/superlocalmemory/server/routes/profiles.py +24 -14
- package/src/superlocalmemory/server/routes/v3_api.py +97 -20
- package/src/superlocalmemory/server/unified_daemon.py +261 -60
- package/src/superlocalmemory/storage/migration_runner.py +44 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +32 -3
- package/src/superlocalmemory/ui/index.html +32 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +48 -0
- package/src/superlocalmemory/ui/js/memory-chat.js +2 -2
- package/src/superlocalmemory/ui/js/profiles.js +11 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +42 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.7.7] - 2026-07-20 — Profile isolation and runtime integrity
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixes daemon-aware profile switching and profile-isolated CLI reads/writes. Profile transitions now drain admitted operations, atomically rebind the resident engine, persist only after a successful transition, and return a generation-stamped acknowledgement to CLI, dashboard/API, and MCP callers without restarting the daemon.
|
|
13
|
+
- Dashboard mode, provider, embedding, and memory-visibility changes now take effect through the same daemon-owned runtime transition boundary. Existing custom embedding settings survive setup-mode changes, and Optimize cache vectors follow the configured embedding dimension instead of assuming 768 dimensions.
|
|
14
|
+
- Migration reconciliation now accepts only explicitly allowlisted historical hashes and verifies the complete required schema before updating migration metadata; unknown or structurally incomplete drift fails closed.
|
|
15
|
+
- Restored cross-platform UI test discovery and added dependency and high-severity static security gates to CI.
|
|
16
|
+
- Upgraded the audited web, MCP, cryptography, and Transformers dependency stack to patched releases. Three narrowly scoped NLTK, setuptools, and PyTorch advisories are tracked as exact, dated exceptions; PyTorch remains on the proven 2.11 runtime because the combined native/ML upgrade produced a full-suite process crash and could not be attributed safely to one package.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Dashboard controls and API/CLI configuration support for the default write scope and explicit shared/global recall opt-ins. Personal-only recall remains the default.
|
|
21
|
+
|
|
22
|
+
## [3.7.6] - 2026-07-19 — Auth, upgrade, and embedding-dimension fixes
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Writes authenticated by the daemon capability or the dashboard install token are no longer rejected once an `api_key` file is configured. The write path ran a redundant second gate that only understood `X-SLM-API-Key`, so capability-authenticated MCP `remember` write-throughs and install-token dashboard writes / config tests returned 401 "Invalid or missing API key" whenever opt-in API-key auth was enabled. The mutation-actor gate — which already accepts the daemon capability, the install token, a matching API key, or an uncredentialed loopback caller — is now the single authoritative write boundary. (#71, #73, #74)
|
|
27
|
+
- Upgrading an install across a benign migration DDL change no longer leaves the daemon permanently `not_ready`. On a `ddl_sha256` mismatch for a migration already marked complete, the runner now consults the migration's `verify()`; when the schema end-state is present it reconciles the log to the current hash instead of failing readiness. Real drift with an absent schema is still surfaced as a failure. (#70)
|
|
28
|
+
- The LanceDB vector backend now follows the configured embedding dimension instead of a hardcoded 768, so custom OpenAI-compatible endpoints (for example 1024-d Qwen3-Embedding) no longer hit a vector-dimension mismatch on initialization. An existing store's on-disk width is always honored, keeping already-materialized data readable after a configuration change. (#72)
|
|
29
|
+
|
|
30
|
+
### Improved
|
|
31
|
+
|
|
32
|
+
- `slm setup` skips the local 768-d embedding-model download when a remote/OpenAI-compatible embedding endpoint is configured, instead of forcing an unnecessary model fetch. (#72)
|
|
33
|
+
- Aligned retrieval documentation and docstrings with the shipped architecture: five parallel candidate producers (semantic, BM25, temporal, spreading-activation, Hopfield) feed single-pass RRF fusion, followed by optional cross-encoder rerank and an entity-graph post-fusion score enhancement. The entity graph is not a sixth parallel candidate producer.
|
|
34
|
+
|
|
8
35
|
## [3.7.5] - 2026-07-18 — Complete Scale Engine projection parity
|
|
9
36
|
|
|
10
37
|
### Improved
|
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V3.7.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V3.7.7</h1>
|
|
9
9
|
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
|
|
10
10
|
<em>Local-first agent memory with explicit operating modes, auditable retrieval, and optional Optimize tools.</em></p>
|
|
11
|
-
<p align="center"><code>v3.7.
|
|
11
|
+
<p align="center"><code>v3.7.7</code> — <strong>Daemon-aware profile isolation, atomic runtime configuration, and verified migration integrity.</strong><br/>
|
|
12
12
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
13
13
|
<p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.7",
|
|
4
4
|
"description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"prepack": "node scripts/build-plugin.mjs && node scripts/prepack.js",
|
|
52
52
|
"postinstall": "node scripts/postinstall.js",
|
|
53
53
|
"preuninstall": "node scripts/preuninstall.js",
|
|
54
|
-
"test": "node
|
|
54
|
+
"test": "node scripts/run-ui-tests.mjs"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=18.0.0",
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.7.
|
|
1
|
+
superlocalmemory==3.7.7
|
|
@@ -142,9 +142,10 @@ once you have the `fact_id` for full content.
|
|
|
142
142
|
|
|
143
143
|
## How multi-channel retrieval works
|
|
144
144
|
|
|
145
|
-
`recall` runs
|
|
146
|
-
BM25, temporal recency, and
|
|
147
|
-
Rank Fusion (RRF)
|
|
145
|
+
`recall` runs five candidate producers in parallel — semantic vector similarity,
|
|
146
|
+
lexical BM25, temporal recency, spreading activation, and Hopfield — then fuses
|
|
147
|
+
them with Reciprocal Rank Fusion (RRF), applies a reranker, and layers an
|
|
148
|
+
optional entity-graph post-fusion score enhancement. The `channel_weights` field in the
|
|
148
149
|
response shows how each channel contributed for that query. Weights adapt over
|
|
149
150
|
time based on engagement signals attributed via `session_id`.
|
|
150
151
|
|
package/plugin-src/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.7.
|
|
1
|
+
superlocalmemory==3.7.7
|
|
@@ -142,9 +142,10 @@ once you have the `fact_id` for full content.
|
|
|
142
142
|
|
|
143
143
|
## How multi-channel retrieval works
|
|
144
144
|
|
|
145
|
-
`recall` runs
|
|
146
|
-
BM25, temporal recency, and
|
|
147
|
-
Rank Fusion (RRF)
|
|
145
|
+
`recall` runs five candidate producers in parallel — semantic vector similarity,
|
|
146
|
+
lexical BM25, temporal recency, spreading activation, and Hopfield — then fuses
|
|
147
|
+
them with Reciprocal Rank Fusion (RRF), applies a reranker, and layers an
|
|
148
|
+
optional entity-graph post-fusion score enhancement. The `channel_weights` field in the
|
|
148
149
|
response shows how each channel contributed for that query. Weights adapt over
|
|
149
150
|
time based on engagement signals attributed via `session_id`.
|
|
150
151
|
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superlocalmemory"
|
|
3
|
-
version = "3.7.
|
|
3
|
+
version = "3.7.7"
|
|
4
4
|
description = "Local-first agent memory with auditable hybrid retrieval"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-or-later"
|
|
@@ -37,12 +37,12 @@ dependencies = [
|
|
|
37
37
|
"numpy==2.4.4",
|
|
38
38
|
"scipy==1.17.1",
|
|
39
39
|
"networkx==3.6.1",
|
|
40
|
-
"mcp==1.
|
|
40
|
+
"mcp==1.28.1",
|
|
41
41
|
"python-dateutil==2.9.0.post0",
|
|
42
42
|
"rank-bm25==0.2.2",
|
|
43
43
|
"vadersentiment==3.3.2",
|
|
44
44
|
"einops==0.8.2",
|
|
45
|
-
"fastapi[all]==0.
|
|
45
|
+
"fastapi[all]==0.139.2",
|
|
46
46
|
"uvicorn==0.46.0",
|
|
47
47
|
"websockets==16.0",
|
|
48
48
|
"zeroconf>=0.140",
|
|
@@ -59,7 +59,7 @@ dependencies = [
|
|
|
59
59
|
"psutil==7.2.2",
|
|
60
60
|
"structlog==25.5.0",
|
|
61
61
|
"portalocker==3.2.0",
|
|
62
|
-
"cryptography==
|
|
62
|
+
"cryptography==48.0.1",
|
|
63
63
|
# Semantic search + cross-encoder reranker. Do NOT use
|
|
64
64
|
# sentence-transformers[onnx] — its extras pull optimum which
|
|
65
65
|
# overrides the sentence-transformers pin via transitive deps.
|
|
@@ -67,8 +67,8 @@ dependencies = [
|
|
|
67
67
|
"sentence-transformers==5.3.0",
|
|
68
68
|
"optimum==2.1.0",
|
|
69
69
|
"onnxruntime==1.24.4",
|
|
70
|
-
"transformers==
|
|
71
|
-
"huggingface_hub==
|
|
70
|
+
"transformers==5.5.4",
|
|
71
|
+
"huggingface_hub==1.5.0",
|
|
72
72
|
"torch==2.11.0",
|
|
73
73
|
"scikit-learn==1.8.0",
|
|
74
74
|
# Vector KNN extension for the semantic channel.
|
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.7.
|
|
35
|
+
__version__ = "3.7.7"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -612,6 +612,8 @@ def cmd_config(args: Namespace) -> None:
|
|
|
612
612
|
"evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
|
|
613
613
|
"mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
|
|
614
614
|
"graph_backend", "vector_backend", "scale_engine_state",
|
|
615
|
+
"scope.default_scope", "scope.recall_include_global",
|
|
616
|
+
"scope.recall_include_shared",
|
|
615
617
|
}
|
|
616
618
|
if key not in _ALLOWED_CONFIG_KEYS:
|
|
617
619
|
if use_json:
|
|
@@ -650,6 +652,60 @@ def cmd_config(args: Namespace) -> None:
|
|
|
650
652
|
except ValueError:
|
|
651
653
|
parsed_value = value
|
|
652
654
|
|
|
655
|
+
if key == "scope.default_scope" and parsed_value not in {
|
|
656
|
+
"personal", "shared", "global",
|
|
657
|
+
}:
|
|
658
|
+
message = "scope.default_scope must be personal, shared, or global"
|
|
659
|
+
if use_json:
|
|
660
|
+
from superlocalmemory.cli.json_output import json_print
|
|
661
|
+
json_print("config", error={
|
|
662
|
+
"code": "INVALID_VALUE", "message": message,
|
|
663
|
+
})
|
|
664
|
+
else:
|
|
665
|
+
print(f"Error: {message}")
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
if key in {
|
|
668
|
+
"scope.recall_include_global", "scope.recall_include_shared",
|
|
669
|
+
} and not isinstance(parsed_value, bool):
|
|
670
|
+
message = f"{key} must be true or false"
|
|
671
|
+
if use_json:
|
|
672
|
+
from superlocalmemory.cli.json_output import json_print
|
|
673
|
+
json_print("config", error={
|
|
674
|
+
"code": "INVALID_VALUE", "message": message,
|
|
675
|
+
})
|
|
676
|
+
else:
|
|
677
|
+
print(f"Error: {message}")
|
|
678
|
+
sys.exit(1)
|
|
679
|
+
|
|
680
|
+
if key.startswith("scope."):
|
|
681
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
682
|
+
|
|
683
|
+
if is_daemon_running():
|
|
684
|
+
field = key.split(".", 1)[1]
|
|
685
|
+
result = daemon_request(
|
|
686
|
+
"PUT", "/api/v3/scope/config", {field: parsed_value},
|
|
687
|
+
)
|
|
688
|
+
if not isinstance(result, dict) or result.get("success") is not True:
|
|
689
|
+
message = "resident daemon rejected the scope configuration"
|
|
690
|
+
if use_json:
|
|
691
|
+
from superlocalmemory.cli.json_output import json_print
|
|
692
|
+
json_print("config", error={
|
|
693
|
+
"code": "CONFIG_APPLY_FAILED", "message": message,
|
|
694
|
+
})
|
|
695
|
+
else:
|
|
696
|
+
print(f"Error: {message}")
|
|
697
|
+
sys.exit(1)
|
|
698
|
+
old_value = None
|
|
699
|
+
if use_json:
|
|
700
|
+
from superlocalmemory.cli.json_output import json_print
|
|
701
|
+
json_print("config", data={
|
|
702
|
+
"key": key, "old_value": old_value,
|
|
703
|
+
"new_value": result.get(field), "runtime": "daemon",
|
|
704
|
+
})
|
|
705
|
+
else:
|
|
706
|
+
print(f"{key}: applied to resident daemon -> {result.get(field)}")
|
|
707
|
+
return
|
|
708
|
+
|
|
653
709
|
# Set via dot-notation (e.g. "evolution.enabled" -> cfg["evolution"]["enabled"])
|
|
654
710
|
parts = key.split(".")
|
|
655
711
|
node = cfg
|
|
@@ -1579,10 +1635,47 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1579
1635
|
from superlocalmemory.core.config import SLMConfig
|
|
1580
1636
|
|
|
1581
1637
|
config = SLMConfig.load()
|
|
1638
|
+
daemon_status = None
|
|
1639
|
+
try:
|
|
1640
|
+
from superlocalmemory.cli.daemon import (
|
|
1641
|
+
daemon_request,
|
|
1642
|
+
is_daemon_running,
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
if is_daemon_running():
|
|
1646
|
+
candidate = daemon_request("GET", "/status")
|
|
1647
|
+
if isinstance(candidate, dict) and candidate.get("profile"):
|
|
1648
|
+
daemon_status = candidate
|
|
1649
|
+
except Exception:
|
|
1650
|
+
logger.debug(
|
|
1651
|
+
"cmd_status: daemon runtime status unavailable; using offline view",
|
|
1652
|
+
exc_info=True,
|
|
1653
|
+
)
|
|
1582
1654
|
|
|
1583
1655
|
if getattr(args, 'json', False):
|
|
1584
1656
|
from superlocalmemory.cli.json_output import json_print
|
|
1585
1657
|
|
|
1658
|
+
if daemon_status is not None:
|
|
1659
|
+
data = {
|
|
1660
|
+
"mode": str(daemon_status.get("mode", "unknown")).upper(),
|
|
1661
|
+
"provider": daemon_status.get("provider", "none"),
|
|
1662
|
+
"profile": daemon_status["profile"],
|
|
1663
|
+
"base_dir": daemon_status.get("base_dir", str(config.base_dir)),
|
|
1664
|
+
"db_path": daemon_status.get("db_path", str(config.db_path)),
|
|
1665
|
+
"db_size_mb": float(daemon_status.get("db_size_mb", 0.0)),
|
|
1666
|
+
"fact_count": int(daemon_status.get("fact_count", 0)),
|
|
1667
|
+
"entity_count": int(daemon_status.get("entity_count", 0)),
|
|
1668
|
+
"edge_count": int(daemon_status.get("edge_count", 0)),
|
|
1669
|
+
"profile_generation": int(
|
|
1670
|
+
daemon_status.get("profile_generation", 0)
|
|
1671
|
+
),
|
|
1672
|
+
}
|
|
1673
|
+
json_print("status", data=data, next_actions=[
|
|
1674
|
+
{"command": "slm health --json", "description": "Check math layer health"},
|
|
1675
|
+
{"command": "slm list --json", "description": "List recent memories"},
|
|
1676
|
+
])
|
|
1677
|
+
return
|
|
1678
|
+
|
|
1586
1679
|
# WP-02 D8: canonical key set — db_size_mb always present (0.0 if absent).
|
|
1587
1680
|
db_size_mb = 0.0
|
|
1588
1681
|
if config.db_path.exists():
|
|
@@ -1635,6 +1728,7 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1635
1728
|
"fact_count": fact_count,
|
|
1636
1729
|
"entity_count": entity_count,
|
|
1637
1730
|
"edge_count": edge_count,
|
|
1731
|
+
"profile_generation": 0,
|
|
1638
1732
|
}
|
|
1639
1733
|
json_print("status", data=data, next_actions=[
|
|
1640
1734
|
{"command": "slm health --json", "description": "Check math layer health"},
|
|
@@ -1645,6 +1739,10 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1645
1739
|
print("SuperLocalMemory V3")
|
|
1646
1740
|
print(f" Mode: {config.mode.value.upper()}")
|
|
1647
1741
|
print(f" Provider: {config.llm.provider or 'none'}")
|
|
1742
|
+
print(
|
|
1743
|
+
f" Profile: "
|
|
1744
|
+
f"{daemon_status.get('profile') if daemon_status else config.active_profile}"
|
|
1745
|
+
)
|
|
1648
1746
|
print(f" Base dir: {config.base_dir}")
|
|
1649
1747
|
print(f" Database: {config.db_path}")
|
|
1650
1748
|
if config.db_path.exists():
|
|
@@ -2522,6 +2620,42 @@ def cmd_dashboard(args: Namespace) -> None:
|
|
|
2522
2620
|
# -- Profiles (supports --json) -------------------------------------------
|
|
2523
2621
|
|
|
2524
2622
|
|
|
2623
|
+
def _switch_profile_runtime(config, profile_name: str) -> dict:
|
|
2624
|
+
"""Switch through the resident daemon, or persist an offline fallback."""
|
|
2625
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
2626
|
+
|
|
2627
|
+
if is_daemon_running():
|
|
2628
|
+
result = daemon_request(
|
|
2629
|
+
"POST",
|
|
2630
|
+
f"/api/profiles/{profile_name}/switch",
|
|
2631
|
+
)
|
|
2632
|
+
if not result or not result.get("success"):
|
|
2633
|
+
raise RuntimeError(
|
|
2634
|
+
"resident daemon did not acknowledge the profile switch"
|
|
2635
|
+
)
|
|
2636
|
+
acknowledged = str(result.get("active_profile", ""))
|
|
2637
|
+
if acknowledged != profile_name:
|
|
2638
|
+
raise RuntimeError(
|
|
2639
|
+
"resident daemon acknowledged a different active profile"
|
|
2640
|
+
)
|
|
2641
|
+
return {
|
|
2642
|
+
"action": "switched",
|
|
2643
|
+
"profile": acknowledged,
|
|
2644
|
+
"generation": int(result.get("generation", 0)),
|
|
2645
|
+
"runtime": "daemon",
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
from superlocalmemory.server.profile_runtime import persist_active_profile
|
|
2649
|
+
|
|
2650
|
+
persist_active_profile(profile_name)
|
|
2651
|
+
config.active_profile = profile_name
|
|
2652
|
+
return {
|
|
2653
|
+
"action": "switched",
|
|
2654
|
+
"profile": profile_name,
|
|
2655
|
+
"runtime": "offline",
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
|
|
2525
2659
|
def cmd_profile(args: Namespace) -> None:
|
|
2526
2660
|
"""Profile management (list, switch, create).
|
|
2527
2661
|
|
|
@@ -2532,7 +2666,7 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
2532
2666
|
from superlocalmemory.storage.database import DatabaseManager
|
|
2533
2667
|
from superlocalmemory.storage import schema
|
|
2534
2668
|
from superlocalmemory.server.routes.helpers import (
|
|
2535
|
-
ensure_profile_in_json,
|
|
2669
|
+
ensure_profile_in_json,
|
|
2536
2670
|
)
|
|
2537
2671
|
|
|
2538
2672
|
config = SLMConfig.load()
|
|
@@ -2552,10 +2686,25 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
2552
2686
|
{"command": "slm profile switch <name> --json", "description": "Switch profile"},
|
|
2553
2687
|
])
|
|
2554
2688
|
elif args.action == "switch":
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2689
|
+
rows = db.execute(
|
|
2690
|
+
"SELECT 1 FROM profiles WHERE profile_id = ?",
|
|
2691
|
+
(args.name,),
|
|
2692
|
+
)
|
|
2693
|
+
if not rows:
|
|
2694
|
+
json_print("profile", error={
|
|
2695
|
+
"code": "PROFILE_NOT_FOUND",
|
|
2696
|
+
"message": f"Profile '{args.name}' does not exist.",
|
|
2697
|
+
})
|
|
2698
|
+
sys.exit(1)
|
|
2699
|
+
try:
|
|
2700
|
+
result = _switch_profile_runtime(config, args.name)
|
|
2701
|
+
except Exception as exc:
|
|
2702
|
+
json_print("profile", error={
|
|
2703
|
+
"code": "PROFILE_SWITCH_FAILED",
|
|
2704
|
+
"message": str(exc),
|
|
2705
|
+
})
|
|
2706
|
+
sys.exit(1)
|
|
2707
|
+
json_print("profile", data=result)
|
|
2559
2708
|
elif args.action == "create":
|
|
2560
2709
|
db.execute(
|
|
2561
2710
|
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
@@ -2576,10 +2725,21 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
2576
2725
|
d = dict(r)
|
|
2577
2726
|
print(f" - {d['profile_id']}: {d.get('name', '')}")
|
|
2578
2727
|
elif args.action == "switch":
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2728
|
+
rows = db.execute(
|
|
2729
|
+
"SELECT 1 FROM profiles WHERE profile_id = ?",
|
|
2730
|
+
(args.name,),
|
|
2731
|
+
)
|
|
2732
|
+
if not rows:
|
|
2733
|
+
print(f"Profile '{args.name}' does not exist.", file=sys.stderr)
|
|
2734
|
+
sys.exit(1)
|
|
2735
|
+
try:
|
|
2736
|
+
result = _switch_profile_runtime(config, args.name)
|
|
2737
|
+
except Exception as exc:
|
|
2738
|
+
print(f"Profile switch failed: {exc}", file=sys.stderr)
|
|
2739
|
+
sys.exit(1)
|
|
2740
|
+
generation = result.get("generation")
|
|
2741
|
+
suffix = f" (generation {generation})" if generation is not None else ""
|
|
2742
|
+
print(f"Switched to profile: {args.name}{suffix}")
|
|
2583
2743
|
elif args.action == "create":
|
|
2584
2744
|
db.execute(
|
|
2585
2745
|
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
@@ -22,6 +22,7 @@ import subprocess
|
|
|
22
22
|
import sys
|
|
23
23
|
import time
|
|
24
24
|
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
# ---------------------------------------------------------------------------
|
|
@@ -227,6 +228,46 @@ def _download_compressor(model_name: str) -> bool:
|
|
|
227
228
|
return False
|
|
228
229
|
|
|
229
230
|
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# Embedding-provider detection (v3.7.6 #72)
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def _embedding_is_remote(config: Any) -> bool:
|
|
236
|
+
"""True when embeddings come from a remote/OpenAI-compatible endpoint.
|
|
237
|
+
|
|
238
|
+
In that case the local sentence-transformers download (768d nomic) is
|
|
239
|
+
unnecessary and would only waste bandwidth/disk (#72). Detection is by
|
|
240
|
+
provider name or the presence of a configured HTTP endpoint.
|
|
241
|
+
"""
|
|
242
|
+
emb = getattr(config, "embedding", None)
|
|
243
|
+
if emb is None:
|
|
244
|
+
return False
|
|
245
|
+
provider = (getattr(emb, "provider", "") or "").strip().lower()
|
|
246
|
+
endpoint = (
|
|
247
|
+
getattr(emb, "api_endpoint", "")
|
|
248
|
+
or getattr(emb, "base_url", "")
|
|
249
|
+
or ""
|
|
250
|
+
)
|
|
251
|
+
return provider in ("openai", "openai-compatible", "remote") or bool(endpoint)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _build_wizard_config(mode):
|
|
255
|
+
"""Apply mode-owned presets without erasing existing user-owned blocks."""
|
|
256
|
+
from superlocalmemory.core.config import SLMConfig
|
|
257
|
+
from superlocalmemory.infra.data_root import state_path
|
|
258
|
+
|
|
259
|
+
template = SLMConfig.for_mode(mode)
|
|
260
|
+
if not state_path("config.json").exists():
|
|
261
|
+
return template
|
|
262
|
+
existing = SLMConfig.load()
|
|
263
|
+
existing.mode = mode
|
|
264
|
+
existing.llm = template.llm
|
|
265
|
+
existing.retrieval = template.retrieval
|
|
266
|
+
existing.math = template.math
|
|
267
|
+
existing.channel_weights = template.channel_weights
|
|
268
|
+
return existing
|
|
269
|
+
|
|
270
|
+
|
|
230
271
|
# ---------------------------------------------------------------------------
|
|
231
272
|
# Verification
|
|
232
273
|
# ---------------------------------------------------------------------------
|
|
@@ -373,7 +414,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
373
414
|
from superlocalmemory.storage.models import Mode
|
|
374
415
|
|
|
375
416
|
mode_map = {"a": Mode.A, "b": Mode.B, "c": Mode.C}
|
|
376
|
-
config =
|
|
417
|
+
config = _build_wizard_config(mode_map[choice])
|
|
377
418
|
|
|
378
419
|
# -- Multi-scope (shared memory) opt-in — v3.6.15 --
|
|
379
420
|
# OFF by default: your memories stay private to this profile (3.6.14 behaviour).
|
|
@@ -455,7 +496,17 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
455
496
|
print()
|
|
456
497
|
print("─── Step 4/10: Download Embedding Model ───")
|
|
457
498
|
|
|
458
|
-
if
|
|
499
|
+
if _embedding_is_remote(config):
|
|
500
|
+
# v3.7.6 (#72): a remote/OpenAI-compatible embedding endpoint supplies
|
|
501
|
+
# its own vectors (any width, e.g. 1024d Qwen3-Embedding), so pulling the
|
|
502
|
+
# local 768d nomic model would be wasted bandwidth and could imply a
|
|
503
|
+
# dimension that does not match the endpoint.
|
|
504
|
+
emb = config.embedding
|
|
505
|
+
print(" ✓ Skipped — remote/OpenAI-compatible embedding endpoint configured")
|
|
506
|
+
print(f" provider={getattr(emb, 'provider', '?')}, "
|
|
507
|
+
f"dimension={getattr(emb, 'dimension', '?')}")
|
|
508
|
+
print(" No local embedding model needed.")
|
|
509
|
+
elif not st_ok:
|
|
459
510
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
460
511
|
print(" Models will download on first use.")
|
|
461
512
|
else:
|
|
@@ -354,7 +354,12 @@ class BackendOrchestrator:
|
|
|
354
354
|
try:
|
|
355
355
|
from superlocalmemory.vector.lancedb_backend import LanceDBVectorBackend
|
|
356
356
|
lance_path = self._data_dir / "lance"
|
|
357
|
-
|
|
357
|
+
# v3.7.6 (#72): honor the configured embedding width instead of the
|
|
358
|
+
# hardcoded 768d, so custom endpoints (e.g. 1024d Qwen3-Embedding) work.
|
|
359
|
+
dimension = getattr(
|
|
360
|
+
getattr(self._config, "embedding", None), "dimension", None
|
|
361
|
+
)
|
|
362
|
+
self._lancedb = LanceDBVectorBackend(str(lance_path), dimension=dimension)
|
|
358
363
|
self._update_status("lancedb", "not_initialized")
|
|
359
364
|
logger.info("LanceDB initialized at %s", lance_path)
|
|
360
365
|
except Exception as exc:
|
|
@@ -258,7 +258,7 @@ class RetrievalConfig:
|
|
|
258
258
|
# Reranking (V3.3.2: ONNX backend enabled for all modes)
|
|
259
259
|
# V3.4.2: Tested gte-reranker-modernbert-base (8K context) — REGRESSED
|
|
260
260
|
# LoCoMo from 68.4% to 64.1%. Reverted to MiniLM-L-12-v2. The 512-token
|
|
261
|
-
# limit is acceptable because SLM's
|
|
261
|
+
# limit is acceptable because SLM's multi-producer retrieval pre-filters
|
|
262
262
|
# relevant facts before reranking. See bench-v342-locomo.md.
|
|
263
263
|
use_cross_encoder: bool = True
|
|
264
264
|
cross_encoder_model: str = "cross-encoder/ms-marco-MiniLM-L-12-v2"
|
|
@@ -51,8 +51,9 @@ class MemoryEngine:
|
|
|
51
51
|
"""Main orchestrator for the SuperLocalMemory V3 memory system.
|
|
52
52
|
|
|
53
53
|
Wires encoding (fact extraction, entity resolution, graph building,
|
|
54
|
-
consolidation) with retrieval (
|
|
55
|
-
reranking) and all supporting
|
|
54
|
+
consolidation) with retrieval (five candidate producers -> RRF fusion,
|
|
55
|
+
reranking, entity-graph post-fusion enhancement) and all supporting
|
|
56
|
+
layers (trust, learning, compliance).
|
|
56
57
|
|
|
57
58
|
Usage::
|
|
58
59
|
|
|
@@ -500,7 +500,9 @@ def init_retrieval(
|
|
|
500
500
|
trust_scorer: Any,
|
|
501
501
|
vector_store: Any = None,
|
|
502
502
|
) -> Any:
|
|
503
|
-
"""Create the RetrievalEngine
|
|
503
|
+
"""Create the RetrievalEngine — five candidate producers (semantic, BM25,
|
|
504
|
+
temporal, spreading_activation, hopfield) plus the entity graph used for
|
|
505
|
+
post-fusion score enhancement. Returns it."""
|
|
504
506
|
from superlocalmemory.retrieval.engine import RetrievalEngine
|
|
505
507
|
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
506
508
|
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
@@ -487,7 +487,15 @@ class ScaleEngineManager:
|
|
|
487
487
|
def _real_backend_factory(self, cozo_dir: Path, lance_dir: Path) -> tuple[Any, Any]:
|
|
488
488
|
from superlocalmemory.graph.cozo_backend import CozoDBGraphBackend
|
|
489
489
|
from superlocalmemory.vector.lancedb_backend import LanceDBVectorBackend
|
|
490
|
-
|
|
490
|
+
# v3.7.6 (#72): promote the configured embedding width into the new
|
|
491
|
+
# LanceDB store so a 1024d (or other) custom endpoint survives promotion.
|
|
492
|
+
dimension = getattr(
|
|
493
|
+
getattr(self.config, "embedding", None), "dimension", None
|
|
494
|
+
)
|
|
495
|
+
return (
|
|
496
|
+
CozoDBGraphBackend(str(cozo_dir / "graph")),
|
|
497
|
+
LanceDBVectorBackend(str(lance_dir), dimension=dimension),
|
|
498
|
+
)
|
|
491
499
|
|
|
492
500
|
def _readonly_connection(self) -> sqlite3.Connection:
|
|
493
501
|
conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True)
|
|
@@ -310,7 +310,7 @@ def run_store(
|
|
|
310
310
|
# that fact extraction may abstract away (dates, names, specifics).
|
|
311
311
|
# This ensures BM25 and semantic search can always find the original text.
|
|
312
312
|
# V3.3.12: Extract entities from verbatim content so entity channel + temporal
|
|
313
|
-
# channel can find it (was entities=[] which
|
|
313
|
+
# channel can find it (was entities=[] which blinded the entity-graph and temporal signals).
|
|
314
314
|
# V3.3.20: Stronger verbatim filter — skip greetings, filler, short phrases.
|
|
315
315
|
# Verbatim facts with just "Hey! How are you?" dilute embeddings and add noise.
|
|
316
316
|
_MIN_VERBATIM_WORDS = 8
|
|
@@ -17,7 +17,7 @@ the local memories before committing to the external call.
|
|
|
17
17
|
|
|
18
18
|
PERFORMANCE
|
|
19
19
|
===========
|
|
20
|
-
Cost: ~500-800ms warm (full
|
|
20
|
+
Cost: ~500-800ms warm (full recall via SLM daemon). Fires only on
|
|
21
21
|
WebSearch and WebFetch (5-20× per typical session), so per-session overhead
|
|
22
22
|
is ~5-15s in exchange for grounded answers. NOT suitable for UserPromptSubmit
|
|
23
23
|
(too frequent — would be a perf disaster).
|
|
@@ -267,7 +267,7 @@ def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
# v3.4.43 — default PreToolUse entry: pre-web recall on WebSearch/WebFetch.
|
|
270
|
-
# Fires `slm hook before_web` which runs a
|
|
270
|
+
# Fires `slm hook before_web` which runs a recall on the search
|
|
271
271
|
# query/URL and injects results as a system-reminder BEFORE the web call.
|
|
272
272
|
# Encourages Claude to consider local memories before paying for new web
|
|
273
273
|
# research. Independent of `include_gate` — this is value-add, not gating.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
"""
|
|
4
|
+
"""API-key primitives used by the unified identity boundary.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
A configured key authorizes remote callers that present ``X-SLM-API-Key``.
|
|
7
|
+
The unified daemon separately trusts exact-process capabilities, the
|
|
8
|
+
same-origin dashboard install token, and uncredentialed loopback peers as the
|
|
9
|
+
local OS-user boundary. Read endpoints remain open for backward compatibility.
|
|
10
10
|
|
|
11
11
|
V3 change: base directory moved from ``~/.claude-memory/`` to
|
|
12
12
|
``~/.superlocalmemory/``.
|