superlocalmemory 4.1.13 → 4.1.14

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 (740) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +3 -3
  4. package/package.json +1 -8
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +1 -1
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +1 -1
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +1 -1
  19. package/plugin/skills/slm-recall/SKILL.md +1 -1
  20. package/plugin/skills/slm-remember/SKILL.md +1 -1
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +1 -1
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/agents/slm-memory-advisor.md +1 -1
  25. package/plugin-src/agents/slm-optimize-advisor.md +1 -1
  26. package/plugin-src/rules/AGENTS.md +1 -1
  27. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  38. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  39. package/scripts/postinstall.js +71 -2
  40. package/pyproject.toml +0 -250
  41. package/src/superlocalmemory/__init__.py +0 -82
  42. package/src/superlocalmemory/access/__init__.py +0 -3
  43. package/src/superlocalmemory/access/rbac.py +0 -575
  44. package/src/superlocalmemory/attribution/__init__.py +0 -9
  45. package/src/superlocalmemory/attribution/signer.py +0 -173
  46. package/src/superlocalmemory/attribution/watermark.py +0 -189
  47. package/src/superlocalmemory/brain/__init__.py +0 -5
  48. package/src/superlocalmemory/brain/truth.py +0 -418
  49. package/src/superlocalmemory/cli/__init__.py +0 -5
  50. package/src/superlocalmemory/cli/__main__.py +0 -17
  51. package/src/superlocalmemory/cli/_lazy_init.py +0 -115
  52. package/src/superlocalmemory/cli/cache_cmd.py +0 -198
  53. package/src/superlocalmemory/cli/commands.py +0 -4710
  54. package/src/superlocalmemory/cli/compress_cmd.py +0 -151
  55. package/src/superlocalmemory/cli/context_commands.py +0 -193
  56. package/src/superlocalmemory/cli/daemon.py +0 -909
  57. package/src/superlocalmemory/cli/db_migrate.py +0 -150
  58. package/src/superlocalmemory/cli/diagnostics_cmd.py +0 -101
  59. package/src/superlocalmemory/cli/escape_hatch.py +0 -220
  60. package/src/superlocalmemory/cli/evidence_cmd.py +0 -103
  61. package/src/superlocalmemory/cli/gdpr_cmd.py +0 -792
  62. package/src/superlocalmemory/cli/gdpr_io.py +0 -109
  63. package/src/superlocalmemory/cli/help_cmd.py +0 -197
  64. package/src/superlocalmemory/cli/host_upgrades.py +0 -189
  65. package/src/superlocalmemory/cli/ingest_cmd.py +0 -327
  66. package/src/superlocalmemory/cli/json_output.py +0 -81
  67. package/src/superlocalmemory/cli/loop_cmd.py +0 -187
  68. package/src/superlocalmemory/cli/main.py +0 -1145
  69. package/src/superlocalmemory/cli/mesh_cmd.py +0 -38
  70. package/src/superlocalmemory/cli/migrate_cmd.py +0 -55
  71. package/src/superlocalmemory/cli/ops_cmd.py +0 -281
  72. package/src/superlocalmemory/cli/optimize_cmd.py +0 -179
  73. package/src/superlocalmemory/cli/optimize_constants.py +0 -31
  74. package/src/superlocalmemory/cli/pending_store.py +0 -296
  75. package/src/superlocalmemory/cli/proxy_cmd.py +0 -108
  76. package/src/superlocalmemory/cli/scale_engine_cmd.py +0 -56
  77. package/src/superlocalmemory/cli/service_installer.py +0 -373
  78. package/src/superlocalmemory/cli/setup_wizard.py +0 -1162
  79. package/src/superlocalmemory/cli/summary_cmd.py +0 -215
  80. package/src/superlocalmemory/cli/version_banner.py +0 -202
  81. package/src/superlocalmemory/cli/wizard_v3426_options.py +0 -129
  82. package/src/superlocalmemory/code_graph/__init__.py +0 -46
  83. package/src/superlocalmemory/code_graph/blast_radius.py +0 -177
  84. package/src/superlocalmemory/code_graph/bridge/__init__.py +0 -36
  85. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +0 -490
  86. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +0 -206
  87. package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +0 -159
  88. package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +0 -170
  89. package/src/superlocalmemory/code_graph/bridge/maintenance.py +0 -220
  90. package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +0 -152
  91. package/src/superlocalmemory/code_graph/changes.py +0 -363
  92. package/src/superlocalmemory/code_graph/communities.py +0 -299
  93. package/src/superlocalmemory/code_graph/config.py +0 -154
  94. package/src/superlocalmemory/code_graph/database.py +0 -526
  95. package/src/superlocalmemory/code_graph/extractors/__init__.py +0 -95
  96. package/src/superlocalmemory/code_graph/extractors/python.py +0 -413
  97. package/src/superlocalmemory/code_graph/extractors/typescript.py +0 -556
  98. package/src/superlocalmemory/code_graph/flows.py +0 -350
  99. package/src/superlocalmemory/code_graph/git_hooks.py +0 -226
  100. package/src/superlocalmemory/code_graph/graph_engine.py +0 -295
  101. package/src/superlocalmemory/code_graph/graph_store.py +0 -335
  102. package/src/superlocalmemory/code_graph/incremental.py +0 -200
  103. package/src/superlocalmemory/code_graph/models.py +0 -130
  104. package/src/superlocalmemory/code_graph/parser.py +0 -687
  105. package/src/superlocalmemory/code_graph/resolver.py +0 -321
  106. package/src/superlocalmemory/code_graph/search.py +0 -460
  107. package/src/superlocalmemory/code_graph/service.py +0 -95
  108. package/src/superlocalmemory/code_graph/watcher.py +0 -207
  109. package/src/superlocalmemory/compliance/__init__.py +0 -0
  110. package/src/superlocalmemory/compliance/abac.py +0 -204
  111. package/src/superlocalmemory/compliance/audit.py +0 -385
  112. package/src/superlocalmemory/compliance/eu_ai_act.py +0 -101
  113. package/src/superlocalmemory/compliance/gdpr.py +0 -1479
  114. package/src/superlocalmemory/compliance/lifecycle.py +0 -158
  115. package/src/superlocalmemory/compliance/retention.py +0 -415
  116. package/src/superlocalmemory/compliance/scheduler.py +0 -217
  117. package/src/superlocalmemory/contracts/__init__.py +0 -1
  118. package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +0 -92
  119. package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +0 -46
  120. package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +0 -59
  121. package/src/superlocalmemory/contracts/v402.py +0 -62
  122. package/src/superlocalmemory/core/__init__.py +0 -0
  123. package/src/superlocalmemory/core/actor_context.py +0 -166
  124. package/src/superlocalmemory/core/admission.py +0 -769
  125. package/src/superlocalmemory/core/backend_orchestrator.py +0 -637
  126. package/src/superlocalmemory/core/block_hygiene.py +0 -147
  127. package/src/superlocalmemory/core/community_summary.py +0 -267
  128. package/src/superlocalmemory/core/component_healer.py +0 -144
  129. package/src/superlocalmemory/core/component_registry.py +0 -514
  130. package/src/superlocalmemory/core/config.py +0 -2204
  131. package/src/superlocalmemory/core/consolidation_engine.py +0 -983
  132. package/src/superlocalmemory/core/context_cache.py +0 -574
  133. package/src/superlocalmemory/core/derivation_lineage.py +0 -246
  134. package/src/superlocalmemory/core/embedding_worker.py +0 -208
  135. package/src/superlocalmemory/core/embeddings.py +0 -1052
  136. package/src/superlocalmemory/core/engine.py +0 -1395
  137. package/src/superlocalmemory/core/engine_capabilities.py +0 -24
  138. package/src/superlocalmemory/core/engine_ingestion.py +0 -983
  139. package/src/superlocalmemory/core/engine_lock.py +0 -75
  140. package/src/superlocalmemory/core/engine_wiring.py +0 -776
  141. package/src/superlocalmemory/core/entity_community.py +0 -178
  142. package/src/superlocalmemory/core/error_envelope.py +0 -60
  143. package/src/superlocalmemory/core/evidence_bundle.py +0 -528
  144. package/src/superlocalmemory/core/fact_consolidator.py +0 -812
  145. package/src/superlocalmemory/core/file_lock.py +0 -92
  146. package/src/superlocalmemory/core/graph_analyzer.py +0 -456
  147. package/src/superlocalmemory/core/graph_metrics.py +0 -597
  148. package/src/superlocalmemory/core/graph_pruner.py +0 -939
  149. package/src/superlocalmemory/core/health_monitor.py +0 -338
  150. package/src/superlocalmemory/core/hooks.py +0 -65
  151. package/src/superlocalmemory/core/ingest_gate.py +0 -133
  152. package/src/superlocalmemory/core/ingest_policy.py +0 -38
  153. package/src/superlocalmemory/core/ingestion_command.py +0 -1042
  154. package/src/superlocalmemory/core/injection.py +0 -434
  155. package/src/superlocalmemory/core/install_detector.py +0 -131
  156. package/src/superlocalmemory/core/key_expander.py +0 -138
  157. package/src/superlocalmemory/core/lifecycle_state.py +0 -153
  158. package/src/superlocalmemory/core/maintenance.py +0 -777
  159. package/src/superlocalmemory/core/maintenance_scheduler.py +0 -507
  160. package/src/superlocalmemory/core/materialization_control.py +0 -20
  161. package/src/superlocalmemory/core/mcp_embedder_proxy.py +0 -89
  162. package/src/superlocalmemory/core/memory_health.py +0 -266
  163. package/src/superlocalmemory/core/mode_capability.py +0 -111
  164. package/src/superlocalmemory/core/modes.py +0 -168
  165. package/src/superlocalmemory/core/mutations.py +0 -688
  166. package/src/superlocalmemory/core/ollama_embedder.py +0 -266
  167. package/src/superlocalmemory/core/ollama_validator.py +0 -315
  168. package/src/superlocalmemory/core/operation_policy.py +0 -92
  169. package/src/superlocalmemory/core/operation_policy_registry.py +0 -542
  170. package/src/superlocalmemory/core/operation_request.py +0 -127
  171. package/src/superlocalmemory/core/ops_remediation.py +0 -542
  172. package/src/superlocalmemory/core/pii.py +0 -105
  173. package/src/superlocalmemory/core/platform_utils.py +0 -138
  174. package/src/superlocalmemory/core/profiles.py +0 -234
  175. package/src/superlocalmemory/core/progressive_abstraction.py +0 -208
  176. package/src/superlocalmemory/core/projection_drain.py +0 -380
  177. package/src/superlocalmemory/core/queue_consumer.py +0 -168
  178. package/src/superlocalmemory/core/ram_lock.py +0 -160
  179. package/src/superlocalmemory/core/rate_limit.py +0 -151
  180. package/src/superlocalmemory/core/recall_gate.py +0 -95
  181. package/src/superlocalmemory/core/recall_pipeline.py +0 -1337
  182. package/src/superlocalmemory/core/recall_queue.py +0 -377
  183. package/src/superlocalmemory/core/recall_worker.py +0 -414
  184. package/src/superlocalmemory/core/registry.py +0 -121
  185. package/src/superlocalmemory/core/remember_admission.py +0 -161
  186. package/src/superlocalmemory/core/remember_runtime.py +0 -1190
  187. package/src/superlocalmemory/core/remote_mode.py +0 -214
  188. package/src/superlocalmemory/core/reranker_worker.py +0 -338
  189. package/src/superlocalmemory/core/safe_fs.py +0 -108
  190. package/src/superlocalmemory/core/scale_autopromote.py +0 -196
  191. package/src/superlocalmemory/core/scale_engine.py +0 -915
  192. package/src/superlocalmemory/core/score_contract.py +0 -82
  193. package/src/superlocalmemory/core/security_primitives.py +0 -672
  194. package/src/superlocalmemory/core/session_identity.py +0 -98
  195. package/src/superlocalmemory/core/shadow_router.py +0 -319
  196. package/src/superlocalmemory/core/slm_disabled.py +0 -85
  197. package/src/superlocalmemory/core/status_contract.py +0 -108
  198. package/src/superlocalmemory/core/store_pipeline.py +0 -1404
  199. package/src/superlocalmemory/core/summarizer.py +0 -200
  200. package/src/superlocalmemory/core/tier_manager.py +0 -461
  201. package/src/superlocalmemory/core/topic_signature.py +0 -156
  202. package/src/superlocalmemory/core/transactions/__init__.py +0 -78
  203. package/src/superlocalmemory/core/transactions/concrete_owners.py +0 -604
  204. package/src/superlocalmemory/core/transactions/erasure.py +0 -825
  205. package/src/superlocalmemory/core/transactions/manifest.py +0 -255
  206. package/src/superlocalmemory/core/transactions/manifest_key.py +0 -155
  207. package/src/superlocalmemory/core/transactions/obligations.py +0 -272
  208. package/src/superlocalmemory/core/transactions/owners.py +0 -114
  209. package/src/superlocalmemory/core/transactions/reconciler.py +0 -285
  210. package/src/superlocalmemory/core/transactions/service.py +0 -330
  211. package/src/superlocalmemory/core/worker_pool.py +0 -377
  212. package/src/superlocalmemory/core/working_memory.py +0 -288
  213. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  214. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +0 -374
  215. package/src/superlocalmemory/dynamics/eap_scheduler.py +0 -294
  216. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +0 -171
  217. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +0 -227
  218. package/src/superlocalmemory/encoding/__init__.py +0 -0
  219. package/src/superlocalmemory/encoding/auto_linker.py +0 -308
  220. package/src/superlocalmemory/encoding/cognitive_consolidator.py +0 -899
  221. package/src/superlocalmemory/encoding/consolidator.py +0 -472
  222. package/src/superlocalmemory/encoding/context_generator.py +0 -175
  223. package/src/superlocalmemory/encoding/emotional.py +0 -189
  224. package/src/superlocalmemory/encoding/entity_reflexion.py +0 -200
  225. package/src/superlocalmemory/encoding/entity_resolver.py +0 -687
  226. package/src/superlocalmemory/encoding/entropy_gate.py +0 -101
  227. package/src/superlocalmemory/encoding/fact_extractor.py +0 -877
  228. package/src/superlocalmemory/encoding/foresight.py +0 -93
  229. package/src/superlocalmemory/encoding/graph_builder.py +0 -346
  230. package/src/superlocalmemory/encoding/observation_builder.py +0 -177
  231. package/src/superlocalmemory/encoding/prospective_markers.py +0 -262
  232. package/src/superlocalmemory/encoding/scene_builder.py +0 -410
  233. package/src/superlocalmemory/encoding/signal_inference.py +0 -90
  234. package/src/superlocalmemory/encoding/temporal_parser.py +0 -432
  235. package/src/superlocalmemory/encoding/temporal_validator.py +0 -572
  236. package/src/superlocalmemory/encoding/type_router.py +0 -237
  237. package/src/superlocalmemory/evaluation/__init__.py +0 -13
  238. package/src/superlocalmemory/evaluation/calibration.py +0 -308
  239. package/src/superlocalmemory/evolution/__init__.py +0 -29
  240. package/src/superlocalmemory/evolution/blind_verifier.py +0 -122
  241. package/src/superlocalmemory/evolution/budget.py +0 -356
  242. package/src/superlocalmemory/evolution/evolution_store.py +0 -619
  243. package/src/superlocalmemory/evolution/llm_dispatch.py +0 -559
  244. package/src/superlocalmemory/evolution/model_selection.py +0 -175
  245. package/src/superlocalmemory/evolution/mutation_generator.py +0 -226
  246. package/src/superlocalmemory/evolution/skill_activator.py +0 -270
  247. package/src/superlocalmemory/evolution/skill_evolver.py +0 -928
  248. package/src/superlocalmemory/evolution/triggers.py +0 -376
  249. package/src/superlocalmemory/evolution/types.py +0 -114
  250. package/src/superlocalmemory/graph/__init__.py +0 -9
  251. package/src/superlocalmemory/graph/cozo_adjacency.py +0 -122
  252. package/src/superlocalmemory/graph/cozo_backend.py +0 -751
  253. package/src/superlocalmemory/hooks/__init__.py +0 -3
  254. package/src/superlocalmemory/hooks/_outcome_common.py +0 -523
  255. package/src/superlocalmemory/hooks/adapter_base.py +0 -347
  256. package/src/superlocalmemory/hooks/antigravity_adapter.py +0 -171
  257. package/src/superlocalmemory/hooks/auto_capture.py +0 -133
  258. package/src/superlocalmemory/hooks/auto_invoker.py +0 -521
  259. package/src/superlocalmemory/hooks/auto_parameterize.py +0 -147
  260. package/src/superlocalmemory/hooks/auto_recall.py +0 -191
  261. package/src/superlocalmemory/hooks/auto_recall_hook.py +0 -251
  262. package/src/superlocalmemory/hooks/before_web_hook.py +0 -131
  263. package/src/superlocalmemory/hooks/claude_code_hooks.py +0 -637
  264. package/src/superlocalmemory/hooks/codex_assets.py +0 -251
  265. package/src/superlocalmemory/hooks/codex_hooks.py +0 -186
  266. package/src/superlocalmemory/hooks/context_payload.py +0 -311
  267. package/src/superlocalmemory/hooks/copilot_adapter.py +0 -208
  268. package/src/superlocalmemory/hooks/cross_platform_connector.py +0 -90
  269. package/src/superlocalmemory/hooks/cursor_adapter.py +0 -173
  270. package/src/superlocalmemory/hooks/hook_daemon.py +0 -295
  271. package/src/superlocalmemory/hooks/hook_handlers.py +0 -822
  272. package/src/superlocalmemory/hooks/ide_connector.py +0 -246
  273. package/src/superlocalmemory/hooks/memory_protocol.py +0 -158
  274. package/src/superlocalmemory/hooks/portable_kit.py +0 -755
  275. package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -183
  276. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +0 -351
  277. package/src/superlocalmemory/hooks/prewarm_auth.py +0 -187
  278. package/src/superlocalmemory/hooks/rules_engine.py +0 -99
  279. package/src/superlocalmemory/hooks/session_registry.py +0 -330
  280. package/src/superlocalmemory/hooks/stop_outcome_hook.py +0 -138
  281. package/src/superlocalmemory/hooks/sync_loop.py +0 -114
  282. package/src/superlocalmemory/hooks/topic_shift_hook.py +0 -302
  283. package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -131
  284. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +0 -210
  285. package/src/superlocalmemory/infra/__init__.py +0 -3
  286. package/src/superlocalmemory/infra/auth_middleware.py +0 -145
  287. package/src/superlocalmemory/infra/backup.py +0 -974
  288. package/src/superlocalmemory/infra/backup_obligations.py +0 -423
  289. package/src/superlocalmemory/infra/cache_manager.py +0 -267
  290. package/src/superlocalmemory/infra/cloud_backup.py +0 -788
  291. package/src/superlocalmemory/infra/daemon_identity.py +0 -300
  292. package/src/superlocalmemory/infra/data_root.py +0 -238
  293. package/src/superlocalmemory/infra/event_bus.py +0 -637
  294. package/src/superlocalmemory/infra/local_diagnostics.py +0 -327
  295. package/src/superlocalmemory/infra/pid_manager.py +0 -193
  296. package/src/superlocalmemory/infra/process_identity.py +0 -180
  297. package/src/superlocalmemory/infra/process_reaper.py +0 -624
  298. package/src/superlocalmemory/infra/rate_limiter.py +0 -228
  299. package/src/superlocalmemory/infra/self_heal.py +0 -401
  300. package/src/superlocalmemory/infra/version_integrity.py +0 -229
  301. package/src/superlocalmemory/ingestion/__init__.py +0 -13
  302. package/src/superlocalmemory/ingestion/adapter_manager.py +0 -255
  303. package/src/superlocalmemory/ingestion/base_adapter.py +0 -171
  304. package/src/superlocalmemory/ingestion/calendar_adapter.py +0 -349
  305. package/src/superlocalmemory/ingestion/credentials.py +0 -125
  306. package/src/superlocalmemory/ingestion/gmail_adapter.py +0 -378
  307. package/src/superlocalmemory/ingestion/parsers.py +0 -100
  308. package/src/superlocalmemory/ingestion/transcript_adapter.py +0 -161
  309. package/src/superlocalmemory/integrations/__init__.py +0 -1
  310. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +0 -431
  311. package/src/superlocalmemory/integrations/bounded_loops_v051.py +0 -236
  312. package/src/superlocalmemory/learning/__init__.py +0 -0
  313. package/src/superlocalmemory/learning/adaptive.py +0 -172
  314. package/src/superlocalmemory/learning/arm_catalog.py +0 -97
  315. package/src/superlocalmemory/learning/assertion_miner.py +0 -403
  316. package/src/superlocalmemory/learning/bandit.py +0 -654
  317. package/src/superlocalmemory/learning/bandit_cache.py +0 -131
  318. package/src/superlocalmemory/learning/behavioral.py +0 -542
  319. package/src/superlocalmemory/learning/bootstrap.py +0 -298
  320. package/src/superlocalmemory/learning/consolidation_cycle.py +0 -398
  321. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +0 -115
  322. package/src/superlocalmemory/learning/consolidation_worker.py +0 -261
  323. package/src/superlocalmemory/learning/cross_project.py +0 -408
  324. package/src/superlocalmemory/learning/database.py +0 -698
  325. package/src/superlocalmemory/learning/dedup_hnsw.py +0 -413
  326. package/src/superlocalmemory/learning/engagement.py +0 -487
  327. package/src/superlocalmemory/learning/engagement_features.py +0 -279
  328. package/src/superlocalmemory/learning/ensemble.py +0 -309
  329. package/src/superlocalmemory/learning/entity_compiler.py +0 -356
  330. package/src/superlocalmemory/learning/fact_outcome_joins.py +0 -207
  331. package/src/superlocalmemory/learning/features.py +0 -138
  332. package/src/superlocalmemory/learning/feedback.py +0 -724
  333. package/src/superlocalmemory/learning/forgetting_scheduler.py +0 -375
  334. package/src/superlocalmemory/learning/hnsw_dedup.py +0 -69
  335. package/src/superlocalmemory/learning/labeler.py +0 -85
  336. package/src/superlocalmemory/learning/legacy_migration.py +0 -316
  337. package/src/superlocalmemory/learning/lightgbm_subprocess.py +0 -236
  338. package/src/superlocalmemory/learning/memory_merge.py +0 -175
  339. package/src/superlocalmemory/learning/model_cache.py +0 -267
  340. package/src/superlocalmemory/learning/model_rollback.py +0 -281
  341. package/src/superlocalmemory/learning/outcome_queue.py +0 -306
  342. package/src/superlocalmemory/learning/outcomes.py +0 -286
  343. package/src/superlocalmemory/learning/pattern_miner.py +0 -465
  344. package/src/superlocalmemory/learning/pattern_miner_constants.py +0 -90
  345. package/src/superlocalmemory/learning/pcos.py +0 -291
  346. package/src/superlocalmemory/learning/project_context.py +0 -366
  347. package/src/superlocalmemory/learning/propensity.py +0 -131
  348. package/src/superlocalmemory/learning/ranker.py +0 -300
  349. package/src/superlocalmemory/learning/ranker_common.py +0 -163
  350. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +0 -210
  351. package/src/superlocalmemory/learning/ranker_retrain_online.py +0 -423
  352. package/src/superlocalmemory/learning/reward.py +0 -888
  353. package/src/superlocalmemory/learning/reward_archive.py +0 -223
  354. package/src/superlocalmemory/learning/reward_boost.py +0 -211
  355. package/src/superlocalmemory/learning/reward_from_outcomes.py +0 -365
  356. package/src/superlocalmemory/learning/reward_model.py +0 -144
  357. package/src/superlocalmemory/learning/reward_proxy.py +0 -578
  358. package/src/superlocalmemory/learning/shadow_test.py +0 -524
  359. package/src/superlocalmemory/learning/signal_kinds.py +0 -79
  360. package/src/superlocalmemory/learning/signal_worker.py +0 -268
  361. package/src/superlocalmemory/learning/signals.py +0 -646
  362. package/src/superlocalmemory/learning/skill_performance_miner.py +0 -422
  363. package/src/superlocalmemory/learning/source_quality.py +0 -828
  364. package/src/superlocalmemory/learning/trigram_index.py +0 -548
  365. package/src/superlocalmemory/learning/workflows.py +0 -309
  366. package/src/superlocalmemory/llm/__init__.py +0 -0
  367. package/src/superlocalmemory/llm/backbone.py +0 -364
  368. package/src/superlocalmemory/loops/__init__.py +0 -58
  369. package/src/superlocalmemory/loops/budget.py +0 -58
  370. package/src/superlocalmemory/loops/engine.py +0 -174
  371. package/src/superlocalmemory/loops/ledger.py +0 -298
  372. package/src/superlocalmemory/loops/models.py +0 -152
  373. package/src/superlocalmemory/loops/rules.py +0 -52
  374. package/src/superlocalmemory/math/__init__.py +0 -0
  375. package/src/superlocalmemory/math/ebbinghaus.py +0 -352
  376. package/src/superlocalmemory/math/fisher.py +0 -356
  377. package/src/superlocalmemory/math/fisher_quantized.py +0 -255
  378. package/src/superlocalmemory/math/hopfield.py +0 -282
  379. package/src/superlocalmemory/math/langevin.py +0 -411
  380. package/src/superlocalmemory/math/polar_quant.py +0 -414
  381. package/src/superlocalmemory/math/qjl.py +0 -115
  382. package/src/superlocalmemory/math/sheaf.py +0 -261
  383. package/src/superlocalmemory/math/turbo_quant.py +0 -318
  384. package/src/superlocalmemory/mcp/__init__.py +0 -0
  385. package/src/superlocalmemory/mcp/_daemon_proxy.py +0 -203
  386. package/src/superlocalmemory/mcp/_pool_adapter.py +0 -181
  387. package/src/superlocalmemory/mcp/_stdin_guard.py +0 -60
  388. package/src/superlocalmemory/mcp/agent_context.py +0 -115
  389. package/src/superlocalmemory/mcp/cli_fallback.py +0 -602
  390. package/src/superlocalmemory/mcp/http_transport.py +0 -85
  391. package/src/superlocalmemory/mcp/profiles.py +0 -154
  392. package/src/superlocalmemory/mcp/resources.py +0 -281
  393. package/src/superlocalmemory/mcp/server.py +0 -480
  394. package/src/superlocalmemory/mcp/session_binding.py +0 -98
  395. package/src/superlocalmemory/mcp/shared.py +0 -112
  396. package/src/superlocalmemory/mcp/tools.py +0 -18
  397. package/src/superlocalmemory/mcp/tools_active.py +0 -958
  398. package/src/superlocalmemory/mcp/tools_brain.py +0 -298
  399. package/src/superlocalmemory/mcp/tools_code_graph.py +0 -1717
  400. package/src/superlocalmemory/mcp/tools_context.py +0 -239
  401. package/src/superlocalmemory/mcp/tools_core.py +0 -1112
  402. package/src/superlocalmemory/mcp/tools_evolution.py +0 -343
  403. package/src/superlocalmemory/mcp/tools_learning.py +0 -393
  404. package/src/superlocalmemory/mcp/tools_loops.py +0 -345
  405. package/src/superlocalmemory/mcp/tools_mesh.py +0 -429
  406. package/src/superlocalmemory/mcp/tools_ops.py +0 -115
  407. package/src/superlocalmemory/mcp/tools_optimize.py +0 -322
  408. package/src/superlocalmemory/mcp/tools_summaries.py +0 -147
  409. package/src/superlocalmemory/mcp/tools_v28.py +0 -292
  410. package/src/superlocalmemory/mcp/tools_v3.py +0 -398
  411. package/src/superlocalmemory/mcp/tools_v33.py +0 -507
  412. package/src/superlocalmemory/mesh/__init__.py +0 -12
  413. package/src/superlocalmemory/mesh/broker.py +0 -812
  414. package/src/superlocalmemory/mesh/broker_security.py +0 -470
  415. package/src/superlocalmemory/mesh/discovery.py +0 -365
  416. package/src/superlocalmemory/mesh/lock_protocol.py +0 -313
  417. package/src/superlocalmemory/mesh/node_identity.py +0 -97
  418. package/src/superlocalmemory/mesh/outbox_remote.py +0 -429
  419. package/src/superlocalmemory/mesh/remote_sync.py +0 -829
  420. package/src/superlocalmemory/mesh/state_sync.py +0 -286
  421. package/src/superlocalmemory/migrations/__init__.py +0 -5
  422. package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +0 -144
  423. package/src/superlocalmemory/optimize/NOTICE +0 -6
  424. package/src/superlocalmemory/optimize/__init__.py +0 -0
  425. package/src/superlocalmemory/optimize/adapters/__init__.py +0 -68
  426. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +0 -120
  427. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +0 -112
  428. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +0 -122
  429. package/src/superlocalmemory/optimize/adapters/wrap.py +0 -228
  430. package/src/superlocalmemory/optimize/cache/__init__.py +0 -31
  431. package/src/superlocalmemory/optimize/cache/boundary_store.py +0 -488
  432. package/src/superlocalmemory/optimize/cache/centroid_store.py +0 -199
  433. package/src/superlocalmemory/optimize/cache/context_key.py +0 -67
  434. package/src/superlocalmemory/optimize/cache/exact.py +0 -88
  435. package/src/superlocalmemory/optimize/cache/invalidation.py +0 -36
  436. package/src/superlocalmemory/optimize/cache/key_builder.py +0 -111
  437. package/src/superlocalmemory/optimize/cache/manager.py +0 -737
  438. package/src/superlocalmemory/optimize/cache/semantic.py +0 -637
  439. package/src/superlocalmemory/optimize/cache/stampede.py +0 -50
  440. package/src/superlocalmemory/optimize/compress/__init__.py +0 -17
  441. package/src/superlocalmemory/optimize/compress/align.py +0 -159
  442. package/src/superlocalmemory/optimize/compress/ccr.py +0 -116
  443. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +0 -71
  444. package/src/superlocalmemory/optimize/compress/router.py +0 -667
  445. package/src/superlocalmemory/optimize/config/__init__.py +0 -56
  446. package/src/superlocalmemory/optimize/config/defaults.py +0 -43
  447. package/src/superlocalmemory/optimize/config/schema.py +0 -327
  448. package/src/superlocalmemory/optimize/config/store.py +0 -270
  449. package/src/superlocalmemory/optimize/metrics/__init__.py +0 -8
  450. package/src/superlocalmemory/optimize/metrics/counters.py +0 -155
  451. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -87
  452. package/src/superlocalmemory/optimize/metrics/exporters.py +0 -77
  453. package/src/superlocalmemory/optimize/metrics/persistence.py +0 -115
  454. package/src/superlocalmemory/optimize/proxy/__init__.py +0 -28
  455. package/src/superlocalmemory/optimize/proxy/_helpers.py +0 -730
  456. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +0 -375
  457. package/src/superlocalmemory/optimize/proxy/capture.py +0 -550
  458. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +0 -528
  459. package/src/superlocalmemory/optimize/proxy/lifecycle.py +0 -126
  460. package/src/superlocalmemory/optimize/proxy/openai_surface.py +0 -465
  461. package/src/superlocalmemory/optimize/proxy/server.py +0 -199
  462. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +0 -246
  463. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  464. package/src/superlocalmemory/optimize/storage/db.py +0 -1185
  465. package/src/superlocalmemory/optimize/storage/schema.py +0 -205
  466. package/src/superlocalmemory/parameterization/__init__.py +0 -47
  467. package/src/superlocalmemory/parameterization/cross_project.py +0 -12
  468. package/src/superlocalmemory/parameterization/pattern_extractor.py +0 -584
  469. package/src/superlocalmemory/parameterization/pii_filter.py +0 -106
  470. package/src/superlocalmemory/parameterization/prompt_injector.py +0 -219
  471. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +0 -281
  472. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +0 -542
  473. package/src/superlocalmemory/parameterization/workflow_miner.py +0 -17
  474. package/src/superlocalmemory/reliability/__init__.py +0 -45
  475. package/src/superlocalmemory/reliability/join_liveness.py +0 -301
  476. package/src/superlocalmemory/reliability/prior_distance.py +0 -243
  477. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  478. package/src/superlocalmemory/retrieval/agentic.py +0 -367
  479. package/src/superlocalmemory/retrieval/ann_index.py +0 -235
  480. package/src/superlocalmemory/retrieval/bm25_channel.py +0 -451
  481. package/src/superlocalmemory/retrieval/bridge_discovery.py +0 -253
  482. package/src/superlocalmemory/retrieval/channel_registry.py +0 -154
  483. package/src/superlocalmemory/retrieval/channel_status.py +0 -117
  484. package/src/superlocalmemory/retrieval/engine.py +0 -1615
  485. package/src/superlocalmemory/retrieval/entity_channel.py +0 -994
  486. package/src/superlocalmemory/retrieval/forgetting_filter.py +0 -160
  487. package/src/superlocalmemory/retrieval/fusion.py +0 -81
  488. package/src/superlocalmemory/retrieval/graph_adjacency.py +0 -219
  489. package/src/superlocalmemory/retrieval/hopfield_channel.py +0 -465
  490. package/src/superlocalmemory/retrieval/profile_channel.py +0 -105
  491. package/src/superlocalmemory/retrieval/quantization_aware_search.py +0 -147
  492. package/src/superlocalmemory/retrieval/remote_reranker.py +0 -758
  493. package/src/superlocalmemory/retrieval/reranker.py +0 -674
  494. package/src/superlocalmemory/retrieval/scope_policy.py +0 -126
  495. package/src/superlocalmemory/retrieval/semantic_channel.py +0 -638
  496. package/src/superlocalmemory/retrieval/spreading.py +0 -288
  497. package/src/superlocalmemory/retrieval/spreading_activation.py +0 -616
  498. package/src/superlocalmemory/retrieval/strategy.py +0 -248
  499. package/src/superlocalmemory/retrieval/temporal_channel.py +0 -433
  500. package/src/superlocalmemory/retrieval/temporal_frame.py +0 -102
  501. package/src/superlocalmemory/retrieval/temporal_utils.py +0 -122
  502. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +0 -499
  503. package/src/superlocalmemory/retrieval/time_window.py +0 -181
  504. package/src/superlocalmemory/retrieval/vector_store.py +0 -863
  505. package/src/superlocalmemory/server/__init__.py +0 -1
  506. package/src/superlocalmemory/server/api.py +0 -310
  507. package/src/superlocalmemory/server/asset_versions.py +0 -171
  508. package/src/superlocalmemory/server/bandit_loops.py +0 -158
  509. package/src/superlocalmemory/server/config_file.py +0 -90
  510. package/src/superlocalmemory/server/consolidation_runner.py +0 -140
  511. package/src/superlocalmemory/server/egress_policy.py +0 -258
  512. package/src/superlocalmemory/server/loopback.py +0 -85
  513. package/src/superlocalmemory/server/middleware/__init__.py +0 -11
  514. package/src/superlocalmemory/server/middleware/security_headers.py +0 -144
  515. package/src/superlocalmemory/server/origin.py +0 -55
  516. package/src/superlocalmemory/server/profile_runtime.py +0 -515
  517. package/src/superlocalmemory/server/rbac_enforce.py +0 -194
  518. package/src/superlocalmemory/server/recall_health.py +0 -343
  519. package/src/superlocalmemory/server/recall_serializer.py +0 -320
  520. package/src/superlocalmemory/server/route_mutations.py +0 -104
  521. package/src/superlocalmemory/server/routes/__init__.py +0 -4
  522. package/src/superlocalmemory/server/routes/abstraction.py +0 -314
  523. package/src/superlocalmemory/server/routes/adapters.py +0 -63
  524. package/src/superlocalmemory/server/routes/agents.py +0 -303
  525. package/src/superlocalmemory/server/routes/backup.py +0 -869
  526. package/src/superlocalmemory/server/routes/behavioral.py +0 -659
  527. package/src/superlocalmemory/server/routes/brain.py +0 -1892
  528. package/src/superlocalmemory/server/routes/chat.py +0 -393
  529. package/src/superlocalmemory/server/routes/compliance.py +0 -533
  530. package/src/superlocalmemory/server/routes/config_api.py +0 -703
  531. package/src/superlocalmemory/server/routes/data_io.py +0 -329
  532. package/src/superlocalmemory/server/routes/entity.py +0 -237
  533. package/src/superlocalmemory/server/routes/events.py +0 -214
  534. package/src/superlocalmemory/server/routes/evolution.py +0 -510
  535. package/src/superlocalmemory/server/routes/helpers.py +0 -499
  536. package/src/superlocalmemory/server/routes/ingest.py +0 -137
  537. package/src/superlocalmemory/server/routes/insights.py +0 -366
  538. package/src/superlocalmemory/server/routes/learning.py +0 -834
  539. package/src/superlocalmemory/server/routes/learning_telemetry.py +0 -154
  540. package/src/superlocalmemory/server/routes/lifecycle.py +0 -184
  541. package/src/superlocalmemory/server/routes/memories.py +0 -1661
  542. package/src/superlocalmemory/server/routes/mesh.py +0 -517
  543. package/src/superlocalmemory/server/routes/mesh_lock.py +0 -54
  544. package/src/superlocalmemory/server/routes/mesh_state.py +0 -63
  545. package/src/superlocalmemory/server/routes/optimize.py +0 -197
  546. package/src/superlocalmemory/server/routes/prewarm.py +0 -173
  547. package/src/superlocalmemory/server/routes/profiles.py +0 -292
  548. package/src/superlocalmemory/server/routes/ratelimit.py +0 -132
  549. package/src/superlocalmemory/server/routes/rbac.py +0 -366
  550. package/src/superlocalmemory/server/routes/stats.py +0 -385
  551. package/src/superlocalmemory/server/routes/tiers.py +0 -222
  552. package/src/superlocalmemory/server/routes/timeline.py +0 -258
  553. package/src/superlocalmemory/server/routes/token.py +0 -90
  554. package/src/superlocalmemory/server/routes/v3_api.py +0 -3023
  555. package/src/superlocalmemory/server/routes/ws.py +0 -171
  556. package/src/superlocalmemory/server/security_middleware.py +0 -89
  557. package/src/superlocalmemory/server/ui.py +0 -354
  558. package/src/superlocalmemory/server/unified_daemon.py +0 -6326
  559. package/src/superlocalmemory/server/write_identity.py +0 -195
  560. package/src/superlocalmemory/storage/__init__.py +0 -0
  561. package/src/superlocalmemory/storage/_migration_internals.py +0 -638
  562. package/src/superlocalmemory/storage/_schema_version.py +0 -174
  563. package/src/superlocalmemory/storage/access_log.py +0 -170
  564. package/src/superlocalmemory/storage/admission_codec.py +0 -129
  565. package/src/superlocalmemory/storage/admission_journal.py +0 -843
  566. package/src/superlocalmemory/storage/agent_experience.py +0 -546
  567. package/src/superlocalmemory/storage/backup.py +0 -531
  568. package/src/superlocalmemory/storage/correction_cases.py +0 -670
  569. package/src/superlocalmemory/storage/database.py +0 -3180
  570. package/src/superlocalmemory/storage/deferred_writes.py +0 -209
  571. package/src/superlocalmemory/storage/embedding_codec.py +0 -200
  572. package/src/superlocalmemory/storage/embedding_migrator.py +0 -672
  573. package/src/superlocalmemory/storage/erasure_fence.py +0 -45
  574. package/src/superlocalmemory/storage/execution_learning.py +0 -285
  575. package/src/superlocalmemory/storage/external_evidence.py +0 -359
  576. package/src/superlocalmemory/storage/generation_fence.py +0 -63
  577. package/src/superlocalmemory/storage/lineage_retention.py +0 -236
  578. package/src/superlocalmemory/storage/logical_edges.py +0 -86
  579. package/src/superlocalmemory/storage/memory_write.py +0 -115
  580. package/src/superlocalmemory/storage/migration_runner.py +0 -895
  581. package/src/superlocalmemory/storage/migration_v33.py +0 -140
  582. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +0 -67
  583. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +0 -107
  584. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +0 -38
  585. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +0 -46
  586. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +0 -75
  587. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +0 -75
  588. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +0 -63
  589. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +0 -94
  590. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +0 -80
  591. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +0 -87
  592. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +0 -72
  593. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +0 -55
  594. package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +0 -45
  595. package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +0 -58
  596. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +0 -120
  597. package/src/superlocalmemory/storage/migrations/M017_ccq_scope_column.py +0 -79
  598. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +0 -120
  599. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +0 -54
  600. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +0 -52
  601. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +0 -108
  602. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +0 -86
  603. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +0 -194
  604. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +0 -87
  605. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +0 -90
  606. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +0 -136
  607. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +0 -163
  608. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +0 -305
  609. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +0 -137
  610. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +0 -93
  611. package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +0 -80
  612. package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +0 -188
  613. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +0 -148
  614. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +0 -58
  615. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +0 -113
  616. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +0 -107
  617. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +0 -162
  618. package/src/superlocalmemory/storage/migrations/M038_learning_feedback_channel.py +0 -77
  619. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +0 -137
  620. package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +0 -254
  621. package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +0 -189
  622. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +0 -245
  623. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +0 -512
  624. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +0 -127
  625. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +0 -158
  626. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +0 -620
  627. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +0 -306
  628. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +0 -229
  629. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +0 -201
  630. package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +0 -70
  631. package/src/superlocalmemory/storage/migrations/__init__.py +0 -103
  632. package/src/superlocalmemory/storage/migrations.py +0 -333
  633. package/src/superlocalmemory/storage/models.py +0 -500
  634. package/src/superlocalmemory/storage/projection_outbox.py +0 -346
  635. package/src/superlocalmemory/storage/quantized_store.py +0 -280
  636. package/src/superlocalmemory/storage/read_connection.py +0 -115
  637. package/src/superlocalmemory/storage/retention_policy.py +0 -860
  638. package/src/superlocalmemory/storage/schema.py +0 -1108
  639. package/src/superlocalmemory/storage/schema_code_graph.py +0 -282
  640. package/src/superlocalmemory/storage/schema_v32.py +0 -382
  641. package/src/superlocalmemory/storage/schema_v3410.py +0 -159
  642. package/src/superlocalmemory/storage/schema_v3411.py +0 -149
  643. package/src/superlocalmemory/storage/schema_v343.py +0 -315
  644. package/src/superlocalmemory/storage/schema_v345.py +0 -109
  645. package/src/superlocalmemory/storage/schema_v347.py +0 -140
  646. package/src/superlocalmemory/storage/sqlite_vectors.py +0 -169
  647. package/src/superlocalmemory/storage/v2_migrator.py +0 -466
  648. package/src/superlocalmemory/storage/write_coordinator.py +0 -949
  649. package/src/superlocalmemory/storage/write_lock.py +0 -88
  650. package/src/superlocalmemory/summaries/__init__.py +0 -37
  651. package/src/superlocalmemory/summaries/base.py +0 -267
  652. package/src/superlocalmemory/summaries/daily_reflection.py +0 -340
  653. package/src/superlocalmemory/summaries/non_answer.py +0 -223
  654. package/src/superlocalmemory/summaries/project_work_log.py +0 -440
  655. package/src/superlocalmemory/summaries/session_summary.py +0 -311
  656. package/src/superlocalmemory/trust/__init__.py +0 -0
  657. package/src/superlocalmemory/trust/gate.py +0 -171
  658. package/src/superlocalmemory/trust/provenance.py +0 -124
  659. package/src/superlocalmemory/trust/scorer.py +0 -413
  660. package/src/superlocalmemory/trust/signals.py +0 -153
  661. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +0 -64
  662. package/src/superlocalmemory/ui/assets/slm-icon.svg +0 -36
  663. package/src/superlocalmemory/ui/css/brain.css +0 -409
  664. package/src/superlocalmemory/ui/css/design-system.css +0 -696
  665. package/src/superlocalmemory/ui/css/legacy-dashboard.css +0 -663
  666. package/src/superlocalmemory/ui/css/neural-glass.css +0 -1599
  667. package/src/superlocalmemory/ui/css/od-bridge.css +0 -158
  668. package/src/superlocalmemory/ui/favicon.svg +0 -36
  669. package/src/superlocalmemory/ui/index.html +0 -1638
  670. package/src/superlocalmemory/ui/js/agents.js +0 -192
  671. package/src/superlocalmemory/ui/js/auto-settings.js +0 -624
  672. package/src/superlocalmemory/ui/js/brain.js +0 -1400
  673. package/src/superlocalmemory/ui/js/clusters.js +0 -326
  674. package/src/superlocalmemory/ui/js/compliance.js +0 -307
  675. package/src/superlocalmemory/ui/js/core.js +0 -566
  676. package/src/superlocalmemory/ui/js/dashboard.js +0 -503
  677. package/src/superlocalmemory/ui/js/event-delegation.js +0 -113
  678. package/src/superlocalmemory/ui/js/events.js +0 -178
  679. package/src/superlocalmemory/ui/js/fact-detail.js +0 -142
  680. package/src/superlocalmemory/ui/js/feedback.js +0 -339
  681. package/src/superlocalmemory/ui/js/graph-event-bus.js +0 -83
  682. package/src/superlocalmemory/ui/js/graph-filters.js +0 -220
  683. package/src/superlocalmemory/ui/js/graph-ui.js +0 -214
  684. package/src/superlocalmemory/ui/js/ide-status.js +0 -115
  685. package/src/superlocalmemory/ui/js/init.js +0 -54
  686. package/src/superlocalmemory/ui/js/knowledge-graph.js +0 -945
  687. package/src/superlocalmemory/ui/js/lifecycle.js +0 -387
  688. package/src/superlocalmemory/ui/js/math-health.js +0 -114
  689. package/src/superlocalmemory/ui/js/memories.js +0 -394
  690. package/src/superlocalmemory/ui/js/memory-chat.js +0 -371
  691. package/src/superlocalmemory/ui/js/memory-timeline.js +0 -265
  692. package/src/superlocalmemory/ui/js/modal.js +0 -733
  693. package/src/superlocalmemory/ui/js/ng-entities.js +0 -298
  694. package/src/superlocalmemory/ui/js/ng-health.js +0 -208
  695. package/src/superlocalmemory/ui/js/ng-ingestion.js +0 -203
  696. package/src/superlocalmemory/ui/js/ng-mesh.js +0 -374
  697. package/src/superlocalmemory/ui/js/ng-shell.js +0 -524
  698. package/src/superlocalmemory/ui/js/ng-skills.js +0 -663
  699. package/src/superlocalmemory/ui/js/od-agents.js +0 -588
  700. package/src/superlocalmemory/ui/js/od-auth-gate.js +0 -257
  701. package/src/superlocalmemory/ui/js/od-backup.js +0 -878
  702. package/src/superlocalmemory/ui/js/od-boundedloops.js +0 -324
  703. package/src/superlocalmemory/ui/js/od-brain.js +0 -1095
  704. package/src/superlocalmemory/ui/js/od-compliance-ext.js +0 -301
  705. package/src/superlocalmemory/ui/js/od-components.js +0 -147
  706. package/src/superlocalmemory/ui/js/od-entities.js +0 -622
  707. package/src/superlocalmemory/ui/js/od-graph.js +0 -776
  708. package/src/superlocalmemory/ui/js/od-health.js +0 -579
  709. package/src/superlocalmemory/ui/js/od-mcp.js +0 -508
  710. package/src/superlocalmemory/ui/js/od-memories.js +0 -1499
  711. package/src/superlocalmemory/ui/js/od-mesh.js +0 -645
  712. package/src/superlocalmemory/ui/js/od-operations.js +0 -1268
  713. package/src/superlocalmemory/ui/js/od-ops-health.js +0 -417
  714. package/src/superlocalmemory/ui/js/od-optimize.js +0 -828
  715. package/src/superlocalmemory/ui/js/od-settings.js +0 -1275
  716. package/src/superlocalmemory/ui/js/od-shell.js +0 -819
  717. package/src/superlocalmemory/ui/js/od-skills.js +0 -600
  718. package/src/superlocalmemory/ui/js/od-team.js +0 -265
  719. package/src/superlocalmemory/ui/js/optimize.js +0 -191
  720. package/src/superlocalmemory/ui/js/profiles.js +0 -362
  721. package/src/superlocalmemory/ui/js/quick-actions.js +0 -334
  722. package/src/superlocalmemory/ui/js/recall-lab.js +0 -373
  723. package/src/superlocalmemory/ui/js/search.js +0 -86
  724. package/src/superlocalmemory/ui/js/settings.js +0 -556
  725. package/src/superlocalmemory/ui/js/timeline.js +0 -62
  726. package/src/superlocalmemory/ui/js/trust-dashboard.js +0 -225
  727. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +0 -2018
  728. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  729. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  730. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +0 -7
  731. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +0 -6
  732. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +0 -2
  733. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +0 -2
  734. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +0 -2
  735. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +0 -8
  736. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  737. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  738. package/src/superlocalmemory/ui/vendor/sigma.min.js +0 -1
  739. package/src/superlocalmemory/vector/__init__.py +0 -9
  740. package/src/superlocalmemory/vector/lancedb_backend.py +0 -366
@@ -1,3023 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """V3 API endpoints for the SuperLocalMemory dashboard."""
6
-
7
- from __future__ import annotations
8
-
9
- import json
10
- import logging
11
- from pathlib import Path
12
- import os
13
- from fastapi import APIRouter, HTTPException, Request
14
- from fastapi.responses import JSONResponse
15
- from superlocalmemory.core.session_identity import synthetic_session_id
16
- from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
17
- from superlocalmemory.core.status_contract import (
18
- COUNT_QUERIES,
19
- counts_from_sqlite,
20
- projection_queue_depth,
21
- store_size_mb,
22
- )
23
- from superlocalmemory.server.routes.helpers import SLM_VERSION, get_read_connection
24
- from superlocalmemory.server.route_mutations import authorize_route_mutation
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
- def _signal_session_id() -> str:
29
- """A name for the caller, for matching an outcome back to this recall.
30
-
31
- The agent id the request arrived under, when it arrived under one. Falls
32
- back to the workspace, which keeps dashboard and scripted traffic separable
33
- from an agent's. Never empty: an unnamed recall leaves no record.
34
- """
35
- try:
36
- from superlocalmemory.mcp.agent_context import get_current_agent_id
37
-
38
- agent = str(get_current_agent_id() or "").strip()
39
- if agent:
40
- return synthetic_session_id("agent", agent)
41
- except Exception: # noqa: BLE001 -- naming the caller must never fail a read
42
- pass
43
- try:
44
- from superlocalmemory.server.routes.helpers import get_active_profile
45
-
46
- return synthetic_session_id("api", str(get_active_profile()))
47
- except Exception: # noqa: BLE001
48
- return synthetic_session_id("api", "default")
49
-
50
-
51
- router = APIRouter(prefix="/api/v3", tags=["v3"])
52
-
53
-
54
- def _internal_error() -> JSONResponse:
55
- """SEC-H-02: log the full traceback server-side, return a generic message.
56
-
57
- Returning ``str(e)`` to the client leaked DB schema (column/constraint
58
- names), the data-directory filesystem path, and — for config routes — could
59
- surface LLM config internals. Call this only from inside an ``except`` block
60
- so ``logger.exception`` captures the active traceback.
61
- """
62
- logger.exception("v3_api request failed")
63
- return JSONResponse({"error": "Internal server error"}, status_code=500)
64
-
65
-
66
- async def _apply_runtime_config(request: Request, config, *, mode_change: bool) -> None:
67
- """Persist and hot-swap config only after the daemon transition succeeds."""
68
- import asyncio
69
-
70
- authorization = authorize_route_mutation(
71
- request,
72
- operation="update",
73
- source_agent_id="dashboard-config",
74
- profile_id=getattr(config, "active_profile", "default"),
75
- )
76
- from superlocalmemory.server.profile_runtime import reconfigure_daemon_engine
77
-
78
- await asyncio.to_thread(
79
- reconfigure_daemon_engine,
80
- request.app.state,
81
- config,
82
- mode_change=mode_change,
83
- )
84
- authorization.complete()
85
-
86
-
87
- def _require_manage(request: Request) -> None:
88
- """Use one explicit authorization boundary for dashboard configuration."""
89
- from superlocalmemory.server.rbac_enforce import require_manage
90
-
91
- require_manage(request)
92
-
93
-
94
- def _resolve_profile(request: Request, requested: str = "") -> str:
95
- """Resolve a profile override and enforce READ on its actual target."""
96
- from superlocalmemory.access.rbac import Permission
97
- from superlocalmemory.server.rbac_enforce import require_permission
98
- from superlocalmemory.server.routes.helpers import get_active_profile
99
-
100
- if requested and not isinstance(requested, str):
101
- raise HTTPException(status_code=422, detail="profile must be a string")
102
- profile = requested.strip() if requested else get_active_profile()
103
- require_permission(request, Permission.READ, profile=profile)
104
- return profile
105
-
106
-
107
- def _resolve_mutation_profile(requested: object) -> str:
108
- """Resolve a body-supplied profile without silently coercing bad input."""
109
- from superlocalmemory.server.routes.helpers import get_active_profile
110
-
111
- if requested is not None and not isinstance(requested, str):
112
- raise HTTPException(status_code=422, detail="profile must be a string")
113
- return requested.strip() if requested and requested.strip() else get_active_profile()
114
-
115
-
116
- def _require_manage_for_profile(request: Request, profile: str) -> None:
117
- """Enforce MANAGE on the resolved mutation target before any side effect."""
118
- from superlocalmemory.server.rbac_enforce import require_manage
119
-
120
- require_manage(request, profile=profile)
121
-
122
-
123
- # ── Dashboard ────────────────────────────────────────────────
124
-
125
- @router.get("/dashboard")
126
- async def dashboard(request: Request):
127
- """Dashboard summary: mode, memory count, health score, recent activity."""
128
- try:
129
- from superlocalmemory.core.config import SLMConfig
130
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
131
- from superlocalmemory.server.profile_runtime import get_profile_runtime
132
-
133
- active_profile = get_profile_runtime(request.app.state).snapshot.profile_id
134
-
135
- # Read stats directly from SQLite (dashboard doesn't load engine)
136
- memory_count = 0
137
- counts = dict.fromkeys(COUNT_QUERIES, 0)
138
- queue_depth = 0
139
- db_path = config.base_dir / "memory.db"
140
- if db_path.exists():
141
- try:
142
- conn = get_read_connection(db_path)
143
- counts = counts_from_sqlite(conn, active_profile)
144
- queue_depth = projection_queue_depth(conn)
145
- cursor = conn.cursor()
146
- try:
147
- try:
148
- cursor.execute(
149
- "SELECT COUNT(*) FROM memories WHERE profile_id = ?",
150
- (active_profile,),
151
- )
152
- except Exception:
153
- cursor.execute(
154
- "SELECT COUNT(*) FROM memories WHERE profile = ?",
155
- (active_profile,),
156
- )
157
- memory_count = cursor.fetchone()[0]
158
- except Exception:
159
- pass
160
- conn.close()
161
- except Exception:
162
- pass
163
-
164
- from superlocalmemory.core.modes import dashboard_mode_fields
165
-
166
- # Mode record is the single source of truth for locality claims (F-03).
167
- payload = {
168
- "mode": config.mode.value,
169
- "mode_name": {"a": "Local Guardian", "b": "Smart Local", "c": "Full Power"}.get(config.mode.value, "Unknown"),
170
- "provider": config.llm.provider or "none",
171
- "model": config.llm.model or "",
172
- "memory_count": memory_count,
173
- "profile": active_profile,
174
- "base_dir": str(config.base_dir),
175
- "version": SLM_VERSION,
176
- # The counts and the store's own address were missing here while
177
- # every other status surface carried them, so the one surface a
178
- # person actually looks at could not answer "is the graph healthy".
179
- "db_path": str(db_path),
180
- "db_size_mb": store_size_mb(db_path),
181
- "profile_generation": get_profile_runtime(
182
- request.app.state,
183
- ).snapshot.generation,
184
- # Facts stored but not yet in the graph and vector projections. A
185
- # number that does not fall is a projection that has stopped
186
- # keeping up, which is otherwise invisible: nothing errors, the
187
- # memory is safely in SQLite, and recall just quietly gets worse.
188
- "projection_queue_depth": queue_depth,
189
- **counts,
190
- }
191
- payload.update(dashboard_mode_fields(config.mode))
192
- return payload
193
- except Exception as e:
194
- return _internal_error()
195
-
196
-
197
- # ── Mode ─────────────────────────────────────────────────────
198
-
199
- @router.get("/mode")
200
- async def get_mode(request: Request):
201
- """Get current mode, provider, model — single source of truth for UI."""
202
- try:
203
- from superlocalmemory.core.config import SLMConfig
204
- from urllib.parse import urlparse
205
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
206
- current = config.mode.value
207
- # SEC-L-01: expose only the endpoint HOST, not the full URL. A full
208
- # api_base (e.g. an internal Azure OpenAI deployment path) leaks
209
- # infrastructure topology to non-admin (viewer) users.
210
- _api_base = config.llm.api_base or ""
211
- _endpoint_host = urlparse(_api_base).netloc if _api_base else ""
212
- return {
213
- "mode": current,
214
- "provider": config.llm.provider or "none",
215
- "model": config.llm.model or "",
216
- "has_key": bool(config.llm.api_key),
217
- "endpoint": _endpoint_host,
218
- "capabilities": {
219
- "llm_available": bool(config.llm.provider),
220
- "cross_encoder": config.retrieval.use_cross_encoder if hasattr(config, 'retrieval') else False,
221
- },
222
- }
223
- except Exception as e:
224
- return _internal_error()
225
-
226
-
227
- @router.put("/mode")
228
- async def set_mode(request: Request):
229
- """Switch operating mode. Body: {"mode": "a"|"b"|"c"}"""
230
- # SEC-H-01: mode switch reconfigures the whole engine — admin-only (MANAGE)
231
- # on top of machine auth. Owner keeps MANAGE; personal mode unaffected.
232
- from superlocalmemory.server.rbac_enforce import require_manage
233
- require_manage(request)
234
- try:
235
- body = await request.json()
236
- new_mode = body.get("mode", "").lower()
237
- if new_mode not in ("a", "b", "c"):
238
- return JSONResponse({"error": "Invalid mode. Use a, b, or c."}, status_code=400)
239
-
240
- from superlocalmemory.core.config import SLMConfig
241
- from superlocalmemory.storage.models import Mode
242
- from superlocalmemory.server.routes.helpers import log_mode_change
243
- old_config = SLMConfig.load()
244
- old_mode = old_config.mode.value
245
-
246
- # Safety: a bare ``{mode:"c"}`` body (e.g., a stray dashboard button
247
- # click) used to silently auto-default the model to
248
- # ``anthropic/claude-sonnet-4`` with no API key, writing phantom state
249
- # into config.json. Refuse that path — Mode C requires explicit
250
- # provider+key via POST /api/v3/mode/set.
251
- if new_mode == "c" and not old_config.llm.api_key:
252
- return JSONResponse(
253
- {
254
- "error": (
255
- "Mode C requires a cloud API key. "
256
- "Configure provider + key in Settings → Step 2 "
257
- "(uses POST /api/v3/mode/set)."
258
- ),
259
- "code": "mode_c_requires_api_key",
260
- },
261
- status_code=400,
262
- )
263
-
264
- # Apply new mode's structural presets (retrieval, math, channel_weights)
265
- # by building a fresh template, then graft them onto the loaded config so
266
- # all user-tuned blocks (forgetting, injection, consolidation, scope, …)
267
- # are preserved across the mode switch.
268
- _template = SLMConfig.for_mode(Mode(new_mode))
269
- old_config.mode = Mode(new_mode)
270
- old_config.retrieval = _template.retrieval
271
- old_config.math = _template.math
272
- old_config.channel_weights = _template.channel_weights
273
- new_config = old_config
274
-
275
- await _apply_runtime_config(request, new_config, mode_change=True)
276
-
277
- # Audit the change before we lose context — proves who/when/what.
278
- # Captures the phantom-write case where `for_mode(C)` auto-defaults
279
- # the model to "anthropic/claude-sonnet-4" (see core/config.py).
280
- log_mode_change(
281
- old_mode, new_mode,
282
- provider=new_config.llm.provider,
283
- model=new_config.llm.model,
284
- source="PUT /api/v3/mode",
285
- )
286
-
287
- # V3.3: Check if embedding model changed — flag for re-indexing
288
- needs_reindex = (
289
- old_config.embedding.provider != new_config.embedding.provider
290
- or old_config.embedding.model_name != new_config.embedding.model_name
291
- )
292
-
293
- return {
294
- "success": True,
295
- "mode": new_mode,
296
- "needs_reindex": needs_reindex,
297
- "message": "Embedding re-indexing will run on next recall." if needs_reindex else "",
298
- }
299
- except Exception as e:
300
- return _internal_error()
301
-
302
-
303
- def apply_settings_update(config: "SLMConfig", payload: dict) -> "SLMConfig":
304
- """Credential-safe LLM config update for the settings dashboard (fixes #119).
305
-
306
- This is the SINGLE authoritative place where incoming dashboard save payloads
307
- are merged onto the stored config. The POST /api/v3/mode/set handler calls
308
- this function after its HTTP-layer concerns (SSRF guard, auth) are settled,
309
- so the test suite (test_acceptance_core.py::P3) tests real product
310
- behaviour — not a reimplementation.
311
-
312
- SEC-L-01 PRESERVED:
313
- The GET /mode route returns only ``urlparse(api_base).netloc`` (the host),
314
- never the full URL. When the UI echoes that value back on save,
315
- apply_settings_update detects the scheme-less netloc and silently restores
316
- the stored full URL (scheme + path). This means the server-side logic
317
- internally reads the stored URL but NEVER returns it to callers — viewer
318
- users remain unable to discover the full endpoint topology.
319
-
320
- Credential semantics:
321
- api_key blank/omitted → UNCHANGED (browsers never repopulate pw fields)
322
- api_key non-blank → applied
323
- clear_api_key = True → explicit "" (deliberate wipe — only way to clear)
324
-
325
- endpoint absent/empty → UNCHANGED
326
- endpoint == stored netloc (no scheme "://") → UNCHANGED (redacted echo)
327
- endpoint with "://" → applied
328
- clear_base_url = True → explicit "" (deliberate wipe)
329
-
330
- Security rule: if the destination provider or endpoint genuinely changes AND
331
- no new key was supplied, the stored key is cleared to prevent accidental
332
- credential reuse across providers. An explicit clear_api_key=True is
333
- always honoured regardless.
334
-
335
- NEVER logs api_key.
336
- """
337
- import dataclasses as _dc
338
- from urllib.parse import urlparse, urlsplit, urlunsplit
339
-
340
- stored = config.llm
341
- stored_key: str = stored.api_key
342
- stored_base: str = stored.api_base or ""
343
-
344
- # ── endpoint ─────────────────────────────────────────────────────────────
345
- raw_endpoint: str = (payload.get("endpoint") or payload.get("base_url") or "").strip()
346
- clear_endpoint: bool = payload.get("clear_base_url") is True
347
-
348
- if clear_endpoint:
349
- new_base = ""
350
- endpoint_changed = bool(stored_base)
351
- elif not raw_endpoint:
352
- # Nothing supplied — keep stored URL intact.
353
- new_base = stored_base
354
- endpoint_changed = False
355
- else:
356
- # Detect the SEC-L-01 redacted echo: GET /mode returns
357
- # urlparse(api_base).netloc (host only, no scheme, no path). If the
358
- # client sent that exact string back, it did NOT change the endpoint —
359
- # restore the full stored URL. We compare the raw string directly to
360
- # stored_netloc so that hosts with ports ("api.host.com:443") also match.
361
- stored_netloc: str = urlparse(stored_base).netloc if stored_base else ""
362
- is_redacted_echo: bool = bool(stored_netloc) and (raw_endpoint == stored_netloc)
363
-
364
- if is_redacted_echo:
365
- new_base = stored_base
366
- endpoint_changed = False
367
- else:
368
- new_base = raw_endpoint
369
- # Canonical comparison (strip trailing slash, normalise scheme/host
370
- # case) so a harmless trailing-slash difference does not clear the key.
371
- def _canonical(u: str) -> str:
372
- p = urlsplit(u)
373
- return urlunsplit((
374
- p.scheme.lower(), p.netloc.lower(),
375
- p.path.rstrip("/"), p.query, "",
376
- ))
377
- endpoint_changed = _canonical(new_base) != _canonical(stored_base)
378
-
379
- # ── provider ─────────────────────────────────────────────────────────────
380
- raw_provider: str = (payload.get("provider") or "").strip()
381
- if raw_provider == "none":
382
- new_provider = "" # "none" sentinel means "clear provider"
383
- elif raw_provider:
384
- new_provider = raw_provider
385
- else:
386
- new_provider = stored.provider or ""
387
-
388
- provider_changed: bool = new_provider != (stored.provider or "")
389
-
390
- # ── api_key ───────────────────────────────────────────────────────────────
391
- # Evaluated AFTER endpoint/provider so we know whether the destination changed.
392
- raw_key: str = (payload.get("api_key") or "").strip()
393
- clear_key: bool = payload.get("clear_api_key") is True
394
- destination_changed: bool = provider_changed or endpoint_changed
395
-
396
- if clear_key:
397
- new_key = "" # explicit user wipe
398
- elif raw_key:
399
- new_key = raw_key # user supplied a replacement key
400
- elif destination_changed:
401
- # Security: destination changed but no new key → clear to prevent
402
- # the stored credential from being silently redirected to a new
403
- # provider or endpoint the user may not own.
404
- new_key = ""
405
- else:
406
- # Blank key + same destination = browser did not repopulate the
407
- # password field. Preserve the stored key verbatim.
408
- new_key = stored_key
409
-
410
- # ── model ────────────────────────────────────────────────────────────────
411
- raw_model: str = (payload.get("model") or "").strip()
412
- new_model: str = raw_model if raw_model else (stored.model or "")
413
-
414
- # ── apply ─────────────────────────────────────────────────────────────────
415
- # dataclasses.replace preserves temperature / max_tokens / timeout_seconds.
416
- # Assigning to config.llm works because SLMConfig is not frozen.
417
- # NEVER include new_key in any log call.
418
- config.llm = _dc.replace(
419
- stored,
420
- provider=new_provider,
421
- model=new_model,
422
- api_key=new_key,
423
- api_base=new_base,
424
- )
425
- return config
426
-
427
-
428
- @router.post("/mode/set")
429
- async def set_full_config(request: Request):
430
- """Save mode + provider + model + API key together.
431
-
432
- V3.4.24: Also accepts embedding_* fields for custom embedding endpoints.
433
- """
434
- # SEC-H-01: sets the LLM provider + API key for the whole engine — admin-only.
435
- from superlocalmemory.server.rbac_enforce import require_manage
436
- require_manage(request)
437
- try:
438
- body = await request.json()
439
- if not isinstance(body, dict):
440
- return JSONResponse({"error": "Request body must be a JSON object"}, status_code=400)
441
- from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
442
- from superlocalmemory.storage.models import Mode
443
- from superlocalmemory.server.routes.helpers import log_mode_change
444
-
445
- config = SLMConfig.load()
446
- old_mode = config.mode.value
447
-
448
- def _nonblank(name: str) -> str | None:
449
- value = body.get(name)
450
- if value is None:
451
- return None
452
- if not isinstance(value, str):
453
- raise ValueError(f"{name} must be a string")
454
- return value.strip() or None
455
-
456
- new_mode = (_nonblank("mode") or config.mode.value).lower()
457
- provider_input = _nonblank("provider")
458
- model_input = _nonblank("model")
459
- api_key_input = _nonblank("api_key")
460
- base_url_input = _nonblank("base_url")
461
- endpoint_input = _nonblank("endpoint")
462
- clear_api_key = body.get("clear_api_key") is True
463
- clear_base_url = body.get("clear_base_url") is True
464
-
465
- if clear_api_key and api_key_input:
466
- return JSONResponse(
467
- {"error": "api_key cannot be replaced and cleared together"},
468
- status_code=400,
469
- )
470
- if clear_base_url and (base_url_input or endpoint_input):
471
- return JSONResponse(
472
- {"error": "base_url cannot be replaced and cleared together"},
473
- status_code=400,
474
- )
475
- if base_url_input and endpoint_input and base_url_input != endpoint_input:
476
- return JSONResponse(
477
- {"error": "base_url and endpoint must match when both are supplied"},
478
- status_code=400,
479
- )
480
-
481
- if new_mode not in ("a", "b", "c"):
482
- return JSONResponse({"error": "Invalid mode"}, status_code=400)
483
-
484
- # Resolve the effective endpoint value before SSRF validation.
485
- # Only the Ollama default injection happens here; the fallback to the
486
- # stored URL (and redacted-echo detection) live inside
487
- # apply_settings_update so that the P3 test exercises
488
- # the same code path as the HTTP handler — not a reimplementation.
489
- _raw_ep: str = "" if clear_base_url else (base_url_input or endpoint_input or "")
490
- if (
491
- not _raw_ep
492
- and provider_input == "ollama"
493
- and config.llm.provider != "ollama"
494
- and not clear_base_url
495
- ):
496
- _raw_ep = "http://localhost:11434"
497
-
498
- # SSRF guard — fires only for genuinely new egress destinations.
499
- # Redacted echoes (the netloc-only string GET /mode returns per
500
- # SEC-L-01) are NOT outbound targets; they will be silently replaced
501
- # with the stored full URL by apply_settings_update.
502
- if _raw_ep and (base_url_input is not None or endpoint_input is not None):
503
- from urllib.parse import urlparse as _up
504
- _stored_nl: str = _up(config.llm.api_base or "").netloc
505
- _is_redacted_echo: bool = bool(_stored_nl) and (_raw_ep == _stored_nl)
506
- if not _is_redacted_echo:
507
- client = getattr(request, "client", None)
508
- endpoint_error = _validate_provider_url(
509
- _raw_ep, getattr(client, "host", "") if client else ""
510
- )
511
- if endpoint_error:
512
- return JSONResponse({"error": endpoint_error}, status_code=400)
513
-
514
- # Build the normalised body for apply_settings_update. Inject the
515
- # resolved endpoint (Ollama default when applicable) so that the
516
- # credential-safe logic sees the approved value. Consolidate
517
- # base_url → endpoint to keep apply_settings_update's lookup simple.
518
- _apply_body: dict = dict(body)
519
- if _raw_ep or clear_base_url:
520
- _apply_body["endpoint"] = _raw_ep
521
- _apply_body.pop("base_url", None)
522
-
523
- # Credential-safe LLM update. All field presence, preservation, and
524
- # security-clearing rules live here. Other config blocks (forgetting,
525
- # injection, retrieval, math, consolidation, scope, …) are preserved
526
- # because we loaded the full existing config at the top of this handler.
527
- config = apply_settings_update(config, _apply_body)
528
-
529
- # Mode switch — happens after LLM update so the correct provider/key
530
- # is already in place when the runtime engine is reconfigured.
531
- config.mode = Mode(new_mode)
532
-
533
- # Update embedding only when the dashboard explicitly sent those fields;
534
- # absence means "leave it alone" (AIDEV-86 / broader fix).
535
- _emb_fields = ("embedding_provider", "embedding_endpoint", "embedding_key",
536
- "embedding_model", "embedding_dimension")
537
- if any(k in body for k in _emb_fields):
538
- _old_emb = config.embedding
539
- _new_provider = body.get("embedding_provider", "")
540
- _new_model = body.get("embedding_model", "")
541
- _new_dim = int(body.get("embedding_dimension", 0) or 0)
542
- # The same range the other save route enforces. Without it a
543
- # dashboard save with no dimension field stored a width of zero.
544
- if _new_dim and not (64 <= _new_dim <= 8192):
545
- return JSONResponse(
546
- {"error": f"Dimension must be 64-8192, got {_new_dim}"},
547
- status_code=400,
548
- )
549
- # The SECOND way to change the embedding model, and it was
550
- # unguarded. Switching mode from the dashboard carries the embedding
551
- # fields, so a width that the store cannot hold arrived here
552
- # untouched while the other route refused it — one door bolted, the
553
- # other open.
554
- if not bool(body.get("force")):
555
- _refusal = _refuse_incompatible_embedding(
556
- config, config.embedding, _new_model, _new_dim,
557
- new_provider=_new_provider,
558
- )
559
- if _refusal is not None:
560
- return _refusal
561
- config.embedding = EmbeddingConfig(
562
- provider=_new_provider,
563
- api_endpoint=body.get("embedding_endpoint", ""),
564
- api_key=body.get("embedding_key", ""),
565
- model_name=_new_model,
566
- dimension=_new_dim or _old_emb.dimension,
567
- # Not naming these reset them to defaults, so every embedding
568
- # save from the dashboard silently put the local model back to
569
- # whatever ships — the exact defect the other route had.
570
- ollama_model=(
571
- _new_model if _new_provider == "ollama" and _new_model
572
- else _old_emb.ollama_model
573
- ),
574
- ollama_base_url=_old_emb.ollama_base_url,
575
- api_version=_old_emb.api_version,
576
- deployment_name=_old_emb.deployment_name,
577
- )
578
-
579
- # When the mode actually changed, apply the new mode's structural presets
580
- # (retrieval topology, math thresholds, channel weights) so the user gets
581
- # the right runtime behaviour for their chosen mode.
582
- if new_mode != old_mode:
583
- _template = SLMConfig.for_mode(Mode(new_mode))
584
- config.retrieval = _template.retrieval
585
- config.math = _template.math
586
- config.channel_weights = _template.channel_weights
587
-
588
- # v3.6.12 (settings-1): mode_change=True is required to persist the new
589
- # mode — save() without it hits a guard that preserves the old mode.
590
- await _apply_runtime_config(request, config, mode_change=True)
591
-
592
- log_mode_change(
593
- old_mode, new_mode,
594
- provider=config.llm.provider,
595
- model=config.llm.model,
596
- source="POST /api/v3/mode/set",
597
- )
598
-
599
- # Recycle only out-of-process fallbacks; the resident daemon engine was
600
- # already acknowledged and hot-swapped by _apply_runtime_config().
601
- try:
602
- from superlocalmemory.core.worker_pool import WorkerPool
603
- WorkerPool.shared().shutdown()
604
- except Exception:
605
- pass
606
-
607
- return {
608
- "success": True,
609
- "mode": new_mode,
610
- "provider": config.llm.provider or "none",
611
- "model": config.llm.model,
612
- "embedding_provider": config.embedding.provider,
613
- "embedding_model": config.embedding.model_name,
614
- "embedding_dimension": config.embedding.dimension,
615
- }
616
- except ValueError as exc:
617
- return JSONResponse({"error": str(exc)}, status_code=400)
618
- except Exception as e:
619
- return _internal_error()
620
-
621
-
622
- # ── V3.4.24: Embedding Configuration ────────────────────────────────
623
-
624
- @router.get("/embedding/config")
625
- async def get_embedding_config(request: Request):
626
- """Return current embedding configuration."""
627
- try:
628
- from superlocalmemory.core.config import SLMConfig
629
- # The daemon may already be running a freshly hot-swapped config while
630
- # a profile update is still being persisted. The dashboard must show
631
- # that live truth, never silently replace it with disk defaults.
632
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
633
- emb = config.embedding
634
- return {
635
- "provider": emb.provider,
636
- "model_name": emb.model_name,
637
- "dimension": emb.dimension,
638
- "api_endpoint": emb.api_endpoint,
639
- "has_key": bool(emb.api_key),
640
- "is_openai_compatible": emb.is_openai_compatible,
641
- "mode": config.mode.value,
642
- }
643
- except Exception as e:
644
- return _internal_error()
645
-
646
-
647
- def _refuse_incompatible_embedding(
648
- config, old_emb, new_model, new_dim, new_provider=None,
649
- ):
650
- """None when the change is safe, otherwise the 409 to return instead.
651
-
652
- Fail-open on anything it cannot determine: a store with no vectors yet, a
653
- model server that is not running, an unreadable database. Refusing on
654
- "I could not tell" would block a legitimate first-time setup, and the
655
- dimension a caller declares is still checked against the store either way.
656
- """
657
- from fastapi.responses import JSONResponse as _JSON
658
-
659
- try:
660
- from superlocalmemory.core.ollama_validator import (
661
- EMBEDDING,
662
- stored_embedding_dimension,
663
- validate_ollama_model,
664
- )
665
-
666
- db_path = Path(config.base_dir) / "memory.db"
667
- stored = stored_embedding_dimension(db_path)
668
- if stored is None:
669
- return None
670
-
671
- # The provider being SAVED, falling back to the current one when the
672
- # caller is not changing it. Reading only the current provider meant
673
- # that SWITCHING to a local model never probed at all — and switching
674
- # is exactly when the width changes.
675
- effective_provider = (
676
- new_provider
677
- if new_provider is not None
678
- else getattr(old_emb, "provider", "")
679
- )
680
- measured = None
681
- if effective_provider == "ollama" or getattr(old_emb, "provider", "") == "ollama":
682
- # The model being SAVED, not the one already configured. Probing the
683
- # old one always matched the stored width and therefore always
684
- # allowed the change — the guard measured the thing it was not
685
- # protecting against.
686
- probe = validate_ollama_model(
687
- new_model or getattr(old_emb, "ollama_model", ""),
688
- EMBEDDING,
689
- base_url=getattr(old_emb, "ollama_base_url", "")
690
- or "http://localhost:11434",
691
- )
692
- measured = probe.dimension if probe.ok else None
693
-
694
- declared = int(new_dim or 0)
695
- if measured is None and declared <= 0:
696
- # Nothing to compare: the server could not be asked and the caller
697
- # named no width. Allowing is the fail-open the first-time setup
698
- # needs; the other route's probe still guards the common path.
699
- return None
700
- effective = measured if measured is not None else declared
701
- if effective == stored:
702
- return None
703
-
704
- return _JSON(
705
- {
706
- "error": "embedding_width_mismatch",
707
- "stored_dimension": stored,
708
- "requested_dimension": effective,
709
- "model_name": new_model,
710
- "detail": (
711
- f"{new_model} produces {effective}-dimensional vectors and "
712
- f"this store holds {stored}-dimensional ones. Vectors of "
713
- f"different widths cannot be compared, so every memory "
714
- f"already stored would become unfindable by meaning. "
715
- f"Rebuild them first with: slm db migrate — or resend with "
716
- f"force=true if they have already been rebuilt."
717
- ),
718
- },
719
- status_code=409,
720
- )
721
- except Exception: # noqa: BLE001 - never block a save on the check failing
722
- logger.exception("embedding width pre-check failed; allowing the save")
723
- return None
724
-
725
-
726
- @router.put("/embedding/config")
727
- async def set_embedding_config(request: Request):
728
- """Update embedding configuration independently of mode switch."""
729
- # SEC-H-01: swapping the embedding provider/key affects all recall — admin-only.
730
- from superlocalmemory.server.rbac_enforce import require_manage
731
- require_manage(request)
732
- try:
733
- body = await request.json()
734
- from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
735
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
736
-
737
- new_provider = body.get("provider", config.embedding.provider)
738
- new_model = body.get("model_name", config.embedding.model_name)
739
- new_dim = int(body.get("dimension", config.embedding.dimension) or 768)
740
- if not (64 <= new_dim <= 8192):
741
- return JSONResponse({"error": f"Dimension must be 64-8192, got {new_dim}"}, status_code=400)
742
- new_endpoint = body.get("api_endpoint", config.embedding.api_endpoint)
743
- new_key = body.get("api_key", config.embedding.api_key)
744
-
745
- old_emb = config.embedding
746
-
747
- # A width that disagrees with what the store already holds is refused
748
- # here, at the moment of writing, not merely offered as a check the
749
- # caller may or may not have run. Vectors of different widths cannot be
750
- # compared, so the store would keep answering similarity questions and
751
- # every answer would be noise. ``force=true`` is the escape hatch for
752
- # somebody who has already re-embedded.
753
- if not bool(body.get("force")):
754
- refusal = _refuse_incompatible_embedding(
755
- config, old_emb, new_model, new_dim, new_provider=new_provider,
756
- )
757
- if refusal is not None:
758
- return refusal
759
-
760
- config.embedding = EmbeddingConfig(
761
- model_name=new_model,
762
- dimension=new_dim,
763
- provider=new_provider,
764
- api_endpoint=new_endpoint,
765
- api_key=new_key,
766
- # In Ollama mode the embedder resolves its model from
767
- # ``ollama_model``, so keeping the old value here made a rename a
768
- # no-op that still answered "success". A caller naming a model gets
769
- # that model.
770
- ollama_model=(
771
- new_model if new_provider == "ollama" and new_model
772
- else old_emb.ollama_model
773
- ),
774
- ollama_base_url=old_emb.ollama_base_url,
775
- api_version=old_emb.api_version,
776
- deployment_name=old_emb.deployment_name,
777
- )
778
- await _apply_runtime_config(request, config, mode_change=False)
779
-
780
- needs_reindex = (
781
- old_emb.provider != new_provider
782
- or old_emb.model_name != new_model
783
- or old_emb.dimension != new_dim
784
- )
785
-
786
- # Kill workers so next request uses new config
787
- try:
788
- from superlocalmemory.core.worker_pool import WorkerPool
789
- WorkerPool.shared().shutdown()
790
- except Exception:
791
- pass
792
- return {
793
- "success": True,
794
- "provider": new_provider,
795
- "model_name": new_model,
796
- "dimension": new_dim,
797
- "needs_reindex": needs_reindex,
798
- }
799
- except Exception as e:
800
- return _internal_error()
801
-
802
-
803
- @router.get("/scope/config")
804
- async def get_scope_config(request: Request):
805
- """Return runtime multi-scope defaults used by daemon writes and recalls."""
806
- try:
807
- from superlocalmemory.core.config import SLMConfig
808
-
809
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
810
- return {"success": True, **config.scope.as_dict()}
811
- except Exception as exc:
812
- return _internal_error()
813
-
814
-
815
- @router.put("/scope/config")
816
- async def set_scope_config(request: Request):
817
- """Validate, persist, and hot-apply explicit multi-scope defaults."""
818
- # SEC-H-01: scope defaults govern cross-profile recall visibility — admin-only.
819
- from superlocalmemory.server.rbac_enforce import require_manage
820
- require_manage(request)
821
- try:
822
- body = await request.json()
823
- from superlocalmemory.core.config import SLMConfig, ScopeConfig
824
-
825
- config = SLMConfig.load()
826
- current = config.scope
827
- default_scope = body.get("default_scope", current.default_scope)
828
- if default_scope not in {"personal", "shared", "global"}:
829
- return JSONResponse(
830
- {"error": "default_scope must be personal, shared, or global"},
831
- status_code=400,
832
- )
833
- include_global = body.get(
834
- "recall_include_global", current.recall_include_global,
835
- )
836
- include_shared = body.get(
837
- "recall_include_shared", current.recall_include_shared,
838
- )
839
- if not isinstance(include_global, bool) or not isinstance(include_shared, bool):
840
- return JSONResponse(
841
- {"error": "recall scope flags must be booleans"},
842
- status_code=400,
843
- )
844
- config.scope = ScopeConfig(
845
- default_scope=default_scope,
846
- recall_include_global=include_global,
847
- recall_include_shared=include_shared,
848
- )
849
- await _apply_runtime_config(request, config, mode_change=False)
850
- return {"success": True, **config.scope.as_dict()}
851
- except Exception as exc:
852
- return _internal_error()
853
-
854
-
855
- @router.post("/embedding/test")
856
- async def test_embedding_endpoint(request: Request):
857
- """Test connectivity to a custom embedding endpoint."""
858
- _require_manage(request)
859
- try:
860
- import httpx
861
- body = await request.json()
862
- endpoint = body.get("api_endpoint", "").rstrip("/")
863
- model = body.get("model_name", "test")
864
- api_key = body.get("api_key", "")
865
-
866
- if not endpoint:
867
- return JSONResponse({"error": "No endpoint provided"}, status_code=400)
868
-
869
- client_host = request.client.host if request.client else ""
870
- error = _validate_provider_url(endpoint, client_host)
871
- if error:
872
- return JSONResponse({"error": error}, status_code=400)
873
-
874
- if not endpoint.endswith("/embeddings"):
875
- endpoint = f"{endpoint}/embeddings"
876
-
877
- headers = {"Content-Type": "application/json"}
878
- if api_key:
879
- headers["Authorization"] = f"Bearer {api_key}"
880
-
881
- payload = {"input": ["test embedding connection"], "model": model}
882
-
883
- with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
884
- resp = client.post(endpoint, headers=headers, json=payload)
885
- resp.raise_for_status()
886
- data = resp.json()
887
- emb_data = data.get("data", [])
888
- if emb_data:
889
- dim = len(emb_data[0].get("embedding", []))
890
- return {
891
- "success": True,
892
- "message": f"Connected! Dimension: {dim}",
893
- "dimension": dim,
894
- }
895
- return {"success": False, "error": "No embedding data returned"}
896
- except httpx.HTTPStatusError as e:
897
- return {"success": False, "error": f"HTTP {e.response.status_code}"}
898
- except httpx.ConnectError:
899
- return {"success": False, "error": "Cannot reach the embedding server. Is it running?"}
900
- except httpx.TimeoutException:
901
- return {"success": False, "error": "Connection timed out after 15 seconds."}
902
- except Exception as e:
903
- return {"success": False, "error": type(e).__name__}
904
-
905
-
906
- @router.get("/embed/ping")
907
- async def embed_ping():
908
- """V3.5.9: Liveness probe for McpEmbedderProxy. Returns 200 when daemon
909
- embedder is ready so the proxy knows the daemon is reachable."""
910
- try:
911
- from .helpers import get_engine_lazy
912
- # We just need to confirm the route is alive — engine check is optional
913
- return {"ok": True}
914
- except Exception:
915
- logger.exception("embedder liveness probe failed")
916
- return JSONResponse({"ok": False, "error": "Internal server error"}, status_code=503)
917
-
918
-
919
- @router.post("/embed")
920
- async def embed_texts(request: Request):
921
- """V3.5.9: Embed texts via daemon's FULL engine for McpEmbedderProxy.
922
-
923
- MCP processes run LIGHT engines (no ONNX worker). This endpoint lets them
924
- delegate embed_batch() to the daemon's single real embedder — one ONNX
925
- worker total across all sessions (fixes PR #30 NULL embedder bug).
926
- """
927
- import asyncio
928
- try:
929
- body = await request.json()
930
- texts = body.get("texts", [])
931
- if not texts:
932
- return {"embeddings": []}
933
-
934
- from .helpers import get_engine_lazy
935
- engine = get_engine_lazy(request.app.state)
936
- if engine is None or engine._embedder is None:
937
- return JSONResponse(
938
- {"error": "Embedder not available in daemon"},
939
- status_code=503,
940
- )
941
-
942
- loop = asyncio.get_event_loop()
943
- embeddings = await loop.run_in_executor(
944
- None,
945
- lambda: engine._embedder.embed_batch(texts),
946
- )
947
- return {"embeddings": embeddings}
948
- except Exception as e:
949
- return _internal_error()
950
-
951
-
952
- def _validate_provider_url(url: str, client_host: str) -> str | None:
953
- """SSRF guard for outbound provider-test fetches (v3.6.12 ssrf-1).
954
-
955
- Returns an error string if ``url`` is unsafe, else None. ALWAYS blocks
956
- non-http(s) and cloud-metadata hosts. A LOOPBACK caller (the local
957
- dashboard) may legitimately test local/LAN LLM endpoints, so private IPs
958
- are allowed for it. A NON-loopback caller may not make the server fetch
959
- private/loopback/link-local/reserved targets — that is the SSRF abuse.
960
- """
961
- from superlocalmemory.server.egress_policy import (
962
- EgressActor,
963
- EgressVerdict,
964
- validate_egress_url,
965
- )
966
- from superlocalmemory.server.loopback import is_loopback as _is_loopback_host
967
-
968
- is_local = _is_loopback_host(client_host)
969
- # SLM_REMOTE residue (#40): an allowlisted LAN dashboard is trusted exactly
970
- # like the loopback one and may probe its own LAN LLM endpoint. This does
971
- # NOT relax the SSRF guard for arbitrary remote callers —
972
- # is_lan_client_allowed is False unless remote mode is ON *and* the client
973
- # IP is in SLM_MCP_ALLOWED_HOSTS.
974
- is_lan = False
975
- try:
976
- from superlocalmemory.core.remote_mode import is_lan_client_allowed
977
- is_lan = bool(is_lan_client_allowed(client_host))
978
- except Exception: # pragma: no cover — defensive, never weaken on import error
979
- is_lan = False
980
-
981
- result = validate_egress_url(
982
- url, EgressActor(is_local=is_local, is_lan=is_lan)
983
- )
984
-
985
- if result.verdict is EgressVerdict.ALLOW:
986
- return None
987
- if result.verdict is EgressVerdict.DENY_SCHEME:
988
- return "Only http/https endpoints are supported"
989
- if result.verdict is EgressVerdict.DENY_METADATA:
990
- return "Cloud metadata endpoints are not allowed"
991
- if result.verdict in (
992
- EgressVerdict.DENY_CREDENTIALS,
993
- EgressVerdict.DENY_FRAGMENT,
994
- ):
995
- return "Endpoint URL must not embed credentials or fragments"
996
- if result.verdict is EgressVerdict.DENY_DNS_FAILURE:
997
- return "Endpoint host could not be resolved"
998
- # DENY_PRIVATE / DENY_MIXED_DNS / DENY_HOST
999
- return "Internal/private endpoints are not allowed from a remote client"
1000
-
1001
-
1002
- @router.post("/provider/test")
1003
- async def test_provider(request: Request):
1004
- """Test connectivity to an LLM provider."""
1005
- try:
1006
- import httpx
1007
- body = await request.json()
1008
- provider = body.get("provider", "")
1009
- model = body.get("model", "")
1010
- api_key = body.get("api_key", "")
1011
- _client_host = request.client.host if request.client else ""
1012
-
1013
- if provider == "ollama":
1014
- endpoint = body.get("endpoint", "http://localhost:11434")
1015
- _err = _validate_provider_url(endpoint, _client_host)
1016
- if _err:
1017
- return {"success": False, "error": _err}
1018
- with httpx.Client(timeout=httpx.Timeout(5.0)) as c:
1019
- resp = c.get(f"{endpoint}/api/tags")
1020
- resp.raise_for_status()
1021
- models = [m["name"] for m in resp.json().get("models", [])]
1022
- # Match either exact name or base name (strip ":tag" suffix on both sides)
1023
- model_base = model.split(":")[0] if model else ""
1024
- found = any(m == model or m.split(":")[0] == model_base for m in models) if model else len(models) > 0
1025
- return {
1026
- "success": found,
1027
- "message": f"Ollama OK, {len(models)} models" + (f", '{model}' available" if found and model else ""),
1028
- }
1029
-
1030
- if provider == "openrouter":
1031
- if not api_key:
1032
- api_key = os.environ.get("OPENROUTER_API_KEY", "")
1033
- if not api_key:
1034
- return {"success": False, "error": "API key required"}
1035
- with httpx.Client(timeout=httpx.Timeout(10.0)) as c:
1036
- resp = c.get("https://openrouter.ai/api/v1/models", headers={"Authorization": f"Bearer {api_key}"})
1037
- resp.raise_for_status()
1038
- return {"success": True, "message": "OpenRouter connected, key valid"}
1039
-
1040
- if provider == "openai":
1041
- # V3.5.9: custom/local endpoint — api_key is optional (llama.cpp, LM Studio etc.)
1042
- custom_endpoint = body.get("base_url", "").strip() or body.get("endpoint", "").strip()
1043
- if custom_endpoint:
1044
- _err = _validate_provider_url(custom_endpoint, _client_host)
1045
- if _err:
1046
- return {"success": False, "error": _err}
1047
- headers_test = {"Content-Type": "application/json"}
1048
- if api_key:
1049
- headers_test["Authorization"] = f"Bearer {api_key}"
1050
- base = custom_endpoint.rstrip("/")
1051
- if not base.endswith("chat/completions"):
1052
- base = f"{base}/chat/completions"
1053
- with httpx.Client(timeout=httpx.Timeout(10.0)) as c:
1054
- # Probe with a minimal chat request (models list not universal on local servers)
1055
- probe = {"model": body.get("model", "test"), "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}
1056
- resp = c.post(base, headers=headers_test, json=probe)
1057
- if resp.status_code in (200, 400, 422):
1058
- return {"success": True, "message": f"Custom endpoint reachable (HTTP {resp.status_code})"}
1059
- resp.raise_for_status()
1060
- return {"success": True, "message": "Custom OpenAI-compatible endpoint connected"}
1061
- if not api_key:
1062
- return {"success": False, "error": "API key required for official OpenAI endpoint"}
1063
- with httpx.Client(timeout=httpx.Timeout(10.0)) as c:
1064
- resp = c.get("https://api.openai.com/v1/models", headers={"Authorization": f"Bearer {api_key}"})
1065
- resp.raise_for_status()
1066
- return {"success": True, "message": "OpenAI connected, key valid"}
1067
-
1068
- if provider == "anthropic":
1069
- if not api_key:
1070
- return {"success": False, "error": "API key required"}
1071
- # Anthropic doesn't have a models list endpoint, just verify key format
1072
- if api_key.startswith("sk-ant-"):
1073
- return {"success": True, "message": "Anthropic key format valid"}
1074
- return {"success": False, "error": "Key should start with sk-ant-"}
1075
-
1076
- return {"success": False, "error": f"Unknown provider: {provider}"}
1077
- except httpx.ConnectError:
1078
- return {"success": False, "error": "Cannot connect — is the service running?"}
1079
- except httpx.HTTPStatusError as e:
1080
- return {"success": False, "error": f"HTTP {e.response.status_code}: Invalid key or endpoint"}
1081
- except Exception as e:
1082
- logger.exception("verify_key failed")
1083
- return {"success": False, "error": "internal error"}
1084
-
1085
-
1086
- @router.get("/ollama/status")
1087
- async def ollama_status():
1088
- """Check if Ollama is running and list available models."""
1089
- try:
1090
- import httpx
1091
- with httpx.Client(timeout=httpx.Timeout(5.0)) as client:
1092
- resp = client.get("http://localhost:11434/api/tags")
1093
- resp.raise_for_status()
1094
- data = resp.json()
1095
- models = [
1096
- {"name": m["name"], "size": m.get("size", 0)}
1097
- for m in data.get("models", [])
1098
- ]
1099
- return {"running": True, "models": models, "count": len(models)}
1100
- except Exception:
1101
- return {"running": False, "models": [], "count": 0}
1102
-
1103
-
1104
- # ── Provider ─────────────────────────────────────────────────
1105
-
1106
- @router.get("/providers")
1107
- async def list_providers():
1108
- """List available LLM providers with presets."""
1109
- try:
1110
- from superlocalmemory.core.config import SLMConfig
1111
- return {"providers": SLMConfig.provider_presets()}
1112
- except Exception as exc:
1113
- logger.exception("list_providers failed")
1114
- return {"error": "could not load provider list", "providers": []}
1115
-
1116
-
1117
- @router.get("/provider")
1118
- async def get_provider():
1119
- """Get current provider configuration (API key masked)."""
1120
- try:
1121
- from superlocalmemory.core.config import SLMConfig
1122
- config = SLMConfig.load()
1123
- key = config.llm.api_key
1124
- masked = f"****{key[-4:]}" if len(key) > 8 else "****" if key else ""
1125
- return {
1126
- "provider": config.llm.provider or "none",
1127
- "model": config.llm.model,
1128
- "base_url": config.llm.api_base,
1129
- "api_key_masked": masked,
1130
- "has_key": bool(key),
1131
- }
1132
- except Exception as exc:
1133
- logger.exception("get_provider failed")
1134
- return {"error": "could not load configuration", "provider": "unknown"}
1135
-
1136
-
1137
- @router.put("/provider")
1138
- async def set_provider(request: Request):
1139
- """Set LLM provider. Body: {"provider": "openai", "api_key": "...", "model": "..."}"""
1140
- # Swapping the provider + API key redirects all LLM traffic (prompt/output
1141
- # interception risk) — admin-only, on top of mutation auth.
1142
- from superlocalmemory.server.route_mutations import authorize_route_mutation
1143
- from superlocalmemory.server.rbac_enforce import require_manage
1144
- _auth = authorize_route_mutation(request, operation="update",
1145
- source_agent_id="http-set-provider")
1146
- require_manage(request)
1147
- try:
1148
- body = await request.json()
1149
- provider = body.get("provider", "")
1150
- api_key = body.get("api_key", "")
1151
- model = body.get("model", "")
1152
- base_url = body.get("base_url", "")
1153
-
1154
- from superlocalmemory.core.config import SLMConfig, LLMConfig
1155
- config = SLMConfig.load()
1156
-
1157
- # Use preset base_url if not provided
1158
- if not base_url:
1159
- presets = SLMConfig.provider_presets()
1160
- preset = presets.get(provider, {})
1161
- base_url = preset.get("base_url", "")
1162
- if not model:
1163
- model = preset.get("model", "")
1164
-
1165
- # Mutate only the LLM block — all other config is preserved.
1166
- config.llm = LLMConfig(
1167
- provider=provider,
1168
- model=model,
1169
- api_key=api_key,
1170
- api_base=base_url,
1171
- )
1172
- config.save()
1173
-
1174
- return {"success": True, "provider": provider, "model": model}
1175
- except Exception as e:
1176
- return _internal_error()
1177
-
1178
-
1179
- # ── Recall Trace ─────────────────────────────────────────────
1180
-
1181
- @router.post("/recall/trace")
1182
- async def recall_trace(request: Request):
1183
- """Recall with per-channel score breakdown.
1184
-
1185
- v3.4.64: Replaced WorkerPool.shared() (subprocess, blocks event loop,
1186
- worker crashes after ~15s) with daemon engine via run_in_executor.
1187
- Same fix as POST /api/search in v3.4.63.
1188
- """
1189
- import asyncio
1190
- import time as _time
1191
- try:
1192
- body = await request.json()
1193
- query = body.get("query", "")
1194
- limit = body.get("limit", CANONICAL_RECALL_LIMIT)
1195
- window = body.get("window", "") or ""
1196
- as_of_raw = (body.get("as_of", "") or "").strip()
1197
- raw_known_as_of = body.get("known_as_of", "")
1198
- raw_valid_at = body.get("valid_at", "")
1199
- if raw_known_as_of is not None and not isinstance(raw_known_as_of, str):
1200
- return JSONResponse({"error": "invalid_known_as_of"}, status_code=400)
1201
- if raw_valid_at is not None and not isinstance(raw_valid_at, str):
1202
- return JSONResponse({"error": "invalid_valid_at"}, status_code=400)
1203
- known_as_of_raw = (raw_known_as_of or "").strip()
1204
- valid_at_raw = (raw_valid_at or "").strip()
1205
- include_unknown = body.get("include_unknown", False)
1206
- if not isinstance(include_unknown, bool):
1207
- return JSONResponse({"error": "invalid_include_unknown"}, status_code=400)
1208
-
1209
- # Normalize as_of at HTTP boundary. Invalid → 400.
1210
- _as_of: str | None = None
1211
- if as_of_raw:
1212
- from superlocalmemory.retrieval.temporal_utils import normalize_as_of
1213
- _as_of = normalize_as_of(as_of_raw)
1214
- if _as_of is None:
1215
- return JSONResponse(
1216
- {"error": "invalid_as_of", "raw": as_of_raw}, status_code=400
1217
- )
1218
- def _normalize_named_time(raw: str, error: str) -> str | None | JSONResponse:
1219
- if not raw:
1220
- return None
1221
- from superlocalmemory.retrieval.temporal_utils import normalize_as_of
1222
- normalized = normalize_as_of(raw)
1223
- if normalized is None:
1224
- return JSONResponse({"error": error, "raw": raw}, status_code=400)
1225
- return normalized
1226
- _known_as_of = _normalize_named_time(known_as_of_raw, "invalid_known_as_of")
1227
- if isinstance(_known_as_of, JSONResponse):
1228
- return _known_as_of
1229
- _valid_at = _normalize_named_time(valid_at_raw, "invalid_valid_at")
1230
- if isinstance(_valid_at, JSONResponse):
1231
- return _valid_at
1232
-
1233
- # Use daemon engine — already loaded, shares warm page cache.
1234
- # run_in_executor keeps event loop alive so browser doesn't abort.
1235
- from .helpers import get_engine_lazy
1236
- engine = get_engine_lazy(request.app.state)
1237
- if engine is None:
1238
- return JSONResponse({"error": "Engine not initialised"}, status_code=503)
1239
-
1240
- loop = asyncio.get_event_loop()
1241
- t0 = _time.monotonic()
1242
- response = await loop.run_in_executor(
1243
- None,
1244
- lambda: engine.recall(
1245
- query, limit=limit, fast=False,
1246
- window=window or None, as_of=_as_of,
1247
- known_as_of=_known_as_of, valid_at=_valid_at,
1248
- include_unknown=include_unknown,
1249
- # Whoever asked, by the name they arrived under. Without a name
1250
- # the record of this recall is discarded and no outcome
1251
- # reported afterwards can be matched back to it.
1252
- session_id=_signal_session_id(),
1253
- ),
1254
- )
1255
- elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
1256
-
1257
- from superlocalmemory.server.recall_serializer import (
1258
- recall_response_metadata,
1259
- serialize_recall_response,
1260
- )
1261
- results, no_confident_match = serialize_recall_response(
1262
- response,
1263
- limit=limit,
1264
- per_fact_max=300,
1265
- total_max=max(300, limit * 300),
1266
- )
1267
-
1268
- # Record learning signals (non-blocking, non-critical)
1269
- try:
1270
- _record_learning_signals(query, results)
1271
- except Exception as _sig_exc:
1272
- import logging as _log
1273
- _log.getLogger(__name__).warning("Learning signal error: %s", _sig_exc)
1274
-
1275
- return {
1276
- "query": query,
1277
- "query_type": getattr(response, "query_type", "semantic"),
1278
- "result_count": len(results),
1279
- "retrieval_time_ms": elapsed_ms,
1280
- "results": results,
1281
- "synthesis": "",
1282
- "no_confident_match": no_confident_match,
1283
- **recall_response_metadata(response),
1284
- }
1285
- except Exception as e:
1286
- return _internal_error()
1287
-
1288
-
1289
- def _record_learning_signals(query: str, results: list) -> None:
1290
- """Record feedback + co-retrieval + confidence boost for any recall."""
1291
- from superlocalmemory.core.config import SLMConfig
1292
-
1293
- from superlocalmemory.infra.data_root import canonical_data_root
1294
- slm_dir = canonical_data_root()
1295
- config = SLMConfig.load()
1296
- pid = config.active_profile
1297
- fact_ids = [r.get("fact_id", "") for r in results[:10] if r.get("fact_id")]
1298
- if not fact_ids:
1299
- return
1300
-
1301
- try:
1302
- from superlocalmemory.learning.feedback import FeedbackCollector
1303
- collector = FeedbackCollector(slm_dir / "learning.db")
1304
- collector.record_implicit(
1305
- profile_id=pid, query=query,
1306
- fact_ids_returned=fact_ids, fact_ids_available=fact_ids,
1307
- )
1308
- except Exception:
1309
- pass
1310
-
1311
- try:
1312
- from superlocalmemory.learning.signals import LearningSignals
1313
- signals = LearningSignals(slm_dir / "learning.db")
1314
- signals.record_co_retrieval(pid, fact_ids)
1315
- except Exception:
1316
- pass
1317
-
1318
- try:
1319
- from superlocalmemory.learning.signals import LearningSignals
1320
- mem_db = str(slm_dir / "memory.db")
1321
- for fid in fact_ids[:5]:
1322
- LearningSignals.boost_confidence(mem_db, fid)
1323
- except Exception:
1324
- pass
1325
-
1326
-
1327
- # ── Trust Dashboard ──────────────────────────────────────────
1328
-
1329
- @router.get("/trust/dashboard")
1330
- async def trust_dashboard(request: Request):
1331
- """Trust overview: per-agent scores, alerts. Queries DB directly."""
1332
- try:
1333
- from superlocalmemory.core.config import SLMConfig
1334
- from superlocalmemory.storage.database import DatabaseManager
1335
- from superlocalmemory.storage import schema as _schema
1336
- config = SLMConfig.load()
1337
- pid = config.active_profile
1338
-
1339
- db_path = config.db_path
1340
- db = DatabaseManager(db_path)
1341
- db.initialize(_schema)
1342
-
1343
- # Query trust scores from DB
1344
- agents = []
1345
- try:
1346
- rows = db.execute(
1347
- "SELECT target_id, target_type, trust_score, evidence_count, "
1348
- "last_updated FROM trust_scores WHERE profile_id = ? "
1349
- "ORDER BY trust_score DESC",
1350
- (pid,),
1351
- )
1352
- for r in rows:
1353
- d = dict(r)
1354
- agents.append({
1355
- "target_id": d.get("target_id", ""),
1356
- "target_type": d.get("target_type", ""),
1357
- "trust_score": round(float(d.get("trust_score", 0.5)), 3),
1358
- "evidence_count": d.get("evidence_count", 0),
1359
- "last_updated": d.get("last_updated", ""),
1360
- })
1361
- except Exception:
1362
- pass
1363
-
1364
- # Aggregate stats
1365
- avg = round(sum(a["trust_score"] for a in agents) / len(agents), 3) if agents else 0.5
1366
- alerts = [a for a in agents if a["trust_score"] < 0.3]
1367
-
1368
- return {
1369
- "agents": agents,
1370
- "avg_trust": avg,
1371
- "alerts": alerts,
1372
- "total": len(agents),
1373
- "profile": pid,
1374
- }
1375
- except Exception as e:
1376
- return _internal_error()
1377
-
1378
-
1379
- # ── Math Health ──────────────────────────────────────────────
1380
-
1381
- @router.get("/math/health")
1382
- async def math_health(request: Request):
1383
- """Mathematical layer health: Fisher, sheaf, Langevin status. Queries DB directly."""
1384
- try:
1385
- # v3.6.12 (math-1): report CONFIG-DERIVED status, not a hardcoded
1386
- # "active"/"healthy" for every layer. The old code had a dead `if engine:`
1387
- # (engine was always None) and returned all-green unconditionally — a
1388
- # false-assurance pane. We can't probe the recall subprocess from here,
1389
- # so report the real configured mode/threshold/temperature and label the
1390
- # status "configured" (or "unknown" if config can't load).
1391
- from superlocalmemory.core.config import SLMConfig
1392
- config = SLMConfig.load()
1393
- math = getattr(config, "math", None)
1394
- _status = "configured" if math is not None else "unknown"
1395
- health = {
1396
- "fisher": {
1397
- "status": _status,
1398
- "description": "Fisher-Rao information geometry for similarity",
1399
- "mode": getattr(math, "fisher_mode", None) if math else None,
1400
- },
1401
- "sheaf": {
1402
- "status": _status,
1403
- "description": "Sheaf cohomology for consistency detection",
1404
- "threshold": getattr(math, "sheaf_contradiction_threshold", None) if math else None,
1405
- },
1406
- "langevin": {
1407
- "status": _status,
1408
- "description": "Riemannian Langevin dynamics for lifecycle",
1409
- "temperature": getattr(math, "langevin_temperature", None) if math else None,
1410
- },
1411
- }
1412
- return {
1413
- "health": health,
1414
- "overall": _status,
1415
- "note": "config-derived; not a live runtime probe",
1416
- }
1417
- except Exception as e:
1418
- return _internal_error()
1419
-
1420
-
1421
- # ── Auto-Capture / Auto-Recall Config ────────────────────────
1422
-
1423
- @router.get("/auto-capture/config")
1424
- async def get_auto_capture_config():
1425
- """Get auto-capture configuration."""
1426
- try:
1427
- from superlocalmemory.hooks.rules_engine import RulesEngine
1428
- from superlocalmemory.core.config import DEFAULT_BASE_DIR
1429
- rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
1430
- return {"config": rules.get_capture_config()}
1431
- except Exception as exc:
1432
- logger.exception("get_auto_capture_config failed")
1433
- return {"error": "internal error", "config": {}}
1434
-
1435
-
1436
- @router.put("/auto-capture/config")
1437
- async def set_auto_capture_config(request: Request):
1438
- """Update auto-capture config. Body: {"enabled": true, "capture_decisions": true, ...}"""
1439
- _require_manage(request)
1440
- try:
1441
- body = await request.json()
1442
- from superlocalmemory.hooks.rules_engine import RulesEngine
1443
- from superlocalmemory.core.config import DEFAULT_BASE_DIR
1444
- config_path = DEFAULT_BASE_DIR / "config.json"
1445
- rules = RulesEngine(config_path=config_path)
1446
- for key, value in body.items():
1447
- rules.update_rule("auto_capture", key, value)
1448
- rules.save(config_path)
1449
- return {"success": True, "config": rules.get_capture_config()}
1450
- except Exception as e:
1451
- return _internal_error()
1452
-
1453
-
1454
- @router.get("/auto-recall/config")
1455
- async def get_auto_recall_config():
1456
- """Get auto-recall configuration."""
1457
- try:
1458
- from superlocalmemory.hooks.rules_engine import RulesEngine
1459
- from superlocalmemory.core.config import DEFAULT_BASE_DIR
1460
- rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
1461
- return {"config": rules.get_recall_config()}
1462
- except Exception as exc:
1463
- logger.exception("get_auto_recall_config failed")
1464
- return {"error": "internal error", "config": {}}
1465
-
1466
-
1467
- @router.put("/auto-recall/config")
1468
- async def set_auto_recall_config(request: Request):
1469
- """Update auto-recall config."""
1470
- _require_manage(request)
1471
- try:
1472
- body = await request.json()
1473
- from superlocalmemory.hooks.rules_engine import RulesEngine
1474
- from superlocalmemory.core.config import DEFAULT_BASE_DIR
1475
- config_path = DEFAULT_BASE_DIR / "config.json"
1476
- rules = RulesEngine(config_path=config_path)
1477
- for key, value in body.items():
1478
- rules.update_rule("auto_recall", key, value)
1479
- rules.save(config_path)
1480
- return {"success": True, "config": rules.get_recall_config()}
1481
- except Exception as e:
1482
- return _internal_error()
1483
-
1484
-
1485
- # ── Runtime behaviour config (v3.8.2 UX-1) ──────────────────
1486
- # User-facing settings that take effect LIVE (no restart) via the
1487
- # reconfigure_daemon_engine hot-swap path AND persist across restarts via
1488
- # SLMConfig.save(). Scoped deliberately to fields that save() round-trips:
1489
- # retrieval (asdict) + injection (explicit). Excludes setup-time/internal
1490
- # knobs and any field save() doesn't persist (which would silently revert).
1491
- _RUNTIME_CONFIG_FIELDS = (
1492
- # (section, field, kind, min, max)
1493
- ("retrieval", "top_k", "int", 1, 200),
1494
- ("retrieval", "use_cross_encoder", "bool", None, None),
1495
- ("injection", "enabled", "bool", None, None),
1496
- ("injection", "core_block_enabled", "bool", None, None),
1497
- ("injection", "core_block_max_facts", "int", 0, 50),
1498
- )
1499
-
1500
-
1501
- def _runtime_config_snapshot(config) -> dict:
1502
- """Current values of the exposed runtime fields, grouped by section."""
1503
- out: dict = {}
1504
- for section, field, *_ in _RUNTIME_CONFIG_FIELDS:
1505
- sec = getattr(config, section, None)
1506
- out.setdefault(section, {})[field] = (
1507
- getattr(sec, field, None) if sec is not None else None
1508
- )
1509
- return out
1510
-
1511
-
1512
- @router.get("/runtime/config")
1513
- async def get_runtime_config(request: Request):
1514
- """User-facing runtime behaviour settings.
1515
-
1516
- Recall depth (top_k), reranker on/off (use_cross_encoder), and memory
1517
- injection (master + core-block). All apply live via the daemon hot-swap —
1518
- no restart — and persist across restarts.
1519
- """
1520
- try:
1521
- from superlocalmemory.core.config import SLMConfig
1522
- config = getattr(request.app.state, "config", None) or SLMConfig.load()
1523
- return {"success": True, "config": _runtime_config_snapshot(config)}
1524
- except Exception:
1525
- return _internal_error()
1526
-
1527
-
1528
- @router.put("/runtime/config")
1529
- async def set_runtime_config(request: Request):
1530
- """Validate, persist, and hot-apply runtime behaviour settings.
1531
-
1532
- Body: ``{"retrieval": {"top_k": 30}, "injection": {"enabled": false}}``.
1533
- Only known fields are accepted; a bad type/range is rejected 400 and
1534
- nothing is applied (fail-fast — never let a bad value wedge the engine).
1535
- """
1536
- _require_manage(request)
1537
- try:
1538
- import dataclasses as _dc
1539
- from superlocalmemory.core.config import SLMConfig
1540
-
1541
- body = await request.json()
1542
- if not isinstance(body, dict):
1543
- return JSONResponse({"error": "body must be an object"}, status_code=400)
1544
-
1545
- # Validate everything BEFORE mutating anything.
1546
- updates: dict[str, dict] = {}
1547
- for section, field, kind, lo, hi in _RUNTIME_CONFIG_FIELDS:
1548
- sec_in = body.get(section)
1549
- if not isinstance(sec_in, dict) or field not in sec_in:
1550
- continue
1551
- val = sec_in[field]
1552
- if kind == "bool":
1553
- if not isinstance(val, bool):
1554
- return JSONResponse(
1555
- {"error": f"{section}.{field} must be true or false"},
1556
- status_code=400,
1557
- )
1558
- elif kind == "int":
1559
- # bool is an int subclass — reject it explicitly.
1560
- if isinstance(val, bool) or not isinstance(val, int):
1561
- return JSONResponse(
1562
- {"error": f"{section}.{field} must be an integer"},
1563
- status_code=400,
1564
- )
1565
- if (lo is not None and val < lo) or (hi is not None and val > hi):
1566
- return JSONResponse(
1567
- {"error": f"{section}.{field} must be between {lo} and {hi}"},
1568
- status_code=400,
1569
- )
1570
- updates.setdefault(section, {})[field] = val
1571
-
1572
- if not updates:
1573
- return JSONResponse(
1574
- {"error": "no known runtime settings in request body"},
1575
- status_code=400,
1576
- )
1577
-
1578
- # These are plain behaviour flags read per-recall — NOT model/mode
1579
- # swaps. So we apply them the light way: swap the sub-config objects on
1580
- # the LIVE config the daemon+engine already hold, then persist to disk.
1581
- # This avoids the heavyweight engine drain+rebuild (which reloads
1582
- # models and can time out draining in-flight recalls) — the running
1583
- # engine simply reads the new values on its next recall/injection.
1584
- live = getattr(request.app.state, "config", None) or SLMConfig.load()
1585
- targets = [live]
1586
- engine = getattr(request.app.state, "engine", None)
1587
- eng_cfg = getattr(engine, "_config", None) if engine is not None else None
1588
- if eng_cfg is not None and eng_cfg is not live:
1589
- targets.append(eng_cfg)
1590
- for cfg in targets:
1591
- for section, changes in updates.items():
1592
- sec = getattr(cfg, section, None)
1593
- if sec is None:
1594
- continue
1595
- # replace() yields a NEW sub-config (works frozen or mutable);
1596
- # assigning the attribute is an atomic reference swap.
1597
- setattr(cfg, section, _dc.replace(sec, **changes))
1598
- request.app.state.config = live
1599
- live.save(mode_change=False) # durable across restart
1600
- return {"success": True, "config": _runtime_config_snapshot(live)}
1601
- except Exception:
1602
- return _internal_error()
1603
-
1604
-
1605
- # ── IDE Status ───────────────────────────────────────────────
1606
-
1607
- @router.get("/ide/status")
1608
- async def ide_status():
1609
- """Get IDE connection status."""
1610
- try:
1611
- from superlocalmemory.hooks.ide_connector import IDEConnector
1612
- connector = IDEConnector()
1613
- return {"ides": connector.get_status()}
1614
- except Exception as exc:
1615
- logger.exception("ide_status failed")
1616
- return {"error": "internal error", "ides": []}
1617
-
1618
-
1619
- @router.post("/ide/connect")
1620
- async def ide_connect(request: Request):
1621
- """Connect an IDE. Body: {"ide": "cursor"} or {} for all."""
1622
- try:
1623
- body = await request.json()
1624
- ide = body.get("ide", "")
1625
-
1626
- from superlocalmemory.hooks.ide_connector import IDEConnector
1627
- connector = IDEConnector()
1628
-
1629
- if ide:
1630
- success = connector.connect(ide)
1631
- return {"success": success, "ide": ide}
1632
- else:
1633
- results = connector.connect_all()
1634
- return {"results": results}
1635
- except Exception as e:
1636
- return _internal_error()
1637
-
1638
-
1639
- # ── Active Memory (V3.1) ────────────────────────────────────
1640
-
1641
- @router.get("/learning/signals")
1642
- async def learning_signals():
1643
- """Get zero-cost learning signal statistics."""
1644
- try:
1645
- from superlocalmemory.learning.signals import LearningSignals
1646
- from superlocalmemory.core.config import SLMConfig
1647
- from superlocalmemory.server.routes.helpers import DB_PATH
1648
- learning_db = DB_PATH.parent / "learning.db"
1649
- signals = LearningSignals(learning_db)
1650
- config = SLMConfig.load()
1651
- pid = config.active_profile
1652
- return {"success": True, **signals.get_signal_stats(pid)}
1653
- except Exception as exc:
1654
- logger.exception("learning_signals failed")
1655
- return {"success": False, "error": "internal error"}
1656
-
1657
-
1658
- @router.post("/learning/consolidate")
1659
- async def run_consolidation(request: Request):
1660
- """Run sleep-time consolidation. Body: {dry_run: true/false}."""
1661
- try:
1662
- body = await request.json()
1663
- dry_run = body.get("dry_run", False)
1664
- # Run the full cycle OUT OF PROCESS. Step 5 trains the LightGBM ranker
1665
- # (online _run_shadow_cycle or legacy cold-start); importing lightgbm
1666
- # in the torch-warm daemon loads a second libomp and SIGSEGVs it (see
1667
- # lightgbm_subprocess module docstring). Isolation is the fix.
1668
- from superlocalmemory.learning.lightgbm_subprocess import (
1669
- run_consolidation_isolated,
1670
- )
1671
- from superlocalmemory.core.config import SLMConfig
1672
- from superlocalmemory.server.routes.helpers import DB_PATH
1673
- from fastapi.concurrency import run_in_threadpool
1674
-
1675
- config = SLMConfig.load()
1676
- learning_db = DB_PATH.parent / "learning.db"
1677
- _require_manage_for_profile(request, config.active_profile)
1678
- authorization = authorize_route_mutation(
1679
- request,
1680
- operation="update",
1681
- source_agent_id="http-learning-consolidate",
1682
- profile_id=config.active_profile,
1683
- )
1684
- result = await run_in_threadpool(
1685
- run_consolidation_isolated,
1686
- str(DB_PATH),
1687
- str(learning_db),
1688
- config.active_profile,
1689
- dry_run=dry_run,
1690
- )
1691
- if result.get("error"):
1692
- return {"success": False, "error": result["error"]}
1693
- # Step-5 training may have promoted a new model — drop the daemon's
1694
- # cached model so the next recall reloads it from learning.db.
1695
- if not dry_run:
1696
- try:
1697
- from superlocalmemory.learning.model_cache import invalidate
1698
- invalidate(config.active_profile)
1699
- except Exception:
1700
- pass
1701
- stats = result.get("stats") or {}
1702
- authorization.complete()
1703
- return {"success": True, **stats}
1704
- except HTTPException:
1705
- raise
1706
- except Exception as exc:
1707
- logger.exception("run_consolidation failed")
1708
- return {"success": False, "error": "internal error"}
1709
-
1710
-
1711
- @router.get("/hooks/status")
1712
- async def hooks_status():
1713
- """Check if Claude Code hooks are installed."""
1714
- try:
1715
- from superlocalmemory.hooks.claude_code_hooks import check_status
1716
- return {"success": True, **check_status()}
1717
- except Exception as exc:
1718
- logger.exception("hooks_status failed")
1719
- return {"success": False, "error": "internal error"}
1720
-
1721
-
1722
- # ── Phase 6: V3.2 API Endpoints ──────────────────────────────
1723
- # 9 new endpoints for the V3.2 dashboard tabs:
1724
- # Auto-Invoke (2), Associations (2), Consolidation (2),
1725
- # Core Memory (2), VectorStore (1)
1726
- #
1727
- # Rules enforced:
1728
- # 01 - Profile scoping on ALL endpoints
1729
- # 06 - No engine import from routes (direct sqlite3)
1730
- # 11 - Parameterized SQL everywhere
1731
- # 18 - WorkerPool for POST consolidation/trigger
1732
- # 19 - Silent errors with JSONResponse
1733
- # ──────────────────────────────────────────────────────────────
1734
-
1735
-
1736
- def _load_auto_invoke_json() -> dict:
1737
- """Load auto-invoke config from config.json's auto_invoke section."""
1738
- from superlocalmemory.server.routes.helpers import MEMORY_DIR
1739
- config_path = MEMORY_DIR / "config.json"
1740
- if config_path.exists():
1741
- try:
1742
- data = json.loads(config_path.read_text())
1743
- return data.get("auto_invoke", {})
1744
- except (json.JSONDecodeError, IOError):
1745
- pass
1746
- return {}
1747
-
1748
-
1749
- def _save_auto_invoke_json(auto_invoke_data: dict) -> None:
1750
- """Persist auto-invoke config into config.json's auto_invoke section."""
1751
- from superlocalmemory.server.routes.helpers import MEMORY_DIR
1752
- config_path = MEMORY_DIR / "config.json"
1753
- cfg: dict = {}
1754
- if config_path.exists():
1755
- try:
1756
- cfg = json.loads(config_path.read_text())
1757
- except (json.JSONDecodeError, IOError):
1758
- pass
1759
- cfg["auto_invoke"] = auto_invoke_data
1760
- config_path.parent.mkdir(parents=True, exist_ok=True)
1761
- config_path.write_text(json.dumps(cfg, indent=2))
1762
-
1763
-
1764
- # ── 1. GET /api/v3/auto-invoke/config ─────────────────────────
1765
-
1766
- @router.get("/auto-invoke/config")
1767
- async def get_auto_invoke_config(request: Request):
1768
- """Get current auto-invoke configuration."""
1769
- try:
1770
- from superlocalmemory.core.config import AutoInvokeConfig
1771
- defaults = AutoInvokeConfig()
1772
-
1773
- persisted = _load_auto_invoke_json()
1774
-
1775
- return {
1776
- "enabled": persisted.get("enabled", defaults.enabled),
1777
- "min_score": persisted.get("min_score", defaults.fok_threshold),
1778
- "weights": persisted.get("weights", dict(defaults.weights)),
1779
- "act_r_mode": persisted.get("act_r_mode", defaults.use_act_r),
1780
- "invocation_count": persisted.get("invocation_count", 0),
1781
- "last_invocation": persisted.get("last_invocation", None),
1782
- }
1783
- except Exception as e:
1784
- return _internal_error()
1785
-
1786
-
1787
- # ── 2. PUT /api/v3/auto-invoke/config ─────────────────────────
1788
-
1789
- @router.put("/auto-invoke/config")
1790
- async def set_auto_invoke_config(request: Request):
1791
- """Update auto-invoke configuration.
1792
-
1793
- Body: {"enabled": true, "min_score": 0.15, "weights": {...}}
1794
- """
1795
- _require_manage(request)
1796
- try:
1797
- body = await request.json()
1798
-
1799
- # Validate min_score range
1800
- min_score = body.get("min_score")
1801
- if min_score is not None and (min_score < 0 or min_score > 1):
1802
- return JSONResponse(
1803
- {"error": "min_score must be between 0 and 1"},
1804
- status_code=400,
1805
- )
1806
-
1807
- # Load existing, merge updates
1808
- from superlocalmemory.core.config import AutoInvokeConfig
1809
- defaults = AutoInvokeConfig()
1810
- persisted = _load_auto_invoke_json()
1811
-
1812
- updated = {
1813
- "enabled": body.get("enabled", persisted.get("enabled", defaults.enabled)),
1814
- "min_score": body.get("min_score", persisted.get("min_score", defaults.fok_threshold)),
1815
- "weights": body.get("weights", persisted.get("weights", dict(defaults.weights))),
1816
- "act_r_mode": body.get("act_r_mode", persisted.get("act_r_mode", defaults.use_act_r)),
1817
- "invocation_count": persisted.get("invocation_count", 0),
1818
- "last_invocation": persisted.get("last_invocation", None),
1819
- }
1820
- _save_auto_invoke_json(updated)
1821
-
1822
- return updated
1823
- except Exception as e:
1824
- return _internal_error()
1825
-
1826
-
1827
- # ── 3. GET /api/v3/associations ───────────────────────────────
1828
-
1829
- @router.get("/associations")
1830
- async def get_associations(
1831
- request: Request,
1832
- limit: int = 50,
1833
- type: str = "",
1834
- profile: str = "",
1835
- ):
1836
- """Get association edges for a profile with content previews."""
1837
- pid = _resolve_profile(request, profile)
1838
- try:
1839
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1840
- import sqlite3
1841
-
1842
- if not DB_PATH.exists():
1843
- return {"edges": [], "total": 0}
1844
-
1845
- conn = get_read_connection(DB_PATH)
1846
- conn.row_factory = sqlite3.Row
1847
-
1848
- # Build query with optional type filter (parameterized)
1849
- params: list = [pid]
1850
- sql = (
1851
- "SELECT ae.edge_id, ae.source_fact_id, ae.target_fact_id, "
1852
- "ae.association_type, ae.weight, ae.co_access_count, ae.created_at, "
1853
- "sf.content AS source_content, tf.content AS target_content "
1854
- "FROM association_edges ae "
1855
- "LEFT JOIN atomic_facts sf ON sf.fact_id = ae.source_fact_id "
1856
- "LEFT JOIN atomic_facts tf ON tf.fact_id = ae.target_fact_id "
1857
- "WHERE ae.profile_id = ? "
1858
- )
1859
- if type:
1860
- sql += "AND ae.association_type = ? "
1861
- params.append(type)
1862
- sql += "ORDER BY ae.created_at DESC LIMIT ?"
1863
- params.append(limit)
1864
-
1865
- rows = conn.execute(sql, params).fetchall()
1866
-
1867
- # Total count (separate query for pagination info)
1868
- count_sql = "SELECT COUNT(*) FROM association_edges WHERE profile_id = ?"
1869
- count_params: list = [pid]
1870
- if type:
1871
- count_sql += " AND association_type = ?"
1872
- count_params.append(type)
1873
- total = conn.execute(count_sql, count_params).fetchone()[0]
1874
-
1875
- conn.close()
1876
-
1877
- edges = []
1878
- for r in rows:
1879
- row = dict(r)
1880
- source_content = row.get("source_content") or ""
1881
- target_content = row.get("target_content") or ""
1882
- edges.append({
1883
- "edge_id": row["edge_id"],
1884
- "source_fact_id": row["source_fact_id"],
1885
- "target_fact_id": row["target_fact_id"],
1886
- "association_type": row["association_type"],
1887
- "weight": round(float(row["weight"]), 3),
1888
- "co_access_count": row["co_access_count"],
1889
- "created_at": row["created_at"],
1890
- "source_preview": source_content[:100],
1891
- "target_preview": target_content[:100],
1892
- })
1893
-
1894
- return {"edges": edges, "total": total}
1895
- except Exception as e:
1896
- return _internal_error()
1897
-
1898
-
1899
- # ── 4. GET /api/v3/associations/stats ─────────────────────────
1900
-
1901
- @router.get("/associations/stats")
1902
- async def get_association_stats(request: Request, profile: str = ""):
1903
- """Get association graph statistics."""
1904
- pid = _resolve_profile(request, profile)
1905
- try:
1906
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1907
- import sqlite3
1908
-
1909
- if not DB_PATH.exists():
1910
- return {
1911
- "total_edges": 0,
1912
- "by_type": {},
1913
- "community_count": 0,
1914
- "avg_weight": 0.0,
1915
- "top_connected_facts": [],
1916
- }
1917
-
1918
- conn = get_read_connection(DB_PATH)
1919
- conn.row_factory = sqlite3.Row
1920
-
1921
- # Total edges
1922
- total = conn.execute(
1923
- "SELECT COUNT(*) FROM association_edges WHERE profile_id = ?",
1924
- (pid,),
1925
- ).fetchone()[0]
1926
-
1927
- # Edges by type
1928
- by_type_rows = conn.execute(
1929
- "SELECT association_type, COUNT(*) AS cnt "
1930
- "FROM association_edges WHERE profile_id = ? "
1931
- "GROUP BY association_type",
1932
- (pid,),
1933
- ).fetchall()
1934
- by_type = {row["association_type"]: row["cnt"] for row in by_type_rows}
1935
-
1936
- # Average weight
1937
- avg_row = conn.execute(
1938
- "SELECT AVG(weight) AS avg_w FROM association_edges "
1939
- "WHERE profile_id = ?",
1940
- (pid,),
1941
- ).fetchone()
1942
- avg_weight = round(float(avg_row["avg_w"] or 0), 3)
1943
-
1944
- # Community count from fact_importance table
1945
- community_count = 0
1946
- try:
1947
- cc_row = conn.execute(
1948
- "SELECT COUNT(DISTINCT community_id) AS cnt "
1949
- "FROM fact_importance "
1950
- "WHERE profile_id = ? AND community_id IS NOT NULL",
1951
- (pid,),
1952
- ).fetchone()
1953
- community_count = cc_row["cnt"] if cc_row else 0
1954
- except Exception:
1955
- pass
1956
-
1957
- # Top connected facts (by degree = count of edges as source or target)
1958
- top_facts = []
1959
- try:
1960
- degree_rows = conn.execute(
1961
- "SELECT fact_id, degree FROM ("
1962
- " SELECT source_fact_id AS fact_id, COUNT(*) AS degree "
1963
- " FROM association_edges WHERE profile_id = ? "
1964
- " GROUP BY source_fact_id "
1965
- " UNION ALL "
1966
- " SELECT target_fact_id AS fact_id, COUNT(*) AS degree "
1967
- " FROM association_edges WHERE profile_id = ? "
1968
- " GROUP BY target_fact_id "
1969
- ") GROUP BY fact_id ORDER BY SUM(degree) DESC LIMIT 5",
1970
- (pid, pid),
1971
- ).fetchall()
1972
- for dr in degree_rows:
1973
- fact_id = dr["fact_id"]
1974
- preview_row = conn.execute(
1975
- "SELECT content FROM atomic_facts WHERE fact_id = ?",
1976
- (fact_id,),
1977
- ).fetchone()
1978
- preview = (dict(preview_row).get("content", "")[:80]) if preview_row else ""
1979
- top_facts.append({
1980
- "fact_id": fact_id,
1981
- "degree": dr["degree"],
1982
- "preview": preview,
1983
- })
1984
- except Exception:
1985
- pass
1986
-
1987
- conn.close()
1988
-
1989
- return {
1990
- "total_edges": total,
1991
- "by_type": by_type,
1992
- "community_count": community_count,
1993
- "avg_weight": avg_weight,
1994
- "top_connected_facts": top_facts,
1995
- }
1996
- except Exception as e:
1997
- return _internal_error()
1998
-
1999
-
2000
- # ── 5. GET /api/v3/consolidation/status ───────────────────────
2001
-
2002
- @router.get("/consolidation/status")
2003
- async def get_consolidation_status(request: Request, profile: str = ""):
2004
- """Get consolidation status and last run results."""
2005
- pid = _resolve_profile(request, profile)
2006
- try:
2007
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2008
- from superlocalmemory.core.config import SLMConfig
2009
- import sqlite3
2010
-
2011
- config = SLMConfig.load()
2012
- cons_cfg = config.consolidation
2013
-
2014
- result: dict = {
2015
- "enabled": cons_cfg.enabled,
2016
- "last_run": None,
2017
- "last_result": None,
2018
- "triggers": {
2019
- "session_end": cons_cfg.session_trigger,
2020
- "idle_timeout": cons_cfg.idle_timeout_seconds,
2021
- "step_count": cons_cfg.step_count_trigger,
2022
- "scheduled_sessions": cons_cfg.scheduled_sessions,
2023
- },
2024
- "store_count_since_last": 0,
2025
- }
2026
-
2027
- if not DB_PATH.exists():
2028
- return result
2029
-
2030
- conn = get_read_connection(DB_PATH)
2031
- conn.row_factory = sqlite3.Row
2032
-
2033
- # Last consolidation log entry
2034
- try:
2035
- last_row = conn.execute(
2036
- "SELECT timestamp, action_type, reason "
2037
- "FROM consolidation_log "
2038
- "WHERE profile_id = ? ORDER BY timestamp DESC LIMIT 1",
2039
- (pid,),
2040
- ).fetchone()
2041
- if last_row:
2042
- result["last_run"] = dict(last_row).get("timestamp")
2043
- except Exception:
2044
- pass
2045
-
2046
- # Count blocks compiled (proxy for last consolidation result)
2047
- try:
2048
- block_count = conn.execute(
2049
- "SELECT COUNT(*) FROM core_memory_blocks WHERE profile_id = ?",
2050
- (pid,),
2051
- ).fetchone()[0]
2052
- edge_count = conn.execute(
2053
- "SELECT COUNT(*) FROM association_edges WHERE profile_id = ?",
2054
- (pid,),
2055
- ).fetchone()[0]
2056
- result["last_result"] = {
2057
- "blocks_compiled": block_count,
2058
- "total_edges": edge_count,
2059
- }
2060
- except Exception:
2061
- pass
2062
-
2063
- # Store count since last consolidation
2064
- try:
2065
- if result["last_run"]:
2066
- sc = conn.execute(
2067
- "SELECT COUNT(*) FROM atomic_facts "
2068
- "WHERE profile_id = ? AND created_at > ?",
2069
- (pid, result["last_run"]),
2070
- ).fetchone()[0]
2071
- result["store_count_since_last"] = sc
2072
- else:
2073
- sc = conn.execute(
2074
- "SELECT COUNT(*) FROM atomic_facts WHERE profile_id = ?",
2075
- (pid,),
2076
- ).fetchone()[0]
2077
- result["store_count_since_last"] = sc
2078
- except Exception:
2079
- pass
2080
-
2081
- conn.close()
2082
- return result
2083
- except Exception as e:
2084
- return _internal_error()
2085
-
2086
-
2087
- # ── 6. POST /api/v3/consolidation/trigger ─────────────────────
2088
-
2089
- @router.post("/consolidation/trigger")
2090
- async def trigger_consolidation(request: Request):
2091
- """Trigger consolidation manually.
2092
-
2093
- Body: {"lightweight": false, "profile": ""}
2094
- Runs under the daemon's profile-runtime operation lease (Rule 18) so a
2095
- concurrent profile switch cannot commit mid-consolidation.
2096
- """
2097
- try:
2098
- body = await request.json()
2099
- lightweight = body.get("lightweight", False)
2100
- profile = body.get("profile", "")
2101
-
2102
- from superlocalmemory.server.routes.helpers import get_active_profile
2103
- pid = _resolve_mutation_profile(profile)
2104
- _require_manage_for_profile(request, pid)
2105
- authorization = authorize_route_mutation(
2106
- request,
2107
- operation="update",
2108
- source_agent_id="http-consolidation-trigger",
2109
- profile_id=pid,
2110
- )
2111
-
2112
- # 4.0.8: the body of this handler moved to server/consolidation_runner
2113
- # so the periodic daemon trigger and this endpoint run the SAME code
2114
- # under the SAME lock. Two copies would be two definitions of
2115
- # "consolidated", and only one of them would get maintained.
2116
- from superlocalmemory.server.consolidation_runner import (
2117
- run_full_consolidation,
2118
- )
2119
-
2120
- # background=true returns as soon as the pass is scheduled. The
2121
- # session-end hook needs this: a full consolidation runs for seconds to
2122
- # minutes, and a hook that waits for it either blocks the user's shell
2123
- # or times out and wrongly concludes the run failed.
2124
- if body.get("background"):
2125
- import asyncio as _asyncio
2126
-
2127
- _app_state = request.app.state
2128
- _asyncio.create_task(
2129
- run_full_consolidation(
2130
- _app_state, pid, lightweight=lightweight, trigger="hook",
2131
- )
2132
- )
2133
- authorization.complete()
2134
- return {"success": True, "started": True, "background": True}
2135
-
2136
- result = await run_full_consolidation(
2137
- request.app.state, pid, lightweight=lightweight, trigger="http",
2138
- )
2139
- authorization.complete()
2140
- return {"success": True, **result}
2141
- except HTTPException:
2142
- raise
2143
- except Exception as e:
2144
- return _internal_error()
2145
-
2146
-
2147
- # ── 7. GET /api/v3/core-memory ────────────────────────────────
2148
-
2149
- @router.get("/core-memory")
2150
- async def get_core_memory(request: Request, profile: str = ""):
2151
- """Get all Core Memory blocks for a profile."""
2152
- pid = _resolve_profile(request, profile)
2153
- try:
2154
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2155
- import sqlite3
2156
-
2157
- if not DB_PATH.exists():
2158
- return {"blocks": [], "total_chars": 0, "char_limit": 2000}
2159
-
2160
- conn = get_read_connection(DB_PATH)
2161
- conn.row_factory = sqlite3.Row
2162
-
2163
- rows = conn.execute(
2164
- "SELECT block_id, block_type, content, char_count, version, "
2165
- "compiled_by, updated_at FROM core_memory_blocks "
2166
- "WHERE profile_id = ? ORDER BY block_type",
2167
- (pid,),
2168
- ).fetchall()
2169
-
2170
- conn.close()
2171
-
2172
- blocks = []
2173
- total_chars = 0
2174
- for r in rows:
2175
- row = dict(r)
2176
- char_count = row.get("char_count", 0) or len(row.get("content", ""))
2177
- total_chars += char_count
2178
- blocks.append({
2179
- "block_id": row["block_id"],
2180
- "block_type": row["block_type"],
2181
- "content": row["content"],
2182
- "char_count": char_count,
2183
- "version": row["version"],
2184
- "compiled_by": row["compiled_by"],
2185
- "updated_at": row["updated_at"],
2186
- })
2187
-
2188
- return {"blocks": blocks, "total_chars": total_chars, "char_limit": 2000}
2189
- except Exception as e:
2190
- return _internal_error()
2191
-
2192
-
2193
- # ── 8. PUT /api/v3/core-memory/{block_id} ─────────────────────
2194
-
2195
- @router.put("/core-memory/{block_id}")
2196
- async def update_core_memory_block(block_id: str, request: Request):
2197
- """Update a Core Memory block's content manually.
2198
-
2199
- Body: {"content": "Updated content..."}
2200
- """
2201
- try:
2202
- body = await request.json()
2203
- content = body.get("content")
2204
- if content is None:
2205
- return JSONResponse(
2206
- {"error": "content field is required"},
2207
- status_code=400,
2208
- )
2209
-
2210
- from superlocalmemory.server.routes.helpers import DB_PATH, get_active_profile
2211
- from superlocalmemory.storage.memory_write import memory_write
2212
- from datetime import datetime, timezone
2213
-
2214
- if not DB_PATH.exists():
2215
- return JSONResponse(
2216
- {"error": "Database not found"},
2217
- status_code=404,
2218
- )
2219
-
2220
- pid = get_active_profile()
2221
- authorization = authorize_route_mutation(
2222
- request,
2223
- operation="update",
2224
- source_agent_id="http-core-memory-update",
2225
- profile_id=pid,
2226
- fact_id=block_id,
2227
- content_preview=str(content),
2228
- )
2229
-
2230
- now = datetime.now(timezone.utc).isoformat()
2231
- # memory_write: process write lock + busy_timeout.
2232
- # SELECT + UPDATE + read-back are atomic inside the same connection.
2233
- with memory_write(DB_PATH) as conn:
2234
- # Verify block exists
2235
- existing = conn.execute(
2236
- "SELECT block_id, profile_id, block_type, version "
2237
- "FROM core_memory_blocks WHERE block_id = ? AND profile_id = ?",
2238
- (block_id, pid),
2239
- ).fetchone()
2240
-
2241
- if not existing:
2242
- raise HTTPException(status_code=404, detail=f"Block {block_id} not found")
2243
-
2244
- new_version = dict(existing)["version"] + 1
2245
- conn.execute(
2246
- "UPDATE core_memory_blocks SET content = ?, char_count = ?, "
2247
- "version = ?, compiled_by = 'manual', updated_at = ? "
2248
- "WHERE block_id = ? AND profile_id = ?",
2249
- (content, len(content), new_version, now, block_id, pid),
2250
- )
2251
- # Read back updated block while connection is still open.
2252
- updated = conn.execute(
2253
- "SELECT block_id, block_type, content, char_count, version, "
2254
- "compiled_by, updated_at FROM core_memory_blocks "
2255
- "WHERE block_id = ? AND profile_id = ?",
2256
- (block_id, pid),
2257
- ).fetchone()
2258
- updated_dict = dict(updated) if updated else {"block_id": block_id, "updated": True}
2259
-
2260
- authorization.complete()
2261
- return updated_dict
2262
- except HTTPException:
2263
- raise
2264
- except Exception as e:
2265
- return _internal_error()
2266
-
2267
-
2268
- # ── 9. GET /api/v3/vector-store/status ────────────────────────
2269
-
2270
- @router.get("/vector-store/status")
2271
- async def get_vector_store_status(request: Request, profile: str = ""):
2272
- """Get VectorStore health and statistics."""
2273
- pid = _resolve_profile(request, profile)
2274
- try:
2275
- from superlocalmemory.core.config import SLMConfig
2276
- from superlocalmemory.server.routes.helpers import DB_PATH
2277
- import sqlite3
2278
-
2279
- config = SLMConfig.load()
2280
-
2281
- result: dict = {
2282
- "available": False,
2283
- "provider": "sqlite-vec",
2284
- "dimension": config.embedding.dimension,
2285
- "embedding_model": config.embedding.model_name,
2286
- "total_vectors": 0,
2287
- "binary_quantization": False,
2288
- "binary_quantization_threshold": 100000,
2289
- "fallback_to_ann": False,
2290
- }
2291
-
2292
- # Check if sqlite-vec extension is available
2293
- try:
2294
- import sqlite_vec # noqa: F401
2295
- result["available"] = True
2296
- except ImportError:
2297
- result["fallback_to_ann"] = True
2298
-
2299
- # Count vectors in embedding_metadata
2300
- if DB_PATH.exists():
2301
- try:
2302
- conn = get_read_connection(DB_PATH)
2303
- count = conn.execute(
2304
- "SELECT COUNT(*) FROM embedding_metadata WHERE profile_id = ?",
2305
- (pid,),
2306
- ).fetchone()[0]
2307
- result["total_vectors"] = count
2308
- conn.close()
2309
- except Exception:
2310
- pass
2311
-
2312
- return result
2313
- except Exception as e:
2314
- return _internal_error()
2315
-
2316
-
2317
- # ── Phase 10: V3.3 API Endpoints ────────────────────────────
2318
- # 7 new endpoints for the V3.3 dashboard:
2319
- # Forgetting (2), Quantization (1), CCQ (1),
2320
- # Soft Prompts (1), Process Health (1), V3.3 Overview (1)
2321
- #
2322
- # Rules enforced:
2323
- # 01 - Profile scoping on ALL endpoints
2324
- # 06 - No engine import from routes (direct sqlite3)
2325
- # 11 - Parameterized SQL everywhere
2326
- # 19 - Silent errors with JSONResponse
2327
- # ──────────────────────────────────────────────────────────────
2328
-
2329
-
2330
- # ── 1a. GET /api/v3/forgetting/stats ────────────────────────
2331
-
2332
- @router.get("/forgetting/stats")
2333
- async def forgetting_stats(request: Request, profile: str = ""):
2334
- """Get memory retention zone distribution."""
2335
- pid = _resolve_profile(request, profile)
2336
- try:
2337
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2338
- import sqlite3 as _sqlite3
2339
-
2340
- zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
2341
- total = 0
2342
-
2343
- if not DB_PATH.exists():
2344
- return {"total": total, "zones": zones}
2345
-
2346
- conn = get_read_connection(DB_PATH)
2347
- conn.row_factory = _sqlite3.Row
2348
-
2349
- try:
2350
- rows = conn.execute(
2351
- "SELECT lifecycle_zone, COUNT(*) AS cnt "
2352
- "FROM fact_retention WHERE profile_id = ? "
2353
- "GROUP BY lifecycle_zone",
2354
- (pid,),
2355
- ).fetchall()
2356
- for row in rows:
2357
- zone = dict(row)["lifecycle_zone"]
2358
- cnt = dict(row)["cnt"]
2359
- if zone in zones:
2360
- zones[zone] = cnt
2361
- total += cnt
2362
- except Exception:
2363
- # Table may not exist in older DBs -- graceful fallback
2364
- pass
2365
-
2366
- conn.close()
2367
- return {"total": total, "zones": zones}
2368
- except Exception as e:
2369
- return _internal_error()
2370
-
2371
-
2372
- # ── 1b. POST /api/v3/forgetting/run ─────────────────────────
2373
-
2374
- @router.post("/forgetting/run")
2375
- async def run_forgetting(request: Request):
2376
- """Trigger a forgetting decay cycle.
2377
-
2378
- Body: {"profile": ""} (optional profile override).
2379
- """
2380
- try:
2381
- body = await request.json()
2382
- profile = body.get("profile", "")
2383
-
2384
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2385
- from superlocalmemory.storage.memory_write import memory_write as _memory_write
2386
- pid = _resolve_mutation_profile(profile)
2387
- _require_manage_for_profile(request, pid)
2388
-
2389
- if not DB_PATH.exists():
2390
- return {"success": False, "error": "Database not found"}
2391
-
2392
- authorization = authorize_route_mutation(
2393
- request,
2394
- operation="update",
2395
- source_agent_id="http-forgetting-run",
2396
- profile_id=pid,
2397
- )
2398
-
2399
- # memory_write: process write lock + busy_timeout — all UPDATEs atomic.
2400
- updated = 0
2401
- try:
2402
- with _memory_write(DB_PATH) as conn:
2403
- # Apply Ebbinghaus decay: reduce retention for facts not accessed recently
2404
- # Formula: retention *= exp(-0.1) for each cycle (simplified batch decay)
2405
- conn.execute(
2406
- "UPDATE fact_retention "
2407
- "SET retention_score = MAX(0.0, retention_score * 0.9), "
2408
- " last_computed_at = datetime('now') "
2409
- "WHERE profile_id = ? "
2410
- "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2411
- (pid,),
2412
- )
2413
- updated = conn.total_changes
2414
-
2415
- # Transition zones based on new retention scores
2416
- zone_thresholds = [
2417
- ("forgotten", 0.05),
2418
- ("archive", 0.15),
2419
- ("cold", 0.35),
2420
- ("warm", 0.65),
2421
- ]
2422
- for zone, threshold in zone_thresholds:
2423
- conn.execute(
2424
- "UPDATE fact_retention "
2425
- "SET lifecycle_zone = ? "
2426
- "WHERE profile_id = ? "
2427
- "AND retention_score < ? "
2428
- "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2429
- (zone, pid, threshold),
2430
- )
2431
-
2432
- # Ensure high-retention facts are active
2433
- conn.execute(
2434
- "UPDATE fact_retention "
2435
- "SET lifecycle_zone = 'active' "
2436
- "WHERE profile_id = ? AND retention_score >= 0.65 "
2437
- "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2438
- (pid,),
2439
- )
2440
-
2441
- from superlocalmemory.core.lifecycle_state import reconcile_profile_lifecycle
2442
- reconcile_profile_lifecycle(conn, pid)
2443
- except Exception as exc:
2444
- logger.exception("run_forgetting decay failed")
2445
- return {"success": False, "error": "internal error"}
2446
-
2447
- authorization.complete()
2448
- return {"success": True, "facts_decayed": updated, "profile": pid}
2449
- except HTTPException:
2450
- raise
2451
- except Exception as e:
2452
- return _internal_error()
2453
-
2454
-
2455
- # ── 1c. GET /api/v3/quantization/stats ──────────────────────
2456
-
2457
- @router.get("/quantization/stats")
2458
- async def quantization_stats(request: Request, profile: str = ""):
2459
- """Get embedding quantization tier distribution."""
2460
- pid = _resolve_profile(request, profile)
2461
- try:
2462
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2463
- import sqlite3 as _sqlite3
2464
-
2465
- tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
2466
- total = 0
2467
- compression_ratio = 1.0
2468
-
2469
- if not DB_PATH.exists():
2470
- return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
2471
-
2472
- conn = get_read_connection(DB_PATH)
2473
- conn.row_factory = _sqlite3.Row
2474
-
2475
- try:
2476
- rows = conn.execute(
2477
- "SELECT quantization_level, COUNT(*) AS cnt "
2478
- "FROM embedding_quantization_metadata "
2479
- "WHERE profile_id = ? "
2480
- "GROUP BY quantization_level",
2481
- (pid,),
2482
- ).fetchall()
2483
- for row in rows:
2484
- level = dict(row)["quantization_level"]
2485
- cnt = dict(row)["cnt"]
2486
- if level in tiers:
2487
- tiers[level] = cnt
2488
- total += cnt
2489
- except Exception:
2490
- pass
2491
-
2492
- # Compute compression ratio from actual sizes if available
2493
- try:
2494
- size_row = conn.execute(
2495
- "SELECT "
2496
- "SUM(CASE WHEN bit_width = 32 THEN 768 * 4 ELSE "
2497
- " COALESCE(compressed_size_bytes, 768 * bit_width / 8) END) AS actual, "
2498
- "SUM(768 * 4) AS uncompressed "
2499
- "FROM embedding_quantization_metadata "
2500
- "WHERE profile_id = ?",
2501
- (pid,),
2502
- ).fetchone()
2503
- if size_row:
2504
- d = dict(size_row)
2505
- uncompressed = d.get("uncompressed") or 0
2506
- actual = d.get("actual") or 0
2507
- if actual > 0 and uncompressed > 0:
2508
- compression_ratio = round(uncompressed / actual, 2)
2509
- except Exception:
2510
- pass
2511
-
2512
- conn.close()
2513
- return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
2514
- except Exception as e:
2515
- return _internal_error()
2516
-
2517
-
2518
- # ── 1d. GET /api/v3/ccq/blocks ──────────────────────────────
2519
-
2520
- @router.get("/ccq/blocks")
2521
- async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
2522
- """Get CCQ consolidated blocks."""
2523
- pid = _resolve_profile(request, profile)
2524
- try:
2525
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2526
- import sqlite3 as _sqlite3
2527
-
2528
- if not DB_PATH.exists():
2529
- return {"blocks": [], "total": 0}
2530
-
2531
- conn = get_read_connection(DB_PATH)
2532
- conn.row_factory = _sqlite3.Row
2533
-
2534
- blocks = []
2535
- total = 0
2536
- try:
2537
- rows = conn.execute(
2538
- "SELECT block_id, content, source_fact_ids, char_count, "
2539
- "compiled_by, cluster_id, created_at "
2540
- "FROM ccq_consolidated_blocks "
2541
- "WHERE profile_id = ? "
2542
- "ORDER BY created_at DESC LIMIT ?",
2543
- (pid, limit),
2544
- ).fetchall()
2545
-
2546
- for row in rows:
2547
- d = dict(row)
2548
- source_ids = []
2549
- try:
2550
- source_ids = json.loads(d.get("source_fact_ids", "[]"))
2551
- except (json.JSONDecodeError, TypeError):
2552
- pass
2553
- blocks.append({
2554
- "block_id": d["block_id"],
2555
- "content": d["content"],
2556
- "source_fact_count": len(source_ids),
2557
- "char_count": d["char_count"],
2558
- "compiled_by": d["compiled_by"],
2559
- "cluster_id": d["cluster_id"],
2560
- "created_at": d["created_at"],
2561
- })
2562
-
2563
- count_row = conn.execute(
2564
- "SELECT COUNT(*) FROM ccq_consolidated_blocks "
2565
- "WHERE profile_id = ?",
2566
- (pid,),
2567
- ).fetchone()
2568
- total = count_row[0] if count_row else 0
2569
- except Exception:
2570
- pass
2571
-
2572
- conn.close()
2573
- return {"blocks": blocks, "total": total}
2574
- except Exception as e:
2575
- return _internal_error()
2576
-
2577
-
2578
- # ── 1e. GET /api/v3/soft-prompts ─────────────────────────────
2579
-
2580
- @router.get("/soft-prompts")
2581
- async def get_soft_prompts(request: Request, profile: str = ""):
2582
- """Get active soft prompt templates."""
2583
- pid = _resolve_profile(request, profile)
2584
- try:
2585
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2586
- import sqlite3 as _sqlite3
2587
-
2588
- if not DB_PATH.exists():
2589
- return {"prompts": [], "total": 0, "total_tokens": 0}
2590
-
2591
- conn = get_read_connection(DB_PATH)
2592
- conn.row_factory = _sqlite3.Row
2593
-
2594
- prompts = []
2595
- total_tokens = 0
2596
- try:
2597
- rows = conn.execute(
2598
- "SELECT prompt_id, category, content, confidence, "
2599
- "effectiveness, token_count, retention_score, "
2600
- "active, version, created_at, updated_at "
2601
- "FROM soft_prompt_templates "
2602
- "WHERE profile_id = ? AND active = 1 "
2603
- "ORDER BY confidence DESC",
2604
- (pid,),
2605
- ).fetchall()
2606
-
2607
- for row in rows:
2608
- d = dict(row)
2609
- tokens = d.get("token_count", 0)
2610
- total_tokens += tokens
2611
- prompts.append({
2612
- "prompt_id": d["prompt_id"],
2613
- "category": d["category"],
2614
- "content": d["content"][:200],
2615
- "confidence": round(float(d["confidence"]), 3),
2616
- "effectiveness": round(float(d.get("effectiveness", 0.5)), 3),
2617
- "token_count": tokens,
2618
- "retention_score": round(float(d.get("retention_score", 1.0)), 3),
2619
- "version": d["version"],
2620
- "created_at": d["created_at"],
2621
- })
2622
- except Exception:
2623
- pass
2624
-
2625
- conn.close()
2626
- return {"prompts": prompts, "total": len(prompts), "total_tokens": total_tokens}
2627
- except Exception as e:
2628
- return _internal_error()
2629
-
2630
-
2631
- # ── 1f. GET /api/v3/health/processes ─────────────────────────
2632
-
2633
- @router.get("/health/processes")
2634
- async def process_health(request: Request):
2635
- """Get SLM process health status."""
2636
- _resolve_profile(request)
2637
- try:
2638
- import os as _os
2639
-
2640
- processes = {
2641
- "mcp_server": {"pid": _os.getpid(), "status": "running"},
2642
- "parent": {"pid": _os.getppid(), "status": "unknown"},
2643
- }
2644
-
2645
- # Check parent process
2646
- try:
2647
- _os.kill(_os.getppid(), 0)
2648
- processes["parent"]["status"] = "running"
2649
- except ProcessLookupError:
2650
- processes["parent"]["status"] = "dead"
2651
- except PermissionError:
2652
- processes["parent"]["status"] = "running"
2653
- except OSError:
2654
- processes["parent"]["status"] = "unknown"
2655
-
2656
- # Check worker pool status
2657
- worker_status = "unavailable"
2658
- try:
2659
- from superlocalmemory.core.worker_pool import WorkerPool
2660
- pool = WorkerPool.shared()
2661
- worker_status = "running" if pool else "stopped"
2662
- except Exception:
2663
- pass
2664
- processes["worker_pool"] = {"status": worker_status}
2665
-
2666
- # Memory usage of current process (approximate)
2667
- from superlocalmemory.core.platform_utils import get_rss_mb
2668
- memory_mb = round(get_rss_mb(), 1)
2669
-
2670
- return {
2671
- "processes": processes,
2672
- "memory_mb": memory_mb,
2673
- "healthy": processes["parent"]["status"] != "dead",
2674
- }
2675
- except Exception as e:
2676
- return _internal_error()
2677
-
2678
-
2679
- # ── 1g. GET /api/v3/v33/overview ─────────────────────────────
2680
-
2681
- # ── v3.4.1: Graph Communities ──────────────────────────────────
2682
-
2683
- @router.get("/graph/communities")
2684
- async def get_graph_communities(request: Request, profile: str = ""):
2685
- """Get community assignments with TF-IDF labels, entities, and colors.
2686
-
2687
- v3.4.1: Uses TF-IDF labels from config table (computed by GraphAnalyzer
2688
- at consolidation time). Falls back to inline word frequency if labels
2689
- not yet computed.
2690
- """
2691
- pid = _resolve_profile(request, profile)
2692
- try:
2693
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2694
- import sqlite3
2695
-
2696
- if not DB_PATH.exists():
2697
- return {"communities": [], "total": 0}
2698
-
2699
- conn = get_read_connection(DB_PATH)
2700
- conn.row_factory = sqlite3.Row
2701
-
2702
- # Get community member counts and average pagerank
2703
- comm_rows = conn.execute(
2704
- "SELECT community_id, COUNT(*) AS member_count, "
2705
- "AVG(pagerank_score) AS pagerank_avg "
2706
- "FROM fact_importance "
2707
- "WHERE profile_id = ? AND community_id IS NOT NULL "
2708
- "GROUP BY community_id ORDER BY member_count DESC",
2709
- (pid,),
2710
- ).fetchall()
2711
-
2712
- if not comm_rows:
2713
- conn.close()
2714
- return {"communities": [], "total": 0}
2715
-
2716
- # Load TF-IDF labels from config table
2717
- tfidf_labels = {}
2718
- try:
2719
- label_key = "community_labels_" + pid
2720
- label_row = conn.execute(
2721
- "SELECT value FROM config WHERE key = ?",
2722
- (label_key,),
2723
- ).fetchone()
2724
- if label_row:
2725
- import json as _json
2726
- tfidf_labels = _json.loads(dict(label_row)["value"])
2727
- except Exception:
2728
- pass
2729
-
2730
- cluster_colors = [
2731
- '#667eea', '#764ba2', '#43e97b', '#38f9d7',
2732
- '#4facfe', '#00f2fe', '#f093fb', '#f5576c',
2733
- '#fa709a', '#fee140', '#30cfd0', '#330867',
2734
- ]
2735
-
2736
- communities = []
2737
- for row in comm_rows:
2738
- d = dict(row)
2739
- comm_id = d["community_id"]
2740
-
2741
- # Use TF-IDF label if available, else generate inline
2742
- label = tfidf_labels.get(str(comm_id), "")
2743
- if not label:
2744
- # Inline fallback: word frequency from top facts
2745
- try:
2746
- fact_rows = conn.execute(
2747
- "SELECT af.content FROM fact_importance fi "
2748
- "JOIN atomic_facts af ON fi.fact_id = af.fact_id "
2749
- "WHERE fi.profile_id = ? AND fi.community_id = ? "
2750
- "ORDER BY fi.pagerank_score DESC LIMIT 20",
2751
- (pid, comm_id),
2752
- ).fetchall()
2753
- from collections import defaultdict as _ddict
2754
- wf = _ddict(int)
2755
- sw = {"the", "a", "an", "is", "was", "are", "to", "of",
2756
- "in", "for", "on", "with", "at", "by", "from",
2757
- "and", "but", "or", "not", "it", "this", "that",
2758
- "i", "we", "they", "he", "she", "you", "my"}
2759
- for fr in fact_rows:
2760
- for w in (dict(fr).get("content", "")).lower().split():
2761
- w = w.strip(".,;:!?\"'()[]{}")
2762
- if len(w) > 2 and w not in sw:
2763
- wf[w] += 1
2764
- top = sorted(wf.items(), key=lambda x: x[1], reverse=True)[:3]
2765
- label = ", ".join(w for w, _ in top) if top else f"Community {comm_id}"
2766
- except Exception:
2767
- label = f"Community {comm_id}"
2768
-
2769
- # Get top entities from canonical_entities_json
2770
- top_entities = []
2771
- try:
2772
- ent_rows = conn.execute(
2773
- "SELECT af.canonical_entities_json FROM fact_importance fi "
2774
- "JOIN atomic_facts af ON fi.fact_id = af.fact_id "
2775
- "WHERE fi.profile_id = ? AND fi.community_id = ? "
2776
- "ORDER BY fi.pagerank_score DESC LIMIT 10",
2777
- (pid, comm_id),
2778
- ).fetchall()
2779
- import json as _json2
2780
- entity_counts: dict = {}
2781
- for er in ent_rows:
2782
- raw = dict(er).get("canonical_entities_json", "")
2783
- if raw:
2784
- try:
2785
- for ent in _json2.loads(raw):
2786
- entity_counts[ent] = entity_counts.get(ent, 0) + 1
2787
- except (ValueError, TypeError):
2788
- pass
2789
- top_entities = sorted(
2790
- entity_counts, key=entity_counts.get, reverse=True,
2791
- )[:5]
2792
- except Exception:
2793
- pass
2794
-
2795
- communities.append({
2796
- "community_id": comm_id,
2797
- "label": label,
2798
- "member_count": d["member_count"],
2799
- "top_entities": top_entities,
2800
- "color": cluster_colors[comm_id % len(cluster_colors)],
2801
- "pagerank_avg": round(float(d["pagerank_avg"] or 0), 4),
2802
- })
2803
-
2804
- conn.close()
2805
- return {"communities": communities, "total": len(communities)}
2806
-
2807
- except Exception as e:
2808
- return _internal_error()
2809
-
2810
-
2811
- @router.post("/graph/run-communities")
2812
- async def run_community_detection(request: Request):
2813
- """Trigger community detection manually (runs GraphAnalyzer)."""
2814
- try:
2815
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2816
- from superlocalmemory.storage.database import DatabaseManager
2817
- from superlocalmemory.storage import schema as _schema
2818
- from superlocalmemory.core.graph_analyzer import GraphAnalyzer
2819
-
2820
- pid = get_active_profile()
2821
- _require_manage_for_profile(request, pid)
2822
- authorization = authorize_route_mutation(
2823
- request,
2824
- operation="update",
2825
- source_agent_id="http-community-detection",
2826
- profile_id=pid,
2827
- )
2828
- db = DatabaseManager(DB_PATH)
2829
- db.initialize(_schema)
2830
-
2831
- analyzer = GraphAnalyzer(db)
2832
- result = analyzer.compute_and_store(pid)
2833
- authorization.complete()
2834
- return {"success": True, **result}
2835
- except HTTPException:
2836
- raise
2837
- except Exception as e:
2838
- return _internal_error()
2839
-
2840
-
2841
- @router.get("/v33/overview")
2842
- async def v33_overview(request: Request, profile: str = ""):
2843
- """Get SLM 3.3 feature overview -- all new capabilities at a glance."""
2844
- pid = _resolve_profile(request, profile)
2845
- try:
2846
- from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
2847
- import sqlite3 as _sqlite3
2848
-
2849
- overview: dict = {
2850
- "version": "3.3",
2851
- "profile": pid,
2852
- "forgetting": {"total": 0, "zones": {}},
2853
- "quantization": {"total": 0, "tiers": {}, "compression_ratio": 1.0},
2854
- "ccq": {"blocks": 0, "facts_archived": 0},
2855
- "soft_prompts": {"total": 0, "total_tokens": 0},
2856
- "hopfield": {
2857
- "available": False,
2858
- "description": "Modern Continuous Hopfield Network retrieval channel",
2859
- },
2860
- "process_health": {"healthy": True},
2861
- }
2862
-
2863
- if not DB_PATH.exists():
2864
- return overview
2865
-
2866
- conn = get_read_connection(DB_PATH)
2867
- conn.row_factory = _sqlite3.Row
2868
-
2869
- # Forgetting stats
2870
- try:
2871
- zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
2872
- rows = conn.execute(
2873
- "SELECT lifecycle_zone, COUNT(*) AS cnt "
2874
- "FROM fact_retention WHERE profile_id = ? "
2875
- "GROUP BY lifecycle_zone",
2876
- (pid,),
2877
- ).fetchall()
2878
- total_fg = 0
2879
- for row in rows:
2880
- d = dict(row)
2881
- zone = d["lifecycle_zone"]
2882
- if zone in zones:
2883
- zones[zone] = d["cnt"]
2884
- total_fg += d["cnt"]
2885
- overview["forgetting"] = {"total": total_fg, "zones": zones}
2886
- except Exception:
2887
- pass
2888
-
2889
- # Quantization stats
2890
- try:
2891
- tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
2892
- rows = conn.execute(
2893
- "SELECT quantization_level, COUNT(*) AS cnt "
2894
- "FROM embedding_quantization_metadata "
2895
- "WHERE profile_id = ? GROUP BY quantization_level",
2896
- (pid,),
2897
- ).fetchall()
2898
- total_q = 0
2899
- for row in rows:
2900
- d = dict(row)
2901
- level = d["quantization_level"]
2902
- if level in tiers:
2903
- tiers[level] = d["cnt"]
2904
- total_q += d["cnt"]
2905
- overview["quantization"] = {
2906
- "total": total_q, "tiers": tiers, "compression_ratio": 1.0,
2907
- }
2908
- except Exception:
2909
- pass
2910
-
2911
- # CCQ stats
2912
- try:
2913
- block_count = conn.execute(
2914
- "SELECT COUNT(*) FROM ccq_consolidated_blocks "
2915
- "WHERE profile_id = ?", (pid,),
2916
- ).fetchone()[0]
2917
- # Count archived facts (lifecycle='archived' from CCQ)
2918
- archived_count = 0
2919
- try:
2920
- archived_count = conn.execute(
2921
- "SELECT COUNT(*) FROM atomic_facts "
2922
- "WHERE profile_id = ? AND lifecycle = 'archived'",
2923
- (pid,),
2924
- ).fetchone()[0]
2925
- except Exception:
2926
- pass
2927
- overview["ccq"] = {
2928
- "blocks": block_count,
2929
- "facts_archived": archived_count,
2930
- }
2931
- except Exception:
2932
- pass
2933
-
2934
- # Soft prompts stats
2935
- try:
2936
- prompt_rows = conn.execute(
2937
- "SELECT COUNT(*) AS cnt, COALESCE(SUM(token_count), 0) AS tokens "
2938
- "FROM soft_prompt_templates "
2939
- "WHERE profile_id = ? AND active = 1",
2940
- (pid,),
2941
- ).fetchone()
2942
- if prompt_rows:
2943
- d = dict(prompt_rows)
2944
- overview["soft_prompts"] = {
2945
- "total": d["cnt"],
2946
- "total_tokens": d["tokens"],
2947
- }
2948
- except Exception:
2949
- pass
2950
-
2951
- # Hopfield channel availability
2952
- try:
2953
- from superlocalmemory.retrieval.hopfield_channel import HopfieldChannel # noqa: F401
2954
- overview["hopfield"]["available"] = True
2955
- except ImportError:
2956
- pass
2957
-
2958
- # Process health
2959
- try:
2960
- import os as _os
2961
- _os.kill(_os.getppid(), 0)
2962
- overview["process_health"] = {"healthy": True}
2963
- except ProcessLookupError:
2964
- overview["process_health"] = {"healthy": False}
2965
- except (PermissionError, OSError):
2966
- overview["process_health"] = {"healthy": True}
2967
-
2968
- conn.close()
2969
- return overview
2970
- except Exception as e:
2971
- return _internal_error()
2972
-
2973
-
2974
- # ── MCP Profiles ─────────────────────────────────────────────
2975
-
2976
- @router.get("/mcp/profiles")
2977
- async def get_mcp_profiles(request: Request):
2978
- """MCP profile summary: current profile, all profiles with tool counts and names.
2979
-
2980
- Reads only the env var SLM_MCP_PROFILE and the pure-data profiles module.
2981
- No engine required — safe to call at any point in the daemon lifecycle.
2982
-
2983
- Note on 'current': when SLM_MCP_PROFILE is unset the MCP server falls
2984
- back to the legacy _ESSENTIAL_TOOLS set. The UI reports 'core' for that
2985
- state because 'core' is the recommended named-profile equivalent for new
2986
- installs and is the closest documented starting point for users.
2987
- """
2988
- try:
2989
- from superlocalmemory.mcp.profiles import (
2990
- _PROFILE_DEFINITIONS,
2991
- _PROFILE_ALIASES,
2992
- PROFILE_DESCRIPTIONS,
2993
- )
2994
-
2995
- raw_profile = os.environ.get("SLM_MCP_PROFILE", "").strip().lower()
2996
- canonical = _PROFILE_ALIASES.get(raw_profile, raw_profile)
2997
- # Blank env var, "whole", or an unknown value all resolve to "core"
2998
- # for UI display purposes (safe, conservative default).
2999
- if not canonical or canonical == "whole" or canonical not in _PROFILE_DEFINITIONS:
3000
- current = "core"
3001
- else:
3002
- current = canonical
3003
-
3004
- profiles_out: dict = {}
3005
- for name, tool_set in _PROFILE_DEFINITIONS.items():
3006
- profiles_out[name] = {
3007
- "count": len(tool_set),
3008
- "tools": sorted(tool_set),
3009
- "description": PROFILE_DESCRIPTIONS.get(name, ""),
3010
- }
3011
-
3012
- all_tools: set = set()
3013
- for tool_set in _PROFILE_DEFINITIONS.values():
3014
- all_tools.update(tool_set)
3015
-
3016
- return {
3017
- "current": current,
3018
- "profiles": profiles_out,
3019
- "aliases": dict(_PROFILE_ALIASES),
3020
- "total_tools": len(all_tools),
3021
- }
3022
- except Exception:
3023
- return _internal_error()