superlocalmemory 3.7.8 → 3.8.1

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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  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/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  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 +827 -72
  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 +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -19,6 +19,18 @@ logger = logging.getLogger(__name__)
19
19
  router = APIRouter(prefix="/api/v3", tags=["v3"])
20
20
 
21
21
 
22
+ def _internal_error() -> JSONResponse:
23
+ """SEC-H-02: log the full traceback server-side, return a generic message.
24
+
25
+ Returning ``str(e)`` to the client leaked DB schema (column/constraint
26
+ names), the data-directory filesystem path, and — for config routes — could
27
+ surface LLM config internals. Call this only from inside an ``except`` block
28
+ so ``logger.exception`` captures the active traceback.
29
+ """
30
+ logger.exception("v3_api request failed")
31
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
32
+
33
+
22
34
  async def _apply_runtime_config(request: Request, config, *, mode_change: bool) -> None:
23
35
  """Persist and hot-swap config only after the daemon transition succeeds."""
24
36
  import asyncio
@@ -40,6 +52,42 @@ async def _apply_runtime_config(request: Request, config, *, mode_change: bool)
40
52
  authorization.complete()
41
53
 
42
54
 
55
+ def _require_manage(request: Request) -> None:
56
+ """Use one explicit authorization boundary for dashboard configuration."""
57
+ from superlocalmemory.server.rbac_enforce import require_manage
58
+
59
+ require_manage(request)
60
+
61
+
62
+ def _resolve_profile(request: Request, requested: str = "") -> str:
63
+ """Resolve a profile override and enforce READ on its actual target."""
64
+ from superlocalmemory.access.rbac import Permission
65
+ from superlocalmemory.server.rbac_enforce import require_permission
66
+ from superlocalmemory.server.routes.helpers import get_active_profile
67
+
68
+ if requested and not isinstance(requested, str):
69
+ raise HTTPException(status_code=422, detail="profile must be a string")
70
+ profile = requested.strip() if requested else get_active_profile()
71
+ require_permission(request, Permission.READ, profile=profile)
72
+ return profile
73
+
74
+
75
+ def _resolve_mutation_profile(requested: object) -> str:
76
+ """Resolve a body-supplied profile without silently coercing bad input."""
77
+ from superlocalmemory.server.routes.helpers import get_active_profile
78
+
79
+ if requested is not None and not isinstance(requested, str):
80
+ raise HTTPException(status_code=422, detail="profile must be a string")
81
+ return requested.strip() if requested and requested.strip() else get_active_profile()
82
+
83
+
84
+ def _require_manage_for_profile(request: Request, profile: str) -> None:
85
+ """Enforce MANAGE on the resolved mutation target before any side effect."""
86
+ from superlocalmemory.server.rbac_enforce import require_manage
87
+
88
+ require_manage(request, profile=profile)
89
+
90
+
43
91
  # ── Dashboard ────────────────────────────────────────────────
44
92
 
45
93
  @router.get("/dashboard")
@@ -99,7 +147,7 @@ async def dashboard(request: Request):
99
147
  "version": SLM_VERSION,
100
148
  }
101
149
  except Exception as e:
102
- return JSONResponse({"error": str(e)}, status_code=500)
150
+ return _internal_error()
103
151
 
104
152
 
105
153
  # ── Mode ─────────────────────────────────────────────────────
@@ -109,26 +157,36 @@ async def get_mode(request: Request):
109
157
  """Get current mode, provider, model — single source of truth for UI."""
110
158
  try:
111
159
  from superlocalmemory.core.config import SLMConfig
160
+ from urllib.parse import urlparse
112
161
  config = getattr(request.app.state, "config", None) or SLMConfig.load()
113
162
  current = config.mode.value
163
+ # SEC-L-01: expose only the endpoint HOST, not the full URL. A full
164
+ # api_base (e.g. an internal Azure OpenAI deployment path) leaks
165
+ # infrastructure topology to non-admin (viewer) users.
166
+ _api_base = config.llm.api_base or ""
167
+ _endpoint_host = urlparse(_api_base).netloc if _api_base else ""
114
168
  return {
115
169
  "mode": current,
116
170
  "provider": config.llm.provider or "none",
117
171
  "model": config.llm.model or "",
118
172
  "has_key": bool(config.llm.api_key),
119
- "endpoint": config.llm.api_base or "",
173
+ "endpoint": _endpoint_host,
120
174
  "capabilities": {
121
175
  "llm_available": bool(config.llm.provider),
122
176
  "cross_encoder": config.retrieval.use_cross_encoder if hasattr(config, 'retrieval') else False,
123
177
  },
124
178
  }
125
179
  except Exception as e:
126
- return JSONResponse({"error": str(e)}, status_code=500)
180
+ return _internal_error()
127
181
 
128
182
 
129
183
  @router.put("/mode")
130
184
  async def set_mode(request: Request):
131
185
  """Switch operating mode. Body: {"mode": "a"|"b"|"c"}"""
186
+ # SEC-H-01: mode switch reconfigures the whole engine — admin-only (MANAGE)
187
+ # on top of machine auth. Owner keeps MANAGE; personal mode unaffected.
188
+ from superlocalmemory.server.rbac_enforce import require_manage
189
+ require_manage(request)
132
190
  try:
133
191
  body = await request.json()
134
192
  new_mode = body.get("mode", "").lower()
@@ -195,7 +253,7 @@ async def set_mode(request: Request):
195
253
  "message": "Embedding re-indexing will run on next recall." if needs_reindex else "",
196
254
  }
197
255
  except Exception as e:
198
- return JSONResponse({"error": str(e)}, status_code=500)
256
+ return _internal_error()
199
257
 
200
258
 
201
259
  @router.post("/mode/set")
@@ -204,6 +262,9 @@ async def set_full_config(request: Request):
204
262
 
205
263
  V3.4.24: Also accepts embedding_* fields for custom embedding endpoints.
206
264
  """
265
+ # SEC-H-01: sets the LLM provider + API key for the whole engine — admin-only.
266
+ from superlocalmemory.server.rbac_enforce import require_manage
267
+ require_manage(request)
207
268
  try:
208
269
  body = await request.json()
209
270
  new_mode = body.get("mode", "a").lower()
@@ -287,7 +348,7 @@ async def set_full_config(request: Request):
287
348
  "embedding_dimension": config.embedding.dimension,
288
349
  }
289
350
  except Exception as e:
290
- return JSONResponse({"error": str(e)}, status_code=500)
351
+ return _internal_error()
291
352
 
292
353
 
293
354
  # ── V3.4.24: Embedding Configuration ────────────────────────────────
@@ -309,12 +370,15 @@ async def get_embedding_config(request: Request):
309
370
  "mode": config.mode.value,
310
371
  }
311
372
  except Exception as e:
312
- return JSONResponse({"error": str(e)}, status_code=500)
373
+ return _internal_error()
313
374
 
314
375
 
315
376
  @router.put("/embedding/config")
316
377
  async def set_embedding_config(request: Request):
317
378
  """Update embedding configuration independently of mode switch."""
379
+ # SEC-H-01: swapping the embedding provider/key affects all recall — admin-only.
380
+ from superlocalmemory.server.rbac_enforce import require_manage
381
+ require_manage(request)
318
382
  try:
319
383
  body = await request.json()
320
384
  from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
@@ -362,7 +426,7 @@ async def set_embedding_config(request: Request):
362
426
  "needs_reindex": needs_reindex,
363
427
  }
364
428
  except Exception as e:
365
- return JSONResponse({"error": str(e)}, status_code=500)
429
+ return _internal_error()
366
430
 
367
431
 
368
432
  @router.get("/scope/config")
@@ -374,12 +438,15 @@ async def get_scope_config(request: Request):
374
438
  config = getattr(request.app.state, "config", None) or SLMConfig.load()
375
439
  return {"success": True, **config.scope.as_dict()}
376
440
  except Exception as exc:
377
- return JSONResponse({"error": str(exc)}, status_code=500)
441
+ return _internal_error()
378
442
 
379
443
 
380
444
  @router.put("/scope/config")
381
445
  async def set_scope_config(request: Request):
382
446
  """Validate, persist, and hot-apply explicit multi-scope defaults."""
447
+ # SEC-H-01: scope defaults govern cross-profile recall visibility — admin-only.
448
+ from superlocalmemory.server.rbac_enforce import require_manage
449
+ require_manage(request)
383
450
  try:
384
451
  body = await request.json()
385
452
  from superlocalmemory.core.config import SLMConfig, ScopeConfig
@@ -411,15 +478,15 @@ async def set_scope_config(request: Request):
411
478
  await _apply_runtime_config(request, config, mode_change=False)
412
479
  return {"success": True, **config.scope.as_dict()}
413
480
  except Exception as exc:
414
- return JSONResponse({"error": str(exc)}, status_code=500)
481
+ return _internal_error()
415
482
 
416
483
 
417
484
  @router.post("/embedding/test")
418
485
  async def test_embedding_endpoint(request: Request):
419
486
  """Test connectivity to a custom embedding endpoint."""
487
+ _require_manage(request)
420
488
  try:
421
489
  import httpx
422
- from urllib.parse import urlparse
423
490
  body = await request.json()
424
491
  endpoint = body.get("api_endpoint", "").rstrip("/")
425
492
  model = body.get("model_name", "test")
@@ -428,12 +495,10 @@ async def test_embedding_endpoint(request: Request):
428
495
  if not endpoint:
429
496
  return JSONResponse({"error": "No endpoint provided"}, status_code=400)
430
497
 
431
- parsed = urlparse(endpoint)
432
- if parsed.scheme not in ("http", "https"):
433
- return JSONResponse({"error": "Only http/https endpoints supported"}, status_code=400)
434
- host = parsed.hostname or ""
435
- if host in ("169.254.169.254", "metadata.google.internal"):
436
- return JSONResponse({"error": "Cloud metadata endpoints not allowed"}, status_code=400)
498
+ client_host = request.client.host if request.client else ""
499
+ error = _validate_provider_url(endpoint, client_host)
500
+ if error:
501
+ return JSONResponse({"error": error}, status_code=400)
437
502
 
438
503
  if not endpoint.endswith("/embeddings"):
439
504
  endpoint = f"{endpoint}/embeddings"
@@ -475,8 +540,9 @@ async def embed_ping():
475
540
  from .helpers import get_engine_lazy
476
541
  # We just need to confirm the route is alive — engine check is optional
477
542
  return {"ok": True}
478
- except Exception as e:
479
- return JSONResponse({"ok": False, "error": str(e)}, status_code=503)
543
+ except Exception:
544
+ logger.exception("embedder liveness probe failed")
545
+ return JSONResponse({"ok": False, "error": "Internal server error"}, status_code=503)
480
546
 
481
547
 
482
548
  @router.post("/embed")
@@ -509,7 +575,7 @@ async def embed_texts(request: Request):
509
575
  )
510
576
  return {"embeddings": embeddings}
511
577
  except Exception as e:
512
- return JSONResponse({"error": str(e)}, status_code=500)
578
+ return _internal_error()
513
579
 
514
580
 
515
581
  def _validate_provider_url(url: str, client_host: str) -> str | None:
@@ -636,7 +702,8 @@ async def test_provider(request: Request):
636
702
  except httpx.HTTPStatusError as e:
637
703
  return {"success": False, "error": f"HTTP {e.response.status_code}: Invalid key or endpoint"}
638
704
  except Exception as e:
639
- return {"success": False, "error": str(e)}
705
+ logger.exception("verify_key failed")
706
+ return {"success": False, "error": "internal error"}
640
707
 
641
708
 
642
709
  @router.get("/ollama/status")
@@ -666,7 +733,8 @@ async def list_providers():
666
733
  from superlocalmemory.core.config import SLMConfig
667
734
  return {"providers": SLMConfig.provider_presets()}
668
735
  except Exception as exc:
669
- return {"error": str(exc), "providers": []}
736
+ logger.exception("list_providers failed")
737
+ return {"error": "could not load provider list", "providers": []}
670
738
 
671
739
 
672
740
  @router.get("/provider")
@@ -685,12 +753,20 @@ async def get_provider():
685
753
  "has_key": bool(key),
686
754
  }
687
755
  except Exception as exc:
688
- return {"error": str(exc), "provider": "unknown"}
756
+ logger.exception("get_provider failed")
757
+ return {"error": "could not load configuration", "provider": "unknown"}
689
758
 
690
759
 
691
760
  @router.put("/provider")
692
761
  async def set_provider(request: Request):
693
762
  """Set LLM provider. Body: {"provider": "openai", "api_key": "...", "model": "..."}"""
763
+ # Swapping the provider + API key redirects all LLM traffic (prompt/output
764
+ # interception risk) — admin-only, on top of mutation auth.
765
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
766
+ from superlocalmemory.server.rbac_enforce import require_manage
767
+ _auth = authorize_route_mutation(request, operation="update",
768
+ source_agent_id="http-set-provider")
769
+ require_manage(request)
694
770
  try:
695
771
  body = await request.json()
696
772
  provider = body.get("provider", "")
@@ -720,7 +796,7 @@ async def set_provider(request: Request):
720
796
 
721
797
  return {"success": True, "provider": provider, "model": model}
722
798
  except Exception as e:
723
- return JSONResponse({"error": str(e)}, status_code=500)
799
+ return _internal_error()
724
800
 
725
801
 
726
802
  # ── Recall Trace ─────────────────────────────────────────────
@@ -739,6 +815,7 @@ async def recall_trace(request: Request):
739
815
  body = await request.json()
740
816
  query = body.get("query", "")
741
817
  limit = body.get("limit", 10)
818
+ window = body.get("window", "") or ""
742
819
 
743
820
  # Use daemon engine — already loaded, shares warm page cache.
744
821
  # run_in_executor keeps event loop alive so browser doesn't abort.
@@ -751,7 +828,7 @@ async def recall_trace(request: Request):
751
828
  t0 = _time.monotonic()
752
829
  response = await loop.run_in_executor(
753
830
  None,
754
- lambda: engine.recall(query, limit=limit, fast=False),
831
+ lambda: engine.recall(query, limit=limit, fast=False, window=window or None),
755
832
  )
756
833
  elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
757
834
 
@@ -784,7 +861,7 @@ async def recall_trace(request: Request):
784
861
  **recall_response_metadata(response),
785
862
  }
786
863
  except Exception as e:
787
- return JSONResponse({"error": str(e)}, status_code=500)
864
+ return _internal_error()
788
865
 
789
866
 
790
867
  def _record_learning_signals(query: str, results: list) -> None:
@@ -874,7 +951,7 @@ async def trust_dashboard(request: Request):
874
951
  "profile": pid,
875
952
  }
876
953
  except Exception as e:
877
- return JSONResponse({"error": str(e)}, status_code=500)
954
+ return _internal_error()
878
955
 
879
956
 
880
957
  # ── Math Health ──────────────────────────────────────────────
@@ -916,7 +993,7 @@ async def math_health(request: Request):
916
993
  "note": "config-derived; not a live runtime probe",
917
994
  }
918
995
  except Exception as e:
919
- return JSONResponse({"error": str(e)}, status_code=500)
996
+ return _internal_error()
920
997
 
921
998
 
922
999
  # ── Auto-Capture / Auto-Recall Config ────────────────────────
@@ -930,12 +1007,14 @@ async def get_auto_capture_config():
930
1007
  rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
931
1008
  return {"config": rules.get_capture_config()}
932
1009
  except Exception as exc:
933
- return {"error": str(exc), "config": {}}
1010
+ logger.exception("get_auto_capture_config failed")
1011
+ return {"error": "internal error", "config": {}}
934
1012
 
935
1013
 
936
1014
  @router.put("/auto-capture/config")
937
1015
  async def set_auto_capture_config(request: Request):
938
1016
  """Update auto-capture config. Body: {"enabled": true, "capture_decisions": true, ...}"""
1017
+ _require_manage(request)
939
1018
  try:
940
1019
  body = await request.json()
941
1020
  from superlocalmemory.hooks.rules_engine import RulesEngine
@@ -947,7 +1026,7 @@ async def set_auto_capture_config(request: Request):
947
1026
  rules.save(config_path)
948
1027
  return {"success": True, "config": rules.get_capture_config()}
949
1028
  except Exception as e:
950
- return JSONResponse({"error": str(e)}, status_code=500)
1029
+ return _internal_error()
951
1030
 
952
1031
 
953
1032
  @router.get("/auto-recall/config")
@@ -959,12 +1038,14 @@ async def get_auto_recall_config():
959
1038
  rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
960
1039
  return {"config": rules.get_recall_config()}
961
1040
  except Exception as exc:
962
- return {"error": str(exc), "config": {}}
1041
+ logger.exception("get_auto_recall_config failed")
1042
+ return {"error": "internal error", "config": {}}
963
1043
 
964
1044
 
965
1045
  @router.put("/auto-recall/config")
966
1046
  async def set_auto_recall_config(request: Request):
967
1047
  """Update auto-recall config."""
1048
+ _require_manage(request)
968
1049
  try:
969
1050
  body = await request.json()
970
1051
  from superlocalmemory.hooks.rules_engine import RulesEngine
@@ -976,7 +1057,7 @@ async def set_auto_recall_config(request: Request):
976
1057
  rules.save(config_path)
977
1058
  return {"success": True, "config": rules.get_recall_config()}
978
1059
  except Exception as e:
979
- return JSONResponse({"error": str(e)}, status_code=500)
1060
+ return _internal_error()
980
1061
 
981
1062
 
982
1063
  # ── IDE Status ───────────────────────────────────────────────
@@ -989,7 +1070,8 @@ async def ide_status():
989
1070
  connector = IDEConnector()
990
1071
  return {"ides": connector.get_status()}
991
1072
  except Exception as exc:
992
- return {"error": str(exc), "ides": []}
1073
+ logger.exception("ide_status failed")
1074
+ return {"error": "internal error", "ides": []}
993
1075
 
994
1076
 
995
1077
  @router.post("/ide/connect")
@@ -1009,7 +1091,7 @@ async def ide_connect(request: Request):
1009
1091
  results = connector.connect_all()
1010
1092
  return {"results": results}
1011
1093
  except Exception as e:
1012
- return JSONResponse({"error": str(e)}, status_code=500)
1094
+ return _internal_error()
1013
1095
 
1014
1096
 
1015
1097
  # ── Active Memory (V3.1) ────────────────────────────────────
@@ -1027,7 +1109,8 @@ async def learning_signals():
1027
1109
  pid = config.active_profile
1028
1110
  return {"success": True, **signals.get_signal_stats(pid)}
1029
1111
  except Exception as exc:
1030
- return {"success": False, "error": str(exc)}
1112
+ logger.exception("learning_signals failed")
1113
+ return {"success": False, "error": "internal error"}
1031
1114
 
1032
1115
 
1033
1116
  @router.post("/learning/consolidate")
@@ -1049,6 +1132,7 @@ async def run_consolidation(request: Request):
1049
1132
 
1050
1133
  config = SLMConfig.load()
1051
1134
  learning_db = DB_PATH.parent / "learning.db"
1135
+ _require_manage_for_profile(request, config.active_profile)
1052
1136
  authorization = authorize_route_mutation(
1053
1137
  request,
1054
1138
  operation="update",
@@ -1078,7 +1162,8 @@ async def run_consolidation(request: Request):
1078
1162
  except HTTPException:
1079
1163
  raise
1080
1164
  except Exception as exc:
1081
- return {"success": False, "error": str(exc)}
1165
+ logger.exception("run_consolidation failed")
1166
+ return {"success": False, "error": "internal error"}
1082
1167
 
1083
1168
 
1084
1169
  @router.get("/hooks/status")
@@ -1088,7 +1173,8 @@ async def hooks_status():
1088
1173
  from superlocalmemory.hooks.claude_code_hooks import check_status
1089
1174
  return {"success": True, **check_status()}
1090
1175
  except Exception as exc:
1091
- return {"success": False, "error": str(exc)}
1176
+ logger.exception("hooks_status failed")
1177
+ return {"success": False, "error": "internal error"}
1092
1178
 
1093
1179
 
1094
1180
  # ── Phase 6: V3.2 API Endpoints ──────────────────────────────
@@ -1153,7 +1239,7 @@ async def get_auto_invoke_config(request: Request):
1153
1239
  "last_invocation": persisted.get("last_invocation", None),
1154
1240
  }
1155
1241
  except Exception as e:
1156
- return JSONResponse({"error": str(e)}, status_code=500)
1242
+ return _internal_error()
1157
1243
 
1158
1244
 
1159
1245
  # ── 2. PUT /api/v3/auto-invoke/config ─────────────────────────
@@ -1164,6 +1250,7 @@ async def set_auto_invoke_config(request: Request):
1164
1250
 
1165
1251
  Body: {"enabled": true, "min_score": 0.15, "weights": {...}}
1166
1252
  """
1253
+ _require_manage(request)
1167
1254
  try:
1168
1255
  body = await request.json()
1169
1256
 
@@ -1192,7 +1279,7 @@ async def set_auto_invoke_config(request: Request):
1192
1279
 
1193
1280
  return updated
1194
1281
  except Exception as e:
1195
- return JSONResponse({"error": str(e)}, status_code=500)
1282
+ return _internal_error()
1196
1283
 
1197
1284
 
1198
1285
  # ── 3. GET /api/v3/associations ───────────────────────────────
@@ -1205,10 +1292,10 @@ async def get_associations(
1205
1292
  profile: str = "",
1206
1293
  ):
1207
1294
  """Get association edges for a profile with content previews."""
1295
+ pid = _resolve_profile(request, profile)
1208
1296
  try:
1209
1297
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1210
1298
  import sqlite3
1211
- pid = profile or get_active_profile()
1212
1299
 
1213
1300
  if not DB_PATH.exists():
1214
1301
  return {"edges": [], "total": 0}
@@ -1264,7 +1351,7 @@ async def get_associations(
1264
1351
 
1265
1352
  return {"edges": edges, "total": total}
1266
1353
  except Exception as e:
1267
- return JSONResponse({"error": str(e)}, status_code=500)
1354
+ return _internal_error()
1268
1355
 
1269
1356
 
1270
1357
  # ── 4. GET /api/v3/associations/stats ─────────────────────────
@@ -1272,10 +1359,10 @@ async def get_associations(
1272
1359
  @router.get("/associations/stats")
1273
1360
  async def get_association_stats(request: Request, profile: str = ""):
1274
1361
  """Get association graph statistics."""
1362
+ pid = _resolve_profile(request, profile)
1275
1363
  try:
1276
1364
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1277
1365
  import sqlite3
1278
- pid = profile or get_active_profile()
1279
1366
 
1280
1367
  if not DB_PATH.exists():
1281
1368
  return {
@@ -1365,7 +1452,7 @@ async def get_association_stats(request: Request, profile: str = ""):
1365
1452
  "top_connected_facts": top_facts,
1366
1453
  }
1367
1454
  except Exception as e:
1368
- return JSONResponse({"error": str(e)}, status_code=500)
1455
+ return _internal_error()
1369
1456
 
1370
1457
 
1371
1458
  # ── 5. GET /api/v3/consolidation/status ───────────────────────
@@ -1373,12 +1460,12 @@ async def get_association_stats(request: Request, profile: str = ""):
1373
1460
  @router.get("/consolidation/status")
1374
1461
  async def get_consolidation_status(request: Request, profile: str = ""):
1375
1462
  """Get consolidation status and last run results."""
1463
+ pid = _resolve_profile(request, profile)
1376
1464
  try:
1377
1465
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1378
1466
  from superlocalmemory.core.config import SLMConfig
1379
1467
  import sqlite3
1380
1468
 
1381
- pid = profile or get_active_profile()
1382
1469
  config = SLMConfig.load()
1383
1470
  cons_cfg = config.consolidation
1384
1471
 
@@ -1452,7 +1539,7 @@ async def get_consolidation_status(request: Request, profile: str = ""):
1452
1539
  conn.close()
1453
1540
  return result
1454
1541
  except Exception as e:
1455
- return JSONResponse({"error": str(e)}, status_code=500)
1542
+ return _internal_error()
1456
1543
 
1457
1544
 
1458
1545
  # ── 6. POST /api/v3/consolidation/trigger ─────────────────────
@@ -1471,7 +1558,8 @@ async def trigger_consolidation(request: Request):
1471
1558
  profile = body.get("profile", "")
1472
1559
 
1473
1560
  from superlocalmemory.server.routes.helpers import get_active_profile
1474
- pid = profile or get_active_profile()
1561
+ pid = _resolve_mutation_profile(profile)
1562
+ _require_manage_for_profile(request, pid)
1475
1563
  authorization = authorize_route_mutation(
1476
1564
  request,
1477
1565
  operation="update",
@@ -1486,42 +1574,54 @@ async def trigger_consolidation(request: Request):
1486
1574
  # path unconditionally. That dead branch is removed; consolidation
1487
1575
  # always runs directly against a lease-protected DB connection so a
1488
1576
  # concurrent profile switch cannot commit mid-consolidation.
1577
+ #
1578
+ # v3.4.64: ConsolidationEngine.consolidate() is CPU/IO bound (seconds
1579
+ # to minutes). Calling it directly in an async route blocks the ASGI
1580
+ # event loop. Moved into asyncio.to_thread() so the event loop stays
1581
+ # live. The runtime.operation() lease is acquired INSIDE the thread —
1582
+ # blocking a thread is fine; blocking the event loop is not.
1583
+ import asyncio as _asyncio
1489
1584
  from superlocalmemory.core.config import SLMConfig
1490
1585
  from superlocalmemory.storage.database import DatabaseManager
1491
1586
  from superlocalmemory.storage import schema as _schema
1492
1587
  from superlocalmemory.core.consolidation_engine import ConsolidationEngine
1493
1588
  from superlocalmemory.server.profile_runtime import get_profile_runtime
1494
1589
 
1495
- runtime = get_profile_runtime(request.app.state)
1496
- with runtime.operation():
1497
- config = SLMConfig.load()
1498
- db = DatabaseManager(config.db_path)
1499
- db.initialize(_schema)
1500
-
1501
- engine = ConsolidationEngine(
1502
- db=db, config=config.consolidation, slm_config=config,
1503
- )
1504
- result = engine.consolidate(profile_id=pid, lightweight=lightweight)
1590
+ _app_state = request.app.state
1505
1591
 
1506
- # v3.4.1: Auto-trigger behavioral pattern mining after consolidation
1507
- try:
1508
- from superlocalmemory.learning.consolidation_worker import (
1509
- ConsolidationWorker,
1592
+ def _run_consolidation() -> dict:
1593
+ runtime = get_profile_runtime(_app_state)
1594
+ with runtime.operation():
1595
+ config = SLMConfig.load()
1596
+ db = DatabaseManager(config.db_path)
1597
+ db.initialize(_schema)
1598
+ engine = ConsolidationEngine(
1599
+ db=db, config=config.consolidation, slm_config=config,
1510
1600
  )
1511
- learning_db = config.base_dir / "learning.db"
1512
- cw = ConsolidationWorker(str(config.db_path), str(learning_db))
1513
- pattern_count = cw._generate_patterns(pid, False)
1514
- result["patterns_mined"] = pattern_count
1515
- logger.info("Auto-mined %d patterns after consolidation", pattern_count)
1516
- except Exception as exc:
1517
- logger.debug("Pattern mining after consolidation failed: %s", exc)
1601
+ res = engine.consolidate(profile_id=pid, lightweight=lightweight)
1602
+ # v3.4.1: Auto-trigger behavioral pattern mining after consolidation
1603
+ try:
1604
+ from superlocalmemory.learning.consolidation_worker import (
1605
+ ConsolidationWorker,
1606
+ )
1607
+ learning_db = config.base_dir / "learning.db"
1608
+ cw = ConsolidationWorker(str(config.db_path), str(learning_db))
1609
+ pattern_count = cw._generate_patterns(pid, False)
1610
+ res["patterns_mined"] = pattern_count
1611
+ logger.info(
1612
+ "Auto-mined %d patterns after consolidation", pattern_count
1613
+ )
1614
+ except Exception as exc:
1615
+ logger.debug("Pattern mining after consolidation failed: %s", exc)
1616
+ return res
1518
1617
 
1618
+ result = await _asyncio.to_thread(_run_consolidation)
1519
1619
  authorization.complete()
1520
1620
  return {"success": True, **result}
1521
1621
  except HTTPException:
1522
1622
  raise
1523
1623
  except Exception as e:
1524
- return JSONResponse({"error": str(e)}, status_code=500)
1624
+ return _internal_error()
1525
1625
 
1526
1626
 
1527
1627
  # ── 7. GET /api/v3/core-memory ────────────────────────────────
@@ -1529,10 +1629,10 @@ async def trigger_consolidation(request: Request):
1529
1629
  @router.get("/core-memory")
1530
1630
  async def get_core_memory(request: Request, profile: str = ""):
1531
1631
  """Get all Core Memory blocks for a profile."""
1632
+ pid = _resolve_profile(request, profile)
1532
1633
  try:
1533
1634
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1534
1635
  import sqlite3
1535
- pid = profile or get_active_profile()
1536
1636
 
1537
1637
  if not DB_PATH.exists():
1538
1638
  return {"blocks": [], "total_chars": 0, "char_limit": 2000}
@@ -1567,7 +1667,7 @@ async def get_core_memory(request: Request, profile: str = ""):
1567
1667
 
1568
1668
  return {"blocks": blocks, "total_chars": total_chars, "char_limit": 2000}
1569
1669
  except Exception as e:
1570
- return JSONResponse({"error": str(e)}, status_code=500)
1670
+ return _internal_error()
1571
1671
 
1572
1672
 
1573
1673
  # ── 8. PUT /api/v3/core-memory/{block_id} ─────────────────────
@@ -1650,7 +1750,7 @@ async def update_core_memory_block(block_id: str, request: Request):
1650
1750
  except HTTPException:
1651
1751
  raise
1652
1752
  except Exception as e:
1653
- return JSONResponse({"error": str(e)}, status_code=500)
1753
+ return _internal_error()
1654
1754
 
1655
1755
 
1656
1756
  # ── 9. GET /api/v3/vector-store/status ────────────────────────
@@ -1658,6 +1758,7 @@ async def update_core_memory_block(block_id: str, request: Request):
1658
1758
  @router.get("/vector-store/status")
1659
1759
  async def get_vector_store_status(request: Request, profile: str = ""):
1660
1760
  """Get VectorStore health and statistics."""
1761
+ pid = _resolve_profile(request, profile)
1661
1762
  try:
1662
1763
  from superlocalmemory.core.config import SLMConfig
1663
1764
  from superlocalmemory.server.routes.helpers import DB_PATH
@@ -1688,7 +1789,8 @@ async def get_vector_store_status(request: Request, profile: str = ""):
1688
1789
  try:
1689
1790
  conn = sqlite3.connect(str(DB_PATH))
1690
1791
  count = conn.execute(
1691
- "SELECT COUNT(*) FROM embedding_metadata"
1792
+ "SELECT COUNT(*) FROM embedding_metadata WHERE profile_id = ?",
1793
+ (pid,),
1692
1794
  ).fetchone()[0]
1693
1795
  result["total_vectors"] = count
1694
1796
  conn.close()
@@ -1697,7 +1799,7 @@ async def get_vector_store_status(request: Request, profile: str = ""):
1697
1799
 
1698
1800
  return result
1699
1801
  except Exception as e:
1700
- return JSONResponse({"error": str(e)}, status_code=500)
1802
+ return _internal_error()
1701
1803
 
1702
1804
 
1703
1805
  # ── Phase 10: V3.3 API Endpoints ────────────────────────────
@@ -1718,10 +1820,10 @@ async def get_vector_store_status(request: Request, profile: str = ""):
1718
1820
  @router.get("/forgetting/stats")
1719
1821
  async def forgetting_stats(request: Request, profile: str = ""):
1720
1822
  """Get memory retention zone distribution."""
1823
+ pid = _resolve_profile(request, profile)
1721
1824
  try:
1722
1825
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1723
1826
  import sqlite3 as _sqlite3
1724
- pid = profile or get_active_profile()
1725
1827
 
1726
1828
  zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
1727
1829
  total = 0
@@ -1752,7 +1854,7 @@ async def forgetting_stats(request: Request, profile: str = ""):
1752
1854
  conn.close()
1753
1855
  return {"total": total, "zones": zones}
1754
1856
  except Exception as e:
1755
- return JSONResponse({"error": str(e)}, status_code=500)
1857
+ return _internal_error()
1756
1858
 
1757
1859
 
1758
1860
  # ── 1b. POST /api/v3/forgetting/run ─────────────────────────
@@ -1769,7 +1871,8 @@ async def run_forgetting(request: Request):
1769
1871
 
1770
1872
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1771
1873
  import sqlite3 as _sqlite3
1772
- pid = profile or get_active_profile()
1874
+ pid = _resolve_mutation_profile(profile)
1875
+ _require_manage_for_profile(request, pid)
1773
1876
 
1774
1877
  if not DB_PATH.exists():
1775
1878
  return {"success": False, "error": "Database not found"}
@@ -1828,8 +1931,9 @@ async def run_forgetting(request: Request):
1828
1931
 
1829
1932
  conn.commit()
1830
1933
  except Exception as exc:
1934
+ logger.exception("run_forgetting decay failed")
1831
1935
  conn.close()
1832
- return {"success": False, "error": str(exc)}
1936
+ return {"success": False, "error": "internal error"}
1833
1937
 
1834
1938
  conn.close()
1835
1939
  authorization.complete()
@@ -1837,7 +1941,7 @@ async def run_forgetting(request: Request):
1837
1941
  except HTTPException:
1838
1942
  raise
1839
1943
  except Exception as e:
1840
- return JSONResponse({"error": str(e)}, status_code=500)
1944
+ return _internal_error()
1841
1945
 
1842
1946
 
1843
1947
  # ── 1c. GET /api/v3/quantization/stats ──────────────────────
@@ -1845,10 +1949,10 @@ async def run_forgetting(request: Request):
1845
1949
  @router.get("/quantization/stats")
1846
1950
  async def quantization_stats(request: Request, profile: str = ""):
1847
1951
  """Get embedding quantization tier distribution."""
1952
+ pid = _resolve_profile(request, profile)
1848
1953
  try:
1849
1954
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1850
1955
  import sqlite3 as _sqlite3
1851
- pid = profile or get_active_profile()
1852
1956
 
1853
1957
  tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
1854
1958
  total = 0
@@ -1900,7 +2004,7 @@ async def quantization_stats(request: Request, profile: str = ""):
1900
2004
  conn.close()
1901
2005
  return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
1902
2006
  except Exception as e:
1903
- return JSONResponse({"error": str(e)}, status_code=500)
2007
+ return _internal_error()
1904
2008
 
1905
2009
 
1906
2010
  # ── 1d. GET /api/v3/ccq/blocks ──────────────────────────────
@@ -1908,10 +2012,10 @@ async def quantization_stats(request: Request, profile: str = ""):
1908
2012
  @router.get("/ccq/blocks")
1909
2013
  async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
1910
2014
  """Get CCQ consolidated blocks."""
2015
+ pid = _resolve_profile(request, profile)
1911
2016
  try:
1912
2017
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1913
2018
  import sqlite3 as _sqlite3
1914
- pid = profile or get_active_profile()
1915
2019
 
1916
2020
  if not DB_PATH.exists():
1917
2021
  return {"blocks": [], "total": 0}
@@ -1960,7 +2064,7 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
1960
2064
  conn.close()
1961
2065
  return {"blocks": blocks, "total": total}
1962
2066
  except Exception as e:
1963
- return JSONResponse({"error": str(e)}, status_code=500)
2067
+ return _internal_error()
1964
2068
 
1965
2069
 
1966
2070
  # ── 1e. GET /api/v3/soft-prompts ─────────────────────────────
@@ -1968,10 +2072,10 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
1968
2072
  @router.get("/soft-prompts")
1969
2073
  async def get_soft_prompts(request: Request, profile: str = ""):
1970
2074
  """Get active soft prompt templates."""
2075
+ pid = _resolve_profile(request, profile)
1971
2076
  try:
1972
2077
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1973
2078
  import sqlite3 as _sqlite3
1974
- pid = profile or get_active_profile()
1975
2079
 
1976
2080
  if not DB_PATH.exists():
1977
2081
  return {"prompts": [], "total": 0, "total_tokens": 0}
@@ -2013,7 +2117,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
2013
2117
  conn.close()
2014
2118
  return {"prompts": prompts, "total": len(prompts), "total_tokens": total_tokens}
2015
2119
  except Exception as e:
2016
- return JSONResponse({"error": str(e)}, status_code=500)
2120
+ return _internal_error()
2017
2121
 
2018
2122
 
2019
2123
  # ── 1f. GET /api/v3/health/processes ─────────────────────────
@@ -2021,6 +2125,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
2021
2125
  @router.get("/health/processes")
2022
2126
  async def process_health(request: Request):
2023
2127
  """Get SLM process health status."""
2128
+ _resolve_profile(request)
2024
2129
  try:
2025
2130
  import os as _os
2026
2131
 
@@ -2060,7 +2165,7 @@ async def process_health(request: Request):
2060
2165
  "healthy": processes["parent"]["status"] != "dead",
2061
2166
  }
2062
2167
  except Exception as e:
2063
- return JSONResponse({"error": str(e)}, status_code=500)
2168
+ return _internal_error()
2064
2169
 
2065
2170
 
2066
2171
  # ── 1g. GET /api/v3/v33/overview ─────────────────────────────
@@ -2075,10 +2180,10 @@ async def get_graph_communities(request: Request, profile: str = ""):
2075
2180
  at consolidation time). Falls back to inline word frequency if labels
2076
2181
  not yet computed.
2077
2182
  """
2183
+ pid = _resolve_profile(request, profile)
2078
2184
  try:
2079
2185
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2080
2186
  import sqlite3
2081
- pid = profile or get_active_profile()
2082
2187
 
2083
2188
  if not DB_PATH.exists():
2084
2189
  return {"communities": [], "total": 0}
@@ -2192,7 +2297,7 @@ async def get_graph_communities(request: Request, profile: str = ""):
2192
2297
  return {"communities": communities, "total": len(communities)}
2193
2298
 
2194
2299
  except Exception as e:
2195
- return JSONResponse({"error": str(e)}, status_code=500)
2300
+ return _internal_error()
2196
2301
 
2197
2302
 
2198
2303
  @router.post("/graph/run-communities")
@@ -2205,6 +2310,7 @@ async def run_community_detection(request: Request):
2205
2310
  from superlocalmemory.core.graph_analyzer import GraphAnalyzer
2206
2311
 
2207
2312
  pid = get_active_profile()
2313
+ _require_manage_for_profile(request, pid)
2208
2314
  authorization = authorize_route_mutation(
2209
2315
  request,
2210
2316
  operation="update",
@@ -2221,16 +2327,16 @@ async def run_community_detection(request: Request):
2221
2327
  except HTTPException:
2222
2328
  raise
2223
2329
  except Exception as e:
2224
- return JSONResponse({"error": str(e)}, status_code=500)
2330
+ return _internal_error()
2225
2331
 
2226
2332
 
2227
2333
  @router.get("/v33/overview")
2228
2334
  async def v33_overview(request: Request, profile: str = ""):
2229
2335
  """Get SLM 3.3 feature overview -- all new capabilities at a glance."""
2336
+ pid = _resolve_profile(request, profile)
2230
2337
  try:
2231
2338
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2232
2339
  import sqlite3 as _sqlite3
2233
- pid = profile or get_active_profile()
2234
2340
 
2235
2341
  overview: dict = {
2236
2342
  "version": "3.3",
@@ -2354,4 +2460,56 @@ async def v33_overview(request: Request, profile: str = ""):
2354
2460
  conn.close()
2355
2461
  return overview
2356
2462
  except Exception as e:
2357
- return JSONResponse({"error": str(e)}, status_code=500)
2463
+ return _internal_error()
2464
+
2465
+
2466
+ # ── MCP Profiles ─────────────────────────────────────────────
2467
+
2468
+ @router.get("/mcp/profiles")
2469
+ async def get_mcp_profiles(request: Request):
2470
+ """MCP profile summary: current profile, all profiles with tool counts and names.
2471
+
2472
+ Reads only the env var SLM_MCP_PROFILE and the pure-data profiles module.
2473
+ No engine required — safe to call at any point in the daemon lifecycle.
2474
+
2475
+ Note on 'current': when SLM_MCP_PROFILE is unset the MCP server falls
2476
+ back to the legacy _ESSENTIAL_TOOLS set. The UI reports 'core' for that
2477
+ state because 'core' is the recommended named-profile equivalent for new
2478
+ installs and is the closest documented starting point for users.
2479
+ """
2480
+ try:
2481
+ from superlocalmemory.mcp.profiles import (
2482
+ _PROFILE_DEFINITIONS,
2483
+ _PROFILE_ALIASES,
2484
+ PROFILE_DESCRIPTIONS,
2485
+ )
2486
+
2487
+ raw_profile = os.environ.get("SLM_MCP_PROFILE", "").strip().lower()
2488
+ canonical = _PROFILE_ALIASES.get(raw_profile, raw_profile)
2489
+ # Blank env var, "whole", or an unknown value all resolve to "core"
2490
+ # for UI display purposes (safe, conservative default).
2491
+ if not canonical or canonical == "whole" or canonical not in _PROFILE_DEFINITIONS:
2492
+ current = "core"
2493
+ else:
2494
+ current = canonical
2495
+
2496
+ profiles_out: dict = {}
2497
+ for name, tool_set in _PROFILE_DEFINITIONS.items():
2498
+ profiles_out[name] = {
2499
+ "count": len(tool_set),
2500
+ "tools": sorted(tool_set),
2501
+ "description": PROFILE_DESCRIPTIONS.get(name, ""),
2502
+ }
2503
+
2504
+ all_tools: set = set()
2505
+ for tool_set in _PROFILE_DEFINITIONS.values():
2506
+ all_tools.update(tool_set)
2507
+
2508
+ return {
2509
+ "current": current,
2510
+ "profiles": profiles_out,
2511
+ "aliases": dict(_PROFILE_ALIASES),
2512
+ "total_tools": len(all_tools),
2513
+ }
2514
+ except Exception:
2515
+ return _internal_error()