superlocalmemory 3.8.0 → 3.8.2

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 (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -8,36 +8,73 @@ Routes: /api/evolution/status, /api/evolution/enable, /api/evolution/run
8
8
  """
9
9
 
10
10
  import logging
11
- from pathlib import Path
11
+ from types import SimpleNamespace
12
12
  from typing import Optional
13
13
 
14
- from fastapi import APIRouter
14
+ from fastapi import APIRouter, Request
15
15
  from pydantic import BaseModel
16
16
 
17
- from .helpers import get_active_profile, MEMORY_DIR
17
+ from superlocalmemory.server.config_file import read_config, update_config
18
+
19
+ from .helpers import MEMORY_DIR, get_active_profile
18
20
 
19
21
  logger = logging.getLogger("superlocalmemory.routes.evolution")
20
22
  router = APIRouter()
21
23
 
22
24
 
25
+ def _require_read(request: Request) -> None:
26
+ """Guard evolution telemetry with READ on the active profile."""
27
+ from superlocalmemory.access.rbac import Permission
28
+ from superlocalmemory.server.rbac_enforce import require_permission
29
+
30
+ require_permission(request, Permission.READ, profile=get_active_profile())
31
+
32
+
33
+ def _require_manage(request: Request) -> None:
34
+ """Guard evolution mutations with the same RBAC boundary as v3 settings."""
35
+ from superlocalmemory.server.rbac_enforce import require_manage
36
+
37
+ require_manage(request)
38
+
39
+
40
+ def _read_evolution_config() -> dict:
41
+ """Read one process-safe evolution config snapshot."""
42
+ return dict(read_config(MEMORY_DIR / "config.json").get("evolution", {}))
43
+
44
+
45
+ def _update_evolution_config(update) -> dict:
46
+ """Atomically update evolution without losing other config sections."""
47
+ config_path = MEMORY_DIR / "config.json"
48
+
49
+ def mutate(cfg: dict) -> None:
50
+ evolution = cfg.setdefault("evolution", {})
51
+ update(evolution)
52
+
53
+ cfg = update_config(config_path, mutate)
54
+ return dict(cfg.get("evolution", {}))
55
+
56
+
57
+ def _enable_evolution(config: dict) -> None:
58
+ config["enabled"] = True
59
+ config.setdefault("backend", "auto")
60
+
61
+
23
62
  @router.get("/api/evolution/status")
24
- async def evolution_status():
63
+ def evolution_status(request: Request):
25
64
  """Get evolution engine status, backend, and recent history."""
65
+ _require_read(request)
26
66
  try:
27
- import json as _json
28
- from superlocalmemory.evolution.skill_evolver import detect_backend
29
67
  from superlocalmemory.evolution.evolution_store import EvolutionStore
68
+ from superlocalmemory.evolution.skill_evolver import detect_backend
30
69
 
31
- # Read config directly from config.json (SLMConfig.load doesn't serialize evolution)
32
- config_path = MEMORY_DIR / "config.json"
33
- evo_cfg = {}
34
- if config_path.exists():
35
- with open(config_path) as f:
36
- cfg = _json.load(f)
37
- evo_cfg = cfg.get("evolution", {})
38
-
70
+ evo_cfg = _read_evolution_config()
39
71
  enabled = evo_cfg.get("enabled", False)
40
- backend = detect_backend() if enabled else "none"
72
+ backend_setting = evo_cfg.get("backend", "auto")
73
+ backend = (
74
+ detect_backend() if enabled and backend_setting == "auto"
75
+ else backend_setting if enabled
76
+ else "none"
77
+ )
41
78
  db_path = str(MEMORY_DIR / "memory.db")
42
79
 
43
80
  profile_id = get_active_profile()
@@ -49,7 +86,7 @@ async def evolution_status():
49
86
  "enabled": enabled,
50
87
  "backend": backend,
51
88
  "config": {
52
- "backend_setting": evo_cfg.get("backend", "auto"),
89
+ "backend_setting": backend_setting,
53
90
  "max_per_cycle": evo_cfg.get("max_evolutions_per_cycle", 3),
54
91
  "mutation_model": evo_cfg.get("mutation_model", ""),
55
92
  "verify_model": evo_cfg.get("verify_model", ""),
@@ -82,53 +119,26 @@ async def evolution_status():
82
119
 
83
120
 
84
121
  @router.post("/api/evolution/enable")
85
- async def evolution_enable():
86
- """Enable skill evolution engine. Writes directly to config.json."""
122
+ def evolution_enable(request: Request):
123
+ """Enable evolution without replacing the user's selected backend."""
124
+ _require_manage(request)
87
125
  try:
88
- import json as _json
89
-
90
- config_path = MEMORY_DIR / "config.json"
91
- cfg = {}
92
- if config_path.exists():
93
- with open(config_path) as f:
94
- cfg = _json.load(f)
95
-
96
- if "evolution" not in cfg:
97
- cfg["evolution"] = {}
98
- cfg["evolution"]["enabled"] = True
99
- cfg["evolution"]["backend"] = "auto"
100
-
101
- with open(config_path, "w") as f:
102
- _json.dump(cfg, f, indent=2)
103
-
104
- return {"ok": True, "message": "Evolution enabled. Will use auto-detected backend."}
126
+ evolution = _update_evolution_config(_enable_evolution)
127
+ return {
128
+ "ok": True,
129
+ "message": f"Evolution enabled with {evolution['backend']} backend.",
130
+ }
105
131
  except Exception:
106
132
  logger.exception("evolution_enable error")
107
133
  return {"ok": False, "error": "Internal server error"}
108
134
 
109
135
 
110
136
  @router.post("/api/evolution/disable")
111
- async def evolution_disable():
137
+ def evolution_disable(request: Request):
112
138
  """Disable skill evolution engine. Mirrors /api/evolution/enable."""
139
+ _require_manage(request)
113
140
  try:
114
- import json as _json
115
- import os as _os
116
-
117
- config_path = MEMORY_DIR / "config.json"
118
- cfg: dict = {}
119
- if config_path.exists():
120
- with open(config_path) as f:
121
- cfg = _json.load(f)
122
-
123
- if "evolution" not in cfg:
124
- cfg["evolution"] = {}
125
- cfg["evolution"]["enabled"] = False
126
-
127
- # Atomic write — protect against truncated config.json on crash.
128
- tmp = config_path.with_suffix(".json.evo_disable_tmp")
129
- config_path.parent.mkdir(parents=True, exist_ok=True)
130
- tmp.write_text(_json.dumps(cfg, indent=2), encoding="utf-8")
131
- _os.replace(tmp, config_path)
141
+ _update_evolution_config(lambda cfg: cfg.update({"enabled": False}))
132
142
 
133
143
  return {"ok": True, "message": "Evolution disabled."}
134
144
  except Exception:
@@ -137,18 +147,13 @@ async def evolution_disable():
137
147
 
138
148
 
139
149
  @router.post("/api/evolution/run")
140
- async def evolution_run():
150
+ def evolution_run(request: Request):
141
151
  """Manually trigger an evolution cycle."""
152
+ _require_manage(request)
142
153
  try:
143
- import json as _json
144
154
  from superlocalmemory.evolution.skill_evolver import SkillEvolver
145
155
 
146
- config_path = MEMORY_DIR / "config.json"
147
- evo_cfg = {}
148
- if config_path.exists():
149
- with open(config_path) as f:
150
- evo_cfg = _json.load(f).get("evolution", {})
151
-
156
+ evo_cfg = _read_evolution_config()
152
157
  if not evo_cfg.get("enabled", False):
153
158
  return {"ok": False, "error": "Evolution is disabled. Enable first."}
154
159
 
@@ -159,17 +164,17 @@ async def evolution_run():
159
164
  # per-step model fields (v3.7.9) or a dashboard-triggered run would
160
165
  # silently ignore the user's configured models and fall back to the
161
166
  # cheapest defaults.
162
- class _EvoCfg:
163
- enabled = True
164
- backend = evo_cfg.get("backend", "auto")
165
- max_evolutions_per_cycle = evo_cfg.get("max_evolutions_per_cycle", 3)
166
- mutation_model = evo_cfg.get("mutation_model", "")
167
- verify_model = evo_cfg.get("verify_model", "")
168
- confirm_model = evo_cfg.get("confirm_model", "")
169
- class _Cfg:
170
- evolution = _EvoCfg()
171
-
172
- evolver = SkillEvolver(db_path, _Cfg())
167
+ evolution_config = SimpleNamespace(
168
+ enabled=True,
169
+ backend=evo_cfg.get("backend", "auto"),
170
+ max_evolutions_per_cycle=evo_cfg.get("max_evolutions_per_cycle", 3),
171
+ mutation_model=evo_cfg.get("mutation_model", ""),
172
+ verify_model=evo_cfg.get("verify_model", ""),
173
+ confirm_model=evo_cfg.get("confirm_model", ""),
174
+ )
175
+ evolver = SkillEvolver(
176
+ db_path, SimpleNamespace(evolution=evolution_config)
177
+ )
173
178
  result = evolver.run_consolidation_cycle(profile)
174
179
 
175
180
  return {"ok": True, **result}
@@ -188,16 +193,15 @@ class EvolutionConfigUpdate(BaseModel):
188
193
 
189
194
 
190
195
  @router.post("/api/evolution/config")
191
- async def evolution_config(body: EvolutionConfigUpdate):
196
+ def evolution_config(request: Request, body: EvolutionConfigUpdate):
192
197
  """Update evolution config from the dashboard (v3.7.9).
193
198
 
194
199
  Validates model + backend values against the same allow-list the CLI uses
195
200
  (``slm config set evolution.*``) and persists to config.json atomically.
196
201
  Only fields provided in the body are changed.
197
202
  """
203
+ _require_manage(request)
198
204
  try:
199
- import json as _json
200
- import os as _os
201
205
  from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
202
206
 
203
207
  accepted_models = set(_MODEL_ALIASES) | {"", "auto"}
@@ -217,27 +221,23 @@ async def evolution_config(body: EvolutionConfigUpdate):
217
221
  and not 0 < body.max_evolutions_per_cycle <= 50):
218
222
  return {"ok": False, "error": "max_evolutions_per_cycle must be 1..50"}
219
223
 
220
- config_path = MEMORY_DIR / "config.json"
221
- cfg = {}
222
- if config_path.exists():
223
- with open(config_path) as f:
224
- cfg = _json.load(f)
225
- evo = cfg.setdefault("evolution", {})
226
-
227
- for field in ("enabled", "backend", "max_evolutions_per_cycle",
228
- "mutation_model", "verify_model", "confirm_model"):
229
- val = getattr(body, field)
230
- if val is None:
231
- continue
232
- # "auto" is the UI label for the resolver's "" (pick cheapest model).
233
- if field.endswith("_model") and val == "auto":
234
- val = ""
235
- evo[field] = val
236
-
237
- # Atomic write — a truncated config.json would brick every slm call.
238
- tmp = config_path.with_suffix(".json.evo_tmp")
239
- tmp.write_text(_json.dumps(cfg, indent=2), encoding="utf-8")
240
- _os.replace(tmp, config_path)
224
+ def _apply(evo: dict) -> None:
225
+ for field in (
226
+ "enabled",
227
+ "backend",
228
+ "max_evolutions_per_cycle",
229
+ "mutation_model",
230
+ "verify_model",
231
+ "confirm_model",
232
+ ):
233
+ value = getattr(body, field)
234
+ if value is None:
235
+ continue
236
+ if field.endswith("_model") and value == "auto":
237
+ value = ""
238
+ evo[field] = value
239
+
240
+ evo = _update_evolution_config(_apply)
241
241
 
242
242
  return {"ok": True, "config": {
243
243
  "enabled": evo.get("enabled", False),
@@ -253,11 +253,13 @@ async def evolution_config(body: EvolutionConfigUpdate):
253
253
 
254
254
 
255
255
  @router.get("/api/evolution/lineage")
256
- async def evolution_lineage(skill_name: str = ""):
256
+ def evolution_lineage(request: Request, skill_name: str = ""):
257
257
  """Get evolution lineage for a skill or all skills.
258
258
 
259
259
  Returns lineage records and a tree structure grouped by root skill.
260
260
  """
261
+ _require_read(request)
262
+ conn = None
261
263
  try:
262
264
  import sqlite3 as _sqlite3
263
265
 
@@ -287,8 +289,6 @@ async def evolution_lineage(skill_name: str = ""):
287
289
  (profile_id,),
288
290
  ).fetchall()
289
291
 
290
- conn.close()
291
-
292
292
  lineage = [
293
293
  {
294
294
  "id": dict(r)["id"],
@@ -329,3 +329,6 @@ async def evolution_lineage(skill_name: str = ""):
329
329
  except Exception:
330
330
  logger.exception("evolution_lineage error")
331
331
  return {"lineage": [], "lineage_count": 0, "tree": {}, "error": "Internal server error"}
332
+ finally:
333
+ if conn is not None:
334
+ conn.close()