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.
Files changed (44) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/requirements.txt +1 -1
  6. package/plugin/skills/slm-recall/SKILL.md +4 -3
  7. package/plugin-src/manifest.json +1 -1
  8. package/plugin-src/requirements.txt +1 -1
  9. package/plugin-src/skills/slm-recall/SKILL.md +4 -3
  10. package/pyproject.toml +6 -6
  11. package/src/superlocalmemory/__init__.py +1 -1
  12. package/src/superlocalmemory/cli/commands.py +169 -9
  13. package/src/superlocalmemory/cli/setup_wizard.py +53 -2
  14. package/src/superlocalmemory/core/backend_orchestrator.py +6 -1
  15. package/src/superlocalmemory/core/config.py +1 -1
  16. package/src/superlocalmemory/core/engine.py +3 -2
  17. package/src/superlocalmemory/core/engine_wiring.py +3 -1
  18. package/src/superlocalmemory/core/scale_engine.py +9 -1
  19. package/src/superlocalmemory/core/store_pipeline.py +1 -1
  20. package/src/superlocalmemory/hooks/before_web_hook.py +1 -1
  21. package/src/superlocalmemory/hooks/claude_code_hooks.py +1 -1
  22. package/src/superlocalmemory/infra/auth_middleware.py +5 -5
  23. package/src/superlocalmemory/mcp/_daemon_proxy.py +8 -10
  24. package/src/superlocalmemory/mcp/server.py +1 -0
  25. package/src/superlocalmemory/mcp/tools_active.py +11 -7
  26. package/src/superlocalmemory/mcp/tools_core.py +178 -20
  27. package/src/superlocalmemory/optimize/cache/centroid_store.py +21 -3
  28. package/src/superlocalmemory/optimize/cache/manager.py +7 -0
  29. package/src/superlocalmemory/optimize/cache/semantic.py +27 -10
  30. package/src/superlocalmemory/retrieval/engine.py +16 -8
  31. package/src/superlocalmemory/server/profile_runtime.py +384 -0
  32. package/src/superlocalmemory/server/recall_health.py +13 -7
  33. package/src/superlocalmemory/server/routes/chat.py +2 -2
  34. package/src/superlocalmemory/server/routes/helpers.py +9 -16
  35. package/src/superlocalmemory/server/routes/profiles.py +24 -14
  36. package/src/superlocalmemory/server/routes/v3_api.py +97 -20
  37. package/src/superlocalmemory/server/unified_daemon.py +261 -60
  38. package/src/superlocalmemory/storage/migration_runner.py +44 -0
  39. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +32 -3
  40. package/src/superlocalmemory/ui/index.html +32 -1
  41. package/src/superlocalmemory/ui/js/auto-settings.js +48 -0
  42. package/src/superlocalmemory/ui/js/memory-chat.js +2 -2
  43. package/src/superlocalmemory/ui/js/profiles.js +11 -2
  44. 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("SELECT COUNT(*) FROM atomic_facts")
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
- cursor.execute("SELECT COUNT(*) FROM memories")
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": config.active_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.save(mode_change=True)
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
- # Kill existing worker so next request uses new config
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.save()
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."""