superlocalmemory 3.4.32 → 3.4.34
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 +40 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +2 -2
- package/src/superlocalmemory/cli/setup_wizard.py +6 -6
- package/src/superlocalmemory/core/config.py +31 -3
- package/src/superlocalmemory/core/embedding_worker.py +1 -1
- package/src/superlocalmemory/learning/bandit.py +18 -0
- package/src/superlocalmemory/learning/reward_proxy.py +7 -2
- package/src/superlocalmemory/mcp/tools_v3.py +14 -2
- package/src/superlocalmemory/server/routes/v3_api.py +1 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +663 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +448 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +59 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [3.4.34] - 2026-04-25
|
|
14
|
+
|
|
15
|
+
Fix: user's mode choice can no longer be silently overwritten.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Mode protection in `SLMConfig.save()`.** Any `save()` call that would
|
|
19
|
+
change the mode in `config.json` is now blocked unless the caller passes
|
|
20
|
+
`mode_change=True`. This prevents accidental mode resets when code creates
|
|
21
|
+
a fresh `SLMConfig()` (defaults to Mode A) and calls `save()` to persist
|
|
22
|
+
an unrelated field change. A warning is logged when a silent mode change
|
|
23
|
+
is blocked.
|
|
24
|
+
- **MCP `set_mode` preserves user settings.** Previously `set_mode` created
|
|
25
|
+
a fresh `SLMConfig.for_mode()` that lost all user customizations (LLM
|
|
26
|
+
provider, API keys, embedding config, active profile). Now carries forward
|
|
27
|
+
all settings from the existing config, matching the dashboard behavior.
|
|
28
|
+
- All intentional mode-change paths (`slm mode`, MCP `set_mode`, dashboard
|
|
29
|
+
PUT `/api/v3/mode`, setup wizard) pass `mode_change=True`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [3.4.33] - 2026-04-25
|
|
34
|
+
|
|
35
|
+
Fix: daemon leaked SQLite connections to learning.db via bandit threadlocals.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **Bandit threadlocal connection leak.** `reward_proxy.settle_stale_plays`
|
|
39
|
+
creates a `ContextualBandit` that opens a threadlocal connection via
|
|
40
|
+
`_conn_for`. When called from `asyncio.to_thread` (bandit_loops.py,
|
|
41
|
+
every 60 s), each thread-pool thread kept its connection open for the
|
|
42
|
+
process lifetime. Over 24 h this accumulated 12+ leaked file descriptors
|
|
43
|
+
and ~100 MB of wasted SQLite page-cache RAM. New
|
|
44
|
+
`bandit.close_threadlocal_conn()` function, called in the
|
|
45
|
+
`settle_stale_plays` finally block, ensures pool threads release their
|
|
46
|
+
connections immediately.
|
|
47
|
+
- **Corrected embedding worker memory comment.** The `~200MB footprint`
|
|
48
|
+
note was written for `all-MiniLM-L6-v2`; the default model
|
|
49
|
+
`nomic-ai/nomic-embed-text-v1.5` uses ~1.1 GB via ONNX.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
13
53
|
## [3.4.32] - 2026-04-24
|
|
14
54
|
|
|
15
55
|
Fix: concurrent remembers no longer block recalls on the shared embedder.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.34",
|
|
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
|
@@ -639,7 +639,7 @@ def cmd_mode(args: Namespace) -> None:
|
|
|
639
639
|
llm_api_key=config.llm.api_key,
|
|
640
640
|
llm_api_base=config.llm.api_base,
|
|
641
641
|
)
|
|
642
|
-
updated.save()
|
|
642
|
+
updated.save(mode_change=True)
|
|
643
643
|
json_print("mode", data={
|
|
644
644
|
"previous_mode": old_mode, "current_mode": args.value.upper(),
|
|
645
645
|
}, next_actions=[
|
|
@@ -661,7 +661,7 @@ def cmd_mode(args: Namespace) -> None:
|
|
|
661
661
|
llm_api_key=config.llm.api_key,
|
|
662
662
|
llm_api_base=config.llm.api_base,
|
|
663
663
|
)
|
|
664
|
-
updated.save()
|
|
664
|
+
updated.save(mode_change=True)
|
|
665
665
|
print(f"Mode set to: {args.value.upper()}")
|
|
666
666
|
|
|
667
667
|
# V3.3: Check if embedding model changed — inform about re-indexing
|
|
@@ -335,7 +335,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
335
335
|
if choice == "c" and interactive:
|
|
336
336
|
configure_provider(config)
|
|
337
337
|
else:
|
|
338
|
-
config.save()
|
|
338
|
+
config.save(mode_change=True)
|
|
339
339
|
|
|
340
340
|
mode_names = {"a": "Local Guardian", "b": "Smart Local", "c": "Full Power"}
|
|
341
341
|
print(f"\n ✓ Mode {choice.upper()} ({mode_names[choice]}) configured")
|
|
@@ -421,7 +421,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
421
421
|
config.daemon_idle_timeout = 0
|
|
422
422
|
print("\n ✓ 24/7 Always-On mode")
|
|
423
423
|
|
|
424
|
-
config.save()
|
|
424
|
+
config.save(mode_change=True)
|
|
425
425
|
|
|
426
426
|
# -- Step 6: Mesh Communication (v3.4.3) --
|
|
427
427
|
print()
|
|
@@ -441,7 +441,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
441
441
|
print(" Auto-enabling Mesh (non-interactive)")
|
|
442
442
|
|
|
443
443
|
config.mesh_enabled = mesh_choice in ("", "y", "yes")
|
|
444
|
-
config.save()
|
|
444
|
+
config.save(mode_change=True)
|
|
445
445
|
print(f"\n ✓ Mesh {'enabled' if config.mesh_enabled else 'disabled'}")
|
|
446
446
|
|
|
447
447
|
# -- Step 7: Ingestion Adapters (v3.4.3) --
|
|
@@ -502,7 +502,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
502
502
|
print(" Auto-enabling entity compilation (non-interactive)")
|
|
503
503
|
|
|
504
504
|
config.entity_compilation_enabled = ec_choice in ("", "y", "yes")
|
|
505
|
-
config.save()
|
|
505
|
+
config.save(mode_change=True)
|
|
506
506
|
print(f"\n ✓ Entity compilation {'enabled' if config.entity_compilation_enabled else 'disabled'}")
|
|
507
507
|
|
|
508
508
|
# -- Step 9: Skill Evolution (v3.4.11) --
|
|
@@ -661,7 +661,7 @@ def check_first_use(command: str) -> None:
|
|
|
661
661
|
from superlocalmemory.core.config import SLMConfig
|
|
662
662
|
from superlocalmemory.storage.models import Mode
|
|
663
663
|
config = SLMConfig.for_mode(Mode.A)
|
|
664
|
-
config.save()
|
|
664
|
+
config.save(mode_change=True)
|
|
665
665
|
_mark_complete()
|
|
666
666
|
except Exception:
|
|
667
667
|
pass
|
|
@@ -749,6 +749,6 @@ def configure_provider(config: object) -> None:
|
|
|
749
749
|
llm_api_key=api_key,
|
|
750
750
|
llm_api_base=preset["base_url"],
|
|
751
751
|
)
|
|
752
|
-
updated.save()
|
|
752
|
+
updated.save(mode_change=True)
|
|
753
753
|
print(f" Provider: {provider_name}")
|
|
754
754
|
print(f" Model: {preset['model']}")
|
|
@@ -697,8 +697,25 @@ class SLMConfig:
|
|
|
697
697
|
|
|
698
698
|
return config
|
|
699
699
|
|
|
700
|
-
def save(
|
|
701
|
-
|
|
700
|
+
def save(
|
|
701
|
+
self,
|
|
702
|
+
config_path: Path | None = None,
|
|
703
|
+
*,
|
|
704
|
+
mode_change: bool = False,
|
|
705
|
+
) -> None:
|
|
706
|
+
"""Save config to JSON file.
|
|
707
|
+
|
|
708
|
+
v3.4.34: mode protection. If the existing config.json has a mode
|
|
709
|
+
that differs from ``self.mode`` and the caller did NOT pass
|
|
710
|
+
``mode_change=True``, the EXISTING mode is preserved. This
|
|
711
|
+
prevents accidental mode resets when code creates a fresh
|
|
712
|
+
``SLMConfig()`` (defaults to Mode A) and calls ``save()`` to
|
|
713
|
+
persist an unrelated field change.
|
|
714
|
+
|
|
715
|
+
Callers that intentionally switch mode (``slm mode b``, the MCP
|
|
716
|
+
``set_mode`` tool, the dashboard PUT ``/api/v3/mode``) MUST pass
|
|
717
|
+
``mode_change=True``.
|
|
718
|
+
"""
|
|
702
719
|
import json
|
|
703
720
|
path = config_path or (self.base_dir / "config.json")
|
|
704
721
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -710,8 +727,19 @@ class SLMConfig:
|
|
|
710
727
|
except (json.JSONDecodeError, OSError):
|
|
711
728
|
pass
|
|
712
729
|
|
|
730
|
+
# v3.4.34: mode protection — preserve user's mode unless explicitly changing
|
|
731
|
+
effective_mode = self.mode.value
|
|
732
|
+
existing_mode = existing.get("mode")
|
|
733
|
+
if existing_mode and existing_mode != effective_mode and not mode_change:
|
|
734
|
+
logger.warning(
|
|
735
|
+
"SLMConfig.save(): mode change blocked (%s → %s). "
|
|
736
|
+
"Pass mode_change=True to override. Preserving '%s'.",
|
|
737
|
+
existing_mode, effective_mode, existing_mode,
|
|
738
|
+
)
|
|
739
|
+
effective_mode = existing_mode
|
|
740
|
+
|
|
713
741
|
data = {
|
|
714
|
-
"mode":
|
|
742
|
+
"mode": effective_mode,
|
|
715
743
|
"active_profile": self.active_profile,
|
|
716
744
|
"llm": {
|
|
717
745
|
"provider": self.llm.provider,
|
|
@@ -63,7 +63,7 @@ def _load_embedding_model(name: str) -> tuple:
|
|
|
63
63
|
"""
|
|
64
64
|
from sentence_transformers import SentenceTransformer
|
|
65
65
|
|
|
66
|
-
# Tier 1: ONNX (stable memory
|
|
66
|
+
# Tier 1: ONNX (stable memory; ~1.1 GB for nomic-embed-text-v1.5)
|
|
67
67
|
try:
|
|
68
68
|
m = SentenceTransformer(name, backend="onnx", trust_remote_code=True)
|
|
69
69
|
return m, "onnx"
|
|
@@ -176,6 +176,23 @@ def _conn_for(db_path: Path) -> sqlite3.Connection:
|
|
|
176
176
|
return conn
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
def close_threadlocal_conn() -> None:
|
|
180
|
+
"""Close the threadlocal bandit connection on the calling thread.
|
|
181
|
+
|
|
182
|
+
v3.4.33: background callers (asyncio.to_thread pool threads) MUST call
|
|
183
|
+
this after finishing bandit work. Without it, each pool thread keeps a
|
|
184
|
+
leaked connection to learning.db for the process lifetime — observed as
|
|
185
|
+
12+ open file descriptors and ~100 MB wasted page-cache RAM.
|
|
186
|
+
"""
|
|
187
|
+
if _holder.conn is not None:
|
|
188
|
+
try:
|
|
189
|
+
_holder.conn.close()
|
|
190
|
+
except sqlite3.Error: # pragma: no cover
|
|
191
|
+
pass
|
|
192
|
+
_holder.conn = None
|
|
193
|
+
_holder.path = None
|
|
194
|
+
|
|
195
|
+
|
|
179
196
|
def _now_iso() -> str:
|
|
180
197
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
181
198
|
|
|
@@ -520,6 +537,7 @@ def retention_sweep(
|
|
|
520
537
|
__all__ = (
|
|
521
538
|
"BanditChoice",
|
|
522
539
|
"ContextualBandit",
|
|
540
|
+
"close_threadlocal_conn",
|
|
523
541
|
"compute_stratum",
|
|
524
542
|
"current_time_bucket",
|
|
525
543
|
"retention_sweep",
|
|
@@ -317,8 +317,13 @@ def settle_stale_plays(
|
|
|
317
317
|
memory_conn.close()
|
|
318
318
|
except sqlite3.Error: # pragma: no cover
|
|
319
319
|
pass
|
|
320
|
-
#
|
|
321
|
-
|
|
320
|
+
# v3.4.33: close the threadlocal bandit connection so pool threads
|
|
321
|
+
# from asyncio.to_thread don't leak file descriptors to learning.db.
|
|
322
|
+
try:
|
|
323
|
+
from superlocalmemory.learning.bandit import close_threadlocal_conn
|
|
324
|
+
close_threadlocal_conn()
|
|
325
|
+
except Exception: # pragma: no cover — defensive
|
|
326
|
+
pass
|
|
322
327
|
|
|
323
328
|
return settled
|
|
324
329
|
|
|
@@ -70,8 +70,20 @@ def register_v3_tools(server, get_engine: Callable) -> None:
|
|
|
70
70
|
|
|
71
71
|
mode_enum = Mode(mode_lower)
|
|
72
72
|
old_config = SLMConfig.load()
|
|
73
|
-
config = SLMConfig.for_mode(
|
|
74
|
-
|
|
73
|
+
config = SLMConfig.for_mode(
|
|
74
|
+
mode_enum,
|
|
75
|
+
llm_provider=old_config.llm.provider,
|
|
76
|
+
llm_model=old_config.llm.model,
|
|
77
|
+
llm_api_key=old_config.llm.api_key,
|
|
78
|
+
llm_api_base=old_config.llm.api_base,
|
|
79
|
+
embedding_provider=old_config.embedding.provider,
|
|
80
|
+
embedding_endpoint=old_config.embedding.api_endpoint,
|
|
81
|
+
embedding_key=old_config.embedding.api_key,
|
|
82
|
+
embedding_model_name=old_config.embedding.model_name,
|
|
83
|
+
embedding_dimension=old_config.embedding.dimension,
|
|
84
|
+
)
|
|
85
|
+
config.active_profile = old_config.active_profile
|
|
86
|
+
config.save(mode_change=True)
|
|
75
87
|
|
|
76
88
|
# V3.3: Check if embedding model changed — flag for re-indexing
|
|
77
89
|
needs_reindex = (
|
|
@@ -136,7 +136,7 @@ async def set_mode(request: Request):
|
|
|
136
136
|
embedding_dimension=old_config.embedding.dimension,
|
|
137
137
|
)
|
|
138
138
|
new_config.active_profile = old_config.active_profile
|
|
139
|
-
new_config.save()
|
|
139
|
+
new_config.save(mode_change=True)
|
|
140
140
|
|
|
141
141
|
# Audit the change before we lose context — proves who/when/what.
|
|
142
142
|
# Captures the phantom-write case where `for_mode(C)` auto-defaults
|