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
@@ -9,15 +9,27 @@ Routes: /api/learning/status, /api/feedback, /api/feedback/dwell,
9
9
  /api/learning/retrain
10
10
  Uses V3 learning modules: FeedbackCollector, EngagementTracker, AdaptiveLearner.
11
11
  """
12
- import shutil
13
12
  import logging
13
+ import shutil
14
+ import sqlite3
15
+ from contextlib import closing
14
16
  from datetime import datetime, timezone
15
17
  from pathlib import Path
16
18
 
17
- from fastapi import APIRouter
19
+ from fastapi import APIRouter, Request
18
20
  from fastapi.concurrency import run_in_threadpool
19
21
 
20
- from .helpers import get_active_profile, MEMORY_DIR
22
+ from .helpers import MEMORY_DIR, get_active_profile
23
+ from .learning_telemetry import ReadOnlyRankerStore
24
+ from .learning_telemetry import (
25
+ load_model_state as _load_model_state,
26
+ )
27
+ from .learning_telemetry import (
28
+ load_source_quality_state as _load_source_quality_state,
29
+ )
30
+ from .learning_telemetry import (
31
+ sqlite_status as _sqlite_status,
32
+ )
21
33
 
22
34
  logger = logging.getLogger("superlocalmemory.routes.learning")
23
35
  router = APIRouter()
@@ -25,11 +37,47 @@ router = APIRouter()
25
37
  LEARNING_DB = MEMORY_DIR / "learning.db"
26
38
 
27
39
 
40
+ def _require_write(request: Request) -> None:
41
+ from superlocalmemory.access.rbac import Permission
42
+ from superlocalmemory.server.rbac_enforce import require_permission
43
+
44
+ require_permission(request, Permission.WRITE, profile=get_active_profile())
45
+
46
+
47
+ def _require_delete(request: Request) -> None:
48
+ from superlocalmemory.access.rbac import Permission
49
+ from superlocalmemory.server.rbac_enforce import require_permission
50
+
51
+ require_permission(request, Permission.DELETE, profile=get_active_profile())
52
+
53
+
54
+ def _require_manage(request: Request) -> None:
55
+ from superlocalmemory.server.rbac_enforce import require_manage
56
+
57
+ require_manage(request, profile=get_active_profile())
58
+
59
+
28
60
  # ---------------------------------------------------------------------------
29
61
  # LLD-02 §4.10 — Dashboard phase truth
30
62
  # ---------------------------------------------------------------------------
31
63
 
32
64
 
65
+ def _phase_payload(
66
+ phase: int,
67
+ key: str,
68
+ label: str,
69
+ model_active: bool,
70
+ signals: int,
71
+ gates: dict,
72
+ status: str,
73
+ ) -> dict:
74
+ return {
75
+ "phase": phase, "key": key, "label": label,
76
+ "model_active": model_active, "signals": signals,
77
+ "gates": gates, "status": status,
78
+ }
79
+
80
+
33
81
  def _compute_ranker_phase(
34
82
  profile_id: str,
35
83
  *,
@@ -41,60 +89,75 @@ def _compute_ranker_phase(
41
89
  SHA-256 verification on the model_cache load. Tampered bytes fall
42
90
  back to phase 2.
43
91
  """
44
- from superlocalmemory.learning.database import LearningDatabase
45
- from superlocalmemory.learning.model_cache import load_active, invalidate
92
+ from superlocalmemory.learning.model_cache import load_active
93
+ from superlocalmemory.learning.ranker import (
94
+ PHASE_2_THRESHOLD,
95
+ PHASE_3_THRESHOLD,
96
+ )
46
97
 
47
98
  db_path = Path(learning_db_path) if learning_db_path else LEARNING_DB
99
+ gates = {
100
+ "rule_based_min_signals": PHASE_2_THRESHOLD,
101
+ "ml_model_min_signals": PHASE_3_THRESHOLD,
102
+ "ml_model_requires_verified_active_model": True,
103
+ }
48
104
  if not db_path.exists():
49
- return {
50
- "phase": 1,
51
- "label": "Cold start (cross-encoder only)",
52
- "model_active": False,
53
- "signals": 0,
54
- }
105
+ return _phase_payload(
106
+ 1, "baseline", "Cold start (cross-encoder only)", False, 0,
107
+ gates, "missing_database",
108
+ )
55
109
 
56
- db = LearningDatabase(db_path)
110
+ try:
111
+ db = ReadOnlyRankerStore(db_path)
112
+ except Exception as exc:
113
+ logger.warning("ranker database unavailable: %s", exc)
114
+ status = (
115
+ _sqlite_status(exc)
116
+ if isinstance(exc, sqlite3.Error) else "query_error"
117
+ )
118
+ return _phase_payload(
119
+ 1, "baseline", "Cold start (cross-encoder only)", False, 0,
120
+ gates, status,
121
+ )
122
+
123
+ signal_status = "available"
57
124
  try:
58
125
  signals = db.count_signals(profile_id)
59
126
  except Exception as exc:
60
127
  logger.warning("count_signals failed: %s", exc)
61
128
  signals = 0
129
+ signal_status = (
130
+ "missing_table"
131
+ if isinstance(exc, sqlite3.OperationalError)
132
+ and "no such table" in str(exc).lower()
133
+ else (
134
+ _sqlite_status(exc)
135
+ if isinstance(exc, sqlite3.Error) else "query_error"
136
+ )
137
+ )
62
138
 
63
- # Force a cache-bypass load — the dashboard read is rare and we want
64
- # tamper detection to surface immediately.
65
- invalidate(profile_id)
66
139
  try:
67
- model = load_active(db, profile_id, use_cache=False)
140
+ # Official train/promote/rollback paths invalidate this cache. Reuse
141
+ # the verified object here so tab navigation does not deserialize a
142
+ # LightGBM model on every dashboard request.
143
+ model = load_active(db, profile_id, use_cache=True)
68
144
  except Exception as exc:
69
145
  logger.warning("load_active failed: %s", exc)
70
146
  model = None
71
147
 
72
148
  active = model is not None
73
149
 
74
- if active and signals >= 200:
75
- return {
76
- "phase": 3,
77
- "label": "LightGBM ranker active",
78
- "model_active": True,
79
- "signals": signals,
80
- }
81
- if signals >= 50:
82
- return {
83
- "phase": 2,
84
- "label": "Contextual bandit",
85
- "model_active": False,
86
- "signals": signals,
87
- }
88
- return {
89
- "phase": 1,
90
- "label": "Cold start (cross-encoder only)",
91
- "model_active": False,
92
- "signals": signals,
93
- }
150
+ if active and signals >= PHASE_3_THRESHOLD:
151
+ phase = (3, "ml_model", "LightGBM ranker active", True)
152
+ elif signals >= PHASE_2_THRESHOLD:
153
+ phase = (2, "rule_based", "Contextual bandit", False)
154
+ else:
155
+ phase = (1, "baseline", "Cold start (cross-encoder only)", False)
156
+ return _phase_payload(*phase, signals, gates, signal_status)
94
157
 
95
158
 
96
159
  @router.get("/api/learning/ranker_phase")
97
- async def ranker_phase():
160
+ def ranker_phase():
98
161
  """Dashboard endpoint — LLD-02 §4.10 phase truth."""
99
162
  try:
100
163
  profile = get_active_profile()
@@ -106,14 +169,14 @@ async def ranker_phase():
106
169
  LEARNING_AVAILABLE = False
107
170
  BEHAVIORAL_AVAILABLE = False
108
171
  try:
109
- from superlocalmemory.learning.feedback import FeedbackCollector
110
172
  from superlocalmemory.learning.engagement import EngagementTracker
111
- from superlocalmemory.learning.ranker import AdaptiveRanker
173
+ from superlocalmemory.learning.feedback import FeedbackCollector
174
+ from superlocalmemory.learning.ranker import AdaptiveRanker # noqa: F401
112
175
  LEARNING_AVAILABLE = True
113
176
  except ImportError as e:
114
177
  logger.warning("V3 learning primary import failed: %s", e)
115
178
  try:
116
- from superlocalmemory.learning.adaptive import AdaptiveLearner
179
+ from superlocalmemory.learning.adaptive import AdaptiveLearner # noqa: F401
117
180
  LEARNING_AVAILABLE = True
118
181
  except ImportError as e2:
119
182
  logger.warning("V3 learning fallback import failed: %s", e2)
@@ -149,30 +212,73 @@ def _get_engagement() -> "EngagementTracker | None":
149
212
  return _engagement
150
213
 
151
214
 
215
+ def _source_quality_repair_telemetry(request: Request) -> dict:
216
+ status = getattr(
217
+ request.app.state, "source_quality_repair_status", None,
218
+ )
219
+ if not isinstance(status, dict):
220
+ return {
221
+ "state": "not_scheduled",
222
+ "source": "daemon_app_state",
223
+ "last_error": None,
224
+ }
225
+ return {
226
+ "state": str(status.get("state") or "unknown"),
227
+ "source": str(status.get("source") or "daemon_app_state"),
228
+ "batch_size": int(status.get("batch_size") or 0),
229
+ "profiles_total": len(status.get("profiles") or []),
230
+ "profiles_complete": len(status.get("completed_profiles") or []),
231
+ "batches_completed": int(status.get("batches_completed") or 0),
232
+ "scanned": int(status.get("scanned") or 0),
233
+ "observations": int(status.get("observations") or 0),
234
+ "last_error": status.get("last_error"),
235
+ }
236
+
237
+
152
238
  @router.get("/api/learning/status")
153
- async def learning_status():
239
+ def learning_status(request: Request):
154
240
  """Get comprehensive learning system status for dashboard."""
241
+ repair_telemetry = _source_quality_repair_telemetry(request)
155
242
  if not LEARNING_AVAILABLE:
156
243
  return {
157
244
  "available": False, "ranking_phase": None,
245
+ "ranker_phase": None, "ranking_phase_gates": {},
158
246
  "stats": None, "tech_preferences": [], "workflow_patterns": [],
159
247
  "source_scores": {}, "engagement": None,
248
+ "telemetry_status": {
249
+ "source_quality_repair": repair_telemetry["state"],
250
+ },
251
+ "source_quality_repair": repair_telemetry,
160
252
  "message": "Learning features not installed.",
161
253
  }
162
254
 
163
- result = {"available": True}
255
+ result = {
256
+ "available": True,
257
+ "telemetry_status": {
258
+ "source_quality_repair": repair_telemetry["state"],
259
+ },
260
+ "source_quality_repair": repair_telemetry,
261
+ }
164
262
 
165
263
  try:
166
264
  active_profile = get_active_profile()
167
265
  result["active_profile"] = active_profile
266
+ ranker_state = _compute_ranker_phase(active_profile)
267
+ result["ranker_phase"] = ranker_state
268
+ result["ranking_phase"] = ranker_state["key"]
269
+ result["ranking_phase_gates"] = ranker_state["gates"]
270
+ result["telemetry_status"]["ranker"] = ranker_state.get(
271
+ "status", "available",
272
+ )
168
273
 
169
274
  # Real signal count from V3.1 learning_feedback table
170
275
  signal_count = 0
171
276
  unique_queries = 0
172
277
  try:
173
- from superlocalmemory.learning.feedback import FeedbackCollector
174
- from pathlib import Path
175
278
  import sqlite3 as _sqlite3
279
+
280
+ from superlocalmemory.learning.feedback import FeedbackCollector
281
+
176
282
  learning_db = LEARNING_DB
177
283
  if learning_db.exists():
178
284
  collector = FeedbackCollector(learning_db)
@@ -194,25 +300,25 @@ async def learning_status():
194
300
  except Exception:
195
301
  pass
196
302
 
197
- # Ranking phase based on real signal count
198
- if signal_count >= 200:
199
- result["ranking_phase"] = "ml_model"
200
- elif signal_count >= 20:
201
- result["ranking_phase"] = "rule_based"
202
- else:
203
- result["ranking_phase"] = "baseline"
204
-
205
303
  # Feedback stats — merge old system + new V3.1 signals
206
- stats_dict = {"feedback_count": signal_count, "unique_queries": unique_queries, "active_profile": active_profile}
304
+ stats_dict = {
305
+ "feedback_count": signal_count,
306
+ "unique_queries": unique_queries,
307
+ "ranker_signal_count": ranker_state["signals"],
308
+ "active_profile": active_profile,
309
+ }
207
310
  feedback = _get_feedback()
208
311
  if feedback:
209
312
  try:
210
313
  old_stats = feedback.get_feedback_summary(active_profile)
211
314
  if isinstance(old_stats, dict):
212
- old_stats["feedback_count"] = signal_count
213
- old_stats["unique_queries"] = unique_queries
214
- old_stats["active_profile"] = active_profile
215
- stats_dict = old_stats
315
+ stats_dict = {
316
+ **old_stats,
317
+ "feedback_count": signal_count,
318
+ "unique_queries": unique_queries,
319
+ "ranker_signal_count": ranker_state["signals"],
320
+ "active_profile": active_profile,
321
+ }
216
322
  except Exception as exc:
217
323
  logger.debug("feedback summary: %s", exc)
218
324
 
@@ -252,21 +358,37 @@ async def learning_status():
252
358
  # Tech preferences + workflow patterns from V3.1 behavioral store
253
359
  try:
254
360
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
255
- from pathlib import Path
361
+
256
362
  learning_db = LEARNING_DB
257
363
  if learning_db.exists():
258
364
  store = BehavioralPatternStore(str(learning_db))
259
365
  all_patterns = store.get_patterns(profile_id=active_profile)
260
366
  tech = [
261
- {"key": "tech", "value": p.get("metadata", {}).get("value", p.get("pattern_key", "")),
262
- "confidence": p.get("confidence", 0), "evidence": p.get("evidence_count", 0)}
263
- for p in all_patterns if p.get("pattern_type") == "tech_preference"
367
+ {
368
+ "type": "tech_preference",
369
+ "key": p.get("pattern_key", ""),
370
+ "value": p.get("metadata", {}).get(
371
+ "value", p.get("pattern_key", ""),
372
+ ),
373
+ "confidence": p.get("confidence", 0),
374
+ "evidence_count": p.get("evidence_count", 0),
375
+ # Compatibility alias for pre-3.8 dashboard clients.
376
+ "evidence": p.get("evidence_count", 0),
377
+ }
378
+ for p in all_patterns
379
+ if p.get("pattern_type") == "tech_preference"
264
380
  ]
381
+ workflow_types = {
382
+ "temporal", "workflow", "session_activity",
383
+ "co_retrieval_clusters",
384
+ }
265
385
  workflows = [
266
386
  {"type": p.get("pattern_type"), "key": p.get("pattern_key", ""),
267
387
  "value": p.get("metadata", {}).get("value", ""),
268
- "confidence": p.get("confidence", 0)}
269
- for p in all_patterns if p.get("pattern_type") in ("temporal", "interest")
388
+ "confidence": p.get("confidence", 0),
389
+ "evidence_count": p.get("evidence_count", 0)}
390
+ for p in all_patterns
391
+ if p.get("pattern_type") in workflow_types
270
392
  ]
271
393
  result["tech_preferences"] = tech
272
394
  result["workflow_patterns"] = workflows
@@ -276,10 +398,6 @@ async def learning_status():
276
398
  db_size = os.path.getsize(str(learning_db)) // 1024 if learning_db.exists() else 0
277
399
  stats_dict["db_size_kb"] = db_size
278
400
  stats_dict["transferable_patterns"] = len(all_patterns)
279
- stats_dict["models_trained"] = 1 if signal_count >= 200 else 0
280
- stats_dict["tracked_sources"] = len(set(
281
- p.get("pattern_type") for p in all_patterns
282
- ))
283
401
  else:
284
402
  result["tech_preferences"] = []
285
403
  result["workflow_patterns"] = []
@@ -288,11 +406,24 @@ async def learning_status():
288
406
  result["tech_preferences"] = []
289
407
  result["workflow_patterns"] = []
290
408
  result["pattern_error"] = str(exc)
291
- result["source_scores"] = {}
409
+ source_state = _load_source_quality_state(
410
+ active_profile, LEARNING_DB,
411
+ )
412
+ model_state = _load_model_state(active_profile, LEARNING_DB)
413
+ result["source_scores"] = source_state["scores"]
414
+ result["source_scores_source"] = "learning.db:source_quality"
415
+ result["source_scores_are_posterior"] = True
416
+ result["telemetry_status"]["source_quality"] = source_state["status"]
417
+ result["telemetry_status"]["model_state"] = model_state["status"]
418
+ stats_dict["tracked_sources"] = source_state["tracked_sources"]
419
+ stats_dict["models_trained"] = model_state["models_trained"]
420
+ stats_dict["models_active_verified"] = int(
421
+ bool(ranker_state["model_active"]),
422
+ )
292
423
 
293
- except Exception as e:
294
- logger.error("Error getting learning status: %s", e)
295
- result["error"] = str(e)
424
+ except Exception:
425
+ logger.exception("Error getting learning status")
426
+ result["error"] = "Internal server error"
296
427
 
297
428
  return result
298
429
 
@@ -302,8 +433,9 @@ async def learning_status():
302
433
  # ============================================================================
303
434
 
304
435
  @router.post("/api/feedback")
305
- async def record_feedback(data: dict):
436
+ def record_feedback(request: Request, data: dict):
306
437
  """Record explicit feedback from dashboard (thumbs up/down, pin)."""
438
+ _require_write(request)
307
439
  if not LEARNING_AVAILABLE:
308
440
  return {"success": False, "error": "Learning system not available"}
309
441
 
@@ -333,14 +465,15 @@ async def record_feedback(data: dict):
333
465
  "message": f"Feedback recorded: {feedback_type} for memory #{memory_id}",
334
466
  "feedback_id": row_id,
335
467
  }
336
- except Exception as e:
337
- logger.error("Error recording feedback: %s", e)
338
- return {"success": False, "error": str(e)}
468
+ except Exception:
469
+ logger.exception("Error recording feedback")
470
+ return {"success": False, "error": "Internal server error"}
339
471
 
340
472
 
341
473
  @router.post("/api/feedback/dwell")
342
- async def record_dwell(data: dict):
474
+ def record_dwell(request: Request, data: dict):
343
475
  """Record dwell time feedback from dashboard modal."""
476
+ _require_write(request)
344
477
  if not LEARNING_AVAILABLE:
345
478
  return {"success": False, "error": "Learning system not available"}
346
479
 
@@ -378,13 +511,13 @@ async def record_dwell(data: dict):
378
511
  "message": f"Dwell feedback recorded: {feedback_type} ({dwell_seconds:.1f}s)",
379
512
  "feedback_id": row_id,
380
513
  }
381
- except Exception as e:
382
- logger.error("Error recording dwell: %s", e)
383
- return {"success": False, "error": str(e)}
514
+ except Exception:
515
+ logger.exception("Error recording dwell")
516
+ return {"success": False, "error": "Internal server error"}
384
517
 
385
518
 
386
519
  @router.get("/api/feedback/stats")
387
- async def feedback_stats():
520
+ def feedback_stats():
388
521
  """Get feedback signal statistics for dashboard progress bar."""
389
522
  if not LEARNING_AVAILABLE:
390
523
  return {
@@ -413,9 +546,12 @@ async def feedback_stats():
413
546
  "progress": round(progress, 1), "target": target,
414
547
  "by_channel": by_channel, "by_type": by_type, "available": True,
415
548
  }
416
- except Exception as e:
417
- logger.error("Error getting feedback stats: %s", e)
418
- return {"total_signals": 0, "ranking_phase": "baseline", "progress": 0, "error": str(e)}
549
+ except Exception:
550
+ logger.exception("Error getting feedback stats")
551
+ return {
552
+ "total_signals": 0, "ranking_phase": "baseline",
553
+ "progress": 0, "error": "Internal server error",
554
+ }
419
555
 
420
556
 
421
557
  # ============================================================================
@@ -424,7 +560,7 @@ async def feedback_stats():
424
560
 
425
561
 
426
562
  @router.delete("/api/patterns/delete")
427
- async def delete_pattern(data: dict) -> dict:
563
+ def delete_pattern(request: Request, data: dict) -> dict:
428
564
  """S9-DASH-04: delete a single auto-detected pattern by key.
429
565
 
430
566
  Body: ``{pattern_type: str, pattern_key: str}``
@@ -432,6 +568,7 @@ async def delete_pattern(data: dict) -> dict:
432
568
  Returns ``{success: bool, deleted: int}``. The pattern is scoped
433
569
  to the active profile so cross-profile deletion is impossible.
434
570
  """
571
+ _require_delete(request)
435
572
  if not BEHAVIORAL_AVAILABLE:
436
573
  return {"success": False, "error": "Behavioral engine not available"}
437
574
  ptype = (data or {}).get("pattern_type", "")
@@ -455,11 +592,11 @@ async def delete_pattern(data: dict) -> dict:
455
592
  }
456
593
  except Exception as exc: # noqa: BLE001
457
594
  logger.error("delete_pattern failed: %s", exc)
458
- return {"success": False, "error": str(exc)}
595
+ return {"success": False, "error": "internal error"}
459
596
 
460
597
 
461
598
  @router.get("/api/patterns")
462
- async def get_patterns():
599
+ def get_patterns():
463
600
  """Get learned behavioral patterns for the Patterns dashboard tab.
464
601
 
465
602
  v3.4.1: This endpoint was MISSING — patterns.js calls /api/patterns
@@ -510,43 +647,74 @@ async def get_patterns():
510
647
  result["pattern_error"] = str(exc)
511
648
 
512
649
  # Learning signal stats (feedback count, co-retrieval, channel credits)
513
- try:
514
- from superlocalmemory.learning.signals import LearningSignals
515
- signals = LearningSignals(str(LEARNING_DB))
516
- result["signal_stats"] = signals.get_signal_stats(active_profile)
517
- except Exception as exc:
518
- logger.debug("Signal stats unavailable: %s", exc)
650
+ if LEARNING_AVAILABLE:
651
+ try:
652
+ from superlocalmemory.learning.signals import LearningSignals
653
+ signals = LearningSignals(str(LEARNING_DB))
654
+ result["signal_stats"] = signals.get_signal_stats(active_profile)
655
+ except Exception as exc:
656
+ logger.debug("Signal stats unavailable: %s", exc)
519
657
 
520
658
  # Graph intelligence contribution to learning (v3.4.1)
521
659
  try:
522
660
  import sqlite3 as _sqlite3
661
+
523
662
  from superlocalmemory.server.routes.helpers import DB_PATH
524
663
  if DB_PATH.exists():
525
- conn = _sqlite3.connect(str(DB_PATH))
526
- conn.row_factory = _sqlite3.Row
527
- row = conn.execute(
528
- "SELECT COUNT(*) AS cnt, COUNT(DISTINCT community_id) AS communities, "
529
- "ROUND(AVG(pagerank_score), 4) AS avg_pagerank "
530
- "FROM fact_importance WHERE profile_id = ?",
531
- (active_profile,),
532
- ).fetchone()
533
- if row:
534
- d = dict(row)
535
- result["graph_intelligence"] = {
536
- "facts_analyzed": d.get("cnt", 0),
537
- "communities_detected": d.get("communities", 0),
538
- "avg_pagerank": float(d.get("avg_pagerank", 0) or 0),
539
- }
540
- conn.close()
664
+ with closing(_sqlite3.connect(str(DB_PATH))) as conn:
665
+ conn.row_factory = _sqlite3.Row
666
+ row = conn.execute(
667
+ "SELECT COUNT(*) AS cnt, "
668
+ "COUNT(DISTINCT community_id) AS communities, "
669
+ "ROUND(AVG(pagerank_score), 4) AS avg_pagerank "
670
+ "FROM fact_importance WHERE profile_id = ?",
671
+ (active_profile,),
672
+ ).fetchone()
673
+ if row:
674
+ d = dict(row)
675
+ result["graph_intelligence"] = {
676
+ "facts_analyzed": d.get("cnt", 0),
677
+ "communities_detected": d.get("communities", 0),
678
+ "avg_pagerank": float(
679
+ d.get("avg_pagerank", 0) or 0,
680
+ ),
681
+ }
541
682
  except Exception:
542
683
  pass
543
684
 
685
+ all_patterns = [
686
+ pattern
687
+ for category_patterns in patterns.values()
688
+ for pattern in category_patterns
689
+ ]
690
+ confidences = [
691
+ float(pattern["confidence"])
692
+ for pattern in all_patterns
693
+ if pattern.get("confidence") is not None
694
+ ]
695
+ result.update({
696
+ "total_patterns": len(all_patterns),
697
+ "pattern_types": [
698
+ category
699
+ for category, category_patterns in patterns.items()
700
+ if category_patterns
701
+ ],
702
+ "confidence_stats": {
703
+ "avg": (
704
+ sum(confidences) / len(confidences)
705
+ if confidences else 0.0
706
+ ),
707
+ "min": min(confidences) if confidences else 0.0,
708
+ "max": max(confidences) if confidences else 0.0,
709
+ },
710
+ })
544
711
  return result
545
712
 
546
713
 
547
714
  @router.post("/api/learning/backup")
548
- async def learning_backup():
715
+ def learning_backup(request: Request):
549
716
  """Backup learning.db to a timestamped file."""
717
+ _require_manage(request)
550
718
  try:
551
719
  if not LEARNING_DB.exists():
552
720
  return {"success": False, "error": "No learning.db found"}
@@ -561,14 +729,15 @@ async def learning_backup():
561
729
  "path": str(backup_path),
562
730
  "message": f"Learning DB backed up to {backup_name}",
563
731
  }
564
- except Exception as e:
565
- logger.error("Error backing up learning DB: %s", e)
566
- return {"success": False, "error": str(e)}
732
+ except Exception:
733
+ logger.exception("Error backing up learning DB")
734
+ return {"success": False, "error": "Internal server error"}
567
735
 
568
736
 
569
737
  @router.post("/api/learning/reset")
570
- async def learning_reset():
738
+ def learning_reset(request: Request):
571
739
  """Reset all learning data for the active profile. Memories preserved."""
740
+ _require_delete(request)
572
741
  if not LEARNING_AVAILABLE:
573
742
  return {"success": False, "error": "Learning system not available"}
574
743
  try:
@@ -583,17 +752,18 @@ async def learning_reset():
583
752
  }
584
753
  except Exception as exc: # noqa: BLE001
585
754
  logger.error("learning_reset failed: %s", exc)
586
- return {"success": False, "error": str(exc)}
755
+ return {"success": False, "error": "internal error"}
587
756
 
588
757
 
589
758
  @router.post("/api/learning/retrain")
590
- async def learning_retrain(data: dict | None = None):
759
+ async def learning_retrain(request: Request, data: dict | None = None):
591
760
  """Force a retrain of the LightGBM ranker.
592
761
 
593
762
  Body (optional, JSON):
594
763
  ``{"include_synthetic": bool}`` — when True, migrated legacy rows
595
764
  (``is_synthetic=1``) participate in training. Default False.
596
765
  """
766
+ _require_manage(request)
597
767
  if not LEARNING_AVAILABLE:
598
768
  return {"success": False, "error": "Learning system not available"}
599
769
  include_synthetic = bool(
@@ -643,11 +813,11 @@ async def learning_retrain(data: dict | None = None):
643
813
  }
644
814
  except Exception as exc: # noqa: BLE001
645
815
  logger.error("learning_retrain failed: %s", exc)
646
- return {"success": False, "error": str(exc)}
816
+ return {"success": False, "error": "internal error"}
647
817
 
648
818
 
649
819
  @router.post("/api/learning/migrate-legacy")
650
- async def learning_migrate_legacy():
820
+ def learning_migrate_legacy(request: Request):
651
821
  """Copy ``learning_feedback`` rows into LLD-02 tables for training.
652
822
 
653
823
  Idempotent: subsequent calls detect the migration_log sentinel and
@@ -655,6 +825,7 @@ async def learning_migrate_legacy():
655
825
  with ``is_synthetic=1`` to preserve provenance; the trainer must be
656
826
  invoked with ``include_synthetic=True`` to use them.
657
827
  """
828
+ _require_manage(request)
658
829
  if not LEARNING_AVAILABLE:
659
830
  return {"success": False, "error": "Learning system not available"}
660
831
  try:
@@ -665,4 +836,4 @@ async def learning_migrate_legacy():
665
836
  return {"success": True, **stats}
666
837
  except Exception as exc: # noqa: BLE001
667
838
  logger.error("learning_migrate_legacy failed: %s", exc)
668
- return {"success": False, "error": str(exc)}
839
+ return {"success": False, "error": "internal error"}