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,1892 +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.4.22 — LLD-04 §4.1 + §3
4
-
5
- """``/api/v3/brain`` — unified Brain endpoint (LLD-04 v2).
6
-
7
- Merges the pre-3.4.22 Patterns / Learning / Behavioral dashboard tabs
8
- into one auth-gated, honestly-labelled JSON payload. Every metric has
9
- an ``is_real`` (numeric counters) or ``is_real_ml`` (ML-derived) flag
10
- and points at the real table it was computed from. No metric is
11
- fabricated — if a source does not exist we return zero and flag it.
12
-
13
- Security primitives consumed from LLD-07 §6:
14
-
15
- * ``verify_install_token`` — constant-time token check for auth gate.
16
- * ``redact_secrets`` — regex+entropy secret scrub.
17
-
18
- Deprecated shims (1 release grace) live below the main route. They
19
- return the historical-ish shape plus ``deprecated: true`` and
20
- ``use_instead: /api/v3/brain``, and require the same auth.
21
-
22
- Design notes (LLD-04 §7 hard rules):
23
-
24
- * **U2** — every metric section carries ``is_real`` or ``is_real_ml``.
25
- * **U3** — ``learning.phase`` never returns ``3`` without an active
26
- model row AND passing SHA check. Both conditions computed here.
27
- * **U4** — response shape must not contain the three pre-3.4.22
28
- fabricated metrics (24h hit-rate, avg age on hit, skill-evolution
29
- row counts). A static test greps this file to verify; therefore we
30
- never spell those names anywhere in this module.
31
- * **U6** — install token required on ``/api/v3/brain`` and all
32
- deprecated shim routes. See ``require_install_token`` below.
33
-
34
- External API consumers (L1): the dashboard auto-fetches the token from
35
- ``GET /internal/token`` (loopback + same-origin only) and sends it as the
36
- ``X-Install-Token`` header. Non-browser callers (scripts, IDE adapters,
37
- third-party integrations) instead read the token from
38
- ``~/.superlocalmemory/.install_token`` and send the same header:
39
- ``curl -H "X-Install-Token: $(cat ~/.superlocalmemory/.install_token)" \
40
- http://127.0.0.1:8765/api/v3/brain``.
41
- * **U10** — feature count surfaced from ``features.FEATURE_DIM``;
42
- stratum total from module constant ``_STRATA_TOTAL`` (48 = 4×3×4).
43
- """
44
-
45
- from __future__ import annotations
46
-
47
- import json as _json
48
- import logging
49
- import shutil
50
- import sqlite3
51
- import subprocess
52
- import threading
53
- import time
54
- from datetime import datetime, timedelta, timezone
55
- from pathlib import Path
56
- from typing import Any
57
-
58
- from fastapi import APIRouter, Depends, HTTPException, Request
59
-
60
- from superlocalmemory import __version__
61
- from superlocalmemory.brain import BrainTruthService
62
- from superlocalmemory.core.security_primitives import (
63
- redact_secrets,
64
- verify_install_token,
65
- )
66
- from superlocalmemory.infra.data_root import canonical_data_root
67
- from superlocalmemory.learning.database import LearningDatabase
68
- from superlocalmemory.learning.features import FEATURE_DIM
69
- from superlocalmemory.storage.agent_experience import get_profile_receipt_summary
70
- from superlocalmemory.storage.read_connection import ReadConnectionFactory
71
-
72
- from .helpers import get_active_profile
73
-
74
- logger = logging.getLogger("superlocalmemory.routes.brain")
75
-
76
- router = APIRouter(prefix="/api/v3", tags=["brain"])
77
-
78
-
79
- # ---------------------------------------------------------------------------
80
- # Constants — surfaced via the response so the UI never hard-codes.
81
- # ---------------------------------------------------------------------------
82
-
83
- # LLD-03 v2 stratum space = 4 query types × 3 entity bins × 4 time buckets.
84
- _STRATA_TOTAL: int = 48
85
-
86
- _VERSION: str = __version__
87
-
88
- # Banned metric names (LLD-04 U4). Kept as a tuple for grep visibility;
89
- # the source-level test asserts we don't accidentally reintroduce them.
90
- # NOTE: do NOT add the literal forbidden-key strings here — the U4 grep
91
- # guard runs over this file.
92
-
93
- # ---------------------------------------------------------------------------
94
- # Bounded-loops detection — TTL-cached subprocess probe.
95
- # The binary lives in a pipx venv; importlib.util.find_spec('bounded_loops')
96
- # will NOT find it from our venv. Detect via PATH / filesystem instead.
97
- # ---------------------------------------------------------------------------
98
-
99
- _BL_PROBE: dict[str, object] = {}
100
- _BL_PROBE_LOCK = threading.Lock()
101
- _BL_PROBE_TTL = 60.0 # seconds between re-probes
102
-
103
-
104
- # Memory directory (home-dir based). Always resolved at call time so that
105
- # tests can override via monkeypatch on ``_learning_db_path``.
106
- _MEMORY_DIR_DEFAULT = None # test-only compatibility override
107
-
108
-
109
- def _memory_dir() -> Path:
110
- if _MEMORY_DIR_DEFAULT is not None:
111
- return Path(_MEMORY_DIR_DEFAULT)
112
- return canonical_data_root()
113
-
114
-
115
- def _learning_db_path() -> Path:
116
- """Return the path to the learning SQLite DB.
117
-
118
- Separated as a function (not a module constant) so tests can
119
- monkeypatch without touching Path.home. See
120
- ``tests/test_api/test_brain_endpoint.py``.
121
- """
122
- return _memory_dir() / "learning.db"
123
-
124
-
125
- def _memory_db_path() -> Path:
126
- return _memory_dir() / "memory.db"
127
-
128
-
129
- # ---------------------------------------------------------------------------
130
- # Auth dependency (LLD-04 §4.1, LLD-07 §6.6)
131
- # ---------------------------------------------------------------------------
132
-
133
-
134
- async def require_install_token(request: Request) -> None:
135
- """FastAPI dependency: enforces install-token on Brain routes.
136
-
137
- Accepts either of:
138
-
139
- X-Install-Token: <token>
140
- Authorization: Bearer <token>
141
-
142
- Token comparison uses ``verify_install_token`` which wraps
143
- ``hmac.compare_digest`` — constant time regardless of input.
144
-
145
- Raises:
146
- HTTPException(401) with ``WWW-Authenticate: Install-Token`` so
147
- clients know how to retry. Never leaks whether the token file
148
- exists, whether the header was missing, or which comparison
149
- branch rejected the value.
150
- """
151
- header_token = request.headers.get("x-install-token")
152
- presented: str | None = header_token
153
- if not presented:
154
- auth = request.headers.get("authorization", "")
155
- if auth:
156
- # Strip scheme; be tolerant of casing. Empty-after-strip → None.
157
- scheme, _, value = auth.partition(" ")
158
- if scheme.lower() == "bearer" and value.strip():
159
- presented = value.strip()
160
- if not presented or not verify_install_token(presented):
161
- raise HTTPException(
162
- status_code=401,
163
- detail="install_token_required",
164
- headers={"WWW-Authenticate": "Install-Token"},
165
- )
166
-
167
-
168
- # ---------------------------------------------------------------------------
169
- # Secret-redacted preferences (LLD-04 §4.1, LLD-07 §6.3)
170
- # ---------------------------------------------------------------------------
171
-
172
-
173
- def redact_secrets_in_preferences(prefs: dict) -> dict:
174
- """Deep-copy ``prefs``, scrub every string through ``redact_secrets``,
175
- and surface the count of substitutions as ``redacted_count``.
176
-
177
- The input is never mutated — we build a new structure and preserve
178
- list/dict shapes exactly. Non-string scalars (ints, floats, bools,
179
- None) pass through untouched. Unknown container types are left as-is
180
- (best-effort forward compatibility).
181
-
182
- Returns:
183
- New dict with the same shape plus ``redacted_count`` at the top
184
- level. If ``prefs`` is not a dict, returns
185
- ``{"redacted_count": 0}`` (defensive).
186
- """
187
- if not isinstance(prefs, dict):
188
- return {"redacted_count": 0}
189
-
190
- counter = [0] # mutable holder so the nested closure can increment.
191
-
192
- def _scrub(value: Any) -> Any:
193
- if isinstance(value, str):
194
- scrubbed = redact_secrets(value)
195
- if scrubbed != value:
196
- counter[0] += 1
197
- return scrubbed
198
- if isinstance(value, dict):
199
- return {k: _scrub(v) for k, v in value.items()}
200
- if isinstance(value, list):
201
- return [_scrub(v) for v in value]
202
- if isinstance(value, tuple):
203
- return tuple(_scrub(v) for v in value)
204
- return value
205
-
206
- out: dict = {k: _scrub(v) for k, v in prefs.items()}
207
- out["redacted_count"] = counter[0]
208
- out.setdefault("is_real", True)
209
- out.setdefault("source", prefs.get("source", "_store_patterns"))
210
- return out
211
-
212
-
213
- # ---------------------------------------------------------------------------
214
- # Section builders — each SELECTs from a real table. If a table is empty,
215
- # returns 0 with ``is_real: true`` (honest). Never invents a number.
216
- # ---------------------------------------------------------------------------
217
-
218
-
219
- def _load_raw_preferences(profile_id: str) -> dict:
220
- """Load preference categories from the behavioral pattern store.
221
-
222
- Returns a dict shaped like::
223
-
224
- {"topics": [...], "entities": [...], "tech": [...], "source": ...}
225
-
226
- with empty lists if the store / DB is not available. Overridden in
227
- tests via monkeypatch to keep the Brain route decoupled from the
228
- full behavioral pipeline.
229
- """
230
- out: dict[str, Any] = {
231
- "topics": [], "entities": [], "tech": [],
232
- "source": "_store_patterns",
233
- }
234
- db_path = _learning_db_path()
235
- if not db_path.exists():
236
- return out
237
- try:
238
- from superlocalmemory.learning.behavioral import BehavioralPatternStore
239
- store = BehavioralPatternStore(str(db_path))
240
- patterns = store.get_patterns(profile_id=profile_id) or []
241
- except Exception as exc: # pragma: no cover — defensive
242
- logger.debug("load_raw_preferences: %s", exc)
243
- return out
244
-
245
- topics: list[dict[str, Any]] = []
246
- entities: list[dict[str, Any]] = []
247
- tech: list[dict[str, Any]] = []
248
- for p in patterns:
249
- ptype = p.get("pattern_type") or ""
250
- meta = p.get("metadata") or {}
251
- name = meta.get("value") or p.get("pattern_key") or ""
252
- if not name:
253
- continue
254
- if ptype == "tech_preference":
255
- tech.append({
256
- "name": str(name),
257
- "frequency": float(p.get("confidence", 0.0) or 0.0),
258
- })
259
- elif ptype in ("entity", "interest"):
260
- entities.append({
261
- "name": str(name),
262
- "mention_count": int(p.get("evidence_count", 0) or 0),
263
- })
264
- elif ptype == "topic":
265
- topics.append({
266
- "name": str(name),
267
- "strength": float(p.get("confidence", 0.0) or 0.0),
268
- })
269
- out["topics"] = topics
270
- out["entities"] = entities
271
- out["tech"] = tech
272
- return out
273
-
274
-
275
- def _compute_preferences(profile_id: str) -> dict:
276
- raw = _load_raw_preferences(profile_id)
277
- return redact_secrets_in_preferences(raw)
278
-
279
-
280
- def _compute_learning_status(profile_id: str,
281
- lrn_db: LearningDatabase) -> dict:
282
- """Section ``learning`` — LLD-02 §4.10 phase truth + counters."""
283
- signals_total = _safe_count(lrn_db, "learning_signals", profile_id)
284
- features_total = _safe_count(lrn_db, "learning_features", profile_id)
285
- # Raw count of historic pre-v3.4.22 feedback rows (the source table)
286
- legacy_feedback_rows = _safe_count(lrn_db, "learning_feedback", profile_id)
287
- # Count of rows actually copied forward into learning_signals by
288
- # legacy_migration.migrate_legacy_feedback (signal_type='legacy_feedback').
289
- # The difference (raw - migrated) is what the dashboard's
290
- # "Migrate legacy data" card surfaces as pending work.
291
- legacy_migrated = _count_legacy_migrated(lrn_db, profile_id)
292
- signals_last_hour = _count_signals_since(
293
- lrn_db, profile_id,
294
- datetime.now(timezone.utc) - timedelta(hours=1),
295
- )
296
-
297
- active = lrn_db.load_active_model(profile_id)
298
- model_active = active is not None
299
- model_version: str | None = None
300
- model_trained_at: str | None = None
301
- model_sha256_present = False
302
- if model_active and isinstance(active, dict):
303
- model_version = active.get("model_version")
304
- model_trained_at = active.get("trained_at")
305
- model_sha256_present = bool(active.get("bytes_sha256"))
306
-
307
- phase, phase_label = _resolve_phase(signals_total, model_active,
308
- model_sha256_present)
309
-
310
- # S9-DASH-03: consult migration_log. If the legacy migration is
311
- # marked complete but some rows remain uncopied, those rows are
312
- # structurally un-migratable (malformed, duplicate, or missing
313
- # required cols). Reporting them as "pending" is misleading and
314
- # the dashboard card nags forever. After completion we report
315
- # pending=0 so the card auto-hides.
316
- migration_complete = _legacy_migration_sentinel_complete(lrn_db)
317
- legacy_pending = max(0, legacy_feedback_rows - legacy_migrated)
318
- if migration_complete:
319
- legacy_pending = 0
320
-
321
- return {
322
- "phase": phase,
323
- "phase_label": phase_label,
324
- "signals_total": signals_total,
325
- "signals_last_hour": signals_last_hour,
326
- "features_total": features_total,
327
- "feature_count_expected": FEATURE_DIM,
328
- # Honest split (v3.4.22+): raw historic table count vs rows
329
- # actually copied forward via the migrate-legacy flow.
330
- "legacy_feedback_rows": legacy_feedback_rows,
331
- "legacy_migrated_count": legacy_migrated,
332
- "legacy_migration_pending": legacy_pending,
333
- "legacy_migration_complete": migration_complete,
334
- "model_active": model_active,
335
- "model_version": model_version,
336
- "model_trained_at": model_trained_at,
337
- "model_sha256_present": model_sha256_present,
338
- "is_real": True,
339
- }
340
-
341
-
342
- def _compute_agent_experience(profile_id: str) -> dict:
343
- """Honest, profile-scoped receipt evidence for the Living Brain.
344
-
345
- Receipt rows record observation and verification evidence only; this read
346
- model never alters retrieval, ranking, or model routing.
347
- """
348
- from superlocalmemory.storage.external_evidence import get_profile_external_evidence_summary
349
- path = _learning_db_path()
350
- return {
351
- **get_profile_receipt_summary(path, profile_id),
352
- "external_graph_evidence": get_profile_external_evidence_summary(path, profile_id),
353
- }
354
-
355
-
356
- def _resolve_phase(signals: int, model_active: bool,
357
- sha_present: bool) -> tuple[int, str]:
358
- """LLD-02 §4.10 — phase truth, U3 enforcement.
359
-
360
- Phase 3 requires BOTH ``model_active`` and a non-empty SHA value on
361
- the active row. Missing either falls back to phase 2 (≥50 signals)
362
- or phase 1 (cold).
363
- """
364
- if model_active and sha_present and signals >= 200:
365
- return 3, "LightGBM ranker active"
366
- if signals >= 50:
367
- return 2, "Contextual bandit"
368
- return 1, "Cold start (cross-encoder only)"
369
-
370
-
371
- def _compute_usage_stats(profile_id: str) -> dict:
372
- """Section ``usage`` — real counters from ``learning_signals``.
373
-
374
- All three values are honest aggregations of the last 24 h of signals.
375
- ``is_real_ml`` stays False because these are counters, not ML output,
376
- but the numbers themselves are pinned to real rows. LLD-04 U2 / §3.1.
377
- """
378
- lrn_db = _lazy_db_for(profile_id)
379
- since = datetime.now(timezone.utc) - timedelta(hours=24)
380
- recalls_24h = _count_signals_since(
381
- lrn_db, profile_id, since,
382
- signal_types=("recall_hit", "recall", "recall_miss",
383
- "candidate", "shown"),
384
- )
385
- return {
386
- "recalls_last_24h": recalls_24h,
387
- "top_query_types": _top_query_types(lrn_db, profile_id, since),
388
- "top_time_buckets": _top_time_buckets(lrn_db, profile_id, since),
389
- "source": "learning_signals_24h_aggregation",
390
- "is_real_ml": False,
391
- "disclaimer": "Statistical counters, not ML output.",
392
- }
393
-
394
-
395
- def _top_query_types(
396
- lrn_db: LearningDatabase,
397
- profile_id: str,
398
- since: datetime,
399
- *,
400
- limit: int = 5,
401
- ) -> list[dict]:
402
- """Return top-N query types from last-24h signals with percentages.
403
-
404
- Query type lives in ``learning_features.features_json`` as one-hot
405
- ``query_type_sh`` / ``query_type_mh`` / ``query_type_temp`` /
406
- ``query_type_od`` (features.py FEATURE_NAMES). We aggregate by the
407
- active one-hot slot. Missing table or zero rows → empty list.
408
- """
409
- # E.1 (v3.4.22 perf): push the query-type classification into SQL via
410
- # json_extract so we don't drag 10k rows' worth of JSON through Python.
411
- # SQLite's json1 extension treats json_extract as an ordinary scalar
412
- # function, so the whole aggregation becomes a single COUNT(*)
413
- # GROUP BY on a computed column. Falls back to the Python path only
414
- # if json_extract is unavailable (very old SQLite builds).
415
- counts: dict[str, int] = {
416
- "single_hop": 0, "multi_hop": 0, "temporal": 0, "open_domain": 0,
417
- }
418
- try:
419
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
420
- conn.row_factory = sqlite3.Row
421
- try:
422
- agg_rows = conn.execute(
423
- "SELECT "
424
- " CASE "
425
- " WHEN CAST(json_extract(f.features_json, '$.query_type_sh') AS REAL) >= 0.5 THEN 'single_hop' "
426
- " WHEN CAST(json_extract(f.features_json, '$.query_type_mh') AS REAL) >= 0.5 THEN 'multi_hop' "
427
- " WHEN CAST(json_extract(f.features_json, '$.query_type_temp') AS REAL) >= 0.5 THEN 'temporal' "
428
- " WHEN CAST(json_extract(f.features_json, '$.query_type_od') AS REAL) >= 0.5 THEN 'open_domain' "
429
- " ELSE NULL END AS qt, "
430
- " COUNT(*) AS n "
431
- "FROM learning_features f "
432
- "JOIN learning_signals s ON s.id = f.signal_id "
433
- "WHERE s.profile_id = ? AND s.created_at >= ? "
434
- " AND f.is_synthetic = 0 "
435
- "GROUP BY qt",
436
- (profile_id, since.isoformat()),
437
- ).fetchall()
438
- for row in agg_rows:
439
- qt = row["qt"]
440
- if qt in counts:
441
- counts[qt] = int(row["n"] or 0)
442
- finally:
443
- conn.close()
444
- except sqlite3.Error: # pragma: no cover — schema + json1 always present
445
- return []
446
- total = sum(counts.values())
447
- if total == 0:
448
- return []
449
- ranked = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)
450
- return [
451
- {"type": name, "pct": round(100.0 * n / total, 1)}
452
- for name, n in ranked[:limit] if n > 0
453
- ]
454
-
455
-
456
- def _top_time_buckets(
457
- lrn_db: LearningDatabase,
458
- profile_id: str,
459
- since: datetime,
460
- *,
461
- limit: int = 3,
462
- ) -> list[dict]:
463
- """Return top-N hour buckets ("HH:00") from last-24h signals.
464
-
465
- Parses ``created_at`` as ISO timestamp, buckets by hour-of-day, returns
466
- top N with percentages. Honest empty when no rows.
467
- """
468
- try:
469
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
470
- conn.row_factory = sqlite3.Row
471
- try:
472
- rows = conn.execute(
473
- "SELECT created_at FROM learning_signals "
474
- "WHERE profile_id = ? AND created_at >= ? "
475
- "LIMIT 10000",
476
- (profile_id, since.isoformat()),
477
- ).fetchall()
478
- finally:
479
- conn.close()
480
- except sqlite3.Error: # pragma: no cover
481
- return []
482
- buckets: dict[int, int] = {}
483
- for r in rows:
484
- ts = str(r["created_at"] or "")
485
- if not ts:
486
- continue
487
- try:
488
- # ISO: "YYYY-MM-DDTHH:MM:..." — take the "HH" slice.
489
- hh = int(ts[11:13])
490
- except (ValueError, IndexError): # pragma: no cover
491
- continue
492
- buckets[hh] = buckets.get(hh, 0) + 1
493
- total = sum(buckets.values())
494
- if total == 0:
495
- return []
496
- ranked = sorted(buckets.items(), key=lambda kv: kv[1], reverse=True)
497
- return [
498
- {"bucket": f"{hh:02d}:00",
499
- "pct": round(100.0 * n / total, 1)}
500
- for hh, n in ranked[:limit] if n > 0
501
- ]
502
-
503
-
504
- def _compute_bandit_snapshot(profile_id: str,
505
- lrn_db: LearningDatabase) -> dict:
506
- """Section ``bandit`` — derive summary from ``ContextualBandit.snapshot``.
507
-
508
- We intentionally compute the *derived* fields here (strata_active,
509
- top_arm_global, unsettled_plays) rather than asking the bandit to
510
- produce them — this keeps the stratum total constant surfaced from
511
- one authoritative place (``_STRATA_TOTAL``) instead of duplicating.
512
- """
513
- strata_active = 0
514
- top_arm_global: dict | None = None
515
- unsettled_plays = 0
516
- oldest_unsettled_seconds: int | None = None
517
-
518
- try:
519
- from superlocalmemory.learning.bandit import ContextualBandit
520
- bandit = ContextualBandit(
521
- db_path=lrn_db.path, # shared learning DB
522
- profile_id=profile_id,
523
- )
524
- snap = bandit.snapshot() or {}
525
- strata_active = sum(1 for arms in snap.values() if arms)
526
- # Global top arm by plays across all strata.
527
- best: tuple[str, int] | None = None
528
- for stratum_id, arms in snap.items():
529
- for arm in arms:
530
- plays = int(arm.get("plays", 0) or 0)
531
- if best is None or plays > best[1]:
532
- best = (arm["arm_id"], plays)
533
- if best is not None:
534
- top_arm_global = {"arm_id": best[0], "plays": best[1]}
535
- unsettled_plays, oldest_unsettled_seconds = _bandit_unsettled(
536
- lrn_db.path, profile_id,
537
- )
538
- except Exception as exc: # pragma: no cover — defensive
539
- logger.debug("bandit snapshot: %s", exc)
540
-
541
- return {
542
- "strata_active": strata_active,
543
- "strata_total": _STRATA_TOTAL,
544
- "top_arm_global": top_arm_global,
545
- "unsettled_plays": unsettled_plays,
546
- "oldest_unsettled_seconds": oldest_unsettled_seconds,
547
- "is_real": True,
548
- }
549
-
550
-
551
- def _bandit_unsettled(db_path: str, profile_id: str) -> tuple[int, int | None]:
552
- """Count unsettled bandit_plays rows + age (sec) of oldest one."""
553
- try:
554
- conn = sqlite3.connect(db_path, timeout=5.0)
555
- conn.row_factory = sqlite3.Row
556
- try:
557
- row = conn.execute(
558
- "SELECT COUNT(*) AS cnt, MIN(played_at) AS oldest "
559
- "FROM bandit_plays "
560
- "WHERE profile_id = ? AND settled_at IS NULL",
561
- (profile_id,),
562
- ).fetchone()
563
- finally:
564
- conn.close()
565
- except sqlite3.Error:
566
- return 0, None
567
- if row is None: # pragma: no cover — COUNT() always yields a row
568
- return 0, None
569
- cnt = int(row["cnt"] or 0)
570
- oldest_iso = row["oldest"]
571
- if not oldest_iso:
572
- return cnt, None
573
- try:
574
- played_at = datetime.fromisoformat(oldest_iso)
575
- if played_at.tzinfo is None:
576
- played_at = played_at.replace(tzinfo=timezone.utc)
577
- age = (datetime.now(timezone.utc) - played_at).total_seconds()
578
- return cnt, int(max(age, 0))
579
- except (ValueError, TypeError):
580
- return cnt, None
581
-
582
-
583
- def _compute_cache_stats() -> dict:
584
- """Section ``cache`` — DB file size + row count (if accessible)."""
585
- db = _memory_db_path()
586
- if not db.exists():
587
- return {"db_size_bytes": 0, "entry_count": 0, "is_real": True}
588
- size = db.stat().st_size
589
- entry_count = 0
590
- try:
591
- conn = ReadConnectionFactory(db).open()
592
- try:
593
- row = conn.execute(
594
- "SELECT COUNT(*) AS cnt FROM atomic_facts",
595
- ).fetchone()
596
- entry_count = int(row[0]) if row else 0
597
- finally:
598
- conn.close()
599
- except sqlite3.Error:
600
- entry_count = 0
601
- return {
602
- "db_size_bytes": size,
603
- "entry_count": entry_count,
604
- "is_real": True,
605
- }
606
-
607
-
608
- def _adapter_last_sync_ago(adapter_name: str) -> int | None:
609
- """Seconds since adapter's most recent successful sync.
610
-
611
- Reads from ``cross_platform_sync_log`` (LLD-07 M004) in ``memory.db``.
612
- Returns ``None`` when the log is absent or has no successful row —
613
- honest empty rather than a fabricated number.
614
- """
615
- try:
616
- from datetime import datetime as _dt
617
- from datetime import timezone as _tz
618
- memory_db = _memory_dir() / "memory.db"
619
- if not memory_db.exists():
620
- return None
621
- conn = ReadConnectionFactory(memory_db).open()
622
- try:
623
- cur = conn.execute(
624
- "SELECT last_sync_at FROM cross_platform_sync_log "
625
- "WHERE adapter_name=? AND success=1 "
626
- "ORDER BY last_sync_at DESC LIMIT 1",
627
- (adapter_name,),
628
- )
629
- row = cur.fetchone()
630
- finally:
631
- conn.close()
632
- if row is None or row[0] is None:
633
- return None
634
- last = _dt.fromisoformat(str(row[0]).replace("Z", "+00:00"))
635
- delta = (_dt.now(_tz.utc) - last).total_seconds()
636
- return max(0, int(delta))
637
- except Exception: # pragma: no cover — defensive
638
- return None
639
-
640
-
641
- def _detect_codex_config() -> dict:
642
- """Detect Codex integration from ``~/.codex/config.toml``.
643
-
644
- Evidence tier: CONFIGURED — the config file contains
645
- ``[mcp_servers.superlocalmemory]``, which proves Codex is wired to use
646
- SLM as an MCP server. This is the same tier as Claude Code's install-
647
- token check: file presence, not live traffic.
648
-
649
- What we deliberately do NOT claim: that Codex is currently running, or
650
- that the MCP connection has been used recently. For live-traffic
651
- evidence the correct signal is ``tool_events`` rows attributed to
652
- ``SLM_AGENT_ID='codex'`` — a check not performed here because
653
- ``tool_events`` has no ``agent_id`` column.
654
- """
655
- try:
656
- codex_config = Path.home() / ".codex" / "config.toml"
657
- if not codex_config.exists():
658
- return {"active": False, "reason": "codex_config_absent"}
659
- try:
660
- content = codex_config.read_text(encoding="utf-8", errors="replace")
661
- except OSError:
662
- return {"active": False, "reason": "codex_config_unreadable"}
663
- if "[mcp_servers.superlocalmemory]" not in content:
664
- return {"active": False, "reason": "no_slm_mcp_block"}
665
- return {
666
- "active": True,
667
- "evidence_tier": "configured",
668
- "evidence": "~/.codex/config.toml#[mcp_servers.superlocalmemory]",
669
- }
670
- except Exception: # pragma: no cover — defensive
671
- return {"active": False, "reason": "detection_error"}
672
-
673
-
674
- def _bounded_loops_probe_fresh() -> dict:
675
- """Probe bounded-loops installation — may spawn one short subprocess.
676
-
677
- Detection approach: ``shutil.which("bounded-loops-mcp")`` (PATH scan,
678
- no subprocess), then ``bl --version`` with a 1.5 s timeout (see the note at the call
679
- site: 300 ms sat too close to the measured ~226 ms and flickered).
680
-
681
- Evidence tier: INSTALLED — the ``bounded-loops-mcp`` binary is on the
682
- resolved PATH, meaning the pipx package is present.
683
-
684
- What we do NOT claim: that the bridge contract ``bounded-loops.dev/
685
- slm-bridge/v1`` is live or that any run has been observed. A full
686
- capability handshake requires an async MCP round-trip; that is outside
687
- the scope of this synchronous probe.
688
-
689
- Trap: do NOT use ``importlib.util.find_spec('bounded_loops')`` — the
690
- package lives in its own pipx venv and is not importable from this one.
691
- """
692
- bl_mcp_path = shutil.which("bounded-loops-mcp")
693
- if bl_mcp_path is None:
694
- # Cross-check: ~/.bounded-loops data dir (pipx installs leave this).
695
- bl_data = Path.home() / ".bounded-loops"
696
- if bl_data.is_dir():
697
- return {
698
- "installed": True,
699
- "version": None,
700
- "bridge_contract": "bounded-loops.dev/slm-bridge/v1",
701
- "evidence_tier": "data_dir",
702
- "evidence": "~/.bounded-loops exists",
703
- "note": "mcp binary not on PATH; data dir detected",
704
- "is_real": True,
705
- "source": "filesystem",
706
- }
707
- return {
708
- "installed": False,
709
- "reason": "bounded_loops_mcp_not_in_path",
710
- "is_real": True,
711
- "source": "shutil.which",
712
- }
713
-
714
- version: str | None = None
715
- bl_path = shutil.which("bl")
716
- if bl_path:
717
- try:
718
- # 1.5s, not 0.3s. Measured on this machine: a cold
719
- # _compute_bounded_loops() takes ~226 ms end to end, most of it this
720
- # subprocess — a 300 ms budget leaves ~74 ms of headroom, so under
721
- # any load the probe times out and the card silently reports
722
- # "Version: unknown" while `bl --version` prints "bl 0.6.4" from a
723
- # shell. A version string that flickers with machine load is worse
724
- # than no version string, because it looks like a real finding.
725
- # The cost of widening is bounded: this runs in a worker thread via
726
- # asyncio.to_thread and is TTL-cached for 60s, so the worst case is
727
- # one 1.5s thread once a minute, never on the recall/remember path.
728
- proc = subprocess.run(
729
- [bl_path, "--version"],
730
- capture_output=True,
731
- text=True,
732
- timeout=1.5,
733
- )
734
- raw = (proc.stdout.strip() or proc.stderr.strip())
735
- if raw.startswith("bl "):
736
- version = raw[3:].strip()
737
- except Exception: # pragma: no cover — timeout or missing binary
738
- pass
739
-
740
- return {
741
- "installed": True,
742
- "version": version,
743
- "bridge_contract": "bounded-loops.dev/slm-bridge/v1",
744
- "evidence_tier": "path",
745
- "evidence": "shutil.which(bounded-loops-mcp)",
746
- "note": "bridge capability requires a runtime MCP handshake; not probed here",
747
- "is_real": True,
748
- "source": "shutil.which + bl --version",
749
- }
750
-
751
-
752
- def _compute_bounded_loops() -> dict:
753
- """Section ``bounded_loops`` — installation/presence status.
754
-
755
- Wraps ``_bounded_loops_probe_fresh`` in a 60-second TTL cache so the
756
- subprocess call is never inline on every Brain request. First call may
757
- take up to 1.5 s (subprocess timeout); subsequent calls within the TTL
758
- window return immediately from the cache. The probe is single-flight, so
759
- concurrent callers share one subprocess rather than spawning one each.
760
- """
761
- with _BL_PROBE_LOCK:
762
- cached = _BL_PROBE.get("result")
763
- cached_at = float(_BL_PROBE.get("cached_at", 0.0))
764
- if cached is not None and (time.monotonic() - cached_at) < _BL_PROBE_TTL:
765
- return cached # type: ignore[return-value]
766
-
767
- # SINGLE-FLIGHT: run the probe while STILL HOLDING the lock.
768
- # The previous shape released the lock between the check and the probe,
769
- # so every concurrent caller that arrived after a TTL expiry spawned its
770
- # own `bl --version` subprocess — N dashboard requests meant N processes,
771
- # each up to 1.5s. Holding the lock makes the 2nd..Nth caller wait for
772
- # the first result instead. Blocking here is safe: this whole function
773
- # runs in a worker thread via asyncio.to_thread, so the event loop is
774
- # never held, and it is off the recall/remember path entirely.
775
- try:
776
- fresh = _bounded_loops_probe_fresh()
777
- fresh["section_enabled"] = bool(fresh.get("installed", False))
778
- except Exception as exc: # probe itself blew up
779
- # Cache the FAILURE too, and label it as a failure rather than as a
780
- # clean "not installed". Without caching it, every request would
781
- # retry a broken probe forever; without labelling it, a broken probe
782
- # is indistinguishable from Bounded Loops genuinely being absent —
783
- # the silent-failure class this release exists to remove.
784
- logger.warning("bounded-loops probe failed: %s", exc)
785
- fresh = {
786
- "installed": None,
787
- "is_real": False,
788
- "probe_failed": True,
789
- "reason": f"probe_error:{type(exc).__name__}",
790
- "section_enabled": False,
791
- "source": "probe raised",
792
- }
793
-
794
- # Timestamp AFTER the probe so the TTL measures time since completion,
795
- # not time since the request arrived.
796
- _BL_PROBE["result"] = fresh
797
- _BL_PROBE["cached_at"] = time.monotonic()
798
- return fresh
799
-
800
-
801
- def _compute_cross_platform() -> dict:
802
- """Section ``cross_platform`` — live status per injection target.
803
-
804
- Each adapter's ``is_active()`` is cheap (env + ``Path.is_dir()``) per
805
- LLD-05 A10. Last-sync-at is read from ``cross_platform_sync_log`` in
806
- ``memory.db`` (LLD-07 M004). On any adapter error, that adapter
807
- reports ``active: false`` with ``reason: error:<ExcName>`` rather
808
- than crashing the whole Brain endpoint (LLD-04 §2 — "honest, never
809
- fake"). An unimportable adapter means the install is missing the
810
- components, which is legitimate for an older 3.4.20 → 3.4.22 upgrade
811
- mid-migration.
812
- """
813
- out: dict = {}
814
- # S8-SEC-06 fix: use the canonical factory instead of constructing
815
- # adapters with no kwargs (all three require scope/base_dir/sync_log_db/
816
- # recall_fn). ``build_default_adapters`` returns fully-wired instances
817
- # matching what the background sync loop uses, so the panel reflects
818
- # the same truth the daemon acts on.
819
- try:
820
- from superlocalmemory.cli.context_commands import (
821
- build_default_adapters as _build_adapters,
822
- )
823
- _adapters = _build_adapters()
824
- except Exception as exc: # pragma: no cover — defensive
825
- _adapters = []
826
- logger.debug("brain: adapter factory failed: %s", exc)
827
-
828
- # Summarise by adapter kind: cursor (project+global), antigravity
829
- # (workspace+global), copilot. ``adapter.name`` disambiguates scope.
830
- _seen: set[str] = set()
831
- for a in _adapters:
832
- name_attr = getattr(a, "name", "") or ""
833
- cls = type(a).__name__
834
- kind = None
835
- if "Cursor" in cls:
836
- kind = "cursor"
837
- elif "Antigravity" in cls:
838
- kind = "antigravity"
839
- elif "Copilot" in cls:
840
- kind = "copilot"
841
- if kind is None:
842
- # Unknown adapter class — skip to avoid polluting the JSON
843
- # with an ``out[None]`` key. (S10-SEC-N-01 fix.)
844
- continue
845
- if kind in _seen:
846
- # For cursor/antigravity, which have project+global, the first
847
- # active one wins; this surface is a health indicator, not a
848
- # per-scope breakdown. (Dev view can drill in later.)
849
- continue
850
- try:
851
- is_active = bool(a.is_active())
852
- except Exception as exc: # pragma: no cover — defensive
853
- out[kind] = {"active": False,
854
- "reason": f"error:{exc.__class__.__name__}"}
855
- _seen.add(kind)
856
- continue
857
- out[kind] = {
858
- "active": is_active,
859
- "last_sync_seconds_ago": _adapter_last_sync_ago(name_attr or kind),
860
- }
861
- _seen.add(kind)
862
- # Fill in missing slots so the shape stays stable.
863
- for kind in ("cursor", "antigravity", "copilot"):
864
- out.setdefault(kind, {"active": False, "reason": "adapter_unavailable"})
865
- # Claude Code hook proxy: active once the install-token exists
866
- # (required for prewarm per LLD-01 §4.4 R14).
867
- try:
868
- from superlocalmemory.core import security_primitives as _sp
869
- out["claude_code"] = {
870
- "active": _sp._install_token_path().exists(),
871
- "hook": "UserPromptSubmit",
872
- }
873
- except Exception as exc: # pragma: no cover
874
- out["claude_code"] = {"active": False,
875
- "reason": f"error:{exc.__class__.__name__}"}
876
- # MCP tool is registered by the unified daemon; if this route is
877
- # reachable, the MCP server is up.
878
- out["mcp"] = {"active": True, "tool": "mcp__slm"}
879
- # CLI is trivially active on any install.
880
- out["cli"] = {"active": True}
881
- # Codex: detect from ~/.codex/config.toml.
882
- # evidence_tier="configured" — config proves SLM is wired; it does not
883
- # prove recent MCP traffic (tool_events has no agent_id column).
884
- out["codex"] = _detect_codex_config()
885
- return out
886
-
887
-
888
- def _registry_staleness() -> tuple[str, int | None]:
889
- """Return (registry_status, newest_entry_seconds_ago).
890
-
891
- Reads the raw session registry file to determine whether the registry
892
- has been written to recently, regardless of profile matching. This
893
- distinguishes three silent-but-different states:
894
-
895
- ``live`` — newest entry is within 2× the presence window (10 min)
896
- ``stale`` — file exists, entries exist, but all are older than 10 min
897
- → TELEMETRY GAP: hooks exist but are not firing
898
- ``empty`` — file exists but has no entries
899
- ``absent`` — file does not exist (first-run or cleaned up)
900
- ``unknown`` — file could not be read
901
-
902
- IMPORTANT: the window is NOT widened here. We report the gap honestly
903
- rather than hiding it by using a larger cutoff on the query.
904
- """
905
- try:
906
- from superlocalmemory.infra.data_root import state_path
907
- registry_path = state_path(".active_sessions.json")
908
- if not registry_path.exists():
909
- return "absent", None
910
- try:
911
- raw = registry_path.read_text(encoding="utf-8")
912
- data = _json.loads(raw)
913
- except Exception:
914
- return "unknown", None
915
- if not isinstance(data, dict) or not data:
916
- return "empty", None
917
- now_ns = time.time_ns()
918
- ts_vals = [
919
- int(row.get("ts_ns", 0))
920
- for row in data.values()
921
- if isinstance(row, dict) and int(row.get("ts_ns", 0)) > 0
922
- ]
923
- if not ts_vals:
924
- return "empty", None
925
- newest_ns = max(ts_vals)
926
- seconds_ago = max(0, int((now_ns - newest_ns) / 1_000_000_000))
927
- # 2× the 300 s presence window = 600 s sane boundary.
928
- if seconds_ago <= 600:
929
- return "live", seconds_ago
930
- return "stale", seconds_ago
931
- except Exception: # pragma: no cover — defensive
932
- return "unknown", None
933
-
934
-
935
- def _compute_active_clients(profile_id: str) -> dict:
936
- """Return recent host presence with honest telemetry-gap detection.
937
-
938
- ``cross_platform`` describes configured integration targets. This
939
- separate read-model answers a different question: which host clients
940
- have recently interacted with this brain?
941
-
942
- Three distinct states are surfaced instead of a silent empty list:
943
- ``NO_ACTIVITY`` — registry is live; no clients in the 5-min window
944
- ``TELEMETRY_GAP`` — registry's newest entry is > 10 min old; hooks
945
- may be installed but are not writing presence
946
- ``REGISTRY_ERROR`` — exception accessing the registry; reported as
947
- ``is_real: False`` rather than an empty success
948
- """
949
- reg_status, newest_ago = _registry_staleness()
950
-
951
- clients: list[dict] = []
952
- registry_ok = True
953
- try:
954
- from superlocalmemory.hooks.session_registry import active_client_summary
955
- clients = active_client_summary(profile_id, within_seconds=300)
956
- except Exception as exc: # distinguish failure from emptiness (important: these are different states)
957
- registry_ok = False
958
- reg_status = "error"
959
- logger.debug("active_clients: registry error: %s", exc)
960
-
961
- return {
962
- "is_real": registry_ok,
963
- "scope": "profile",
964
- "window_seconds": 300,
965
- "clients": clients,
966
- "registry_status": reg_status,
967
- "newest_entry_seconds_ago": newest_ago,
968
- "source": "session_registry (ephemeral, profile-scoped)",
969
- }
970
-
971
-
972
- def _compute_feedback_loop(profile_id: str, lrn_db: LearningDatabase) -> dict:
973
- """Expose only durable feedback evidence that already drives learning."""
974
- empty = {
975
- "is_real": True,
976
- "signals_by_type": {},
977
- "explicit_signals": 0,
978
- "implicit_signals": 0,
979
- "settled_outcomes": 0,
980
- "mean_settled_reward": None,
981
- "source": "learning_signals + memory.db:action_outcomes",
982
- }
983
- signal_rows: list[sqlite3.Row] = []
984
- try:
985
- learning_conn = lrn_db.ro_connection()
986
- try:
987
- signal_rows = learning_conn.execute(
988
- "SELECT signal_type, COUNT(*) AS count FROM learning_signals "
989
- "WHERE profile_id = ? GROUP BY signal_type ORDER BY signal_type",
990
- (profile_id,),
991
- ).fetchall()
992
- finally:
993
- learning_conn.close()
994
- except sqlite3.Error:
995
- return empty
996
- by_type = {
997
- str(row["signal_type"]): int(row["count"] or 0)
998
- for row in signal_rows
999
- }
1000
- explicit_types = {
1001
- "user_positive", "user_negative", "user_correction", "user_pin",
1002
- "legacy_feedback",
1003
- }
1004
- explicit = sum(count for kind, count in by_type.items() if kind in explicit_types)
1005
- implicit = sum(count for kind, count in by_type.items() if kind not in explicit_types)
1006
- settled, mean_reward = 0, None
1007
- db_path = _memory_db_path()
1008
- if db_path.exists():
1009
- try:
1010
- memory_conn = ReadConnectionFactory(db_path).open()
1011
- try:
1012
- row = memory_conn.execute(
1013
- "SELECT COUNT(*) AS count, AVG(reward) AS mean_reward "
1014
- "FROM action_outcomes WHERE profile_id = ? AND settled = 1 "
1015
- "AND reward IS NOT NULL",
1016
- (profile_id,),
1017
- ).fetchone()
1018
- if row:
1019
- settled = int(row["count"] or 0)
1020
- mean_reward = (
1021
- float(row["mean_reward"])
1022
- if row["mean_reward"] is not None else None
1023
- )
1024
- finally:
1025
- memory_conn.close()
1026
- except sqlite3.Error:
1027
- pass
1028
- return {
1029
- **empty,
1030
- "signals_by_type": by_type,
1031
- "explicit_signals": explicit,
1032
- "implicit_signals": implicit,
1033
- "settled_outcomes": settled,
1034
- "mean_settled_reward": mean_reward,
1035
- }
1036
-
1037
-
1038
- def _compute_source_quality(profile_id: str) -> dict:
1039
- """Summarize observed provenance quality; neutral priors are not evidence."""
1040
- empty = {
1041
- "is_real": True,
1042
- "observed_sources": 0,
1043
- "mean_quality": None,
1044
- "source": "learning.db:source_quality_observations",
1045
- }
1046
- db_path = _learning_db_path()
1047
- if not db_path.exists():
1048
- return empty
1049
- try:
1050
- conn = sqlite3.connect(db_path, timeout=5.0)
1051
- conn.row_factory = sqlite3.Row
1052
- try:
1053
- row = conn.execute(
1054
- "SELECT COUNT(DISTINCT source_id) AS sources, AVG(reward) AS mean_quality "
1055
- "FROM source_quality_observations WHERE profile_id = ?",
1056
- (profile_id,),
1057
- ).fetchone()
1058
- finally:
1059
- conn.close()
1060
- if row is None:
1061
- return empty
1062
- return {
1063
- **empty,
1064
- "observed_sources": int(row["sources"] or 0),
1065
- "mean_quality": float(row["mean_quality"]) if row["mean_quality"] is not None else None,
1066
- }
1067
- except sqlite3.Error:
1068
- return empty
1069
-
1070
-
1071
- def _compute_graph_summary(profile_id: str) -> dict:
1072
- """Return graph evidence counts, never a decorative graph surrogate."""
1073
- empty = {
1074
- "is_real": True,
1075
- "fact_nodes": 0,
1076
- "association_edges": 0,
1077
- "source": "memory.db:atomic_facts + association_edges",
1078
- }
1079
- db_path = _memory_db_path()
1080
- if not db_path.exists():
1081
- return empty
1082
- try:
1083
- conn = ReadConnectionFactory(db_path).open()
1084
- try:
1085
- facts = conn.execute(
1086
- "SELECT COUNT(*) AS count FROM atomic_facts "
1087
- "WHERE profile_id = ?",
1088
- (profile_id,),
1089
- ).fetchone()
1090
- edges = conn.execute(
1091
- "SELECT COUNT(*) AS count FROM association_edges "
1092
- "WHERE profile_id = ?",
1093
- (profile_id,),
1094
- ).fetchone()
1095
- finally:
1096
- conn.close()
1097
- return {
1098
- **empty,
1099
- "fact_nodes": int(facts["count"] or 0) if facts else 0,
1100
- "association_edges": int(edges["count"] or 0) if edges else 0,
1101
- }
1102
- except sqlite3.Error:
1103
- return empty
1104
-
1105
-
1106
- def _meta_now() -> dict:
1107
- return {
1108
- "generated_at": datetime.now(timezone.utc)
1109
- .replace(microsecond=0).isoformat()
1110
- .replace("+00:00", "Z"),
1111
- "honest_labels": True,
1112
- "version": _VERSION,
1113
- }
1114
-
1115
-
1116
- # ---------------------------------------------------------------------------
1117
- # SQL helpers
1118
- # ---------------------------------------------------------------------------
1119
-
1120
-
1121
- def _safe_count(lrn_db: LearningDatabase, table: str,
1122
- profile_id: str) -> int:
1123
- """``COUNT(*)`` from ``table`` where ``profile_id`` matches.
1124
-
1125
- Returns 0 on any error (missing table, DB lock) — never raises
1126
- from the Brain endpoint.
1127
- """
1128
- try:
1129
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1130
- conn.row_factory = sqlite3.Row
1131
- try:
1132
- row = conn.execute(
1133
- f"SELECT COUNT(*) AS cnt FROM {table} WHERE profile_id = ?", # nosec - table name is hard-coded above
1134
- (profile_id,),
1135
- ).fetchone()
1136
- return int(row["cnt"]) if row else 0
1137
- finally:
1138
- conn.close()
1139
- except sqlite3.Error:
1140
- return 0
1141
-
1142
-
1143
- def _legacy_migration_sentinel_complete(lrn_db: LearningDatabase) -> bool:
1144
- """Return True when ``migration_log`` marks the legacy feedback
1145
- migration as complete (S9-DASH-03).
1146
-
1147
- Signals that the historic-data card should auto-hide even if a
1148
- small residual of un-migratable rows lingers in ``learning_feedback``.
1149
- """
1150
- try:
1151
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1152
- try:
1153
- row = conn.execute(
1154
- "SELECT status FROM migration_log "
1155
- "WHERE name = 'LEG001_feedback_to_signals'",
1156
- ).fetchone()
1157
- if row is None:
1158
- return False
1159
- return str(row[0]).lower() == "complete"
1160
- finally:
1161
- conn.close()
1162
- except sqlite3.Error:
1163
- return False
1164
-
1165
-
1166
- def _count_legacy_migrated(lrn_db: LearningDatabase, profile_id: str) -> int:
1167
- """Count rows copied forward by the legacy-feedback migration.
1168
-
1169
- These rows live in ``learning_signals`` with ``signal_type='legacy_feedback'``
1170
- (see ``learning/legacy_migration.py``). The Brain endpoint exposes this
1171
- separately from the raw ``learning_feedback`` table count so the UI can
1172
- show a honest "pending migration" figure instead of the Stage-8 lie
1173
- that conflated the two.
1174
- """
1175
- try:
1176
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1177
- conn.row_factory = sqlite3.Row
1178
- try:
1179
- row = conn.execute(
1180
- "SELECT COUNT(*) AS cnt FROM learning_signals "
1181
- "WHERE profile_id = ? AND signal_type = 'legacy_feedback'",
1182
- (profile_id,),
1183
- ).fetchone()
1184
- return int(row["cnt"]) if row else 0
1185
- finally:
1186
- conn.close()
1187
- except sqlite3.Error:
1188
- return 0
1189
-
1190
-
1191
- def _count_signals_since(
1192
- lrn_db: LearningDatabase,
1193
- profile_id: str,
1194
- since: datetime,
1195
- *,
1196
- signal_types: tuple[str, ...] | None = None,
1197
- ) -> int:
1198
- """Count ``learning_signals`` rows since ``since`` for ``profile_id``."""
1199
- try:
1200
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1201
- conn.row_factory = sqlite3.Row
1202
- try:
1203
- if signal_types:
1204
- placeholders = ",".join("?" * len(signal_types))
1205
- sql = (
1206
- "SELECT COUNT(*) AS cnt FROM learning_signals "
1207
- "WHERE profile_id = ? AND created_at >= ? "
1208
- f"AND signal_type IN ({placeholders})"
1209
- )
1210
- params: tuple[Any, ...] = (
1211
- profile_id, since.isoformat(), *signal_types,
1212
- )
1213
- else:
1214
- sql = (
1215
- "SELECT COUNT(*) AS cnt FROM learning_signals "
1216
- "WHERE profile_id = ? AND created_at >= ?"
1217
- )
1218
- params = (profile_id, since.isoformat())
1219
- row = conn.execute(sql, params).fetchone()
1220
- return int(row["cnt"]) if row else 0
1221
- finally:
1222
- conn.close()
1223
- except sqlite3.Error: # pragma: no cover — table always present in tests
1224
- return 0
1225
-
1226
-
1227
- def _lazy_db_for(profile_id: str) -> LearningDatabase:
1228
- return LearningDatabase(_learning_db_path())
1229
-
1230
-
1231
- _EVOLUTION_MAX_DAYS = 90
1232
- _EVOLUTION_DEFAULT_DAYS = 30
1233
-
1234
-
1235
- def _compute_evolution_timeseries(
1236
- profile_id: str,
1237
- lrn_db: LearningDatabase,
1238
- *,
1239
- days: int = _EVOLUTION_DEFAULT_DAYS,
1240
- ) -> dict:
1241
- """Daily learning-signal counts for ``profile_id`` over the last ``days``.
1242
-
1243
- Returns ``{"days": N, "points": [{"date": "YYYY-MM-DD", "signals": int,
1244
- "patterns_seen": int}, ...], "is_real": True, "source": "learning_signals"}``.
1245
-
1246
- Points are left-aligned to midnight UTC and cover exactly ``days``
1247
- consecutive days including today. Missing days are zero-filled so the
1248
- chart renders a flat line instead of a gap.
1249
- """
1250
- try:
1251
- requested = int(days) if days is not None else _EVOLUTION_DEFAULT_DAYS
1252
- except (TypeError, ValueError):
1253
- requested = _EVOLUTION_DEFAULT_DAYS
1254
- days = max(1, min(requested, _EVOLUTION_MAX_DAYS))
1255
- today = datetime.now(timezone.utc).replace(
1256
- hour=0, minute=0, second=0, microsecond=0,
1257
- )
1258
- start = today - timedelta(days=days - 1)
1259
-
1260
- # Daily counts from learning_signals.
1261
- counts: dict[str, int] = {}
1262
- try:
1263
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1264
- conn.row_factory = sqlite3.Row
1265
- try:
1266
- rows = conn.execute(
1267
- "SELECT substr(created_at, 1, 10) AS d, COUNT(*) AS n "
1268
- "FROM learning_signals "
1269
- "WHERE profile_id = ? AND created_at >= ? "
1270
- "GROUP BY substr(created_at, 1, 10)",
1271
- (profile_id, start.isoformat()),
1272
- ).fetchall()
1273
- counts = {r["d"]: int(r["n"]) for r in rows if r["d"]}
1274
- finally:
1275
- conn.close()
1276
- except sqlite3.Error:
1277
- counts = {}
1278
-
1279
- points: list[dict[str, Any]] = []
1280
- for i in range(days):
1281
- d = start + timedelta(days=i)
1282
- key = d.date().isoformat()
1283
- points.append({
1284
- "date": key,
1285
- "signals": counts.get(key, 0),
1286
- })
1287
-
1288
- total = sum(p["signals"] for p in points)
1289
- return {
1290
- "is_real": True,
1291
- "source": "learning_signals",
1292
- "days": days,
1293
- "total_signals": total,
1294
- "points": points,
1295
- }
1296
-
1297
-
1298
- def _action_outcomes_count(lrn_db: LearningDatabase,
1299
- profile_id: str) -> int:
1300
- """Row count in ``action_outcomes`` for ``profile_id``.
1301
-
1302
- ``action_outcomes`` ships in v3.4.22 (M006). While absent, returns 0.
1303
- """
1304
- try:
1305
- conn = sqlite3.connect(lrn_db.path, timeout=5.0)
1306
- conn.row_factory = sqlite3.Row
1307
- try:
1308
- row = conn.execute(
1309
- "SELECT COUNT(*) AS cnt FROM action_outcomes "
1310
- "WHERE profile_id = ?",
1311
- (profile_id,),
1312
- ).fetchone()
1313
- return int(row["cnt"]) if row else 0
1314
- finally:
1315
- conn.close()
1316
- except sqlite3.Error:
1317
- return 0
1318
-
1319
-
1320
- def _compute_action_outcomes_preview(profile_id: str) -> dict:
1321
- """Count profile-scoped action outcomes from their canonical database."""
1322
- empty = {
1323
- "action_outcomes_rows": 0,
1324
- "source": "memory.db:action_outcomes",
1325
- "is_real": True,
1326
- }
1327
- db_path = _memory_db_path()
1328
- if not db_path.exists():
1329
- return empty
1330
- try:
1331
- conn = ReadConnectionFactory(db_path).open()
1332
- try:
1333
- row = conn.execute(
1334
- "SELECT COUNT(*) FROM action_outcomes WHERE profile_id = ?",
1335
- (profile_id,),
1336
- ).fetchone()
1337
- finally:
1338
- conn.close()
1339
- except sqlite3.Error:
1340
- return empty
1341
- return {**empty, "action_outcomes_rows": int(row[0] or 0) if row else 0}
1342
-
1343
-
1344
- # ---------------------------------------------------------------------------
1345
- # Routes
1346
- # ---------------------------------------------------------------------------
1347
-
1348
-
1349
- def _authorized_profile(request: Request, profile_id: str | None) -> str:
1350
- """Resolve and authorize the exact Brain profile requested by the caller."""
1351
- from superlocalmemory.access.rbac import Permission
1352
- from superlocalmemory.server.rbac_enforce import require_permission
1353
-
1354
- effective_profile = profile_id or get_active_profile()
1355
- require_permission(request, Permission.READ, profile=effective_profile)
1356
- return effective_profile
1357
-
1358
-
1359
- @router.get("/brain", dependencies=[Depends(require_install_token)])
1360
- async def get_brain(request: Request, profile_id: str | None = None) -> dict:
1361
- """Unified Brain endpoint — LLD-04 §3.1.
1362
-
1363
- Fan-out: each section is a synchronous SQLite reader. Running them
1364
- serially was a 7-round-trip chain on the hot path (PERF-v2-05).
1365
- We offload each to the default executor via ``asyncio.to_thread``
1366
- and ``asyncio.gather`` so the wall-clock is dominated by the slowest
1367
- single section, not the sum. Every section already swallows its own
1368
- errors and returns honest-empty on failure, so ``return_exceptions``
1369
- is unnecessary — but we still set ``return_exceptions=True`` as a
1370
- belt-and-suspenders guard so one broken reader can't 500 the whole
1371
- endpoint.
1372
- """
1373
- import asyncio
1374
-
1375
- # Default to the ACTIVE profile (request runtime truth), never literal
1376
- # "default" — the Brain must reflect whichever profile is active.
1377
- profile_id = _authorized_profile(request, profile_id)
1378
- lrn_db = LearningDatabase(_learning_db_path())
1379
- truth_service = BrainTruthService(
1380
- memory_db_path=_memory_dir() / "memory.db",
1381
- learning_db_path=_learning_db_path(),
1382
- )
1383
-
1384
- (
1385
- preferences, learning, usage, bandit_snap, cache,
1386
- cross_platform, outcomes_preview, evolution, active_clients,
1387
- feedback_loop, source_quality, graph_summary, agent_experience,
1388
- brain_truth, bounded_loops,
1389
- ) = await asyncio.gather(
1390
- asyncio.to_thread(_compute_preferences, profile_id),
1391
- asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
1392
- asyncio.to_thread(_compute_usage_stats, profile_id),
1393
- asyncio.to_thread(_compute_bandit_snapshot, profile_id, lrn_db),
1394
- asyncio.to_thread(_compute_cache_stats),
1395
- asyncio.to_thread(_compute_cross_platform),
1396
- asyncio.to_thread(_compute_action_outcomes_preview, profile_id),
1397
- asyncio.to_thread(
1398
- _compute_evolution_timeseries, profile_id, lrn_db,
1399
- days=_EVOLUTION_DEFAULT_DAYS,
1400
- ),
1401
- asyncio.to_thread(_compute_active_clients, profile_id),
1402
- asyncio.to_thread(_compute_feedback_loop, profile_id, lrn_db),
1403
- asyncio.to_thread(_compute_source_quality, profile_id),
1404
- asyncio.to_thread(_compute_graph_summary, profile_id),
1405
- asyncio.to_thread(_compute_agent_experience, profile_id),
1406
- asyncio.to_thread(truth_service.snapshot, profile_id),
1407
- asyncio.to_thread(_compute_bounded_loops),
1408
- return_exceptions=True,
1409
- )
1410
-
1411
- # Replace any per-section exception with an honest-empty dict so the
1412
- # endpoint never propagates a half-rendered payload.
1413
- def _ok(value, fallback):
1414
- if isinstance(value, Exception):
1415
- return fallback
1416
- return value
1417
-
1418
- active_clients = _ok(active_clients, {
1419
- "is_real": False,
1420
- "scope": "profile",
1421
- "clients": [],
1422
- "window_seconds": 300,
1423
- "registry_status": "error",
1424
- "newest_entry_seconds_ago": None,
1425
- "source": "session_registry unavailable",
1426
- })
1427
- feedback_loop = _ok(feedback_loop, {"is_real": True, "signals_by_type": {},
1428
- "explicit_signals": 0, "implicit_signals": 0,
1429
- "settled_outcomes": 0, "mean_settled_reward": None,
1430
- "source": "feedback data unavailable"})
1431
- source_quality = _ok(source_quality, {"is_real": True, "observed_sources": 0,
1432
- "mean_quality": None,
1433
- "source": "source-quality data unavailable"})
1434
- graph_summary = _ok(graph_summary, {"is_real": True, "fact_nodes": 0,
1435
- "association_edges": 0,
1436
- "source": "graph data unavailable"})
1437
- agent_experience = _ok(agent_experience, {
1438
- "is_real": False, "availability": "unavailable",
1439
- "experiences_total": 0, "turns_total": 0,
1440
- "turns_by_state": {}, "claimed_evidence_experiences": 0,
1441
- "source": "receipt data unavailable",
1442
- })
1443
-
1444
- return {
1445
- "profile_id": profile_id,
1446
- "preferences": _ok(preferences, {"is_real": True,
1447
- "topics": [], "entities": [],
1448
- "tech": [], "redacted_count": 0,
1449
- "source": "_store_patterns"}),
1450
- "learning": _ok(learning, {"is_real": True, "phase": 1,
1451
- "signals_total": 0}),
1452
- "usage": _ok(usage, {"is_real_ml": False,
1453
- "recalls_last_24h": 0,
1454
- "top_query_types": [],
1455
- "top_time_buckets": []}),
1456
- "bandit": _ok(bandit_snap, {"is_real": True,
1457
- "strata_active": 0,
1458
- "strata_total": 48}),
1459
- "cache": _ok(cache, {"is_real": True,
1460
- "db_size_bytes": 0,
1461
- "entry_count": 0}),
1462
- "cross_platform": _ok(cross_platform, {}),
1463
- # One canonical, transport-neutral read model for the non-technical
1464
- # Living Brain. Existing top-level sections stay intact for the CLI,
1465
- # MCP and older dashboard clients. This is observation only: it never
1466
- # changes retrieval or model routing.
1467
- "living_brain": {
1468
- "is_real": True,
1469
- "control_plane": "observation_only",
1470
- "connected_clients": active_clients,
1471
- "feedback": feedback_loop,
1472
- "source_quality": source_quality,
1473
- "graph": graph_summary,
1474
- "agent_experience": agent_experience,
1475
- # Additive v4.0.5 contract. Keep the preceding legacy keys for
1476
- # existing dashboard/API clients; new clients should render this
1477
- # one shared, unavailable-aware snapshot.
1478
- "brain_truth": _ok(brain_truth, {
1479
- "availability": "unavailable",
1480
- "source": "BrainTruth service unavailable",
1481
- "control_plane": "observation_only",
1482
- }),
1483
- "source": "local durable stores + ephemeral session registry",
1484
- },
1485
- "evolution_preview": _ok(evolution, {
1486
- "is_real": True, "source": "learning_signals",
1487
- "days": _EVOLUTION_DEFAULT_DAYS, "total_signals": 0, "points": [],
1488
- }),
1489
- "outcomes_preview": _ok(outcomes_preview, {
1490
- "action_outcomes_rows": 0,
1491
- "source": "memory.db:action_outcomes",
1492
- "is_real": True,
1493
- }),
1494
- # S9-defer H-22: live tile data for the Reward / Shadow /
1495
- # Evolution-Cost dashboard tiles. Each block is a honest-empty
1496
- # default when the underlying table is missing (fresh install
1497
- # or older schema) so the UI can render "no data yet" without
1498
- # a 500. All three queries are cheap COUNT / AVG aggregates.
1499
- "reward_preview": _compute_reward_preview(profile_id),
1500
- "shadow_preview": _compute_shadow_preview(profile_id),
1501
- "evolution_cost_preview": _compute_evolution_cost_preview(profile_id),
1502
- "outcome_queue": _compute_outcome_queue_stats(profile_id),
1503
- # ``bounded_loops`` — lazy-probed, TTL-cached installation status.
1504
- # ``section_enabled`` drives the UI panel on/off decision.
1505
- # Evidence tier: path-based (shutil.which); bridge capability not probed.
1506
- # is_real=False and installed=None, NOT installed=False. If the probe
1507
- # task raised, we do not know whether Bounded Loops is installed — and
1508
- # saying "installed: False, is_real: True" asserts absence we never
1509
- # established. That reads to the UI, and to any API consumer, exactly
1510
- # like a machine with Bounded Loops genuinely not present.
1511
- "bounded_loops": _ok(bounded_loops, {
1512
- "is_real": False,
1513
- "installed": None,
1514
- "probe_failed": True,
1515
- "section_enabled": False,
1516
- "source": "bounded_loops_probe unavailable",
1517
- }),
1518
- "meta": _meta_now(),
1519
- }
1520
-
1521
-
1522
- def _compute_outcome_queue_stats(profile_id: str) -> dict:
1523
- """S9-DASH-02: producer-side telemetry for the Brain panel.
1524
-
1525
- Exposes the outcome-queue worker counters + a live count of
1526
- ``pending_outcomes`` so the operator can see the closed loop is
1527
- actually flowing: recall → enqueue → persist → finalize.
1528
- """
1529
- try:
1530
- from superlocalmemory.learning.outcome_queue import (
1531
- get_counters,
1532
- queue_size,
1533
- )
1534
- counters = get_counters()
1535
- qsz = queue_size()
1536
- except Exception:
1537
- counters, qsz = {}, 0
1538
- home = _memory_dir()
1539
- db = home / "memory.db"
1540
- pending_now = 0
1541
- if db.exists():
1542
- try:
1543
- conn = ReadConnectionFactory(db).open()
1544
- try:
1545
- row = conn.execute(
1546
- "SELECT COUNT(*) FROM pending_outcomes "
1547
- "WHERE profile_id = ? AND status = 'pending'",
1548
- (profile_id,),
1549
- ).fetchone()
1550
- if row:
1551
- pending_now = int(row[0] or 0)
1552
- finally:
1553
- conn.close()
1554
- except Exception:
1555
- pass
1556
- return {
1557
- "is_real": True,
1558
- "queue_depth": int(qsz),
1559
- "pending_outcomes_now": pending_now,
1560
- "counters": counters,
1561
- "source": "outcome_queue + pending_outcomes",
1562
- }
1563
-
1564
-
1565
- # ---------------------------------------------------------------------------
1566
- # S9-defer H-22 — dashboard-tile aggregates
1567
- # ---------------------------------------------------------------------------
1568
-
1569
-
1570
- def _compute_reward_preview(profile_id: str) -> dict:
1571
- """Reward-tile aggregate — count + mean reward over the last 24h."""
1572
- import sqlite3
1573
- home = _memory_dir()
1574
- db = home / "memory.db"
1575
- default = {
1576
- "is_real": False, "rows_24h": 0, "mean_reward_24h": 0.0,
1577
- "source": "action_outcomes",
1578
- }
1579
- if not db.exists():
1580
- return default
1581
- try:
1582
- conn = ReadConnectionFactory(db).open()
1583
- try:
1584
- row = conn.execute(
1585
- "SELECT COUNT(*) AS c, AVG(reward) AS m "
1586
- "FROM action_outcomes "
1587
- "WHERE profile_id = ? AND settled = 1 "
1588
- " AND reward IS NOT NULL "
1589
- " AND settled_at >= datetime('now', '-1 day')",
1590
- (profile_id,),
1591
- ).fetchone()
1592
- finally:
1593
- conn.close()
1594
- except sqlite3.Error:
1595
- return default
1596
- if row is None:
1597
- return default
1598
- return {
1599
- "is_real": True,
1600
- "rows_24h": int(row[0] or 0),
1601
- "mean_reward_24h": float(row[1] or 0.0),
1602
- "source": "action_outcomes",
1603
- }
1604
-
1605
-
1606
- def _compute_shadow_preview(profile_id: str) -> dict:
1607
- """Shadow-tile aggregate — latest candidate + paired-obs count."""
1608
- import sqlite3
1609
- default = {
1610
- "is_real": False, "active_candidate_id": None,
1611
- "paired_observations": 0, "rollback_count_90d": 0,
1612
- "source": "learning_model_state+shadow_observations",
1613
- }
1614
- learning_db = _learning_db_path()
1615
- if not learning_db.exists():
1616
- return default
1617
- try:
1618
- conn = sqlite3.connect(str(learning_db), timeout=1.0)
1619
- try:
1620
- row = conn.execute(
1621
- "SELECT id FROM learning_model_state "
1622
- "WHERE profile_id = ? AND is_candidate = 1 "
1623
- "LIMIT 1",
1624
- (profile_id,),
1625
- ).fetchone()
1626
- cand_id = int(row[0]) if row and row[0] is not None else None
1627
- paired = 0
1628
- if cand_id is not None:
1629
- try:
1630
- c = conn.execute(
1631
- "SELECT COUNT(DISTINCT query_id) "
1632
- "FROM shadow_observations "
1633
- "WHERE candidate_id = ? "
1634
- " AND query_id IN ("
1635
- " SELECT query_id FROM shadow_observations "
1636
- " WHERE candidate_id = ? AND arm = 'active' "
1637
- " INTERSECT "
1638
- " SELECT query_id FROM shadow_observations "
1639
- " WHERE candidate_id = ? AND arm = 'candidate' "
1640
- " )",
1641
- (cand_id, cand_id, cand_id),
1642
- ).fetchone()
1643
- paired = int(c[0]) if c and c[0] is not None else 0
1644
- except sqlite3.Error:
1645
- paired = 0
1646
- # Rollback count in the last 90d. The rollback log is a
1647
- # separate table the learning subsystem writes; if it is
1648
- # absent we simply return 0.
1649
- try:
1650
- r2 = conn.execute(
1651
- "SELECT COUNT(*) FROM model_state_history "
1652
- "WHERE profile_id = ? AND action = 'rollback' "
1653
- " AND ts >= datetime('now', '-90 day')",
1654
- (profile_id,),
1655
- ).fetchone()
1656
- rollbacks = int(r2[0]) if r2 and r2[0] is not None else 0
1657
- except sqlite3.Error:
1658
- rollbacks = 0
1659
- finally:
1660
- conn.close()
1661
- except sqlite3.Error:
1662
- return default
1663
- return {
1664
- "is_real": True,
1665
- "active_candidate_id": cand_id,
1666
- "paired_observations": paired,
1667
- "rollback_count_90d": rollbacks,
1668
- "source": "learning_model_state+shadow_observations",
1669
- }
1670
-
1671
-
1672
- def _compute_evolution_cost_preview(profile_id: str) -> dict:
1673
- """Evolution-cost tile — last 7d spend + call count."""
1674
- import sqlite3
1675
- default = {
1676
- "is_real": False, "calls_7d": 0, "cost_usd_7d": 0.0,
1677
- "tokens_in_7d": 0, "tokens_out_7d": 0,
1678
- "source": "evolution_llm_cost_log",
1679
- }
1680
- learning_db = _learning_db_path()
1681
- if not learning_db.exists():
1682
- return default
1683
- try:
1684
- conn = sqlite3.connect(str(learning_db), timeout=1.0)
1685
- try:
1686
- row = conn.execute(
1687
- "SELECT COUNT(*) AS c, "
1688
- " COALESCE(SUM(cost_usd), 0) AS cost, "
1689
- " COALESCE(SUM(tokens_in), 0) AS tin, "
1690
- " COALESCE(SUM(tokens_out), 0) AS tout "
1691
- "FROM evolution_llm_cost_log "
1692
- "WHERE profile_id = ? "
1693
- " AND ts >= datetime('now', '-7 day')",
1694
- (profile_id,),
1695
- ).fetchone()
1696
- finally:
1697
- conn.close()
1698
- except sqlite3.Error:
1699
- return default
1700
- if row is None:
1701
- return default
1702
- return {
1703
- "is_real": True,
1704
- "calls_7d": int(row[0] or 0),
1705
- "cost_usd_7d": float(row[1] or 0.0),
1706
- "tokens_in_7d": int(row[2] or 0),
1707
- "tokens_out_7d": int(row[3] or 0),
1708
- "source": "evolution_llm_cost_log",
1709
- }
1710
-
1711
-
1712
- @router.get("/brain/evolution-timeseries",
1713
- dependencies=[Depends(require_install_token)])
1714
- async def get_brain_evolution_timeseries(
1715
- request: Request,
1716
- profile_id: str | None = None,
1717
- days: int = _EVOLUTION_DEFAULT_DAYS,
1718
- ) -> dict:
1719
- """Daily learning-signal counts for ``profile_id`` over the last ``days``.
1720
-
1721
- ``days`` is clamped to ``[1, 90]`` to keep the response bounded. Each
1722
- missing day is zero-filled so the chart renders a flat line, not a gap.
1723
- """
1724
- import asyncio
1725
-
1726
- profile_id = _authorized_profile(request, profile_id)
1727
- lrn_db = LearningDatabase(_learning_db_path())
1728
- result = await asyncio.to_thread(
1729
- _compute_evolution_timeseries, profile_id, lrn_db, days=days,
1730
- )
1731
- result["meta"] = _meta_now()
1732
- return result
1733
-
1734
-
1735
- # ---------------------------------------------------------------------------
1736
- # Deprecated shims — 1 release grace. All require the install token.
1737
- # ---------------------------------------------------------------------------
1738
-
1739
-
1740
- @router.get("/learning/stats",
1741
- dependencies=[Depends(require_install_token)])
1742
- async def learning_stats_deprecated(
1743
- request: Request, profile_id: str | None = None,
1744
- ) -> dict:
1745
- profile_id = _authorized_profile(request, profile_id)
1746
- lrn_db = LearningDatabase(_learning_db_path())
1747
- return {
1748
- "deprecated": True,
1749
- "use_instead": "/api/v3/brain",
1750
- "learning": _compute_learning_status(profile_id, lrn_db),
1751
- }
1752
-
1753
-
1754
- @router.get("/patterns",
1755
- dependencies=[Depends(require_install_token)])
1756
- async def patterns_deprecated(
1757
- request: Request, profile_id: str | None = None,
1758
- ) -> dict:
1759
- profile_id = _authorized_profile(request, profile_id)
1760
- return {
1761
- "deprecated": True,
1762
- "use_instead": "/api/v3/brain",
1763
- "preferences": _compute_preferences(profile_id),
1764
- }
1765
-
1766
-
1767
- @router.get("/bounded-loops/evidence",
1768
- dependencies=[Depends(require_install_token)])
1769
- async def bounded_loops_evidence(
1770
- request: Request, profile_id: str | None = None, limit: int = 20,
1771
- ) -> dict:
1772
- """Terminal Bounded Loops runs this profile has observed.
1773
-
1774
- Bounded Loops is a SEPARATE PRODUCT. SLM is one optional consumer of a
1775
- document any MCP client can request over the published contract
1776
- ``bounded-loops.dev/slm-bridge/v1``; neither product depends on the other,
1777
- and installing either alone is complete.
1778
-
1779
- What travels is deliberately narrow, and the pane must not imply otherwise:
1780
-
1781
- * **Observation, not authorization.** ``eligible_for_learning`` is a hard
1782
- field in the contract and is always ``False`` in v1. A SUCCEEDED run is
1783
- not permission to retrain, re-rank or route on it. Nothing in SLM treats
1784
- it as such, and this endpoint returns the flag so the UI can say so.
1785
- * **Digests, not paths.** ``workspace_id`` is a hash precisely so a client's
1786
- directory name never reaches a memory system. Gate reasons, artifact
1787
- contents, commands and environment values are excluded at the source.
1788
- * **``local_hash_chain_only``.** The receipt log is an append-only hash
1789
- chain on local disk: tampering is detectable by anyone holding an earlier
1790
- head. That is NOT authentication, notarization or independent audit, and
1791
- calling it "verified" would claim a guarantee no part of the system
1792
- provides.
1793
- * **``demonstration``** separates real execution from a scripted replay. A
1794
- demo run proves the wiring works and proves nothing about the work.
1795
-
1796
- Read-only, off the hot path, and empty is a normal answer — most installs
1797
- have no Bounded Loops at all.
1798
- """
1799
- profile_id = _authorized_profile(request, profile_id)
1800
- limit = max(1, min(int(limit or 20), 100))
1801
-
1802
- out: dict[str, Any] = {
1803
- "contract": "bounded-loops.dev/slm-bridge/v1",
1804
- "control_plane": "observation_only",
1805
- "runs": [],
1806
- "total": 0,
1807
- "demonstration_count": 0,
1808
- "installed": None,
1809
- }
1810
-
1811
- try:
1812
- out["installed"] = bool(_compute_bounded_loops().get("installed"))
1813
- except Exception: # pragma: no cover — presence probe must never 500
1814
- out["installed"] = None
1815
-
1816
- db_path = _learning_db_path()
1817
- if not db_path.exists():
1818
- return out
1819
-
1820
- import sqlite3
1821
-
1822
- try:
1823
- conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=3)
1824
- conn.row_factory = sqlite3.Row
1825
- try:
1826
- rows = conn.execute(
1827
- "SELECT run_id, run_ref, outcome, run_state, demonstration,"
1828
- " eligible_for_learning, terminal_at, observed_at,"
1829
- " receipt_sequence, receipt_trust, workspace_id, contract_id"
1830
- " FROM external_evidence_receipts WHERE profile_id=?"
1831
- " ORDER BY observed_at DESC LIMIT ?",
1832
- (profile_id, limit),
1833
- ).fetchall()
1834
- total = conn.execute(
1835
- "SELECT COUNT(*) AS n FROM external_evidence_receipts"
1836
- " WHERE profile_id=?", (profile_id,),
1837
- ).fetchone()["n"]
1838
- demos = conn.execute(
1839
- "SELECT COUNT(*) AS n FROM external_evidence_receipts"
1840
- " WHERE profile_id=? AND demonstration=1", (profile_id,),
1841
- ).fetchone()["n"]
1842
- finally:
1843
- conn.close()
1844
- except sqlite3.Error as exc:
1845
- # The table only exists once the bridge has been used. Absent is a
1846
- # normal state, not an error, and must not surface as a failed pane.
1847
- logger.debug("bounded-loops evidence unavailable: %s", exc)
1848
- return out
1849
-
1850
- out["total"] = total
1851
- out["demonstration_count"] = demos
1852
- out["runs"] = [
1853
- {
1854
- "run_id": r["run_id"],
1855
- "run_ref": r["run_ref"],
1856
- "outcome": r["outcome"],
1857
- # Both, because the mapping to three buckets loses information: a
1858
- # HALTED run (budget/policy stop) and a FAILED run (work the gate
1859
- # rejected) are different events.
1860
- "run_state": r["run_state"],
1861
- "demonstration": bool(r["demonstration"]),
1862
- "eligible_for_learning": bool(r["eligible_for_learning"]),
1863
- "terminal_at": r["terminal_at"],
1864
- "observed_at": r["observed_at"],
1865
- "receipt_sequence": r["receipt_sequence"],
1866
- "trust": r["receipt_trust"],
1867
- "workspace_id": r["workspace_id"],
1868
- "contract": r["contract_id"],
1869
- }
1870
- for r in rows
1871
- ]
1872
- return out
1873
-
1874
-
1875
- @router.get("/behavioral",
1876
- dependencies=[Depends(require_install_token)])
1877
- async def behavioral_deprecated(
1878
- request: Request, profile_id: str | None = None,
1879
- ) -> dict:
1880
- profile_id = _authorized_profile(request, profile_id)
1881
- return {
1882
- "deprecated": True,
1883
- "use_instead": "/api/v3/brain",
1884
- "usage": _compute_usage_stats(profile_id),
1885
- }
1886
-
1887
-
1888
- __all__ = (
1889
- "router",
1890
- "require_install_token",
1891
- "redact_secrets_in_preferences",
1892
- )