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
|
@@ -19,6 +19,27 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
router = APIRouter(prefix="/api/v3", tags=["v3"])
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
async def _apply_runtime_config(request: Request, config, *, mode_change: bool) -> None:
|
|
23
|
+
"""Persist and hot-swap config only after the daemon transition succeeds."""
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
authorization = authorize_route_mutation(
|
|
27
|
+
request,
|
|
28
|
+
operation="update",
|
|
29
|
+
source_agent_id="dashboard-config",
|
|
30
|
+
profile_id=getattr(config, "active_profile", "default"),
|
|
31
|
+
)
|
|
32
|
+
from superlocalmemory.server.profile_runtime import reconfigure_daemon_engine
|
|
33
|
+
|
|
34
|
+
await asyncio.to_thread(
|
|
35
|
+
reconfigure_daemon_engine,
|
|
36
|
+
request.app.state,
|
|
37
|
+
config,
|
|
38
|
+
mode_change=mode_change,
|
|
39
|
+
)
|
|
40
|
+
authorization.complete()
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
# ── Dashboard ────────────────────────────────────────────────
|
|
23
44
|
|
|
24
45
|
@router.get("/dashboard")
|
|
@@ -26,7 +47,10 @@ async def dashboard(request: Request):
|
|
|
26
47
|
"""Dashboard summary: mode, memory count, health score, recent activity."""
|
|
27
48
|
try:
|
|
28
49
|
from superlocalmemory.core.config import SLMConfig
|
|
29
|
-
config = SLMConfig.load()
|
|
50
|
+
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
51
|
+
from superlocalmemory.server.profile_runtime import get_profile_runtime
|
|
52
|
+
|
|
53
|
+
active_profile = get_profile_runtime(request.app.state).snapshot.profile_id
|
|
30
54
|
|
|
31
55
|
# Read stats directly from SQLite (dashboard doesn't load engine)
|
|
32
56
|
import sqlite3
|
|
@@ -38,12 +62,24 @@ async def dashboard(request: Request):
|
|
|
38
62
|
conn = sqlite3.connect(str(db_path))
|
|
39
63
|
cursor = conn.cursor()
|
|
40
64
|
try:
|
|
41
|
-
cursor.execute(
|
|
65
|
+
cursor.execute(
|
|
66
|
+
"SELECT COUNT(*) FROM atomic_facts WHERE profile_id = ?",
|
|
67
|
+
(active_profile,),
|
|
68
|
+
)
|
|
42
69
|
fact_count = cursor.fetchone()[0]
|
|
43
70
|
except Exception:
|
|
44
71
|
pass
|
|
45
72
|
try:
|
|
46
|
-
|
|
73
|
+
try:
|
|
74
|
+
cursor.execute(
|
|
75
|
+
"SELECT COUNT(*) FROM memories WHERE profile_id = ?",
|
|
76
|
+
(active_profile,),
|
|
77
|
+
)
|
|
78
|
+
except Exception:
|
|
79
|
+
cursor.execute(
|
|
80
|
+
"SELECT COUNT(*) FROM memories WHERE profile = ?",
|
|
81
|
+
(active_profile,),
|
|
82
|
+
)
|
|
47
83
|
memory_count = cursor.fetchone()[0]
|
|
48
84
|
except Exception:
|
|
49
85
|
pass
|
|
@@ -58,7 +94,7 @@ async def dashboard(request: Request):
|
|
|
58
94
|
"model": config.llm.model or "",
|
|
59
95
|
"memory_count": memory_count,
|
|
60
96
|
"fact_count": fact_count,
|
|
61
|
-
"profile":
|
|
97
|
+
"profile": active_profile,
|
|
62
98
|
"base_dir": str(config.base_dir),
|
|
63
99
|
"version": SLM_VERSION,
|
|
64
100
|
}
|
|
@@ -69,11 +105,11 @@ async def dashboard(request: Request):
|
|
|
69
105
|
# ── Mode ─────────────────────────────────────────────────────
|
|
70
106
|
|
|
71
107
|
@router.get("/mode")
|
|
72
|
-
async def get_mode():
|
|
108
|
+
async def get_mode(request: Request):
|
|
73
109
|
"""Get current mode, provider, model — single source of truth for UI."""
|
|
74
110
|
try:
|
|
75
111
|
from superlocalmemory.core.config import SLMConfig
|
|
76
|
-
config = SLMConfig.load()
|
|
112
|
+
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
77
113
|
current = config.mode.value
|
|
78
114
|
return {
|
|
79
115
|
"mode": current,
|
|
@@ -132,9 +168,10 @@ async def set_mode(request: Request):
|
|
|
132
168
|
old_config.retrieval = _template.retrieval
|
|
133
169
|
old_config.math = _template.math
|
|
134
170
|
old_config.channel_weights = _template.channel_weights
|
|
135
|
-
old_config.save(mode_change=True)
|
|
136
171
|
new_config = old_config
|
|
137
172
|
|
|
173
|
+
await _apply_runtime_config(request, new_config, mode_change=True)
|
|
174
|
+
|
|
138
175
|
# Audit the change before we lose context — proves who/when/what.
|
|
139
176
|
# Captures the phantom-write case where `for_mode(C)` auto-defaults
|
|
140
177
|
# the model to "anthropic/claude-sonnet-4" (see core/config.py).
|
|
@@ -151,10 +188,6 @@ async def set_mode(request: Request):
|
|
|
151
188
|
or old_config.embedding.model_name != new_config.embedding.model_name
|
|
152
189
|
)
|
|
153
190
|
|
|
154
|
-
# Invalidate engine; next engine-backed request lazy-inits with new config.
|
|
155
|
-
if hasattr(request.app.state, "engine"):
|
|
156
|
-
request.app.state.engine = None
|
|
157
|
-
|
|
158
191
|
return {
|
|
159
192
|
"success": True,
|
|
160
193
|
"mode": new_mode,
|
|
@@ -227,7 +260,7 @@ async def set_full_config(request: Request):
|
|
|
227
260
|
|
|
228
261
|
# v3.6.12 (settings-1): mode_change=True is required to persist the new
|
|
229
262
|
# mode — save() without it hits a guard that preserves the old mode.
|
|
230
|
-
config
|
|
263
|
+
await _apply_runtime_config(request, config, mode_change=True)
|
|
231
264
|
|
|
232
265
|
log_mode_change(
|
|
233
266
|
old_mode, new_mode,
|
|
@@ -236,16 +269,14 @@ async def set_full_config(request: Request):
|
|
|
236
269
|
source="POST /api/v3/mode/set",
|
|
237
270
|
)
|
|
238
271
|
|
|
239
|
-
#
|
|
272
|
+
# Recycle only out-of-process fallbacks; the resident daemon engine was
|
|
273
|
+
# already acknowledged and hot-swapped by _apply_runtime_config().
|
|
240
274
|
try:
|
|
241
275
|
from superlocalmemory.core.worker_pool import WorkerPool
|
|
242
276
|
WorkerPool.shared().shutdown()
|
|
243
277
|
except Exception:
|
|
244
278
|
pass
|
|
245
279
|
|
|
246
|
-
if hasattr(request.app.state, "engine"):
|
|
247
|
-
request.app.state.engine = None
|
|
248
|
-
|
|
249
280
|
return {
|
|
250
281
|
"success": True,
|
|
251
282
|
"mode": new_mode,
|
|
@@ -309,7 +340,7 @@ async def set_embedding_config(request: Request):
|
|
|
309
340
|
api_version=old_emb.api_version,
|
|
310
341
|
deployment_name=old_emb.deployment_name,
|
|
311
342
|
)
|
|
312
|
-
config
|
|
343
|
+
await _apply_runtime_config(request, config, mode_change=False)
|
|
313
344
|
|
|
314
345
|
needs_reindex = (
|
|
315
346
|
old_emb.provider != new_provider
|
|
@@ -323,9 +354,6 @@ async def set_embedding_config(request: Request):
|
|
|
323
354
|
WorkerPool.shared().shutdown()
|
|
324
355
|
except Exception:
|
|
325
356
|
pass
|
|
326
|
-
if hasattr(request.app.state, "engine"):
|
|
327
|
-
request.app.state.engine = None
|
|
328
|
-
|
|
329
357
|
return {
|
|
330
358
|
"success": True,
|
|
331
359
|
"provider": new_provider,
|
|
@@ -337,6 +365,55 @@ async def set_embedding_config(request: Request):
|
|
|
337
365
|
return JSONResponse({"error": str(e)}, status_code=500)
|
|
338
366
|
|
|
339
367
|
|
|
368
|
+
@router.get("/scope/config")
|
|
369
|
+
async def get_scope_config(request: Request):
|
|
370
|
+
"""Return runtime multi-scope defaults used by daemon writes and recalls."""
|
|
371
|
+
try:
|
|
372
|
+
from superlocalmemory.core.config import SLMConfig
|
|
373
|
+
|
|
374
|
+
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
375
|
+
return {"success": True, **config.scope.as_dict()}
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
return JSONResponse({"error": str(exc)}, status_code=500)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@router.put("/scope/config")
|
|
381
|
+
async def set_scope_config(request: Request):
|
|
382
|
+
"""Validate, persist, and hot-apply explicit multi-scope defaults."""
|
|
383
|
+
try:
|
|
384
|
+
body = await request.json()
|
|
385
|
+
from superlocalmemory.core.config import SLMConfig, ScopeConfig
|
|
386
|
+
|
|
387
|
+
config = SLMConfig.load()
|
|
388
|
+
current = config.scope
|
|
389
|
+
default_scope = body.get("default_scope", current.default_scope)
|
|
390
|
+
if default_scope not in {"personal", "shared", "global"}:
|
|
391
|
+
return JSONResponse(
|
|
392
|
+
{"error": "default_scope must be personal, shared, or global"},
|
|
393
|
+
status_code=400,
|
|
394
|
+
)
|
|
395
|
+
include_global = body.get(
|
|
396
|
+
"recall_include_global", current.recall_include_global,
|
|
397
|
+
)
|
|
398
|
+
include_shared = body.get(
|
|
399
|
+
"recall_include_shared", current.recall_include_shared,
|
|
400
|
+
)
|
|
401
|
+
if not isinstance(include_global, bool) or not isinstance(include_shared, bool):
|
|
402
|
+
return JSONResponse(
|
|
403
|
+
{"error": "recall scope flags must be booleans"},
|
|
404
|
+
status_code=400,
|
|
405
|
+
)
|
|
406
|
+
config.scope = ScopeConfig(
|
|
407
|
+
default_scope=default_scope,
|
|
408
|
+
recall_include_global=include_global,
|
|
409
|
+
recall_include_shared=include_shared,
|
|
410
|
+
)
|
|
411
|
+
await _apply_runtime_config(request, config, mode_change=False)
|
|
412
|
+
return {"success": True, **config.scope.as_dict()}
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
return JSONResponse({"error": str(exc)}, status_code=500)
|
|
415
|
+
|
|
416
|
+
|
|
340
417
|
@router.post("/embedding/test")
|
|
341
418
|
async def test_embedding_endpoint(request: Request):
|
|
342
419
|
"""Test connectivity to a custom embedding endpoint."""
|