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
@@ -0,0 +1,153 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ """Read-only persisted telemetry used by the Living Brain routes."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import sqlite3
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger("superlocalmemory.routes.learning")
12
+
13
+ SOURCE_SCORE_LIMIT = 50
14
+
15
+
16
+ class ReadOnlyRankerStore:
17
+ """Minimal model-cache adapter that never creates or migrates tables."""
18
+
19
+ def __init__(self, db_path: Path) -> None:
20
+ self._db_path = Path(db_path)
21
+
22
+ def count_signals(self, profile_id: str) -> int:
23
+ connection = _readonly_connection(self._db_path)
24
+ try:
25
+ row = connection.execute(
26
+ "SELECT COUNT(*) AS count FROM learning_signals "
27
+ "WHERE profile_id = ?",
28
+ (profile_id,),
29
+ ).fetchone()
30
+ return int(row["count"] if row else 0)
31
+ finally:
32
+ connection.close()
33
+
34
+ def load_active_model(self, profile_id: str) -> dict | None:
35
+ connection = _readonly_connection(self._db_path)
36
+ try:
37
+ row = connection.execute(
38
+ "SELECT state_bytes, bytes_sha256, feature_names, trained_at, "
39
+ "model_version FROM learning_model_state "
40
+ "WHERE profile_id = ? AND is_active = 1 LIMIT 1",
41
+ (profile_id,),
42
+ ).fetchone()
43
+ finally:
44
+ connection.close()
45
+ if row is None:
46
+ return None
47
+ return {
48
+ "state_bytes": bytes(row["state_bytes"]),
49
+ "bytes_sha256": row["bytes_sha256"],
50
+ "feature_names": row["feature_names"],
51
+ "trained_at": row["trained_at"],
52
+ "model_version": row["model_version"],
53
+ }
54
+
55
+
56
+ def _readonly_connection(db_path: Path) -> sqlite3.Connection:
57
+ connection = sqlite3.connect(
58
+ f"file:{db_path}?mode=ro", uri=True, timeout=1.0,
59
+ )
60
+ connection.execute("PRAGMA busy_timeout=1000")
61
+ connection.row_factory = sqlite3.Row
62
+ return connection
63
+
64
+
65
+ def sqlite_status(exc: sqlite3.Error) -> str:
66
+ """Return a stable, non-sensitive dashboard status for SQLite failures."""
67
+ message = str(exc).lower()
68
+ return (
69
+ "database_busy"
70
+ if "locked" in message or "busy" in message
71
+ else "query_error"
72
+ )
73
+
74
+
75
+ def _table_exists(connection: sqlite3.Connection, table: str) -> bool:
76
+ row = connection.execute(
77
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
78
+ (table,),
79
+ ).fetchone()
80
+ return row is not None
81
+
82
+
83
+ def load_source_quality_state(profile_id: str, db_path: Path) -> dict:
84
+ """Read bounded source scores plus the profile's full source count."""
85
+ if not db_path.exists():
86
+ return {
87
+ "scores": {}, "tracked_sources": 0,
88
+ "status": "missing_database",
89
+ }
90
+ try:
91
+ connection = _readonly_connection(db_path)
92
+ try:
93
+ if not _table_exists(connection, "source_quality"):
94
+ return {
95
+ "scores": {}, "tracked_sources": 0,
96
+ "status": "missing_table",
97
+ }
98
+ count_row = connection.execute(
99
+ "SELECT COUNT(DISTINCT source_id) AS count "
100
+ "FROM source_quality WHERE profile_id = ? "
101
+ "AND source_id IS NOT NULL AND source_id != ''",
102
+ (profile_id,),
103
+ ).fetchone()
104
+ rows = connection.execute(
105
+ "SELECT source_id, alpha, beta FROM source_quality "
106
+ "WHERE profile_id = ? "
107
+ "ORDER BY updated_at DESC LIMIT ?",
108
+ (profile_id, SOURCE_SCORE_LIMIT),
109
+ ).fetchall()
110
+ finally:
111
+ connection.close()
112
+ except sqlite3.Error as exc:
113
+ status = sqlite_status(exc)
114
+ logger.warning("source quality telemetry %s: %s", status, exc)
115
+ return {"scores": {}, "tracked_sources": 0, "status": status}
116
+
117
+ scores: dict[str, float] = {}
118
+ for row in rows:
119
+ alpha, beta = float(row["alpha"] or 0), float(row["beta"] or 0)
120
+ source_id = str(row["source_id"] or "")
121
+ if alpha + beta > 0 and source_id and source_id not in scores:
122
+ scores[source_id] = round(alpha / (alpha + beta), 4)
123
+ return {
124
+ "scores": scores,
125
+ "tracked_sources": int(count_row["count"] if count_row else 0),
126
+ "status": "available",
127
+ }
128
+
129
+
130
+ def load_model_state(profile_id: str, db_path: Path) -> dict:
131
+ """Count persisted model artifacts without inferring them from signals."""
132
+ if not db_path.exists():
133
+ return {"models_trained": 0, "status": "missing_database"}
134
+ try:
135
+ connection = _readonly_connection(db_path)
136
+ try:
137
+ if not _table_exists(connection, "learning_model_state"):
138
+ return {"models_trained": 0, "status": "missing_table"}
139
+ row = connection.execute(
140
+ "SELECT COUNT(*) AS count FROM learning_model_state "
141
+ "WHERE profile_id = ?",
142
+ (profile_id,),
143
+ ).fetchone()
144
+ finally:
145
+ connection.close()
146
+ except sqlite3.Error as exc:
147
+ status = sqlite_status(exc)
148
+ logger.warning("model-state telemetry %s: %s", status, exc)
149
+ return {"models_trained": 0, "status": status}
150
+ return {
151
+ "models_trained": int(row["count"] if row else 0),
152
+ "status": "available",
153
+ }
@@ -11,9 +11,10 @@ import json
11
11
  import logging
12
12
  import sqlite3
13
13
 
14
- from fastapi import APIRouter
14
+ from fastapi import APIRouter, Request
15
15
 
16
- from .helpers import get_active_profile, MEMORY_DIR, DB_PATH
16
+ from .helpers import get_active_profile, get_engine_lazy, MEMORY_DIR, DB_PATH
17
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
17
18
 
18
19
  logger = logging.getLogger("superlocalmemory.routes.lifecycle")
19
20
  router = APIRouter()
@@ -121,15 +122,65 @@ async def lifecycle_status():
121
122
  "recent_transitions": [],
122
123
  "age_stats": age_stats,
123
124
  }
124
- except Exception as e:
125
- logger.error("lifecycle_status error: %s", e)
126
- return {"available": False, "error": str(e)}
125
+ except Exception:
126
+ logger.exception("lifecycle_status error")
127
+ return {"available": False, "error": "Internal server error"}
127
128
 
128
129
 
129
130
  @router.post("/api/lifecycle/compact")
130
- async def trigger_compaction(data: dict = {}):
131
- """Trigger lifecycle compaction. Body: {dry_run: true/false}."""
131
+ async def trigger_compaction(request: Request, data: dict = {}):
132
+ """Trigger lifecycle compaction for the active profile.
133
+
134
+ Body: ``{"dry_run": true|false}`` (default true — preview only).
135
+
136
+ Recomputes each fact's lifecycle zone (active→warm→cold→archive) from its
137
+ age/access pattern. dry_run returns the proposed transitions without
138
+ mutating; execute applies them and is profile-scoped + mutation-authorized.
139
+ """
132
140
  if not LIFECYCLE_AVAILABLE:
133
141
  return {"success": False, "error": "Lifecycle engine not available"}
134
142
 
135
- return {"status": "not_implemented", "message": "Coming soon"}
143
+ dry_run = bool((data or {}).get("dry_run", True))
144
+ try:
145
+ engine = get_engine_lazy(request.app.state)
146
+ if engine is None:
147
+ return {"success": False, "error": "Engine not initialized"}
148
+ profile = get_active_profile()
149
+
150
+ mgr = LifecycleManager(engine._db)
151
+ facts = engine._db.get_all_facts(profile)
152
+ candidates = []
153
+ for f in facts:
154
+ new_state = mgr.get_lifecycle_state(f)
155
+ if new_state != f.lifecycle:
156
+ candidates.append({
157
+ "fact_id": f.fact_id,
158
+ "current": getattr(f.lifecycle, "value", str(f.lifecycle)),
159
+ "proposed": getattr(new_state, "value", str(new_state)),
160
+ })
161
+
162
+ applied = 0
163
+ if not dry_run and candidates:
164
+ authorization = authorize_route_mutation(
165
+ request,
166
+ operation="update",
167
+ source_agent_id="http-lifecycle-compact",
168
+ profile_id=profile,
169
+ )
170
+ for c in candidates:
171
+ engine._db.update_fact(c["fact_id"], {"lifecycle": c["proposed"]})
172
+ applied += 1
173
+ authorization.complete()
174
+
175
+ return {
176
+ "success": True,
177
+ "dry_run": dry_run,
178
+ "active_profile": profile,
179
+ "total_facts": len(facts),
180
+ "candidates": len(candidates),
181
+ "applied": applied,
182
+ "transitions": candidates[:20],
183
+ }
184
+ except Exception as exc:
185
+ logger.exception("lifecycle compaction failed")
186
+ return {"success": False, "error": "internal error"}
@@ -20,6 +20,18 @@ logger = logging.getLogger("superlocalmemory.routes.memories")
20
20
  router = APIRouter()
21
21
 
22
22
 
23
+ def _internal_error(detail: str = "Internal server error") -> HTTPException:
24
+ """SEC-H-02: log the full traceback server-side; return a generic message.
25
+
26
+ Returning ``str(e)`` to HTTP clients leaks DB schema (column/constraint
27
+ names), the data-directory filesystem path, and possibly config internals —
28
+ a GDPR Art. 32 gap. Call this only from inside an ``except`` block so
29
+ ``logger.exception`` captures the active traceback automatically.
30
+ """
31
+ logger.exception("memories route error")
32
+ return HTTPException(status_code=500, detail=detail)
33
+
34
+
23
35
  def _get_engine(request: Request):
24
36
  """Get V3 engine from app state, initializing lazily on first call."""
25
37
  return get_engine_lazy(request.app.state)
@@ -41,6 +53,15 @@ def _authorize_memory_mutation(
41
53
  getattr(request.app.state, "daemon_descriptor", None),
42
54
  actor_kind="dashboard",
43
55
  )
56
+ # RBAC (C3): on top of machine auth, enforce the caller's role on the active
57
+ # profile. delete → DELETE; every other mutation → WRITE. A viewer (or an
58
+ # owner in require_login mode) is rejected here; owner/admin/member pass.
59
+ from superlocalmemory.access.rbac import Permission as _Perm
60
+ from superlocalmemory.server.rbac_enforce import require_permission as _rbac_require
61
+ _rbac_require(
62
+ request,
63
+ _Perm.DELETE if operation == "delete" else _Perm.WRITE,
64
+ )
44
65
  engine = _get_engine(request)
45
66
  if engine is None:
46
67
  raise HTTPException(503, detail="Engine not initialized")
@@ -253,6 +274,14 @@ async def get_memories(
253
274
  None,
254
275
  description="Named filter: 'high_reward' | 'being_forgotten'",
255
276
  ),
277
+ scope: Optional[str] = Query(
278
+ None,
279
+ description=(
280
+ "Scope view (v3 only): 'shared' (shared with this profile), "
281
+ "'global' (global memories), 'all' (this profile + global + shared). "
282
+ "Default None = this profile only (unchanged isolation)."
283
+ ),
284
+ ),
256
285
  ):
257
286
  """List memories with optional filtering and pagination.
258
287
 
@@ -274,15 +303,36 @@ async def get_memories(
274
303
  use_v3 = _has_table(cursor, 'atomic_facts')
275
304
 
276
305
  if use_v3:
277
- query = """
278
- SELECT fact_id as id, memory_id, content, fact_type as category,
279
- confidence as importance, access_count,
280
- created_at, created_at as updated_at,
281
- session_id as project_name
282
- FROM atomic_facts WHERE profile_id = ?
283
- """
284
- params = [active_profile]
285
- count_base = "SELECT COUNT(*) as total FROM atomic_facts WHERE profile_id = ?"
306
+ # Scope view clause. Default (scope=None) is this-profile-only —
307
+ # identical isolation to before. Other values widen the view to
308
+ # global and/or shared-with-this-profile memories.
309
+ _esc = active_profile.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
310
+ _shared_pat = f'%"{_esc}"%'
311
+ if scope == "global":
312
+ scope_where = "scope = 'global'"
313
+ scope_params = []
314
+ elif scope == "shared":
315
+ scope_where = "scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'"
316
+ scope_params = [_shared_pat]
317
+ elif scope == "all":
318
+ scope_where = (
319
+ "(profile_id = ? OR scope = 'global' "
320
+ "OR (scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'))"
321
+ )
322
+ scope_params = [active_profile, _shared_pat]
323
+ else:
324
+ scope_where = "profile_id = ?"
325
+ scope_params = [active_profile]
326
+
327
+ query = (
328
+ "SELECT fact_id as id, memory_id, content, fact_type as category, "
329
+ "confidence as importance, access_count, "
330
+ "created_at, created_at as updated_at, "
331
+ "session_id as project_name, scope, shared_with "
332
+ f"FROM atomic_facts WHERE {scope_where}"
333
+ )
334
+ params = list(scope_params)
335
+ count_base = f"SELECT COUNT(*) as total FROM atomic_facts WHERE {scope_where}"
286
336
  else:
287
337
  query = """
288
338
  SELECT id, content, summary, category, project_name, project_path,
@@ -293,7 +343,8 @@ async def get_memories(
293
343
  params = [active_profile]
294
344
  count_base = "SELECT COUNT(*) as total FROM memories WHERE profile = ?"
295
345
 
296
- count_params = [active_profile]
346
+ # count params must mirror the base WHERE params (scope-aware for v3).
347
+ count_params = list(params)
297
348
 
298
349
  if category:
299
350
  if use_v3:
@@ -396,8 +447,8 @@ async def get_memories(
396
447
  "has_more": (offset + limit) < total,
397
448
  }
398
449
 
399
- except Exception as e:
400
- raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
450
+ except Exception:
451
+ raise _internal_error("Database error")
401
452
 
402
453
 
403
454
  @router.get("/api/graph")
@@ -430,8 +481,8 @@ async def get_graph(
430
481
  },
431
482
  }
432
483
 
433
- except Exception as e:
434
- raise HTTPException(status_code=500, detail=f"Graph error: {str(e)}")
484
+ except Exception:
485
+ raise _internal_error("Graph error")
435
486
 
436
487
 
437
488
  @router.post("/api/search")
@@ -455,17 +506,27 @@ async def search_memories(request: Request, body: SearchRequest):
455
506
  # directly in an async route blocks the ASGI event loop — Chrome detects
456
507
  # a stalled connection and aborts with "signal is aborted without reason"
457
508
  # before the response arrives. Fix: run in a thread-pool executor so the
458
- # event loop stays alive to send keepalive frames. Also fast=False skips
459
- # spreading_activation + Hopfield (saves ~7s on cold graph traversal).
509
+ # event loop stays alive to send keepalive frames.
510
+ # v3.4.64 (regression fix): fast=True skips spreading_activation + agentic
511
+ # LLM rounds (saves ~7s on cold graph traversal). fast=False is the SLOW
512
+ # path that enables both; the earlier comment had this inverted.
460
513
  import asyncio
461
514
  import time as _time
462
515
  engine = _get_engine(request)
463
516
  if engine is not None:
464
- loop = asyncio.get_event_loop()
517
+ loop = asyncio.get_running_loop()
465
518
  t0 = _time.monotonic()
519
+ # T-window: prefer an explicit ``window`` spec; otherwise derive a
520
+ # range from the legacy date_from/date_to pair when both are set.
521
+ _window = getattr(body, "window", None) or ""
522
+ if not _window and getattr(body, "date_from", None) and getattr(body, "date_to", None):
523
+ _window = f"{body.date_from}..{body.date_to}"
466
524
  response = await loop.run_in_executor(
467
525
  None,
468
- lambda: engine.recall(body.query, limit=body.limit, fast=False),
526
+ lambda: engine.recall(
527
+ body.query, limit=body.limit, fast=True,
528
+ window=_window or None,
529
+ ),
469
530
  )
470
531
  elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
471
532
  from superlocalmemory.server.recall_serializer import (
@@ -525,8 +586,8 @@ async def search_memories(request: Request, body: SearchRequest):
525
586
  "no_confident_match": False,
526
587
  }
527
588
 
528
- except Exception as e:
529
- raise HTTPException(status_code=500, detail=f"Search error: {str(e)}")
589
+ except Exception:
590
+ raise _internal_error("Search error")
530
591
  finally:
531
592
  end_recall()
532
593
 
@@ -611,8 +672,8 @@ async def get_clusters(request: Request):
611
672
 
612
673
  conn.close()
613
674
  return {"clusters": clusters, "total_clusters": len(clusters), "unclustered_count": unclustered}
614
- except Exception as e:
615
- raise HTTPException(status_code=500, detail=f"Cluster error: {str(e)}")
675
+ except Exception:
676
+ raise _internal_error("Cluster error")
616
677
 
617
678
 
618
679
  @router.get("/api/clusters/{cluster_id}")
@@ -673,17 +734,19 @@ async def get_cluster_detail(request: Request, cluster_id: str, limit: int = Que
673
734
  texts = [m.get("content", "")[:200] for m in members[:10] if m.get("content")]
674
735
  if texts:
675
736
  from superlocalmemory.core.summarizer import Summarizer
676
- from superlocalmemory.server.profile_runtime import get_profile_runtime
677
737
  from superlocalmemory.server.routes.helpers import get_engine_lazy
678
738
 
679
- runtime = get_profile_runtime(request.app.state)
680
- with runtime.operation():
681
- engine = get_engine_lazy(request.app.state)
682
- if engine is not None:
683
- summarizer = Summarizer(engine._config)
684
- summary = summarizer.summarize_cluster(
685
- [{"content": t} for t in texts]
686
- ) or ""
739
+ # v3.4.64: Do NOT call runtime.operation() synchronously in an
740
+ # async route — if a transition is pending, acquire_operation()
741
+ # blocks the event loop thread, deadlocking the drain. The
742
+ # middleware already holds an operation lease for this request;
743
+ # the engine is stable for the full duration of the handler.
744
+ engine = get_engine_lazy(request.app.state)
745
+ if engine is not None:
746
+ summarizer = Summarizer(engine._config)
747
+ summary = summarizer.summarize_cluster(
748
+ [{"content": t} for t in texts]
749
+ ) or ""
687
750
  except Exception:
688
751
  pass
689
752
 
@@ -695,8 +758,8 @@ async def get_cluster_detail(request: Request, cluster_id: str, limit: int = Que
695
758
  }
696
759
  except HTTPException:
697
760
  raise
698
- except Exception as e:
699
- raise HTTPException(status_code=500, detail=f"Cluster detail error: {str(e)}")
761
+ except Exception:
762
+ raise _internal_error("Cluster detail error")
700
763
 
701
764
 
702
765
  @router.get("/api/memories/{memory_id}/facts")
@@ -710,18 +773,21 @@ async def get_memory_facts(request: Request, memory_id: str):
710
773
  engine instead, exactly like the /recall route.
711
774
  """
712
775
  try:
713
- from superlocalmemory.server.profile_runtime import get_profile_runtime
714
776
  from superlocalmemory.server.routes.helpers import get_engine_lazy
715
777
 
716
- runtime = get_profile_runtime(request.app.state)
717
- with runtime.operation():
718
- engine = get_engine_lazy(request.app.state)
719
- if engine is None:
720
- raise HTTPException(status_code=503, detail="Engine not initialized")
721
- active_profile = engine.profile_id
722
- mem_map = engine._db.get_memory_content_batch([memory_id])
723
- original = mem_map.get(memory_id, "")
724
- facts = engine._db.get_facts_by_memory_id(memory_id, active_profile)
778
+ # v3.4.64: Do NOT call runtime.operation() synchronously in an async
779
+ # route — blocks the event loop when a transition is pending. The
780
+ # middleware already holds an operation lease; the engine and its
781
+ # profile_id are stable for the full duration of this request.
782
+ engine = get_engine_lazy(request.app.state)
783
+ if engine is None:
784
+ raise HTTPException(status_code=503, detail="Engine not initialized")
785
+ active_profile = engine.profile_id
786
+ mem_map = engine._db.get_memory_content_batch(
787
+ [memory_id], active_profile, include_global=True, include_shared=True,
788
+ )
789
+ original = mem_map.get(memory_id, "")
790
+ facts = engine._db.get_facts_by_memory_id(memory_id, active_profile)
725
791
  fact_list = [
726
792
  {
727
793
  "fact_id": f.fact_id,
@@ -744,8 +810,8 @@ async def get_memory_facts(request: Request, memory_id: str):
744
810
  }
745
811
  except HTTPException:
746
812
  raise
747
- except Exception as e:
748
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
813
+ except Exception:
814
+ raise _internal_error()
749
815
 
750
816
 
751
817
  @router.get("/api/memories/{memory_id}/detail")
@@ -795,8 +861,8 @@ async def get_memory_detail(request: Request, memory_id: str):
795
861
  }
796
862
  except HTTPException:
797
863
  raise
798
- except Exception as e:
799
- raise HTTPException(status_code=500, detail=f"Detail error: {str(e)}")
864
+ except Exception:
865
+ raise _internal_error("Detail error")
800
866
 
801
867
 
802
868
  @router.get("/api/facts/{fact_id}")
@@ -835,8 +901,8 @@ async def get_fact_detail(request: Request, fact_id: str):
835
901
  return row
836
902
  except HTTPException:
837
903
  raise
838
- except Exception as e:
839
- raise HTTPException(status_code=500, detail=f"Fact detail error: {str(e)}")
904
+ except Exception:
905
+ raise _internal_error("Fact detail error")
840
906
 
841
907
 
842
908
  @router.delete("/api/memories/{fact_id}")
@@ -859,8 +925,8 @@ async def delete_memory(request: Request, fact_id: str):
859
925
  return {"success": True, "deleted": fact_id}
860
926
  except HTTPException:
861
927
  raise
862
- except Exception as e:
863
- raise HTTPException(status_code=500, detail=f"Delete error: {str(e)}")
928
+ except Exception:
929
+ raise _internal_error("Delete error")
864
930
 
865
931
 
866
932
  @router.post("/api/memories/{fact_id}/forget")
@@ -920,8 +986,8 @@ async def forget_memory(request: Request, fact_id: str):
920
986
  return {"success": True, "fact_id": fact_id, "archived_at": archived_at}
921
987
  except HTTPException:
922
988
  raise
923
- except Exception as e:
924
- raise HTTPException(status_code=500, detail=f"Forget error: {str(e)}")
989
+ except Exception:
990
+ raise _internal_error("Forget error")
925
991
 
926
992
 
927
993
  @router.post("/api/memories/{fact_id}/merge")
@@ -989,8 +1055,8 @@ async def merge_memory(request: Request, fact_id: str):
989
1055
  }
990
1056
  except HTTPException:
991
1057
  raise
992
- except Exception as e:
993
- raise HTTPException(status_code=500, detail=f"Merge error: {str(e)}")
1058
+ except Exception:
1059
+ raise _internal_error("Merge error")
994
1060
 
995
1061
 
996
1062
  @router.patch("/api/memories/{fact_id}")
@@ -1022,5 +1088,64 @@ async def edit_memory(request: Request, fact_id: str):
1022
1088
  return {"success": True, "fact_id": fact_id, "content": new_content}
1023
1089
  except HTTPException:
1024
1090
  raise
1025
- except Exception as e:
1026
- raise HTTPException(status_code=500, detail=f"Edit error: {str(e)}")
1091
+ except Exception:
1092
+ raise _internal_error("Edit error")
1093
+
1094
+
1095
+ _VALID_SCOPES = ("personal", "shared", "global")
1096
+
1097
+
1098
+ @router.patch("/api/memories/{fact_id}/scope")
1099
+ async def set_memory_scope(request: Request, fact_id: str):
1100
+ """Set a memory's scope (personal | shared | global) + shared_with.
1101
+
1102
+ Body: {"scope": "shared", "shared_with": "alice,bob"}. shared_with accepts a
1103
+ comma-separated string or a list; it is stored as a JSON array so the
1104
+ _scope_where LIKE match works. Mutation-authorized, and the fact must
1105
+ belong to the active profile (a caller cannot re-scope another profile's
1106
+ fact). This is the write side of multi-scope sharing from the dashboard.
1107
+ """
1108
+ import json as _json
1109
+ try:
1110
+ body = await request.json()
1111
+ scope = (body.get("scope") or "").strip().lower()
1112
+ if scope not in _VALID_SCOPES:
1113
+ raise HTTPException(400, detail=f"scope must be one of {_VALID_SCOPES}")
1114
+
1115
+ raw_shared = body.get("shared_with", [])
1116
+ if isinstance(raw_shared, str):
1117
+ shared_list = [s.strip() for s in raw_shared.split(",") if s.strip()]
1118
+ elif isinstance(raw_shared, list):
1119
+ shared_list = [str(s).strip() for s in raw_shared if str(s).strip()]
1120
+ else:
1121
+ shared_list = []
1122
+ if scope == "shared" and not shared_list:
1123
+ raise HTTPException(
1124
+ 400, detail="shared scope requires at least one profile in shared_with")
1125
+ # global/personal never carry a shared_with list.
1126
+ if scope != "shared":
1127
+ shared_list = []
1128
+
1129
+ engine, active_profile, _ctx = _authorize_memory_mutation(
1130
+ request, "update", fact_id, run_pre_hook=False,
1131
+ )
1132
+ # Ownership check: the fact must belong to the active profile.
1133
+ rows = engine._db.execute(
1134
+ "SELECT 1 FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
1135
+ (fact_id, active_profile),
1136
+ )
1137
+ if not rows:
1138
+ raise HTTPException(404, detail="Memory not found in this profile")
1139
+
1140
+ engine._db.update_fact(fact_id, {
1141
+ "scope": scope,
1142
+ "shared_with": _json.dumps(shared_list),
1143
+ }, profile_id=active_profile)
1144
+ return {
1145
+ "success": True, "fact_id": fact_id, "scope": scope,
1146
+ "shared_with": shared_list, "active_profile": active_profile,
1147
+ }
1148
+ except HTTPException:
1149
+ raise
1150
+ except Exception:
1151
+ raise _internal_error("Scope update error")