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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.53",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.53"
3
+ version = "3.4.54"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
5
5
 
6
- __version__ = "3.4.53"
6
+ __version__ = "3.4.54"
7
7
 
8
8
  _REQUIRED_VERSIONS = {
9
9
  "sentence_transformers": "5.3.0",
@@ -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, _ = _apply_mode_change(args.value)
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, llm_was_set = _apply_mode_change(args.value)
674
+ updated = SLMConfig.switch_mode(args.value)
694
675
  print(f"Mode set to: {args.value.upper()}")
695
-
696
- # v3.4.43: embedding/retrieval are now preserved, so the old
697
- # "Embedding model changed. Re-indexing will run on next recall."
698
- # warning no longer fires from a CLI mode switch — that was the
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.4.53
3
+ Version: 3.4.54
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later