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
@@ -30,6 +30,14 @@ Design notes (LLD-04 §7 hard rules):
30
30
  never spell those names anywhere in this module.
31
31
  * **U6** — install token required on ``/api/v3/brain`` and all
32
32
  deprecated shim routes. See ``require_install_token`` below.
33
+
34
+ External API consumers (L1): the dashboard auto-fetches the token from
35
+ ``GET /internal/token`` (loopback + same-origin only) and sends it as the
36
+ ``X-Install-Token`` header. Non-browser callers (scripts, IDE adapters,
37
+ third-party integrations) instead read the token from
38
+ ``~/.superlocalmemory/.install_token`` and send the same header:
39
+ ``curl -H "X-Install-Token: $(cat ~/.superlocalmemory/.install_token)" \
40
+ http://127.0.0.1:8765/api/v3/brain``.
33
41
  * **U10** — feature count surfaced from ``features.FEATURE_DIM``;
34
42
  stratum total from module constant ``_STRATA_TOTAL`` (48 = 4×3×4).
35
43
  """
@@ -52,6 +60,7 @@ from superlocalmemory.core.security_primitives import (
52
60
  from superlocalmemory.learning.database import LearningDatabase
53
61
  from superlocalmemory.learning.features import FEATURE_DIM
54
62
  from superlocalmemory.infra.data_root import canonical_data_root
63
+ from .helpers import get_active_profile
55
64
 
56
65
  logger = logging.getLogger("superlocalmemory.routes.brain")
57
66
 
@@ -894,13 +903,47 @@ def _action_outcomes_count(lrn_db: LearningDatabase,
894
903
  return 0
895
904
 
896
905
 
906
+ def _compute_action_outcomes_preview(profile_id: str) -> dict:
907
+ """Count profile-scoped action outcomes from their canonical database."""
908
+ empty = {
909
+ "action_outcomes_rows": 0,
910
+ "source": "memory.db:action_outcomes",
911
+ "is_real": True,
912
+ }
913
+ db_path = _memory_db_path()
914
+ if not db_path.exists():
915
+ return empty
916
+ try:
917
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
918
+ try:
919
+ row = conn.execute(
920
+ "SELECT COUNT(*) FROM action_outcomes WHERE profile_id = ?",
921
+ (profile_id,),
922
+ ).fetchone()
923
+ finally:
924
+ conn.close()
925
+ except sqlite3.Error:
926
+ return empty
927
+ return {**empty, "action_outcomes_rows": int(row[0] or 0) if row else 0}
928
+
929
+
897
930
  # ---------------------------------------------------------------------------
898
931
  # Routes
899
932
  # ---------------------------------------------------------------------------
900
933
 
901
934
 
935
+ def _authorized_profile(request: Request, profile_id: str | None) -> str:
936
+ """Resolve and authorize the exact Brain profile requested by the caller."""
937
+ from superlocalmemory.access.rbac import Permission
938
+ from superlocalmemory.server.rbac_enforce import require_permission
939
+
940
+ effective_profile = profile_id or get_active_profile()
941
+ require_permission(request, Permission.READ, profile=effective_profile)
942
+ return effective_profile
943
+
944
+
902
945
  @router.get("/brain", dependencies=[Depends(require_install_token)])
903
- async def get_brain(profile_id: str = "default") -> dict:
946
+ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
904
947
  """Unified Brain endpoint — LLD-04 §3.1.
905
948
 
906
949
  Fan-out: each section is a synchronous SQLite reader. Running them
@@ -915,11 +958,14 @@ async def get_brain(profile_id: str = "default") -> dict:
915
958
  """
916
959
  import asyncio
917
960
 
961
+ # Default to the ACTIVE profile (request runtime truth), never literal
962
+ # "default" — the Brain must reflect whichever profile is active.
963
+ profile_id = _authorized_profile(request, profile_id)
918
964
  lrn_db = LearningDatabase(_learning_db_path())
919
965
 
920
966
  (
921
967
  preferences, learning, usage, bandit_snap, cache,
922
- cross_platform, outcomes_rows, evolution,
968
+ cross_platform, outcomes_preview, evolution,
923
969
  ) = await asyncio.gather(
924
970
  asyncio.to_thread(_compute_preferences, profile_id),
925
971
  asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
@@ -927,7 +973,7 @@ async def get_brain(profile_id: str = "default") -> dict:
927
973
  asyncio.to_thread(_compute_bandit_snapshot, profile_id, lrn_db),
928
974
  asyncio.to_thread(_compute_cache_stats),
929
975
  asyncio.to_thread(_compute_cross_platform),
930
- asyncio.to_thread(_action_outcomes_count, lrn_db, profile_id),
976
+ asyncio.to_thread(_compute_action_outcomes_preview, profile_id),
931
977
  asyncio.to_thread(
932
978
  _compute_evolution_timeseries, profile_id, lrn_db,
933
979
  days=_EVOLUTION_DEFAULT_DAYS,
@@ -965,11 +1011,11 @@ async def get_brain(profile_id: str = "default") -> dict:
965
1011
  "is_real": True, "source": "learning_signals",
966
1012
  "days": _EVOLUTION_DEFAULT_DAYS, "total_signals": 0, "points": [],
967
1013
  }),
968
- "outcomes_preview": {
969
- "action_outcomes_rows":
970
- 0 if isinstance(outcomes_rows, Exception) else outcomes_rows,
971
- "ships_in": "3.4.22",
972
- },
1014
+ "outcomes_preview": _ok(outcomes_preview, {
1015
+ "action_outcomes_rows": 0,
1016
+ "source": "memory.db:action_outcomes",
1017
+ "is_real": True,
1018
+ }),
973
1019
  # S9-defer H-22: live tile data for the Reward / Shadow /
974
1020
  # Evolution-Cost dashboard tiles. Each block is a honest-empty
975
1021
  # default when the underlying table is missing (fresh install
@@ -1176,7 +1222,8 @@ def _compute_evolution_cost_preview(profile_id: str) -> dict:
1176
1222
  @router.get("/brain/evolution-timeseries",
1177
1223
  dependencies=[Depends(require_install_token)])
1178
1224
  async def get_brain_evolution_timeseries(
1179
- profile_id: str = "default",
1225
+ request: Request,
1226
+ profile_id: str | None = None,
1180
1227
  days: int = _EVOLUTION_DEFAULT_DAYS,
1181
1228
  ) -> dict:
1182
1229
  """Daily learning-signal counts for ``profile_id`` over the last ``days``.
@@ -1186,6 +1233,7 @@ async def get_brain_evolution_timeseries(
1186
1233
  """
1187
1234
  import asyncio
1188
1235
 
1236
+ profile_id = _authorized_profile(request, profile_id)
1189
1237
  lrn_db = LearningDatabase(_learning_db_path())
1190
1238
  result = await asyncio.to_thread(
1191
1239
  _compute_evolution_timeseries, profile_id, lrn_db, days=days,
@@ -1201,7 +1249,10 @@ async def get_brain_evolution_timeseries(
1201
1249
 
1202
1250
  @router.get("/learning/stats",
1203
1251
  dependencies=[Depends(require_install_token)])
1204
- async def learning_stats_deprecated(profile_id: str = "default") -> dict:
1252
+ async def learning_stats_deprecated(
1253
+ request: Request, profile_id: str | None = None,
1254
+ ) -> dict:
1255
+ profile_id = _authorized_profile(request, profile_id)
1205
1256
  lrn_db = LearningDatabase(_learning_db_path())
1206
1257
  return {
1207
1258
  "deprecated": True,
@@ -1212,7 +1263,10 @@ async def learning_stats_deprecated(profile_id: str = "default") -> dict:
1212
1263
 
1213
1264
  @router.get("/patterns",
1214
1265
  dependencies=[Depends(require_install_token)])
1215
- async def patterns_deprecated(profile_id: str = "default") -> dict:
1266
+ async def patterns_deprecated(
1267
+ request: Request, profile_id: str | None = None,
1268
+ ) -> dict:
1269
+ profile_id = _authorized_profile(request, profile_id)
1216
1270
  return {
1217
1271
  "deprecated": True,
1218
1272
  "use_instead": "/api/v3/brain",
@@ -1222,7 +1276,10 @@ async def patterns_deprecated(profile_id: str = "default") -> dict:
1222
1276
 
1223
1277
  @router.get("/behavioral",
1224
1278
  dependencies=[Depends(require_install_token)])
1225
- async def behavioral_deprecated(profile_id: str = "default") -> dict:
1279
+ async def behavioral_deprecated(
1280
+ request: Request, profile_id: str | None = None,
1281
+ ) -> dict:
1282
+ profile_id = _authorized_profile(request, profile_id)
1226
1283
  return {
1227
1284
  "deprecated": True,
1228
1285
  "use_instead": "/api/v3/brain",
@@ -101,8 +101,9 @@ async def _stream_chat(
101
101
  memories = await loop.run_in_executor(
102
102
  None, _recall_memories, app_state, query, limit,
103
103
  )
104
- except Exception as exc:
105
- yield _sse_event("error", json.dumps({"message": f"Retrieval failed: {exc}"}))
104
+ except Exception:
105
+ logger.exception("chat: memory retrieval failed")
106
+ yield _sse_event("error", json.dumps({"message": "Memory retrieval failed"}))
106
107
  yield _sse_event("done", "")
107
108
  return
108
109
 
@@ -226,8 +227,9 @@ async def _stream_mode_bc(
226
227
  yield _sse_event("token", token)
227
228
  except httpx.ConnectError:
228
229
  yield _sse_event("token", f"\n\n[Connection failed — is {provider} running?]")
229
- except Exception as exc:
230
- yield _sse_event("token", f"\n\n[LLM error: {exc}]")
230
+ except Exception:
231
+ logger.exception("chat: LLM streaming error")
232
+ yield _sse_event("token", "\n\n[LLM error: request failed]")
231
233
 
232
234
 
233
235
  # ── Ollama Streaming (/api/chat with messages) ───────────────────
@@ -359,7 +361,10 @@ def _recall_via_resident_engine(app_state, query: str, limit: int) -> list:
359
361
  if r.fact.memory_id
360
362
  })
361
363
  memory_map = (
362
- engine._db.get_memory_content_batch(memory_ids) if memory_ids else {}
364
+ engine._db.get_memory_content_batch(
365
+ memory_ids, engine.profile_id,
366
+ include_global=True, include_shared=True,
367
+ ) if memory_ids else {}
363
368
  )
364
369
  from superlocalmemory.server.recall_serializer import (
365
370
  serialize_recall_response,
@@ -13,14 +13,20 @@ import logging
13
13
  import sqlite3
14
14
  from typing import Optional
15
15
 
16
- from fastapi import APIRouter, Query
16
+ from fastapi import APIRouter, Query, Request
17
+ from fastapi.responses import JSONResponse
17
18
 
18
- from .helpers import get_active_profile, MEMORY_DIR, DB_PATH
19
+ from .helpers import get_active_profile, get_engine_lazy, MEMORY_DIR, DB_PATH
20
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
19
21
 
20
22
  logger = logging.getLogger("superlocalmemory.routes.compliance")
21
23
  router = APIRouter()
22
24
 
23
- AUDIT_DB = MEMORY_DIR / "audit.db"
25
+ # The engine's audit post-hooks write to <data_root>/audit_chain.db
26
+ # (core/engine_wiring.py). The route MUST read that same file — an earlier
27
+ # "audit.db" path pointed at a file nothing ever wrote, so the Compliance tab
28
+ # always showed zero events.
29
+ AUDIT_DB = MEMORY_DIR / "audit_chain.db"
24
30
 
25
31
  # Feature detection
26
32
  COMPLIANCE_AVAILABLE = False
@@ -28,6 +34,7 @@ try:
28
34
  from superlocalmemory.compliance.audit import AuditChain
29
35
  from superlocalmemory.compliance.retention import RetentionEngine
30
36
  from superlocalmemory.compliance.abac import ABACEngine
37
+ from superlocalmemory.compliance.gdpr import GDPRCompliance
31
38
  COMPLIANCE_AVAILABLE = True
32
39
  except ImportError:
33
40
  logger.info("V3 compliance engine not available")
@@ -42,22 +49,24 @@ async def compliance_status():
42
49
  try:
43
50
  profile = get_active_profile()
44
51
 
45
- # Audit events
52
+ # Audit events (hash-chained, from the engine's live audit_chain.db).
53
+ # Scope the count + recent list to the active profile.
46
54
  audit_events_count = 0
47
55
  recent_audit_events = []
48
56
  try:
49
57
  audit = AuditChain(str(AUDIT_DB))
50
- audit_events_count = audit.count_events()
51
- recent_audit_events = audit.get_recent_events(limit=30)
58
+ recent_audit_events = audit.query(profile_id=profile, limit=30)
59
+ # get_stats() is global; count this profile's rows for the header.
60
+ audit_events_count = len(audit.query(profile_id=profile, limit=100000))
52
61
  except Exception as exc:
53
62
  logger.debug("audit chain: %s", exc)
54
63
 
55
- # Retention policies
64
+ # Retention policies (scoped to the active profile)
56
65
  retention_policies = []
57
66
  try:
58
67
  conn = sqlite3.connect(str(DB_PATH))
59
68
  engine = RetentionEngine(conn)
60
- retention_policies = engine.list_rules()
69
+ retention_policies = engine.list_rules(profile)
61
70
  conn.close()
62
71
  except Exception as exc:
63
72
  logger.debug("retention engine: %s", exc)
@@ -78,13 +87,14 @@ async def compliance_status():
78
87
  "retention_policies": retention_policies,
79
88
  "abac_policies_count": abac_policies_count,
80
89
  }
81
- except Exception as e:
82
- logger.error("compliance_status error: %s", e)
83
- return {"available": False, "error": str(e)}
90
+ except Exception:
91
+ logger.exception("compliance_status error")
92
+ return {"available": False, "error": "Internal server error"}
84
93
 
85
94
 
86
95
  @router.get("/api/compliance/audit")
87
96
  async def query_audit_trail(
97
+ request: Request,
88
98
  limit: int = Query(default=50, ge=1, le=500),
89
99
  event_type: Optional[str] = Query(default=None),
90
100
  since: Optional[str] = Query(default=None),
@@ -92,20 +102,33 @@ async def query_audit_trail(
92
102
  """Query audit trail events with optional filters."""
93
103
  if not COMPLIANCE_AVAILABLE:
94
104
  return {"available": False, "error": "Compliance engine not available"}
95
-
105
+ # The audit trail reveals operations, actors, and fact_ids. This GET is not
106
+ # covered by the mutation middleware; the same-origin dashboard reads it via
107
+ # a plain GET (no token header), so gate on the loopback-trusted mutation
108
+ # boundary — local owner allowed, remote uncredentialed caller fails closed.
109
+ from superlocalmemory.server.write_identity import require_http_mutation_actor
110
+ require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
111
+ actor_kind="audit-read")
96
112
  try:
113
+ profile = get_active_profile()
97
114
  audit = AuditChain(str(AUDIT_DB))
98
- events = audit.get_recent_events(
99
- limit=limit, event_type=event_type, since=since,
115
+ events = audit.query(
116
+ profile_id=profile, operation=event_type, start_date=since,
117
+ limit=limit,
100
118
  )
101
-
119
+ chain_ok = True
120
+ try:
121
+ chain_ok = audit.verify_integrity()
122
+ except Exception:
123
+ pass
102
124
  return {
103
125
  "available": True, "events": events, "total": len(events),
126
+ "chain_verified": chain_ok, "active_profile": profile,
104
127
  "filters": {"event_type": event_type, "since": since, "limit": limit},
105
128
  }
106
- except Exception as e:
107
- logger.error("query_audit_trail error: %s", e)
108
- return {"available": False, "error": str(e)}
129
+ except Exception:
130
+ logger.exception("query_audit_trail error")
131
+ return {"available": False, "error": "Internal server error"}
109
132
 
110
133
 
111
134
  @router.post("/api/compliance/retention-policy")
@@ -155,6 +178,133 @@ async def create_retention_policy(data: dict):
155
178
  "active_profile": profile,
156
179
  "message": f"Retention policy '{name}' created ({retention_days}d, {action})",
157
180
  }
158
- except Exception as e:
159
- logger.error("create_retention_policy error: %s", e)
160
- return {"success": False, "error": str(e)}
181
+ except Exception:
182
+ logger.exception("create_retention_policy error")
183
+ return {"success": False, "error": "Internal server error"}
184
+
185
+
186
+ @router.delete("/api/compliance/retention-policy")
187
+ async def delete_retention_policy(name: str = Query(...)):
188
+ """Delete a retention policy by name for the active profile."""
189
+ if not COMPLIANCE_AVAILABLE:
190
+ return {"success": False, "error": "Compliance engine not available"}
191
+ try:
192
+ profile = get_active_profile()
193
+ conn = sqlite3.connect(str(DB_PATH))
194
+ engine = RetentionEngine(conn)
195
+ removed = engine.delete_rule(profile, name)
196
+ conn.close()
197
+ if not removed:
198
+ return {"success": False, "error": f"Policy '{name}' not found"}
199
+ return {"success": True, "active_profile": profile,
200
+ "message": f"Retention policy '{name}' deleted"}
201
+ except Exception:
202
+ logger.exception("delete_retention_policy error")
203
+ return {"success": False, "error": "Internal server error"}
204
+
205
+
206
+ @router.post("/api/compliance/retention/enforce")
207
+ async def enforce_retention():
208
+ """Run all retention policies for the active profile now.
209
+
210
+ Moves expired facts to their rule's terminal lifecycle zone (archive/
211
+ tombstone) or counts them (notify). Soft-state only — never a raw delete.
212
+ """
213
+ if not COMPLIANCE_AVAILABLE:
214
+ return {"success": False, "error": "Compliance engine not available"}
215
+ try:
216
+ profile = get_active_profile()
217
+ conn = sqlite3.connect(str(DB_PATH))
218
+ engine = RetentionEngine(conn)
219
+ result = engine.enforce(profile)
220
+ conn.close()
221
+ return {"success": True, **result}
222
+ except Exception:
223
+ logger.exception("enforce_retention error")
224
+ return {"success": False, "error": "Internal server error"}
225
+
226
+
227
+ # ── GDPR Art. 15/20 — Right to Access / Portability ─────────────────────────
228
+
229
+ @router.get("/api/compliance/gdpr/export")
230
+ async def gdpr_export(request: Request):
231
+ """Export ALL data for the active profile (GDPR Art. 20 portability).
232
+
233
+ Comprehensive 14-table export (memories, facts, entities, edges, trust,
234
+ feedback, behavioral patterns, provenance, audit, …) as a downloadable
235
+ JSON attachment. Read-only.
236
+ """
237
+ if not COMPLIANCE_AVAILABLE:
238
+ return {"available": False, "error": "Compliance engine not available"}
239
+ # A full-workspace data dump is an administrative action. Triggered by a
240
+ # top-level navigation (a.href) that cannot carry a credential header, so
241
+ # gate on the loopback-trusted mutation boundary (local owner allowed,
242
+ # remote uncredentialed fails closed) AND require MANAGE — in company mode
243
+ # a session cookie flows on navigation, so a non-admin user is still denied;
244
+ # the machine owner keeps MANAGE.
245
+ from superlocalmemory.server.write_identity import require_http_mutation_actor
246
+ from superlocalmemory.server.rbac_enforce import require_manage
247
+ require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
248
+ actor_kind="gdpr-export")
249
+ require_manage(request)
250
+ try:
251
+ engine = get_engine_lazy(request.app.state)
252
+ if engine is None:
253
+ return {"available": False, "error": "Engine not initialized"}
254
+ profile = get_active_profile()
255
+ data = GDPRCompliance(engine._db).export_profile_data(profile)
256
+ filename = f"slm-export-{profile}.json"
257
+ return JSONResponse(
258
+ content=data,
259
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
260
+ )
261
+ except Exception:
262
+ logger.exception("gdpr_export error")
263
+ return {"available": False, "error": "Internal server error"}
264
+
265
+
266
+ # ── GDPR Art. 17 — Right to Erasure ─────────────────────────────────────────
267
+
268
+ @router.post("/api/compliance/gdpr/erase")
269
+ async def gdpr_erase(request: Request, data: dict = {}):
270
+ """Permanently erase ALL data for the active profile (GDPR Art. 17).
271
+
272
+ IRREVERSIBLE. Requires an explicit ``confirm`` field in the body that
273
+ exactly matches the active profile name — this is the guard against an
274
+ accidental one-click wipe. The 'default' profile can never be erased
275
+ (enforced in GDPRCompliance.forget_profile). Mutation-authorized.
276
+ """
277
+ if not COMPLIANCE_AVAILABLE:
278
+ return {"success": False, "error": "Compliance engine not available"}
279
+ try:
280
+ engine = get_engine_lazy(request.app.state)
281
+ if engine is None:
282
+ return {"success": False, "error": "Engine not initialized"}
283
+ profile = get_active_profile()
284
+ confirm = (data or {}).get("confirm", "")
285
+ if confirm != profile:
286
+ return {
287
+ "success": False,
288
+ "error": (
289
+ "Confirmation required: send {\"confirm\": \"" + profile +
290
+ "\"} to erase this profile. This is irreversible."
291
+ ),
292
+ }
293
+ if profile == "default":
294
+ return {"success": False,
295
+ "error": "The 'default' profile cannot be erased."}
296
+ # Irreversible erasure is admin-only (beyond mutation auth).
297
+ from superlocalmemory.server.rbac_enforce import require_manage
298
+ require_manage(request, profile=profile)
299
+ authorization = authorize_route_mutation(
300
+ request,
301
+ operation="delete",
302
+ source_agent_id="http-gdpr-erase",
303
+ profile_id=profile,
304
+ )
305
+ result = GDPRCompliance(engine._db).forget_profile(profile)
306
+ authorization.complete()
307
+ return {"success": True, "active_profile": profile, **(result or {})}
308
+ except Exception:
309
+ logger.exception("gdpr_erase error")
310
+ return {"success": False, "error": "Internal server error"}