superlocalmemory 3.4.16 → 3.4.18
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 +20 -0
- package/package.json +1 -3
- package/pyproject.toml +10 -1
- package/src/superlocalmemory/cli/setup_wizard.py +30 -0
- package/src/superlocalmemory/server/routes/entity.py +5 -9
- package/src/superlocalmemory/server/routes/helpers.py +120 -15
- package/src/superlocalmemory/server/routes/ingest.py +2 -3
- package/src/superlocalmemory/server/routes/v3_api.py +42 -2
- package/src/superlocalmemory/server/unified_daemon.py +21 -11
- package/src/superlocalmemory.egg-info/PKG-INFO +5 -2
- package/src/superlocalmemory.egg-info/requires.txt +3 -0
- package/docs/ARCHITECTURE.md +0 -149
- package/docs/api-reference.md +0 -284
- package/docs/auto-memory.md +0 -150
- package/docs/cli-reference.md +0 -327
- package/docs/cloud-backup.md +0 -174
- package/docs/compliance.md +0 -191
- package/docs/configuration.md +0 -182
- package/docs/getting-started.md +0 -102
- package/docs/ide-setup.md +0 -261
- package/docs/mcp-tools.md +0 -220
- package/docs/migration-from-v2.md +0 -170
- package/docs/profiles.md +0 -173
- package/docs/screenshots/01-dashboard-main.png +0 -0
- package/docs/screenshots/02-knowledge-graph.png +0 -0
- package/docs/screenshots/03-math-health.png +0 -0
- package/docs/screenshots/03-patterns-learning.png +0 -0
- package/docs/screenshots/04-learning-dashboard.png +0 -0
- package/docs/screenshots/04-recall-lab.png +0 -0
- package/docs/screenshots/05-behavioral-analysis.png +0 -0
- package/docs/screenshots/05-trust-dashboard.png +0 -0
- package/docs/screenshots/06-graph-communities.png +0 -0
- package/docs/screenshots/06-settings.png +0 -0
- package/docs/screenshots/07-memories-blurred.png +0 -0
- package/docs/skill-evolution.md +0 -256
- package/docs/troubleshooting.md +0 -310
- package/docs/v2-archive/ACCESSIBILITY.md +0 -291
- package/docs/v2-archive/ARCHITECTURE.md +0 -886
- package/docs/v2-archive/CLI-COMMANDS-REFERENCE.md +0 -425
- package/docs/v2-archive/COMPRESSION-README.md +0 -390
- package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +0 -300
- package/docs/v2-archive/MCP-MANUAL-SETUP.md +0 -775
- package/docs/v2-archive/MCP-TROUBLESHOOTING.md +0 -787
- package/docs/v2-archive/PATTERN-LEARNING.md +0 -228
- package/docs/v2-archive/PROFILES-GUIDE.md +0 -453
- package/docs/v2-archive/RESET-GUIDE.md +0 -353
- package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +0 -749
- package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +0 -502
- package/docs/v2-archive/UI-SERVER.md +0 -262
- package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +0 -488
- package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +0 -666
- package/docs/v2-archive/WINDOWS-INSTALL-README.txt +0 -34
- package/docs/v2-archive/WINDOWS-POST-INSTALL.txt +0 -45
- package/docs/v2-archive/example_graph_usage.py +0 -146
- package/ui/index.html +0 -1879
- package/ui/js/agents.js +0 -192
- package/ui/js/auto-settings.js +0 -399
- package/ui/js/behavioral.js +0 -276
- package/ui/js/clusters.js +0 -206
- package/ui/js/compliance.js +0 -252
- package/ui/js/core.js +0 -246
- package/ui/js/dashboard.js +0 -110
- package/ui/js/events.js +0 -178
- package/ui/js/fact-detail.js +0 -92
- package/ui/js/feedback.js +0 -333
- package/ui/js/graph-core.js +0 -447
- package/ui/js/graph-filters.js +0 -220
- package/ui/js/graph-interactions.js +0 -351
- package/ui/js/graph-ui.js +0 -214
- package/ui/js/ide-status.js +0 -102
- package/ui/js/init.js +0 -45
- package/ui/js/learning.js +0 -435
- package/ui/js/lifecycle.js +0 -298
- package/ui/js/math-health.js +0 -98
- package/ui/js/memories.js +0 -264
- package/ui/js/modal.js +0 -357
- package/ui/js/patterns.js +0 -93
- package/ui/js/profiles.js +0 -236
- package/ui/js/recall-lab.js +0 -292
- package/ui/js/search.js +0 -59
- package/ui/js/settings.js +0 -224
- package/ui/js/timeline.js +0 -32
- package/ui/js/trust-dashboard.js +0 -73
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [3.4.18] - 2026-04-17
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- pip and npm installs now ship identical functionality. Semantic search and cross-encoder reranking work out of the box on pip (previously required `pip install superlocalmemory[search]`).
|
|
17
|
+
- First pip run auto-installs Claude Code hooks when Claude Code is detected, matching the npm postinstall experience.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [3.4.17] - 2026-04-17
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Entity Explorer no longer stuck on "No entities found" after switching operating modes.
|
|
25
|
+
- Engine-backed routes (entity, ingest, recall, remember, list) auto-recover after mode changes — no daemon restart required.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Mode change audit log at `~/.superlocalmemory/logs/mode-audit.log`.
|
|
29
|
+
- Mode C now requires an explicit API key via Settings to prevent accidental cloud-mode writes.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
13
33
|
## Author
|
|
14
34
|
|
|
15
35
|
**Varun Pratap Bhardwaj**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.18",
|
|
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",
|
|
@@ -65,8 +65,6 @@
|
|
|
65
65
|
"src/",
|
|
66
66
|
"ide/",
|
|
67
67
|
"scripts/",
|
|
68
|
-
"ui/",
|
|
69
|
-
"docs/",
|
|
70
68
|
"pyproject.toml",
|
|
71
69
|
"README.md",
|
|
72
70
|
"LICENSE",
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superlocalmemory"
|
|
3
|
-
version = "3.4.
|
|
3
|
+
version = "3.4.18"
|
|
4
4
|
description = "Information-geometric agent memory with mathematical guarantees"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "AGPL-3.0-or-later"}
|
|
@@ -52,9 +52,18 @@ dependencies = [
|
|
|
52
52
|
# V3.4.3: Unified Brain
|
|
53
53
|
"psutil>=5.9.0",
|
|
54
54
|
"structlog>=24.0.0,<27.0.0",
|
|
55
|
+
# V3.4.18: Semantic search + cross-encoder reranker (npm install parity).
|
|
56
|
+
# Previously under [search] extra — pip users silently lost 30pp of recall
|
|
57
|
+
# quality vs. npm users. Now ships by default for both install paths.
|
|
58
|
+
"sentence-transformers[onnx]>=5.0.0",
|
|
59
|
+
"torch>=2.2.0",
|
|
60
|
+
"scikit-learn>=1.3.0,<2.0.0",
|
|
55
61
|
]
|
|
56
62
|
|
|
57
63
|
[project.optional-dependencies]
|
|
64
|
+
# `search` is now a no-op alias kept for backwards compatibility; the deps
|
|
65
|
+
# moved into core in v3.4.18. ``pip install superlocalmemory[search]`` still
|
|
66
|
+
# works but installs nothing extra.
|
|
58
67
|
search = [
|
|
59
68
|
"sentence-transformers[onnx]>=5.0.0",
|
|
60
69
|
"einops>=0.8.2",
|
|
@@ -621,6 +621,10 @@ def check_first_use(command: str) -> None:
|
|
|
621
621
|
|
|
622
622
|
Called from main.py before dispatching any command.
|
|
623
623
|
Skips for commands that don't need setup (setup, hook, --version, --help).
|
|
624
|
+
|
|
625
|
+
On first use, also auto-installs Claude Code hooks so pip installs have
|
|
626
|
+
the same "it just works" experience as npm installs (npm does this via
|
|
627
|
+
postinstall; pip has no postinstall, so we do it here).
|
|
624
628
|
"""
|
|
625
629
|
# Commands that work without setup
|
|
626
630
|
_SKIP_COMMANDS = {"setup", "init", "hook", "hooks", "reap", "mcp"}
|
|
@@ -641,6 +645,7 @@ def check_first_use(command: str) -> None:
|
|
|
641
645
|
_mark_complete()
|
|
642
646
|
except Exception:
|
|
643
647
|
pass
|
|
648
|
+
_maybe_install_hooks_on_first_use()
|
|
644
649
|
return
|
|
645
650
|
|
|
646
651
|
# Interactive: run the full wizard
|
|
@@ -648,6 +653,31 @@ def check_first_use(command: str) -> None:
|
|
|
648
653
|
print(" First time using SuperLocalMemory!")
|
|
649
654
|
print(" Running setup wizard...\n")
|
|
650
655
|
run_wizard()
|
|
656
|
+
_maybe_install_hooks_on_first_use()
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _maybe_install_hooks_on_first_use() -> None:
|
|
660
|
+
"""Install Claude Code hooks on first SLM run — matches npm postinstall.
|
|
661
|
+
|
|
662
|
+
Install/uninstall parity rules:
|
|
663
|
+
* Skip if the user explicitly opted out via ``slm hooks remove``
|
|
664
|
+
(creates ``~/.superlocalmemory/hooks/.hooks-disabled``).
|
|
665
|
+
* Skip if Claude Code isn't installed (no ``~/.claude/settings.json``
|
|
666
|
+
to merge into).
|
|
667
|
+
* Silent + best-effort: never fail a CLI command because of this.
|
|
668
|
+
"""
|
|
669
|
+
try:
|
|
670
|
+
opt_out = _SLM_HOME / "hooks" / ".hooks-disabled"
|
|
671
|
+
if opt_out.exists():
|
|
672
|
+
return
|
|
673
|
+
claude_settings = Path.home() / ".claude" / "settings.json"
|
|
674
|
+
if not claude_settings.exists():
|
|
675
|
+
return # Claude Code not installed — nothing to hook into.
|
|
676
|
+
from superlocalmemory.hooks.claude_code_hooks import install_hooks
|
|
677
|
+
install_hooks()
|
|
678
|
+
except Exception:
|
|
679
|
+
# Best-effort: parity-fallback, never block CLI.
|
|
680
|
+
pass
|
|
651
681
|
|
|
652
682
|
|
|
653
683
|
# ---------------------------------------------------------------------------
|
|
@@ -8,6 +8,8 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from fastapi import APIRouter, HTTPException, Request, Query
|
|
10
10
|
|
|
11
|
+
from .helpers import require_engine
|
|
12
|
+
|
|
11
13
|
router = APIRouter(prefix="/api/entity", tags=["entity"])
|
|
12
14
|
|
|
13
15
|
|
|
@@ -19,9 +21,7 @@ async def list_entities(
|
|
|
19
21
|
offset: int = Query(default=0, ge=0),
|
|
20
22
|
):
|
|
21
23
|
"""List all entities with basic info (canonical name, type, fact count)."""
|
|
22
|
-
engine = request
|
|
23
|
-
if engine is None:
|
|
24
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
24
|
+
engine = require_engine(request)
|
|
25
25
|
|
|
26
26
|
import sqlite3
|
|
27
27
|
import json
|
|
@@ -75,9 +75,7 @@ async def get_entity(
|
|
|
75
75
|
project: str = Query(default=""),
|
|
76
76
|
):
|
|
77
77
|
"""Get compiled truth + timeline for an entity."""
|
|
78
|
-
engine = request
|
|
79
|
-
if engine is None:
|
|
80
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
78
|
+
engine = require_engine(request)
|
|
81
79
|
|
|
82
80
|
import sqlite3
|
|
83
81
|
import json
|
|
@@ -121,9 +119,7 @@ async def recompile_entity(
|
|
|
121
119
|
project: str = Query(default=""),
|
|
122
120
|
):
|
|
123
121
|
"""Force immediate recompilation of an entity."""
|
|
124
|
-
engine = request
|
|
125
|
-
if engine is None:
|
|
126
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
122
|
+
engine = require_engine(request)
|
|
127
123
|
|
|
128
124
|
import sqlite3
|
|
129
125
|
conn = sqlite3.connect(str(engine._config.db_path))
|
|
@@ -5,18 +5,26 @@
|
|
|
5
5
|
- AGPL-3.0-or-later
|
|
6
6
|
|
|
7
7
|
Shared utilities for all route modules: DB connection, dict factory,
|
|
8
|
-
profile helper, validation, Pydantic models, config paths
|
|
8
|
+
profile helper, validation, Pydantic models, config paths, and the
|
|
9
|
+
shared lazy engine accessor used by every engine-dependent route.
|
|
9
10
|
"""
|
|
11
|
+
import logging
|
|
10
12
|
import re
|
|
11
13
|
import json
|
|
12
14
|
import sqlite3
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from datetime import datetime, timezone
|
|
13
18
|
from pathlib import Path
|
|
14
19
|
from typing import Optional
|
|
15
20
|
|
|
16
|
-
from fastapi import HTTPException
|
|
21
|
+
from fastapi import HTTPException, Request
|
|
17
22
|
from pydantic import BaseModel, Field
|
|
18
23
|
|
|
19
24
|
|
|
25
|
+
_engine_logger = logging.getLogger("superlocalmemory.engine")
|
|
26
|
+
|
|
27
|
+
|
|
20
28
|
# ---------------------------------------------------------------------------
|
|
21
29
|
# Version detection (shared — avoids circular import between ui.py ↔ v3_api.py)
|
|
22
30
|
# ---------------------------------------------------------------------------
|
|
@@ -65,25 +73,122 @@ UI_DIR = Path(__file__).parent.parent / "ui"
|
|
|
65
73
|
PROFILES_DIR = MEMORY_DIR / "profiles"
|
|
66
74
|
|
|
67
75
|
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Engine lifecycle — lazy, thread-safe, recoverable after mode changes.
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
_engine_init_lock = threading.Lock()
|
|
81
|
+
_last_engine_failure: float = 0.0
|
|
82
|
+
_ENGINE_FAILURE_COOLDOWN_S: float = 5.0
|
|
83
|
+
|
|
84
|
+
|
|
68
85
|
def get_engine_lazy(app_state):
|
|
69
|
-
"""
|
|
86
|
+
"""Return ``app_state.engine``, initialising it if None.
|
|
87
|
+
|
|
88
|
+
Why this exists
|
|
89
|
+
---------------
|
|
90
|
+
Mode-change endpoints (``PUT`` / ``POST /api/v3/mode[/set]``) set
|
|
91
|
+
``app.state.engine = None`` so the next request picks up the new config.
|
|
92
|
+
Before this helper, no code path re-created the engine until the daemon
|
|
93
|
+
restarted, breaking every engine-backed route (entity, ingest, tiers,
|
|
94
|
+
recall, remember, list, status).
|
|
95
|
+
|
|
96
|
+
Contract
|
|
97
|
+
--------
|
|
98
|
+
* Returns the cached engine if already initialised.
|
|
99
|
+
* Otherwise acquires a process-wide lock and builds a fresh
|
|
100
|
+
``MemoryEngine`` from the latest ``SLMConfig.load()``.
|
|
101
|
+
* Returns ``None`` if init fails. A brief cooldown prevents hammering
|
|
102
|
+
init when the underlying cause (e.g., corrupt DB) is persistent.
|
|
103
|
+
* Never uses a sticky "already attempted" flag — recovery must be
|
|
104
|
+
automatic after a transient failure.
|
|
105
|
+
"""
|
|
106
|
+
global _last_engine_failure
|
|
107
|
+
|
|
70
108
|
engine = getattr(app_state, "engine", None)
|
|
71
109
|
if engine is not None:
|
|
72
110
|
return engine
|
|
73
|
-
|
|
111
|
+
|
|
112
|
+
now = time.monotonic()
|
|
113
|
+
if _last_engine_failure and (now - _last_engine_failure) < _ENGINE_FAILURE_COOLDOWN_S:
|
|
74
114
|
return None
|
|
115
|
+
|
|
116
|
+
with _engine_init_lock:
|
|
117
|
+
# Double-checked: another thread may have initialised while we waited.
|
|
118
|
+
engine = getattr(app_state, "engine", None)
|
|
119
|
+
if engine is not None:
|
|
120
|
+
return engine
|
|
121
|
+
try:
|
|
122
|
+
from superlocalmemory.core.config import SLMConfig
|
|
123
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
124
|
+
config = SLMConfig.load()
|
|
125
|
+
new_engine = MemoryEngine(config)
|
|
126
|
+
new_engine.initialize()
|
|
127
|
+
app_state.engine = new_engine
|
|
128
|
+
app_state.config = config
|
|
129
|
+
_engine_logger.info(
|
|
130
|
+
"Engine lazy-initialised (mode=%s, profile=%s)",
|
|
131
|
+
getattr(getattr(config, "mode", None), "value", "?"),
|
|
132
|
+
getattr(config, "active_profile", "?"),
|
|
133
|
+
)
|
|
134
|
+
_last_engine_failure = 0.0
|
|
135
|
+
return new_engine
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
_engine_logger.warning("Engine lazy init failed: %s", exc)
|
|
138
|
+
_last_engine_failure = time.monotonic()
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def require_engine(request: Request):
|
|
143
|
+
"""Return the engine or raise ``HTTPException(503)``.
|
|
144
|
+
|
|
145
|
+
Use this in every route that touches ``app.state.engine``. Replaces the
|
|
146
|
+
old ``engine = request.app.state.engine; if engine is None: raise ...``
|
|
147
|
+
pattern with a single call that also lazily re-initialises after a mode
|
|
148
|
+
change.
|
|
149
|
+
"""
|
|
150
|
+
engine = get_engine_lazy(request.app.state)
|
|
151
|
+
if engine is None:
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=503,
|
|
154
|
+
detail="Engine not initialized. Retry in a few seconds — it's warming up.",
|
|
155
|
+
)
|
|
156
|
+
return engine
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def log_mode_change(
|
|
160
|
+
old_mode: str,
|
|
161
|
+
new_mode: str,
|
|
162
|
+
*,
|
|
163
|
+
provider: str = "",
|
|
164
|
+
model: str = "",
|
|
165
|
+
source: str = "api",
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Append an audit entry for every mode change.
|
|
168
|
+
|
|
169
|
+
Lets us trace phantom writes (e.g., a stray dashboard-card button click
|
|
170
|
+
silently flipping the system to Mode C with ``anthropic/claude-sonnet-4``
|
|
171
|
+
as the auto-defaulted model).
|
|
172
|
+
|
|
173
|
+
Audit file: ``<base_dir>/logs/mode-audit.log`` (tab-separated).
|
|
174
|
+
"""
|
|
175
|
+
audit_path = MEMORY_DIR / "logs" / "mode-audit.log"
|
|
75
176
|
try:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
except Exception:
|
|
85
|
-
|
|
86
|
-
|
|
177
|
+
audit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
179
|
+
line = (
|
|
180
|
+
f"{ts}\told={old_mode}\tnew={new_mode}"
|
|
181
|
+
f"\tprovider={provider}\tmodel={model}\tsource={source}\n"
|
|
182
|
+
)
|
|
183
|
+
with open(audit_path, "a", encoding="utf-8") as handle:
|
|
184
|
+
handle.write(line)
|
|
185
|
+
except Exception as exc: # Logging must never break the mode-change path.
|
|
186
|
+
_engine_logger.warning("mode audit write failed: %s", exc)
|
|
187
|
+
|
|
188
|
+
_engine_logger.info(
|
|
189
|
+
"Mode change: %s→%s provider=%s model=%s (%s)",
|
|
190
|
+
old_mode, new_mode, provider, model, source,
|
|
191
|
+
)
|
|
87
192
|
|
|
88
193
|
|
|
89
194
|
def get_db_connection() -> sqlite3.Connection:
|
|
@@ -44,9 +44,8 @@ async def ingest(req: IngestRequest, request: Request):
|
|
|
44
44
|
"""
|
|
45
45
|
global _active_count
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
47
|
+
from .helpers import require_engine
|
|
48
|
+
engine = require_engine(request)
|
|
50
49
|
|
|
51
50
|
if not req.content:
|
|
52
51
|
raise HTTPException(400, detail="content required")
|
|
@@ -101,7 +101,28 @@ async def set_mode(request: Request):
|
|
|
101
101
|
|
|
102
102
|
from superlocalmemory.core.config import SLMConfig
|
|
103
103
|
from superlocalmemory.storage.models import Mode
|
|
104
|
+
from superlocalmemory.server.routes.helpers import log_mode_change
|
|
104
105
|
old_config = SLMConfig.load()
|
|
106
|
+
old_mode = old_config.mode.value
|
|
107
|
+
|
|
108
|
+
# Safety: a bare ``{mode:"c"}`` body (e.g., a stray dashboard button
|
|
109
|
+
# click) used to silently auto-default the model to
|
|
110
|
+
# ``anthropic/claude-sonnet-4`` with no API key, writing phantom state
|
|
111
|
+
# into config.json. Refuse that path — Mode C requires explicit
|
|
112
|
+
# provider+key via POST /api/v3/mode/set.
|
|
113
|
+
if new_mode == "c" and not old_config.llm.api_key:
|
|
114
|
+
return JSONResponse(
|
|
115
|
+
{
|
|
116
|
+
"error": (
|
|
117
|
+
"Mode C requires a cloud API key. "
|
|
118
|
+
"Configure provider + key in Settings → Step 2 "
|
|
119
|
+
"(uses POST /api/v3/mode/set)."
|
|
120
|
+
),
|
|
121
|
+
"code": "mode_c_requires_api_key",
|
|
122
|
+
},
|
|
123
|
+
status_code=400,
|
|
124
|
+
)
|
|
125
|
+
|
|
105
126
|
new_config = SLMConfig.for_mode(
|
|
106
127
|
Mode(new_mode),
|
|
107
128
|
llm_provider=old_config.llm.provider,
|
|
@@ -112,13 +133,23 @@ async def set_mode(request: Request):
|
|
|
112
133
|
new_config.active_profile = old_config.active_profile
|
|
113
134
|
new_config.save()
|
|
114
135
|
|
|
136
|
+
# Audit the change before we lose context — proves who/when/what.
|
|
137
|
+
# Captures the phantom-write case where `for_mode(C)` auto-defaults
|
|
138
|
+
# the model to "anthropic/claude-sonnet-4" (see core/config.py).
|
|
139
|
+
log_mode_change(
|
|
140
|
+
old_mode, new_mode,
|
|
141
|
+
provider=new_config.llm.provider,
|
|
142
|
+
model=new_config.llm.model,
|
|
143
|
+
source="PUT /api/v3/mode",
|
|
144
|
+
)
|
|
145
|
+
|
|
115
146
|
# V3.3: Check if embedding model changed — flag for re-indexing
|
|
116
147
|
needs_reindex = (
|
|
117
148
|
old_config.embedding.provider != new_config.embedding.provider
|
|
118
149
|
or old_config.embedding.model_name != new_config.embedding.model_name
|
|
119
150
|
)
|
|
120
151
|
|
|
121
|
-
#
|
|
152
|
+
# Invalidate engine; next engine-backed request lazy-inits with new config.
|
|
122
153
|
if hasattr(request.app.state, "engine"):
|
|
123
154
|
request.app.state.engine = None
|
|
124
155
|
|
|
@@ -147,6 +178,9 @@ async def set_full_config(request: Request):
|
|
|
147
178
|
|
|
148
179
|
from superlocalmemory.core.config import SLMConfig
|
|
149
180
|
from superlocalmemory.storage.models import Mode
|
|
181
|
+
from superlocalmemory.server.routes.helpers import log_mode_change
|
|
182
|
+
old = SLMConfig.load()
|
|
183
|
+
old_mode = old.mode.value
|
|
150
184
|
config = SLMConfig.for_mode(
|
|
151
185
|
Mode(new_mode),
|
|
152
186
|
llm_provider=provider if provider != "none" else "",
|
|
@@ -154,10 +188,16 @@ async def set_full_config(request: Request):
|
|
|
154
188
|
llm_api_key=api_key,
|
|
155
189
|
llm_api_base="http://localhost:11434" if provider == "ollama" else "",
|
|
156
190
|
)
|
|
157
|
-
old = SLMConfig.load()
|
|
158
191
|
config.active_profile = old.active_profile
|
|
159
192
|
config.save()
|
|
160
193
|
|
|
194
|
+
log_mode_change(
|
|
195
|
+
old_mode, new_mode,
|
|
196
|
+
provider=config.llm.provider,
|
|
197
|
+
model=config.llm.model,
|
|
198
|
+
source="POST /api/v3/mode/set",
|
|
199
|
+
)
|
|
200
|
+
|
|
161
201
|
# Kill existing worker so next request uses new config
|
|
162
202
|
try:
|
|
163
203
|
from superlocalmemory.core.worker_pool import WorkerPool
|
|
@@ -559,10 +559,25 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
559
559
|
"""Add daemon-specific routes for CLI integration."""
|
|
560
560
|
global _last_activity
|
|
561
561
|
|
|
562
|
+
from superlocalmemory.server.routes.helpers import get_engine_lazy
|
|
563
|
+
|
|
564
|
+
def _get_engine_or_503():
|
|
565
|
+
"""Lazy-init engine; raise 503 if init fails.
|
|
566
|
+
|
|
567
|
+
Shared by every daemon route so a mode switch that nulled
|
|
568
|
+
``application.state.engine`` never leaves the daemon stuck in
|
|
569
|
+
503 until restart.
|
|
570
|
+
"""
|
|
571
|
+
engine = get_engine_lazy(application.state)
|
|
572
|
+
if engine is None:
|
|
573
|
+
raise HTTPException(503, detail="Engine not initialized")
|
|
574
|
+
return engine
|
|
575
|
+
|
|
562
576
|
@application.get("/health")
|
|
563
577
|
async def health():
|
|
564
578
|
_update_activity()
|
|
565
|
-
|
|
579
|
+
# Non-blocking peek: report status without forcing a re-init.
|
|
580
|
+
engine = getattr(application.state, "engine", None)
|
|
566
581
|
return {
|
|
567
582
|
"status": "ok",
|
|
568
583
|
"pid": os.getpid(),
|
|
@@ -574,9 +589,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
574
589
|
async def recall(q: str = "", query: str = "", limit: int = 20):
|
|
575
590
|
_update_activity()
|
|
576
591
|
search_query = q or query # Accept both ?q= and ?query= for compatibility
|
|
577
|
-
engine =
|
|
578
|
-
if engine is None:
|
|
579
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
592
|
+
engine = _get_engine_or_503()
|
|
580
593
|
if not search_query:
|
|
581
594
|
return {"results": [], "count": 0, "query_type": "none", "retrieval_time_ms": 0}
|
|
582
595
|
try:
|
|
@@ -605,9 +618,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
605
618
|
@application.post("/remember")
|
|
606
619
|
async def remember(req: RememberRequest):
|
|
607
620
|
_update_activity()
|
|
608
|
-
engine =
|
|
609
|
-
if engine is None:
|
|
610
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
621
|
+
engine = _get_engine_or_503()
|
|
611
622
|
try:
|
|
612
623
|
metadata = {"tags": req.tags} if req.tags else {}
|
|
613
624
|
fact_ids = engine.store(req.content, metadata=metadata)
|
|
@@ -624,7 +635,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
624
635
|
@application.get("/status")
|
|
625
636
|
async def status():
|
|
626
637
|
_update_activity()
|
|
627
|
-
|
|
638
|
+
# Non-blocking peek — status must never force a re-init.
|
|
639
|
+
engine = getattr(application.state, "engine", None)
|
|
628
640
|
fact_count = engine.fact_count if engine else 0
|
|
629
641
|
mode = engine._config.mode.value if engine and hasattr(engine, '_config') else "unknown"
|
|
630
642
|
return {
|
|
@@ -641,9 +653,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
641
653
|
@application.get("/list")
|
|
642
654
|
async def list_facts(limit: int = 50):
|
|
643
655
|
_update_activity()
|
|
644
|
-
engine =
|
|
645
|
-
if engine is None:
|
|
646
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
656
|
+
engine = _get_engine_or_503()
|
|
647
657
|
try:
|
|
648
658
|
facts = engine.list_facts(limit=limit)
|
|
649
659
|
items = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superlocalmemory
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.18
|
|
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
|
|
@@ -48,6 +48,9 @@ Requires-Dist: rustworkx<1,>=0.15
|
|
|
48
48
|
Requires-Dist: watchdog<6,>=4.0
|
|
49
49
|
Requires-Dist: psutil>=5.9.0
|
|
50
50
|
Requires-Dist: structlog<27.0.0,>=24.0.0
|
|
51
|
+
Requires-Dist: sentence-transformers[onnx]>=5.0.0
|
|
52
|
+
Requires-Dist: torch>=2.2.0
|
|
53
|
+
Requires-Dist: scikit-learn<2.0.0,>=1.3.0
|
|
51
54
|
Provides-Extra: search
|
|
52
55
|
Requires-Dist: sentence-transformers[onnx]>=5.0.0; extra == "search"
|
|
53
56
|
Requires-Dist: einops>=0.8.2; extra == "search"
|
|
@@ -634,7 +637,7 @@ Qualixar is building the open-source infrastructure for AI agent reliability eng
|
|
|
634
637
|
| **[SLM Mesh](https://github.com/qualixar/slm-mesh)** | P2P coordination across AI agent sessions | `npm i slm-mesh` | — |
|
|
635
638
|
| **[SLM MCP Hub](https://github.com/qualixar/slm-mcp-hub)** | Federate 430+ MCP tools through one gateway | `pip install slm-mcp-hub` | — |
|
|
636
639
|
| **[AgentAssay](https://github.com/qualixar/agentassay)** | Token-efficient AI agent testing | `pip install agentassay` | [arXiv:2603.02601](https://arxiv.org/abs/2603.02601) |
|
|
637
|
-
| **[AgentAssert](https://github.com/qualixar/agentassert-abc)** | Behavioral contracts + drift detection |
|
|
640
|
+
| **[AgentAssert](https://github.com/qualixar/agentassert-abc)** | Behavioral contracts + drift detection | `pip install agentassert-abc` | [arXiv:2602.22302](https://arxiv.org/abs/2602.22302) |
|
|
638
641
|
| **[SkillFortify](https://github.com/qualixar/skillfortify)** | Formal verification for AI agent skills | `pip install skillfortify` | [arXiv:2603.00195](https://arxiv.org/abs/2603.00195) |
|
|
639
642
|
|
|
640
643
|
**Zero cloud dependency. Local-first. EU AI Act compliant.**
|