superlocalmemory 3.7.8 → 3.8.0

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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -1
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +94 -10
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +384 -56
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -9,8 +9,10 @@ Routes: /api/evolution/status, /api/evolution/enable, /api/evolution/run
9
9
 
10
10
  import logging
11
11
  from pathlib import Path
12
+ from typing import Optional
12
13
 
13
14
  from fastapi import APIRouter
15
+ from pydantic import BaseModel
14
16
 
15
17
  from .helpers import get_active_profile, MEMORY_DIR
16
18
 
@@ -38,9 +40,10 @@ async def evolution_status():
38
40
  backend = detect_backend() if enabled else "none"
39
41
  db_path = str(MEMORY_DIR / "memory.db")
40
42
 
43
+ profile_id = get_active_profile()
41
44
  store = EvolutionStore(db_path)
42
- stats = store.get_stats()
43
- recent = store.get_recent(limit=10)
45
+ stats = store.get_stats(profile_id)
46
+ recent = store.get_recent(profile_id, limit=10)
44
47
 
45
48
  return {
46
49
  "enabled": enabled,
@@ -48,6 +51,9 @@ async def evolution_status():
48
51
  "config": {
49
52
  "backend_setting": evo_cfg.get("backend", "auto"),
50
53
  "max_per_cycle": evo_cfg.get("max_evolutions_per_cycle", 3),
54
+ "mutation_model": evo_cfg.get("mutation_model", ""),
55
+ "verify_model": evo_cfg.get("verify_model", ""),
56
+ "confirm_model": evo_cfg.get("confirm_model", ""),
51
57
  },
52
58
  "stats": {
53
59
  "total": stats.get("total", 0),
@@ -70,9 +76,9 @@ async def evolution_status():
70
76
  for r in recent
71
77
  ],
72
78
  }
73
- except Exception as e:
74
- logger.debug("evolution_status error: %s", e)
75
- return {"enabled": False, "backend": "none", "error": str(e)}
79
+ except Exception:
80
+ logger.exception("evolution_status error")
81
+ return {"enabled": False, "backend": "none", "error": "Internal server error"}
76
82
 
77
83
 
78
84
  @router.post("/api/evolution/enable")
@@ -96,9 +102,38 @@ async def evolution_enable():
96
102
  _json.dump(cfg, f, indent=2)
97
103
 
98
104
  return {"ok": True, "message": "Evolution enabled. Will use auto-detected backend."}
99
- except Exception as e:
100
- logger.error("evolution_enable error: %s", e)
101
- return {"ok": False, "error": str(e)}
105
+ except Exception:
106
+ logger.exception("evolution_enable error")
107
+ return {"ok": False, "error": "Internal server error"}
108
+
109
+
110
+ @router.post("/api/evolution/disable")
111
+ async def evolution_disable():
112
+ """Disable skill evolution engine. Mirrors /api/evolution/enable."""
113
+ 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)
132
+
133
+ return {"ok": True, "message": "Evolution disabled."}
134
+ except Exception:
135
+ logger.exception("evolution_disable error")
136
+ return {"ok": False, "error": "Internal server error"}
102
137
 
103
138
 
104
139
  @router.post("/api/evolution/run")
@@ -120,11 +155,17 @@ async def evolution_run():
120
155
  profile = get_active_profile()
121
156
  db_path = str(MEMORY_DIR / "memory.db")
122
157
 
123
- # Build a minimal config object for the evolver
158
+ # Build a minimal config object for the evolver. Must carry the
159
+ # per-step model fields (v3.7.9) or a dashboard-triggered run would
160
+ # silently ignore the user's configured models and fall back to the
161
+ # cheapest defaults.
124
162
  class _EvoCfg:
125
163
  enabled = True
126
164
  backend = evo_cfg.get("backend", "auto")
127
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", "")
128
169
  class _Cfg:
129
170
  evolution = _EvoCfg()
130
171
 
@@ -132,9 +173,83 @@ async def evolution_run():
132
173
  result = evolver.run_consolidation_cycle(profile)
133
174
 
134
175
  return {"ok": True, **result}
135
- except Exception as e:
136
- logger.error("evolution_run error: %s", e)
137
- return {"ok": False, "error": str(e)}
176
+ except Exception:
177
+ logger.exception("evolution_run error")
178
+ return {"ok": False, "error": "Internal server error"}
179
+
180
+
181
+ class EvolutionConfigUpdate(BaseModel):
182
+ enabled: Optional[bool] = None
183
+ backend: Optional[str] = None
184
+ max_evolutions_per_cycle: Optional[int] = None
185
+ mutation_model: Optional[str] = None
186
+ verify_model: Optional[str] = None
187
+ confirm_model: Optional[str] = None
188
+
189
+
190
+ @router.post("/api/evolution/config")
191
+ async def evolution_config(body: EvolutionConfigUpdate):
192
+ """Update evolution config from the dashboard (v3.7.9).
193
+
194
+ Validates model + backend values against the same allow-list the CLI uses
195
+ (``slm config set evolution.*``) and persists to config.json atomically.
196
+ Only fields provided in the body are changed.
197
+ """
198
+ try:
199
+ import json as _json
200
+ import os as _os
201
+ from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
202
+
203
+ accepted_models = set(_MODEL_ALIASES) | {"", "auto"}
204
+ accepted_backends = {"auto", "claude", "ollama", "anthropic", "openai"}
205
+
206
+ for field in ("mutation_model", "verify_model", "confirm_model"):
207
+ val = getattr(body, field)
208
+ if val is not None and val not in accepted_models:
209
+ allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
210
+ return {"ok": False, "error": f"{field} must be one of: {allowed}"}
211
+ if body.backend is not None and body.backend not in accepted_backends:
212
+ return {
213
+ "ok": False,
214
+ "error": f"backend must be one of: {', '.join(sorted(accepted_backends))}",
215
+ }
216
+ if (body.max_evolutions_per_cycle is not None
217
+ and not 0 < body.max_evolutions_per_cycle <= 50):
218
+ return {"ok": False, "error": "max_evolutions_per_cycle must be 1..50"}
219
+
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)
241
+
242
+ return {"ok": True, "config": {
243
+ "enabled": evo.get("enabled", False),
244
+ "backend": evo.get("backend", "auto"),
245
+ "max_evolutions_per_cycle": evo.get("max_evolutions_per_cycle", 3),
246
+ "mutation_model": evo.get("mutation_model", ""),
247
+ "verify_model": evo.get("verify_model", ""),
248
+ "confirm_model": evo.get("confirm_model", ""),
249
+ }}
250
+ except Exception:
251
+ logger.exception("evolution_config error")
252
+ return {"ok": False, "error": "Internal server error"}
138
253
 
139
254
 
140
255
  @router.get("/api/evolution/lineage")
@@ -147,6 +262,7 @@ async def evolution_lineage(skill_name: str = ""):
147
262
  import sqlite3 as _sqlite3
148
263
 
149
264
  db_path = str(MEMORY_DIR / "memory.db")
265
+ profile_id = get_active_profile()
150
266
  conn = _sqlite3.connect(db_path, timeout=10)
151
267
  conn.row_factory = _sqlite3.Row
152
268
 
@@ -156,9 +272,9 @@ async def evolution_lineage(skill_name: str = ""):
156
272
  "trigger_type, generation, status, mutation_summary, "
157
273
  "blind_verified, created_at, completed_at "
158
274
  "FROM skill_evolution_log "
159
- "WHERE skill_name = ? OR parent_skill_id = ? "
275
+ "WHERE profile_id = ? AND (skill_name = ? OR parent_skill_id = ?) "
160
276
  "ORDER BY created_at ASC",
161
- (skill_name, skill_name),
277
+ (profile_id, skill_name, skill_name),
162
278
  ).fetchall()
163
279
  else:
164
280
  rows = conn.execute(
@@ -166,7 +282,9 @@ async def evolution_lineage(skill_name: str = ""):
166
282
  "trigger_type, generation, status, mutation_summary, "
167
283
  "blind_verified, created_at, completed_at "
168
284
  "FROM skill_evolution_log "
285
+ "WHERE profile_id = ? "
169
286
  "ORDER BY created_at DESC LIMIT 100",
287
+ (profile_id,),
170
288
  ).fetchall()
171
289
 
172
290
  conn.close()
@@ -208,6 +326,6 @@ async def evolution_lineage(skill_name: str = ""):
208
326
  "lineage_count": len(lineage),
209
327
  "tree": tree,
210
328
  }
211
- except Exception as e:
212
- logger.debug("evolution_lineage error: %s", e)
213
- return {"lineage": [], "lineage_count": 0, "tree": {}, "error": str(e)}
329
+ except Exception:
330
+ logger.exception("evolution_lineage error")
331
+ return {"lineage": [], "lineage_count": 0, "tree": {}, "error": "Internal server error"}
@@ -342,12 +342,23 @@ def set_active_profile_everywhere(name: str) -> None:
342
342
 
343
343
 
344
344
  def delete_profile_from_db(name: str) -> None:
345
- """Delete a profile row from SQLite. ON DELETE CASCADE handles child rows."""
345
+ """Delete a profile row from SQLite.
346
+
347
+ rbac_memberships has no FK to profiles, so CASCADE does not remove role
348
+ grants — they would otherwise survive deletion and silently re-activate if
349
+ a profile of the same name is later recreated. Remove them explicitly.
350
+ """
346
351
  if not DB_PATH.exists():
347
352
  return
348
353
  conn = sqlite3.connect(str(DB_PATH))
349
354
  try:
350
355
  conn.execute("PRAGMA foreign_keys=ON")
356
+ # Purge role grants for this workspace (no FK CASCADE covers these).
357
+ for tbl in ("rbac_memberships",):
358
+ try:
359
+ conn.execute(f"DELETE FROM {tbl} WHERE profile_id = ?", (name,))
360
+ except sqlite3.OperationalError:
361
+ pass # table may not exist on older installs
351
362
  conn.execute("DELETE FROM profiles WHERE profile_id = ?", (name,))
352
363
  conn.commit()
353
364
  finally:
@@ -420,6 +431,10 @@ class SearchRequest(BaseModel):
420
431
  cluster_id: Optional[int] = None
421
432
  date_from: Optional[str] = None
422
433
  date_to: Optional[str] = None
434
+ # T-window: relative span ("7d", "30d", "1y") or explicit range
435
+ # ("2026-07-01..2026-07-31"). When empty, date_from/date_to (if both set)
436
+ # are used as the range. Empty + no dates = no time filter.
437
+ window: Optional[str] = None
423
438
 
424
439
 
425
440
  class ProfileSwitch(BaseModel):
@@ -18,7 +18,7 @@ import threading
18
18
  from datetime import datetime, timezone
19
19
 
20
20
  from fastapi import APIRouter, HTTPException, Request
21
- from pydantic import BaseModel
21
+ from pydantic import BaseModel, Field
22
22
 
23
23
  router = APIRouter(tags=["ingestion"])
24
24
 
@@ -28,7 +28,9 @@ _active_lock = threading.Lock()
28
28
 
29
29
 
30
30
  class IngestRequest(BaseModel):
31
- content: str
31
+ # Cap ingest content (~1 MB) so a single call cannot exhaust memory through
32
+ # the embedding/tokenizer pipeline.
33
+ content: str = Field(..., max_length=1_000_000)
32
34
  source_type: str
33
35
  dedup_key: str
34
36
  metadata: dict = {}
@@ -99,9 +101,10 @@ async def ingest(req: IngestRequest, request: Request):
99
101
  # truth; retries repair this row without repeating the canonical write.
100
102
  engine._db.execute(
101
103
  "INSERT OR IGNORE INTO ingestion_log "
102
- "(source_type, dedup_key, fact_ids, metadata, status, ingested_at) "
103
- "VALUES (?, ?, ?, ?, 'ingested', ?)",
104
+ "(profile_id, source_type, dedup_key, fact_ids, metadata, status, ingested_at) "
105
+ "VALUES (?, ?, ?, ?, ?, 'ingested', ?)",
104
106
  (
107
+ engine._profile_id,
105
108
  req.source_type,
106
109
  req.dedup_key,
107
110
  json.dumps(fact_ids),
@@ -358,10 +358,10 @@ async def insight_action(
358
358
  result = handler(conn, pid, limit=limit, days=days)
359
359
  result["profile"] = pid
360
360
  return result
361
- except Exception as exc:
362
- logger.warning("Insight action %s failed: %s", action_name, exc)
361
+ except Exception:
362
+ logger.exception("Insight action %s failed", action_name)
363
363
  return JSONResponse(
364
- {"error": f"Query failed: {exc}"},
364
+ {"error": "Query failed"},
365
365
  status_code=500,
366
366
  )
367
367
  finally:
@@ -291,8 +291,8 @@ async def learning_status():
291
291
  result["source_scores"] = {}
292
292
 
293
293
  except Exception as e:
294
- logger.error("Error getting learning status: %s", e)
295
- result["error"] = str(e)
294
+ logger.exception("Error getting learning status")
295
+ result["error"] = "Internal server error"
296
296
 
297
297
  return result
298
298
 
@@ -334,8 +334,8 @@ async def record_feedback(data: dict):
334
334
  "feedback_id": row_id,
335
335
  }
336
336
  except Exception as e:
337
- logger.error("Error recording feedback: %s", e)
338
- return {"success": False, "error": str(e)}
337
+ logger.exception("Error recording feedback")
338
+ return {"success": False, "error": "Internal server error"}
339
339
 
340
340
 
341
341
  @router.post("/api/feedback/dwell")
@@ -379,8 +379,8 @@ async def record_dwell(data: dict):
379
379
  "feedback_id": row_id,
380
380
  }
381
381
  except Exception as e:
382
- logger.error("Error recording dwell: %s", e)
383
- return {"success": False, "error": str(e)}
382
+ logger.exception("Error recording dwell")
383
+ return {"success": False, "error": "Internal server error"}
384
384
 
385
385
 
386
386
  @router.get("/api/feedback/stats")
@@ -414,8 +414,8 @@ async def feedback_stats():
414
414
  "by_channel": by_channel, "by_type": by_type, "available": True,
415
415
  }
416
416
  except Exception as e:
417
- logger.error("Error getting feedback stats: %s", e)
418
- return {"total_signals": 0, "ranking_phase": "baseline", "progress": 0, "error": str(e)}
417
+ logger.exception("Error getting feedback stats")
418
+ return {"total_signals": 0, "ranking_phase": "baseline", "progress": 0, "error": "Internal server error"}
419
419
 
420
420
 
421
421
  # ============================================================================
@@ -455,7 +455,7 @@ async def delete_pattern(data: dict) -> dict:
455
455
  }
456
456
  except Exception as exc: # noqa: BLE001
457
457
  logger.error("delete_pattern failed: %s", exc)
458
- return {"success": False, "error": str(exc)}
458
+ return {"success": False, "error": "internal error"}
459
459
 
460
460
 
461
461
  @router.get("/api/patterns")
@@ -562,8 +562,8 @@ async def learning_backup():
562
562
  "message": f"Learning DB backed up to {backup_name}",
563
563
  }
564
564
  except Exception as e:
565
- logger.error("Error backing up learning DB: %s", e)
566
- return {"success": False, "error": str(e)}
565
+ logger.exception("Error backing up learning DB")
566
+ return {"success": False, "error": "Internal server error"}
567
567
 
568
568
 
569
569
  @router.post("/api/learning/reset")
@@ -583,7 +583,7 @@ async def learning_reset():
583
583
  }
584
584
  except Exception as exc: # noqa: BLE001
585
585
  logger.error("learning_reset failed: %s", exc)
586
- return {"success": False, "error": str(exc)}
586
+ return {"success": False, "error": "internal error"}
587
587
 
588
588
 
589
589
  @router.post("/api/learning/retrain")
@@ -643,7 +643,7 @@ async def learning_retrain(data: dict | None = None):
643
643
  }
644
644
  except Exception as exc: # noqa: BLE001
645
645
  logger.error("learning_retrain failed: %s", exc)
646
- return {"success": False, "error": str(exc)}
646
+ return {"success": False, "error": "internal error"}
647
647
 
648
648
 
649
649
  @router.post("/api/learning/migrate-legacy")
@@ -665,4 +665,4 @@ async def learning_migrate_legacy():
665
665
  return {"success": True, **stats}
666
666
  except Exception as exc: # noqa: BLE001
667
667
  logger.error("learning_migrate_legacy failed: %s", exc)
668
- return {"success": False, "error": str(exc)}
668
+ return {"success": False, "error": "internal error"}
@@ -11,9 +11,10 @@ import json
11
11
  import logging
12
12
  import sqlite3
13
13
 
14
- from fastapi import APIRouter
14
+ from fastapi import APIRouter, Request
15
15
 
16
- from .helpers import get_active_profile, MEMORY_DIR, DB_PATH
16
+ from .helpers import get_active_profile, get_engine_lazy, MEMORY_DIR, DB_PATH
17
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
17
18
 
18
19
  logger = logging.getLogger("superlocalmemory.routes.lifecycle")
19
20
  router = APIRouter()
@@ -121,15 +122,65 @@ async def lifecycle_status():
121
122
  "recent_transitions": [],
122
123
  "age_stats": age_stats,
123
124
  }
124
- except Exception as e:
125
- logger.error("lifecycle_status error: %s", e)
126
- return {"available": False, "error": str(e)}
125
+ except Exception:
126
+ logger.exception("lifecycle_status error")
127
+ return {"available": False, "error": "Internal server error"}
127
128
 
128
129
 
129
130
  @router.post("/api/lifecycle/compact")
130
- async def trigger_compaction(data: dict = {}):
131
- """Trigger lifecycle compaction. Body: {dry_run: true/false}."""
131
+ async def trigger_compaction(request: Request, data: dict = {}):
132
+ """Trigger lifecycle compaction for the active profile.
133
+
134
+ Body: ``{"dry_run": true|false}`` (default true — preview only).
135
+
136
+ Recomputes each fact's lifecycle zone (active→warm→cold→archive) from its
137
+ age/access pattern. dry_run returns the proposed transitions without
138
+ mutating; execute applies them and is profile-scoped + mutation-authorized.
139
+ """
132
140
  if not LIFECYCLE_AVAILABLE:
133
141
  return {"success": False, "error": "Lifecycle engine not available"}
134
142
 
135
- return {"status": "not_implemented", "message": "Coming soon"}
143
+ dry_run = bool((data or {}).get("dry_run", True))
144
+ try:
145
+ engine = get_engine_lazy(request.app.state)
146
+ if engine is None:
147
+ return {"success": False, "error": "Engine not initialized"}
148
+ profile = get_active_profile()
149
+
150
+ mgr = LifecycleManager(engine._db)
151
+ facts = engine._db.get_all_facts(profile)
152
+ candidates = []
153
+ for f in facts:
154
+ new_state = mgr.get_lifecycle_state(f)
155
+ if new_state != f.lifecycle:
156
+ candidates.append({
157
+ "fact_id": f.fact_id,
158
+ "current": getattr(f.lifecycle, "value", str(f.lifecycle)),
159
+ "proposed": getattr(new_state, "value", str(new_state)),
160
+ })
161
+
162
+ applied = 0
163
+ if not dry_run and candidates:
164
+ authorization = authorize_route_mutation(
165
+ request,
166
+ operation="update",
167
+ source_agent_id="http-lifecycle-compact",
168
+ profile_id=profile,
169
+ )
170
+ for c in candidates:
171
+ engine._db.update_fact(c["fact_id"], {"lifecycle": c["proposed"]})
172
+ applied += 1
173
+ authorization.complete()
174
+
175
+ return {
176
+ "success": True,
177
+ "dry_run": dry_run,
178
+ "active_profile": profile,
179
+ "total_facts": len(facts),
180
+ "candidates": len(candidates),
181
+ "applied": applied,
182
+ "transitions": candidates[:20],
183
+ }
184
+ except Exception as exc:
185
+ logger.exception("lifecycle compaction failed")
186
+ return {"success": False, "error": "internal error"}