superlocalmemory 3.4.53 → 3.4.54
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +7 -31
- package/src/superlocalmemory/core/config.py +183 -0
- package/src/superlocalmemory/server/unified_daemon.py +3 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.54",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -652,30 +652,11 @@ def cmd_mode(args: Namespace) -> None:
|
|
|
652
652
|
|
|
653
653
|
config = SLMConfig.load()
|
|
654
654
|
|
|
655
|
-
def _apply_mode_change(new_value: str) -> tuple[SLMConfig, bool]:
|
|
656
|
-
"""Mutate-in-place mode switch. Returns (updated_config, llm_was_set).
|
|
657
|
-
|
|
658
|
-
Only changes ``config.mode``. If the user has no LLM provider
|
|
659
|
-
configured AND is moving to Mode B or C, populates the mode's
|
|
660
|
-
default LLM block so the daemon has something to talk to.
|
|
661
|
-
Everything else (embedding, retrieval, evolution, forgetting,
|
|
662
|
-
math, profile) is preserved byte-for-byte.
|
|
663
|
-
"""
|
|
664
|
-
new_mode = Mode(new_value)
|
|
665
|
-
llm_was_set = False
|
|
666
|
-
if new_mode != Mode.A and not config.llm.provider:
|
|
667
|
-
defaults = SLMConfig.for_mode(new_mode)
|
|
668
|
-
config.llm = defaults.llm
|
|
669
|
-
llm_was_set = True
|
|
670
|
-
config.mode = new_mode
|
|
671
|
-
config.save(mode_change=True)
|
|
672
|
-
return config, llm_was_set
|
|
673
|
-
|
|
674
655
|
if getattr(args, 'json', False):
|
|
675
656
|
from superlocalmemory.cli.json_output import json_print
|
|
676
657
|
if args.value:
|
|
677
658
|
old_mode = config.mode.value.upper()
|
|
678
|
-
updated
|
|
659
|
+
updated = SLMConfig.switch_mode(args.value)
|
|
679
660
|
json_print("mode", data={
|
|
680
661
|
"previous_mode": old_mode, "current_mode": args.value.upper(),
|
|
681
662
|
}, next_actions=[
|
|
@@ -690,22 +671,17 @@ def cmd_mode(args: Namespace) -> None:
|
|
|
690
671
|
return
|
|
691
672
|
|
|
692
673
|
if args.value:
|
|
693
|
-
updated
|
|
674
|
+
updated = SLMConfig.switch_mode(args.value)
|
|
694
675
|
print(f"Mode set to: {args.value.upper()}")
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
# symptom of the bug. The warning is retained ONLY as an
|
|
700
|
-
# informational note when LLM defaults were freshly populated.
|
|
701
|
-
if llm_was_set:
|
|
702
|
-
print(f" ℹ LLM provider populated from mode defaults: "
|
|
703
|
-
f"{updated.llm.provider}/{updated.llm.model}. "
|
|
704
|
-
f"Run `slm provider set` to customize.")
|
|
676
|
+
print(f" Embedding: {updated.embedding.provider}/{updated.embedding.model_name}")
|
|
677
|
+
if args.value.lower() != "a":
|
|
678
|
+
print(f" LLM: {updated.llm.provider}/{updated.llm.model}")
|
|
679
|
+
print(f" Reranker: ONNX cross-encoder (enabled)")
|
|
705
680
|
|
|
706
681
|
# V3.3.4: Warn if Mode C lacks cloud API key
|
|
707
682
|
if args.value == "c" and not updated.llm.api_key:
|
|
708
683
|
print(" ⚠ Mode C requires a cloud API key. Run: slm provider set")
|
|
684
|
+
print(" ℹ Run `slm restart` to apply the new mode.")
|
|
709
685
|
else:
|
|
710
686
|
print(f"Current mode: {config.mode.value.upper()}")
|
|
711
687
|
|
|
@@ -12,9 +12,12 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import logging
|
|
15
16
|
from dataclasses import dataclass, field
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
18
21
|
from superlocalmemory.storage.models import Mode
|
|
19
22
|
|
|
20
23
|
|
|
@@ -25,6 +28,9 @@ from superlocalmemory.storage.models import Mode
|
|
|
25
28
|
DEFAULT_BASE_DIR = Path.home() / ".superlocalmemory"
|
|
26
29
|
DEFAULT_DB_NAME = "memory.db"
|
|
27
30
|
DEFAULT_PROFILES_FILE = "profiles.json"
|
|
31
|
+
CURRENT_MODE_FILE = "current_mode"
|
|
32
|
+
# Populated lazily in _get_mode_config_path() to avoid circular imports
|
|
33
|
+
_MODE_CONFIG_NAMES: dict | None = None
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
# ---------------------------------------------------------------------------
|
|
@@ -950,3 +956,180 @@ class SLMConfig:
|
|
|
950
956
|
sheaf_contradiction_threshold=0.65, # Higher for 3072d embeddings
|
|
951
957
|
),
|
|
952
958
|
)
|
|
959
|
+
|
|
960
|
+
# ------------------------------------------------------------------
|
|
961
|
+
# 3-Mode config system (v3.4.54)
|
|
962
|
+
# ------------------------------------------------------------------
|
|
963
|
+
|
|
964
|
+
@staticmethod
|
|
965
|
+
def _mode_config_path(base_dir: Path, mode: "Mode") -> Path:
|
|
966
|
+
"""Return the per-mode config file path."""
|
|
967
|
+
from superlocalmemory.storage.models import Mode as _M
|
|
968
|
+
_names = {
|
|
969
|
+
_M.A: "mode_a.json",
|
|
970
|
+
_M.B: "mode_b.json",
|
|
971
|
+
_M.C: "mode_c.json",
|
|
972
|
+
}
|
|
973
|
+
return base_dir / _names.get(mode, "mode_a.json")
|
|
974
|
+
|
|
975
|
+
@staticmethod
|
|
976
|
+
def read_current_mode(base_dir: Path | None = None) -> str:
|
|
977
|
+
"""Read the current mode from the ``current_mode`` file.
|
|
978
|
+
|
|
979
|
+
Returns ``\"b\"`` (the default) if the file doesn't exist.
|
|
980
|
+
"""
|
|
981
|
+
_base = base_dir or DEFAULT_BASE_DIR
|
|
982
|
+
_f = _base / CURRENT_MODE_FILE
|
|
983
|
+
try:
|
|
984
|
+
return _f.read_text(encoding="utf-8").strip().lower() or "b"
|
|
985
|
+
except (OSError, FileNotFoundError):
|
|
986
|
+
return "b"
|
|
987
|
+
|
|
988
|
+
@staticmethod
|
|
989
|
+
def write_current_mode(mode: str, base_dir: Path | None = None) -> None:
|
|
990
|
+
"""Write the current mode letter to ``current_mode``."""
|
|
991
|
+
_base = base_dir or DEFAULT_BASE_DIR
|
|
992
|
+
_base.mkdir(parents=True, exist_ok=True)
|
|
993
|
+
(_base / CURRENT_MODE_FILE).write_text(
|
|
994
|
+
mode.lower().strip(), encoding="utf-8",
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
@classmethod
|
|
998
|
+
def switch_mode(
|
|
999
|
+
cls,
|
|
1000
|
+
new_mode: str,
|
|
1001
|
+
base_dir: Path | None = None,
|
|
1002
|
+
) -> "SLMConfig":
|
|
1003
|
+
"""Switch to a different mode, preserving the old mode's config.
|
|
1004
|
+
|
|
1005
|
+
v3.4.54: Saves the current config as ``mode_{old}.json``, then
|
|
1006
|
+
loads ``mode_{new}.json`` (or creates it from defaults if it
|
|
1007
|
+
doesn't exist). Writes ``current_mode`` and persists the new
|
|
1008
|
+
active config as ``config.json`` for backward compatibility
|
|
1009
|
+
with tooling that reads it directly.
|
|
1010
|
+
|
|
1011
|
+
Returns the new active config.
|
|
1012
|
+
"""
|
|
1013
|
+
from superlocalmemory.storage.models import Mode as _M
|
|
1014
|
+
_base = base_dir or DEFAULT_BASE_DIR
|
|
1015
|
+
_base.mkdir(parents=True, exist_ok=True)
|
|
1016
|
+
|
|
1017
|
+
old_config = cls.load(_base / "config.json")
|
|
1018
|
+
old_mode = old_config.mode.value.lower()
|
|
1019
|
+
|
|
1020
|
+
new_mode_val = _M(new_mode.lower())
|
|
1021
|
+
|
|
1022
|
+
# 1. Save current config to its per-mode file (unless already
|
|
1023
|
+
# matched — prevents overwriting user customizations)
|
|
1024
|
+
if old_mode != new_mode.lower():
|
|
1025
|
+
old_path = cls._mode_config_path(_base, old_config.mode)
|
|
1026
|
+
old_config.save(old_path)
|
|
1027
|
+
|
|
1028
|
+
# 2. Try to load the target mode's saved config.
|
|
1029
|
+
new_path = cls._mode_config_path(_base, new_mode_val)
|
|
1030
|
+
need_migration = False
|
|
1031
|
+
if new_path.exists():
|
|
1032
|
+
try:
|
|
1033
|
+
new_config = cls.load(new_path)
|
|
1034
|
+
# Ensure the loaded config actually has the right mode
|
|
1035
|
+
if new_config.mode != new_mode_val:
|
|
1036
|
+
new_config = cls.for_mode(new_mode_val, base_dir=_base)
|
|
1037
|
+
except Exception:
|
|
1038
|
+
new_config = cls.for_mode(new_mode_val, base_dir=_base)
|
|
1039
|
+
else:
|
|
1040
|
+
# First time switching to this mode: try migrating from
|
|
1041
|
+
# legacy config.json if it was already in this mode
|
|
1042
|
+
legacy = _base / "config.json"
|
|
1043
|
+
if legacy.exists():
|
|
1044
|
+
try:
|
|
1045
|
+
import json
|
|
1046
|
+
_data = json.loads(legacy.read_text(encoding="utf-8"))
|
|
1047
|
+
_legacy_mode = _data.get("mode", "").lower()
|
|
1048
|
+
if _legacy_mode == new_mode.lower():
|
|
1049
|
+
# config.json IS this mode — use it directly
|
|
1050
|
+
new_config = cls.load(legacy)
|
|
1051
|
+
need_migration = True
|
|
1052
|
+
else:
|
|
1053
|
+
new_config = cls.for_mode(new_mode_val, base_dir=_base)
|
|
1054
|
+
except Exception:
|
|
1055
|
+
new_config = cls.for_mode(new_mode_val, base_dir=_base)
|
|
1056
|
+
else:
|
|
1057
|
+
new_config = cls.for_mode(new_mode_val, base_dir=_base)
|
|
1058
|
+
|
|
1059
|
+
# 3. Save as active config.json (backward compat)
|
|
1060
|
+
new_config.save(_base / "config.json", mode_change=True)
|
|
1061
|
+
|
|
1062
|
+
# 4. Write current_mode
|
|
1063
|
+
cls.write_current_mode(new_mode, _base)
|
|
1064
|
+
|
|
1065
|
+
# 5. On first migration, also save mode_a.json and mode_c.json
|
|
1066
|
+
# so users have a complete set
|
|
1067
|
+
if need_migration:
|
|
1068
|
+
for _m in (_M.A, _M.B, _M.C):
|
|
1069
|
+
_mp = cls._mode_config_path(_base, _m)
|
|
1070
|
+
if not _mp.exists():
|
|
1071
|
+
try:
|
|
1072
|
+
_mc = cls.for_mode(_m, base_dir=_base)
|
|
1073
|
+
_mc.save(_mp)
|
|
1074
|
+
except Exception:
|
|
1075
|
+
pass
|
|
1076
|
+
|
|
1077
|
+
return new_config
|
|
1078
|
+
|
|
1079
|
+
@classmethod
|
|
1080
|
+
def migrate_to_3mode(cls, base_dir: Path | None = None) -> bool:
|
|
1081
|
+
"""One-time migration: config.json → 3-mode system.
|
|
1082
|
+
|
|
1083
|
+
Called on daemon boot. Idempotent — if ``current_mode`` already
|
|
1084
|
+
exists, this is a no-op. Returns True if migration was performed.
|
|
1085
|
+
"""
|
|
1086
|
+
_base = base_dir or DEFAULT_BASE_DIR
|
|
1087
|
+
_current = _base / CURRENT_MODE_FILE
|
|
1088
|
+
if _current.exists():
|
|
1089
|
+
return False # already migrated
|
|
1090
|
+
|
|
1091
|
+
legacy = _base / "config.json"
|
|
1092
|
+
if not legacy.exists():
|
|
1093
|
+
# No config at all — write defaults and current_mode
|
|
1094
|
+
from superlocalmemory.storage.models import Mode as _M
|
|
1095
|
+
_def = cls.for_mode(_M.B, base_dir=_base)
|
|
1096
|
+
_def.save(legacy)
|
|
1097
|
+
cls.write_current_mode("b", _base)
|
|
1098
|
+
for _m in (_M.A, _M.B, _M.C):
|
|
1099
|
+
_mp = cls._mode_config_path(_base, _m)
|
|
1100
|
+
if not _mp.exists():
|
|
1101
|
+
try:
|
|
1102
|
+
_mc = cls.for_mode(_m, base_dir=_base)
|
|
1103
|
+
_mc.save(_mp)
|
|
1104
|
+
except Exception:
|
|
1105
|
+
pass
|
|
1106
|
+
return True
|
|
1107
|
+
|
|
1108
|
+
# Migrate existing config.json → mode_{current}.json
|
|
1109
|
+
try:
|
|
1110
|
+
config = cls.load(legacy)
|
|
1111
|
+
current = config.mode.value.lower()
|
|
1112
|
+
cls.write_current_mode(current, _base)
|
|
1113
|
+
|
|
1114
|
+
# Save current config to its mode file
|
|
1115
|
+
mode_path = cls._mode_config_path(_base, config.mode)
|
|
1116
|
+
config.save(mode_path)
|
|
1117
|
+
|
|
1118
|
+
# Generate other mode files from defaults
|
|
1119
|
+
from superlocalmemory.storage.models import Mode as _M
|
|
1120
|
+
for _m in (_M.A, _M.B, _M.C):
|
|
1121
|
+
_mp = cls._mode_config_path(_base, _m)
|
|
1122
|
+
if not _mp.exists():
|
|
1123
|
+
try:
|
|
1124
|
+
_mc = cls.for_mode(_m, base_dir=_base)
|
|
1125
|
+
_mc.save(_mp)
|
|
1126
|
+
except Exception:
|
|
1127
|
+
pass
|
|
1128
|
+
|
|
1129
|
+
logger.info(
|
|
1130
|
+
"3-mode config system migrated: config.json → mode_%s.json. "
|
|
1131
|
+
"All three mode configs generated.", current,
|
|
1132
|
+
)
|
|
1133
|
+
return True
|
|
1134
|
+
except Exception:
|
|
1135
|
+
return False
|
|
@@ -446,6 +446,9 @@ async def lifespan(application: FastAPI):
|
|
|
446
446
|
from superlocalmemory.core.config import SLMConfig
|
|
447
447
|
from superlocalmemory.core.engine import MemoryEngine
|
|
448
448
|
|
|
449
|
+
# v3.4.54: one-time migration config.json → 3-mode system
|
|
450
|
+
SLMConfig.migrate_to_3mode()
|
|
451
|
+
|
|
449
452
|
config = SLMConfig.load()
|
|
450
453
|
engine = MemoryEngine(config)
|
|
451
454
|
engine.initialize()
|