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,1615 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """SuperLocalMemory V3 — retrieval orchestration.
6
-
7
- Five parallel candidate producers (semantic, BM25, temporal, spreading
8
- activation, and Hopfield) feed single-pass RRF fusion; optional profile hits
9
- can join that fusion input. The entity graph may then score and boost fused
10
- candidates when enabled and within the recall time budget. It is not a sixth
11
- parallel candidate producer. Optional cross-encoder reranking follows fusion.
12
- Replaces V1's broken 10-channel triple-re-fusion pipeline.
13
-
14
- Part of Qualixar | Author: Varun Pratap Bhardwaj
15
- License: AGPL-3.0-or-later
16
- """
17
- from __future__ import annotations
18
-
19
- import concurrent.futures
20
- import functools
21
- import logging
22
- import math
23
- import os
24
- import re
25
- import threading
26
- import time
27
- from typing import TYPE_CHECKING, Any, Protocol
28
-
29
- from superlocalmemory.core.config import (
30
- CANONICAL_RECALL_LIMIT,
31
- ChannelWeights,
32
- RetrievalConfig,
33
- )
34
- from superlocalmemory.retrieval import channel_status as chstat
35
- from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
36
- from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
37
- from superlocalmemory.retrieval.temporal_validity_filter import (
38
- CorrectionAdmissionCache,
39
- admit_correction_candidates,
40
- admit_correction_fusion_results,
41
- )
42
- from superlocalmemory.retrieval.time_window import (
43
- in_window,
44
- infer_window_from_query,
45
- parse_window,
46
- )
47
- from superlocalmemory.storage.models import (
48
- AtomicFact,
49
- Mode,
50
- RecallResponse,
51
- RetrievalResult,
52
- )
53
-
54
- if TYPE_CHECKING:
55
- from superlocalmemory.retrieval.bm25_channel import BM25Channel
56
- from superlocalmemory.retrieval.entity_channel import EntityGraphChannel
57
- from superlocalmemory.retrieval.hopfield_channel import HopfieldChannel
58
- from superlocalmemory.retrieval.semantic_channel import SemanticChannel
59
- from superlocalmemory.retrieval.temporal_channel import TemporalChannel
60
- from superlocalmemory.storage.database import DatabaseManager
61
- from superlocalmemory.trust.scorer import TrustScorer
62
-
63
- logger = logging.getLogger(__name__)
64
-
65
-
66
- # How long the parallel channel phase may run before a channel is abandoned.
67
- #
68
- # This is a guard against a genuinely wedged channel, NOT a speed cutoff, and
69
- # the distinction is the whole point. A channel that misses this limit is
70
- # cancelled and contributes NOTHING to fusion: its candidates are not reordered,
71
- # they are absent. So whenever this limit binds, the answer is decided partly by
72
- # what else the machine happened to be doing — the same question returns a
73
- # different answer under load, which is a correctness failure, not a slow one.
74
- #
75
- # It replaced a 1.4 s cutoff that was chosen to keep the recall p95 low. Six
76
- # runs of identical code against the same 0.95 GB store logged 0, 0, 25, 2, 0
77
- # and 0 abandoned channels; in the third run `hopfield` was cut off on 13 of
78
- # 140 queries and `temporal` on 9, while the first run lost nothing on those
79
- # same queries. That spread was the last remaining source of unrepeatable
80
- # recall, and it is why fixing tie-breaks everywhere else moved top-10 churn
81
- # from 40.7% to 22.9% and left rank-1 disagreement sitting at ~15%: a
82
- # tie-break cannot repair a missing input.
83
- #
84
- # The value comes from the measured cost of the channels themselves, on that
85
- # same store, 140 queries, with the limit raised out of the way so nothing was
86
- # truncated (p95 / max, ms):
87
- #
88
- # temporal 580 / 1983 hopfield 492 / 945 bm25 230 / 1093
89
- # semantic 264 / 662 spreading_activation 238 / 571
90
- #
91
- # The slowest channel's p95 is 580 ms and its worst single run was 1,983 ms, so
92
- # 8 s is roughly four times the worst observed cost — it should never bind on a
93
- # machine that is merely busy. It also stays well inside the daemon's own
94
- # last-resort recall budget (25 s, `_recall_budget_s`), which is the layer that
95
- # exists to catch a true hang and which already tells the caller when it fires
96
- # (`retrieval_mode=degraded_lexical`). Before this change the inner 1.4 s cutoff
97
- # silently overrode that outer promise of "quality recall under load".
98
- #
99
- # Lowering this to improve a latency percentile means buying that percentile
100
- # with missing answers. Per HARD-RULES RULE 6 the ordering is Correct, then
101
- # Complete, then Repeatable, and only then Fast — so if this needs to move,
102
- # measure what it costs in answer quality first and record the number.
103
- CHANNEL_HANG_GUARD_SECONDS = 8.0
104
-
105
-
106
- class CrossEncoderProtocol(Protocol):
107
- """Duck-typed cross-encoder interface."""
108
- def rerank(self, query: str, candidates: list[tuple[str, str]]) -> list[tuple[str, float]]: ...
109
-
110
-
111
- class EmbeddingProvider(Protocol):
112
- """Duck-typed embedding provider."""
113
- def embed(self, text: str) -> list[float]: ...
114
-
115
-
116
- class RetrievalEngine:
117
- """Retrieval orchestrator: five candidate producers -> RRF fusion.
118
-
119
- Five parallel candidate producers (semantic, BM25, temporal,
120
- spreading_activation, hopfield) feed single-pass RRF fusion, followed by
121
- optional cross-encoder rerank and an optional entity-graph post-fusion
122
- score enhancement. Entity graph is not a sixth parallel candidate producer.
123
-
124
- Usage::
125
- engine = RetrievalEngine(db, config, channels, embedder)
126
- response = engine.recall("What did Alice do?", "default", Mode.A)
127
- """
128
-
129
- def __init__(
130
- self, db: DatabaseManager, config: RetrievalConfig,
131
- channels: dict[str, Any],
132
- embedder: EmbeddingProvider | None = None,
133
- reranker: CrossEncoderProtocol | None = None,
134
- strategy: QueryStrategyClassifier | None = None,
135
- base_weights: ChannelWeights | None = None,
136
- profile_channel: Any | None = None,
137
- bridge_discovery: Any | None = None,
138
- trust_scorer: TrustScorer | None = None,
139
- ) -> None:
140
- self._db = db
141
- self._config = config
142
- self._semantic: SemanticChannel | None = channels.get("semantic")
143
- self._bm25: BM25Channel | None = channels.get("bm25")
144
- self._entity: EntityGraphChannel | None = channels.get("entity_graph")
145
- self._temporal: TemporalChannel | None = channels.get("temporal")
146
- # Phase G: Hopfield channel (6th)
147
- self._hopfield: HopfieldChannel | None = channels.get("hopfield")
148
- # Phase 3: Spreading Activation channel
149
- self._spreading_activation = channels.get("spreading_activation")
150
- self._embedder = embedder
151
- self._reranker = reranker
152
- self._strategy = strategy or QueryStrategyClassifier(config=config)
153
- self._base_weights = (base_weights or ChannelWeights()).as_dict()
154
- self._profile_channel = profile_channel
155
- self._bridge = bridge_discovery
156
- self._trust_scorer = trust_scorer
157
- # v3.7.9: scope flags (include_global / include_shared) are now threaded
158
- # as explicit call parameters into every channel's search() method, so
159
- # concurrent recalls each carry their own flags — no shared mutable state,
160
- # no lock needed. The _scope_lock and per-recall attribute-set loop have
161
- # been removed. See defect S01 in the fix/3.7.9 branch notes.
162
- # One executor belongs to one retrieval engine. Creating/destroying six
163
- # worker threads on every recall caused allocator/thread-stack RSS churn
164
- # under sustained sessions. The scope lock already serializes channel
165
- # execution, so one six-worker pool preserves the existing concurrency
166
- # semantics while making ownership and shutdown deterministic.
167
- self._channel_executor = concurrent.futures.ThreadPoolExecutor(
168
- max_workers=6,
169
- thread_name_prefix="slm-recall-channel",
170
- )
171
- self._close_lock = threading.Lock()
172
- self._closed = False
173
-
174
- # V3.3.4: LRU cache for query embeddings (avoids redundant Ollama API calls)
175
- # V3.4.40 (2026-05-09): bumped 64 -> 512. Each cached embedding is ~3KB
176
- # (768 floats × 4 bytes). 512 entries ~1.5MB — trivial memory cost,
177
- # massive latency win on repeated queries (sub-ms vs 200-2000ms ollama).
178
- self._query_embedding_cache: dict[str, list[float]] = {}
179
- self._cache_max_size = 512
180
-
181
- # V3.2: ChannelRegistry for self-registration (Phase 0.5)
182
- from superlocalmemory.retrieval.channel_registry import ChannelRegistry
183
- self._registry = ChannelRegistry()
184
- if self._semantic is not None:
185
- self._registry.register_channel("semantic", self._semantic, needs_embedding=True)
186
- if self._bm25 is not None:
187
- self._registry.register_channel("bm25", self._bm25)
188
- if self._entity is not None:
189
- self._registry.register_channel("entity_graph", self._entity)
190
- if self._temporal is not None:
191
- self._registry.register_channel("temporal", self._temporal)
192
- # Phase G: Hopfield channel (6th) — needs embedding input
193
- if self._hopfield is not None:
194
- self._registry.register_channel("hopfield", self._hopfield, needs_embedding=True)
195
- # Phase 3: Spreading Activation (5th channel) — needs embedding input
196
- if self._spreading_activation is not None:
197
- self._registry.register_channel(
198
- "spreading_activation", self._spreading_activation, needs_embedding=True,
199
- )
200
-
201
- def recall(
202
- self, query: str, profile_id: str,
203
- mode: Mode = Mode.A, limit: int = CANONICAL_RECALL_LIMIT,
204
- *,
205
- extra_disabled_channels: set[str] | None = None,
206
- include_global: bool = False,
207
- include_shared: bool = False,
208
- window: str | tuple[str, str] | None = None,
209
- as_of: str | None = None,
210
- known_as_of: str | None = None,
211
- valid_at: str | None = None,
212
- include_unknown: bool = False,
213
- ) -> RecallResponse:
214
- """Full retrieval pipeline: strategy -> channels -> RRF -> rerank.
215
-
216
- Multi-scope: ``include_global`` / ``include_shared`` control which
217
- scopes participate in retrieval. Both default to False so direct
218
- retrieval-engine callers are private unless they explicitly opt in.
219
-
220
- V3.4.40 (2026-05-09): ``extra_disabled_channels`` allows callers to
221
- skip specific channels for a single recall (e.g. SpreadingActivation
222
- for the ``--fast`` CLI flag) without mutating shared config.
223
-
224
- ``as_of``: Optional ISO 8601 datetime string. When set, the bi-temporal
225
- validity filter treats facts as seen from that point in time —
226
- not-yet-valid and already-expired facts are demoted. Default ``None``
227
- leaves all existing behaviour unchanged.
228
- """
229
- from superlocalmemory.retrieval.temporal_utils import normalize_strict_boundary
230
- known_as_of = normalize_strict_boundary(known_as_of, "known_as_of")
231
- valid_at = normalize_strict_boundary(valid_at, "valid_at")
232
- t0 = time.monotonic()
233
- # NOTE: extra_disabled_channels is passed as an explicit local argument
234
- # to _run_channels() — it is NOT stored on self. Storing it as a shared
235
- # mutable instance attribute (the old self._extra_disabled = ...) caused
236
- # a race condition where two concurrent recalls could overwrite each
237
- # other's channel-disable set (v3.4.64 fix).
238
-
239
- # v3.5.0 diagnostic: stage timing inside retrieval (SLM_RECALL_TIMING=1).
240
- import os as _os_e
241
- import time as _time_e
242
- _et = bool(_os_e.environ.get("SLM_RECALL_TIMING"))
243
- _e0 = _time_e.monotonic()
244
-
245
- def _em(_l: str) -> None:
246
- if _et:
247
- logger.warning("[RECALL-TIMING] engine.%-16s %.0f ms",
248
- _l, (_time_e.monotonic() - _e0) * 1000.0)
249
-
250
- # 1. Classify query, get adaptive weights
251
- strat = self._strategy.classify(query, self._base_weights)
252
- _em("classify")
253
-
254
- # What each channel did, so the caller can tell an answer apart from
255
- # an outage. Owned by this call for the same reason the dropped set is:
256
- # a dict on the engine would have two concurrent recalls overwriting
257
- # each other's report.
258
- channel_status: dict[str, str] = {}
259
-
260
- # Profile shortcut (runs before channel search).
261
- #
262
- # The ablation flag is honoured here too. It was not, so an operator who
263
- # switched this channel off still had it searched, still had its weight
264
- # doubled on a hit, and read a status of "ok" or "empty" — which reports
265
- # their own configuration as a live channel's finding.
266
- _profile_disabled = "profile" in set(self._config.disabled_channels) | set(
267
- extra_disabled_channels or (),
268
- )
269
- if _profile_disabled:
270
- profile_hits = []
271
- channel_status["profile"] = chstat.DISABLED
272
- elif self._profile_channel is not None:
273
- try:
274
- profile_hits = self._profile_channel.search(
275
- query, profile_id, top_k=10,
276
- )
277
- channel_status["profile"] = (
278
- chstat.OK if profile_hits else chstat.EMPTY
279
- )
280
- if profile_hits:
281
- strat.weights["profile"] = 2.0
282
- except Exception as exc:
283
- logger.warning("Profile channel: %s", exc)
284
- profile_hits = []
285
- channel_status["profile"] = chstat.ERROR
286
- else:
287
- profile_hits = []
288
- channel_status["profile"] = chstat.NOT_CONFIGURED
289
-
290
- # Dynamic top-k for aggregation queries
291
- effective_limit = 100 if strat.query_type == "aggregation" else limit
292
-
293
- # 3. Run channels. Both scope flags AND extra_disabled_channels travel as
294
- # explicit call parameters so concurrent recalls with different flags
295
- # cannot corrupt each other. No lock needed — no shared mutable state.
296
- # Owned by this call, so concurrent recalls cannot report each other's
297
- # losses. Non-empty means this answer is incomplete, not just slow.
298
- dropped_channels: set[str] = set()
299
- ch_results = self._run_channels(
300
- query, profile_id, strat,
301
- extra_disabled_channels=extra_disabled_channels,
302
- include_global=include_global, include_shared=include_shared,
303
- as_of=as_of, known_as_of=known_as_of, valid_at=valid_at,
304
- include_unknown=include_unknown,
305
- dropped_channels=dropped_channels,
306
- channel_status=channel_status,
307
- )
308
- _em("run_channels")
309
- # One request may need admission before fusion and again after optional
310
- # bridge/scene expansion. Cache only the IDs checked during this one
311
- # request; every newly expanded candidate remains a hard DB lookup.
312
- correction_admission = CorrectionAdmissionCache()
313
- if profile_hits:
314
- ch_results["profile"] = profile_hits
315
- # The profile shortcut bypasses _run_channels(), so it needs the same
316
- # admission before it can influence fusion or seed graph expansion.
317
- ch_results = admit_correction_candidates(
318
- ch_results, profile_id, self._db, as_of=as_of,
319
- known_as_of=known_as_of, valid_at=valid_at,
320
- include_unknown=include_unknown,
321
- include_global=include_global, include_shared=include_shared,
322
- lifecycle_cache=correction_admission,
323
- )
324
- total = sum(len(v) for v in ch_results.values())
325
-
326
- # 3. Single-pass RRF fusion
327
- ch_results = self._semantic_rank_for_unenriched(ch_results)
328
- fused = weighted_rrf(ch_results, strat.weights, k=self._config.rrf_k)
329
- _em("rrf_fusion")
330
-
331
- # V3.3.21: Cross-channel intersection boost for multi-hop/temporal queries.
332
- # Problem: channels work in ISOLATION. "When did Caroline go to X?" needs
333
- # entity(Caroline) ∩ temporal(date). RRF averages scores but doesn't enforce
334
- # the intersection constraint. Fix: boost facts that appear in 2+ signal-type
335
- # channels (entity+temporal, entity+semantic, temporal+semantic).
336
- if strat.query_type == "multi_hop" and len(ch_results) >= 2:
337
- fused = self._apply_cross_channel_intersection(fused, ch_results, strat)
338
-
339
- # Bridge discovery for multi-hop queries
340
- # V3.3.19: Only bridge.discover() (86ms). Removed bridge.spreading_activation()
341
- # which did per-node SQL queries across 254K edges → 78s latency.
342
- # The SYNAPSE SA channel already provides proper SA with in-memory caching.
343
- # recall-retrieval-01: O(1) membership/score lookups instead of repeated
344
- # O(N) `any(...)`/`next(...)` scans inside the bridge + scene loops
345
- # (was O(N^2) per recall, ~400 ms on large sessions). Kept in sync as
346
- # `fused` grows so behaviour is identical.
347
- fused_ids = {fr.fact_id for fr in fused}
348
- fused_scores = {fr.fact_id: fr.fused_score for fr in fused}
349
-
350
- bridge_query_types = ("multi_hop", "entity", "factual", "general")
351
- if self._bridge is not None and strat.query_type in bridge_query_types:
352
- try:
353
- seed_ids = [fr.fact_id for fr in fused[:10]]
354
- bridges = self._bridge.discover(
355
- seed_ids,
356
- profile_id,
357
- max_bridges=10,
358
- include_global=include_global,
359
- include_shared=include_shared,
360
- )
361
- for fid, score in bridges:
362
- if fid not in fused_ids:
363
- new_score = score * 0.8
364
- fused.append(FusionResult(
365
- fact_id=fid, fused_score=new_score,
366
- channel_ranks={}, channel_scores={},
367
- ))
368
- fused_ids.add(fid)
369
- fused_scores[fid] = new_score
370
- except Exception as exc:
371
- logger.warning("Bridge discovery: %s", exc)
372
-
373
- # Scene expansion (v3.5.0: batch).
374
- #
375
- # This used to be skipped when more than 0.8 s of the recall had already
376
- # elapsed, on the reasoning that the scene signal is nice-to-have and
377
- # never worth delaying a response. The reasoning was wrong, because the
378
- # stage does not merely decorate the answer — it appends candidates that
379
- # can outrank what fusion produced. Gating it on a stopwatch therefore
380
- # made the ANSWER depend on how busy the machine was, and 0.8 s sits on
381
- # top of recall's own median (~1,044 ms on the 0.95 GB archive), so it
382
- # was not a rare safety valve: measured over two runs of 60 queries, the
383
- # two clock gates flipped their decision on 22 of 60, and of the 19
384
- # queries whose answer changed, every one had a flipped gate.
385
- #
386
- # Removing both gates moved rank-1 disagreement between two runs from
387
- # 20.0% to 3.3% and top-10 from 31.7% to 10.0%, for about 100-180 ms of
388
- # p95 (1,191-1,230 ms -> 1,256-1,375 ms, ceiling 2,000 ms). Per
389
- # HARD-RULES RULE 6 that is the correct direction: Correct, Complete,
390
- # Repeatable, and only then Fast.
391
- #
392
- # So do not reintroduce a time condition here. If this stage ever needs
393
- # bounding, bound it by DATA — a candidate count, a scene cap — so the
394
- # same input always takes the same path.
395
- if fused:
396
- try:
397
- top_ids = [fr.fact_id for fr in fused[:20]]
398
- scenes_map = self._db.get_scenes_for_facts_batch(top_ids, profile_id)
399
- expanded_ids: set[str] = set()
400
- for fid in top_ids:
401
- for scene in scenes_map.get(fid, [])[:2]:
402
- for sfid in scene.fact_ids:
403
- if sfid not in fused_ids and sfid not in expanded_ids:
404
- expanded_ids.add(sfid)
405
- new_score = fused_scores.get(fid, 0.5) * 0.8
406
- fused.append(FusionResult(
407
- fact_id=sfid, fused_score=new_score,
408
- channel_ranks={}, channel_scores={},
409
- ))
410
- fused_ids.add(sfid)
411
- fused_scores[sfid] = new_score
412
- except Exception as exc:
413
- logger.warning("Scene expansion: %s", exc)
414
-
415
- # V3.4.11: Entity graph signal enhancement (post-RRF boost)
416
- # Instead of competing as independent channel, entity_graph SCORES
417
- # the candidates from other channels by graph proximity to query entities.
418
- # Research: Microsoft GraphRAG DRIFT, Pistis-RAG cascaded architecture.
419
- # The 0.9 s clock gate that used to guard this stage is gone for the
420
- # reason given above the scene expansion, and it mattered more here:
421
- # this stage re-scores every fused candidate and then re-sorts them, so
422
- # whether it ran decided the top answer outright rather than adding to
423
- # it. Bound by data if it ever needs bounding, never by elapsed time.
424
- if self._entity is None:
425
- channel_status["entity_graph"] = chstat.NOT_CONFIGURED
426
- elif "entity_graph" in set(self._config.disabled_channels) | set(
427
- extra_disabled_channels or (),
428
- ):
429
- # Both the configured set AND the per-call one, the same way the
430
- # profile channel resolves it. Reading only the configured set meant
431
- # a caller that switched this channel off for one recall still had
432
- # it run, and still saw it reported as though it were the caller's
433
- # own setting that was being honoured.
434
- channel_status["entity_graph"] = chstat.DISABLED
435
- elif not fused:
436
- # It did not run, and saying "found nothing" would be a different
437
- # claim. This channel re-scores other channels' candidates rather
438
- # than producing its own, so with nothing fused there is nothing for
439
- # it to do — and if the reason nothing fused is that the other five
440
- # failed, reporting this one as having searched and come back empty
441
- # hides that.
442
- channel_status["entity_graph"] = chstat.NO_CANDIDATES
443
- else:
444
- # One chain, evaluated once. Repeating the three conditions to guard
445
- # the work separately is how a status starts describing a decision
446
- # the code no longer makes.
447
- try:
448
- candidate_ids = [fr.fact_id for fr in fused[:100]]
449
- eg_scores = self._entity.score_candidates(
450
- query,
451
- candidate_ids,
452
- profile_id,
453
- include_global=include_global,
454
- include_shared=include_shared,
455
- )
456
- channel_status["entity_graph"] = (
457
- chstat.OK if eg_scores else chstat.EMPTY
458
- )
459
- if eg_scores:
460
- boosted = []
461
- for fr in fused:
462
- eg_sc = eg_scores.get(fr.fact_id, 0.0)
463
- if eg_sc > 0:
464
- eg_weight = strat.weights.get("entity_graph", 1.0)
465
- boost = 1.0 + eg_sc * eg_weight * 0.3
466
- boosted.append(FusionResult(
467
- fact_id=fr.fact_id,
468
- fused_score=fr.fused_score * boost,
469
- channel_ranks=fr.channel_ranks,
470
- channel_scores={**fr.channel_scores, "entity_graph": eg_sc},
471
- ))
472
- else:
473
- boosted.append(fr)
474
- fused = sorted(boosted, key=lambda r: (-r.fused_score, r.fact_id))
475
- except Exception as exc:
476
- logger.warning("Entity graph signal enhancement: %s", exc)
477
- channel_status["entity_graph"] = chstat.ERROR
478
-
479
- # Brain Core S402: bridge and scene expansion append candidates after
480
- # the channel boundary. Reapply the same hard correction-admission rule
481
- # immediately before any candidate can be materialized or reranked.
482
- fused = admit_correction_fusion_results(
483
- fused, profile_id, self._db, as_of=as_of,
484
- known_as_of=known_as_of, valid_at=valid_at,
485
- include_unknown=include_unknown,
486
- include_global=include_global, include_shared=include_shared,
487
- lifecycle_cache=correction_admission,
488
- )
489
-
490
- _em("expand+entity_enh")
491
-
492
- # T-window: prune candidates to the requested event-time range.
493
- # Event times are fetched for the bounded candidate set only (indexed),
494
- # then in-range facts are kept — before fact load, so out-of-window facts
495
- # are never materialized. T3: when the caller passes no explicit window,
496
- # infer one from natural-language scope in the query ("last week").
497
- # Safety: an EXPLICIT window is authoritative (honoured even if it empties
498
- # the set — the user asked for that scope), but an INFERRED window is
499
- # additive and never makes recall worse — if it would empty the results,
500
- # fall back to the unwindowed set.
501
- _explicit_window = window is not None
502
- _window = window if _explicit_window else infer_window_from_query(query)
503
- if _window is not None and fused:
504
- bounds = parse_window(_window)
505
- if bounds is not None:
506
- etimes = self._db.get_fact_event_times(
507
- [fr.fact_id for fr in fused], profile_id,
508
- )
509
- windowed = [
510
- fr for fr in fused
511
- if in_window(etimes.get(fr.fact_id), bounds)
512
- ]
513
- if windowed or _explicit_window:
514
- fused = windowed
515
- _em("time_window")
516
-
517
- # 4. Load facts for rerank pool
518
- pool = min(len(fused), max(effective_limit * 3, 30))
519
- top = fused[:pool]
520
- facts = self._load_facts(
521
- top,
522
- profile_id,
523
- include_global=include_global,
524
- include_shared=include_shared,
525
- )
526
- _em("load_facts")
527
-
528
- # V3.3.21: Session diversity for aggregation queries.
529
- if strat.query_type == "aggregation" and facts:
530
- top = self._enforce_session_diversity(top, facts, min_sessions=3, top_k=20)
531
-
532
- # v3.6.6: Evidence floor — gate on per-channel scores (NOT fused/RRF score).
533
- # Nonsense queries fuse at 0.75-0.78 because RRF is rank-derived and
534
- # uncalibrated. The discriminator is EARNED CHANNEL EVIDENCE:
535
- # semantic >= min_semantic_evidence (0.60) OR bm25 > 0
536
- # OR entity_graph > 0 OR temporal > 0 OR fact is pinned.
537
- # spreading_activation and hopfield do NOT count — they are associative
538
- # amplifiers that fabricated the nonsense results in calibration tests.
539
- # Kill-switch: SLM_RECALL_NO_FLOOR=1 bypasses the floor.
540
- # Runs BEFORE the cross-encoder so the CE batch contains only
541
- # evidence-qualified candidates. The floor gates on channel_scores
542
- # (semantic, bm25, entity_graph, temporal) which are assigned during
543
- # channel execution and are not affected by CE reranking. Moving the
544
- # floor here does not change which queries abstain; it reduces the CE
545
- # batch from ~180 candidates to the qualified subset (~30–60).
546
- import os as _os_floor
547
- floor_enabled = (
548
- getattr(self._config, "evidence_floor_enabled", True)
549
- and _os_floor.environ.get("SLM_RECALL_NO_FLOOR", "0") != "1"
550
- )
551
- if floor_enabled:
552
- min_sem = getattr(self._config, "min_semantic_evidence", 0.60)
553
- # Qualify the rerank pool BEFORE applying the caller's limit. RRF
554
- # can rank associative-only hits above an exact BM25 match; slicing
555
- # first allowed those hits to occupy every output slot and then be
556
- # removed by the floor, producing a false abstention even though a
557
- # qualified candidate was immediately below the slice.
558
- top = self._apply_evidence_floor(top, facts, min_sem)
559
-
560
- # 5. Cross-encoder rerank (optional, on the evidence-qualified pool)
561
- # Bug 4 fix: reduced alpha for multi-hop/temporal to preserve diversity
562
- # V3.3.21: Skip reranker if worker isn't ready yet (cold start).
563
- # Returns results without CE reranking (~5-10pp lower quality) but instant
564
- # instead of blocking 15-19s on first recall. Worker warms up in background.
565
- reranker_ready = (
566
- self._reranker is not None
567
- and getattr(self._reranker, '_worker_ready', False)
568
- )
569
- reranker_applied = False
570
- reranker_status = (
571
- "fallback_not_ready" if self._reranker is not None
572
- else "not_configured"
573
- )
574
- if reranker_ready and facts:
575
- ce_alpha = 0.5 if strat.query_type in ("multi_hop", "temporal") else 0.75
576
- top, reranker_applied, reranker_status = self._apply_reranker(
577
- query, top, facts, alpha=ce_alpha,
578
- )
579
- elif reranker_ready:
580
- reranker_status = "no_candidates"
581
- _em(f"rerank(ready={reranker_ready})")
582
-
583
- # V3.4.11: Channel diversity — guarantee entity_graph results appear in
584
- # the final output. Applied AFTER reranking and evidence qualification
585
- # so an associative-only candidate cannot be reintroduced after the gate.
586
- final_top = top[:effective_limit]
587
- final_top = self._enforce_channel_diversity(
588
- final_top, fused, ch_results, effective_limit,
589
- )
590
-
591
- # A channel-diversity promotion may come from outside the rerank pool.
592
- # Load only when that happens; ordinary recalls reuse the existing map.
593
- if any(fr.fact_id not in facts for fr in final_top):
594
- facts.update(self._load_facts(
595
- final_top,
596
- profile_id,
597
- include_global=include_global,
598
- include_shared=include_shared,
599
- ))
600
-
601
- # Trim facts to the selected, qualified result set.
602
- selected_ids = {fr.fact_id for fr in final_top}
603
- facts = {fid: f for fid, f in facts.items() if fid in selected_ids}
604
-
605
- # 6. Build response
606
- results = self._build_results(final_top, facts, strat)
607
- ms = (time.monotonic() - t0) * 1000.0
608
- no_match = floor_enabled and len(results) == 0
609
- return RecallResponse(
610
- query=query, mode=mode, results=results,
611
- query_type=strat.query_type, channel_weights=strat.weights,
612
- total_candidates=total, retrieval_time_ms=ms,
613
- no_confident_match=no_match,
614
- reranker_applied=reranker_applied,
615
- reranker_status=reranker_status,
616
- # Q2b: thematic context when the top results cluster in one
617
- # community. Precomputed summary lookup only — no per-query LLM.
618
- community_context=self._community_context(results, profile_id),
619
- incomplete_channels=tuple(sorted(dropped_channels)),
620
- channel_status=dict(channel_status),
621
- )
622
-
623
- # -- Community context (Wave Q2b) --------------------------------------
624
-
625
- def _community_context(
626
- self, results: list[Any], profile_id: str, top_k: int = 8,
627
- ) -> dict | None:
628
- """Attach the precomputed community summary the top results fall into.
629
-
630
- On-device-safe (market CRIT-1): a single read of the ≤N precomputed
631
- community_summaries rows + a membership tally — never a per-query LLM
632
- fan-out. Gated: fires only when >=2 of the top results AND >=40% of
633
- them belong to one community, so precise factual queries are untouched.
634
- Fail-open: any error returns None (recall is never affected).
635
- """
636
- if not results or not getattr(
637
- self._config, "enable_community_context", True,
638
- ):
639
- return None
640
- try:
641
- import json
642
- from collections import Counter
643
-
644
- rows = [
645
- dict(r) for r in self._db.execute(
646
- "SELECT community_id, summary, keywords, fact_ids_json, "
647
- "fact_count FROM community_summaries WHERE profile_id = ?",
648
- (profile_id,),
649
- )
650
- ]
651
- if not rows:
652
- return None
653
-
654
- fact_to_cid: dict[str, int] = {}
655
- summ_by_cid: dict[int, dict] = {}
656
- for r in rows:
657
- cid = int(r["community_id"])
658
- summ_by_cid[cid] = r
659
- try:
660
- for fid in json.loads(r.get("fact_ids_json") or "[]"):
661
- fact_to_cid[str(fid)] = cid
662
- except (ValueError, TypeError):
663
- continue
664
-
665
- top_ids = [
666
- res.fact.fact_id
667
- for res in results[:top_k]
668
- if getattr(res, "fact", None) is not None
669
- ]
670
- tally = Counter(
671
- fact_to_cid[fid] for fid in top_ids if fid in fact_to_cid
672
- )
673
- if not tally:
674
- return None
675
- best_cid, count = tally.most_common(1)[0]
676
- coverage = count / len(top_ids) if top_ids else 0.0
677
- if count < 2 or coverage < 0.4:
678
- return None
679
-
680
- row = summ_by_cid[best_cid]
681
- try:
682
- members = json.loads(row.get("fact_ids_json") or "[]")
683
- except (ValueError, TypeError):
684
- members = []
685
- return {
686
- "community_id": best_cid,
687
- "summary": row.get("summary", ""),
688
- "keywords": row.get("keywords", ""),
689
- "member_fact_ids": members,
690
- "coverage": round(coverage, 3),
691
- "matched_results": count,
692
- }
693
- except Exception as exc:
694
- logger.debug("community context skipped (fail-open): %s", exc)
695
- return None
696
-
697
- # -- Evidence floor (v3.6.6) -------------------------------------------
698
-
699
- @staticmethod
700
- def _apply_evidence_floor(
701
- final_top: list[FusionResult],
702
- facts: dict[str, AtomicFact],
703
- min_semantic: float,
704
- ) -> list[FusionResult]:
705
- """Filter results that earned no channel evidence.
706
-
707
- Keep a result only if it earned:
708
- - semantic cosine >= min_semantic (default 0.60), OR
709
- - bm25 > 0, OR entity_graph > 0, OR temporal > 0, OR
710
- - the underlying fact is pinned.
711
-
712
- spreading_activation and hopfield do NOT count as primary evidence.
713
- Empty result after filtering is a success (no_confident_match=True).
714
- """
715
- kept: list[FusionResult] = []
716
- for fr in final_top:
717
- cs = fr.channel_scores or {}
718
- # Primary channel evidence check
719
- if (
720
- cs.get("semantic", 0.0) >= min_semantic
721
- or cs.get("bm25", 0.0) > 0.0
722
- or cs.get("entity_graph", 0.0) > 0.0
723
- or cs.get("temporal", 0.0) > 0.0
724
- ):
725
- kept.append(fr)
726
- continue
727
- # Pinned fact bypass — always pass regardless of channel scores
728
- fact = facts.get(fr.fact_id)
729
- if fact is not None and getattr(fact, "pinned", False):
730
- kept.append(fr)
731
- return kept
732
-
733
- # -- Cross-channel intersection boost -----------------------------------
734
-
735
- @staticmethod
736
- def _apply_cross_channel_intersection(
737
- fused: list[FusionResult],
738
- ch_results: dict[str, list[tuple[str, float]]],
739
- strat: QueryStrategy,
740
- ) -> list[FusionResult]:
741
- """Boost facts that appear across multiple signal-type channels.
742
-
743
- V3.3.21: Solves the channel isolation problem. When a query has both
744
- entity and temporal signals (e.g., "When did Caroline go to X?"), facts
745
- matching BOTH dimensions should rank higher than facts matching only one.
746
-
747
- Channel groups:
748
- - content: semantic, bm25 (text similarity)
749
- - structure: entity_graph, spreading_activation (graph structure)
750
- - temporal: temporal (date proximity)
751
- - associative: hopfield (pattern completion)
752
-
753
- Boost: facts in 2+ groups get 1.5x, facts in 3+ groups get 2.0x.
754
- """
755
- # Map channels to signal groups
756
- _CHANNEL_GROUPS = {
757
- "semantic": "content", "bm25": "content",
758
- "entity_graph": "structure", "spreading_activation": "structure",
759
- "temporal": "temporal",
760
- "hopfield": "associative",
761
- "profile": "content",
762
- }
763
-
764
- # Build fact_id -> set of signal groups it appears in
765
- fact_groups: dict[str, set[str]] = {}
766
- for ch_name, results in ch_results.items():
767
- group = _CHANNEL_GROUPS.get(ch_name, ch_name)
768
- for fid, _score in results:
769
- if fid not in fact_groups:
770
- fact_groups[fid] = set()
771
- fact_groups[fid].add(group)
772
-
773
- # Apply boost based on cross-group coverage
774
- boosted: list[FusionResult] = []
775
- for fr in fused:
776
- groups = fact_groups.get(fr.fact_id, set())
777
- n_groups = len(groups)
778
- if n_groups >= 3:
779
- boost = 2.0
780
- elif n_groups >= 2:
781
- # Extra boost for temporal+structure intersection (the exact gap)
782
- if "temporal" in groups and "structure" in groups:
783
- boost = 1.8
784
- else:
785
- boost = 1.5
786
- else:
787
- boost = 1.0
788
- boosted.append(FusionResult(
789
- fact_id=fr.fact_id,
790
- fused_score=fr.fused_score * boost,
791
- channel_ranks=fr.channel_ranks,
792
- channel_scores=fr.channel_scores,
793
- ))
794
- boosted.sort(key=lambda r: (-r.fused_score, r.fact_id))
795
- return boosted
796
-
797
- # -- Session diversity enforcement ----------------------------------------
798
-
799
- @staticmethod
800
- def _enforce_session_diversity(
801
- fused: list[FusionResult],
802
- fact_map: dict[str, AtomicFact],
803
- min_sessions: int = 3,
804
- top_k: int = 20,
805
- ) -> list[FusionResult]:
806
- """Ensure top-k results span at least min_sessions different session_ids.
807
-
808
- V3.3.21: Category 1 (aggregation) needs facts from MULTIPLE sessions —
809
- 95.7% of cat 1 questions require cross-session evidence. Without this,
810
- top-20 may cluster around 1-2 sessions, missing scattered mentions.
811
-
812
- Algorithm: if top-k has < min_sessions, promote the highest-scored facts
813
- from underrepresented sessions into the top-k window.
814
- """
815
- if len(fused) <= top_k:
816
- return fused
817
-
818
- top = fused[:top_k]
819
- rest = fused[top_k:]
820
-
821
- sessions_in_top: set[str] = set()
822
- for fr in top:
823
- fact = fact_map.get(fr.fact_id)
824
- if fact and fact.session_id:
825
- sessions_in_top.add(fact.session_id)
826
-
827
- if len(sessions_in_top) >= min_sessions:
828
- return fused
829
-
830
- promoted: list[FusionResult] = []
831
- for fr in rest:
832
- fact = fact_map.get(fr.fact_id)
833
- if fact and fact.session_id and fact.session_id not in sessions_in_top:
834
- sessions_in_top.add(fact.session_id)
835
- promoted.append(fr)
836
- if len(sessions_in_top) >= min_sessions:
837
- break
838
-
839
- if not promoted:
840
- return fused
841
-
842
- promoted_ids = {fr.fact_id for fr in promoted}
843
- remaining = [fr for fr in rest if fr.fact_id not in promoted_ids]
844
- return top + promoted + remaining
845
-
846
- # -- Channel diversity enforcement ----------------------------------------
847
-
848
- @staticmethod
849
- def _enforce_channel_diversity(
850
- top: list,
851
- fused: list,
852
- ch_results: dict[str, list[tuple[str, float]]],
853
- effective_limit: int,
854
- ) -> list:
855
- """Keep strong lexical and structure evidence visible in the result cap.
856
-
857
- A semantic channel with a larger weight can fill a small result limit
858
- even when BM25 has an exact, high-signal hit. That broke the
859
- ``queryable now`` ingestion contract: a freshly inserted FTS row could
860
- exist durably but remain invisible to immediate recall. Reserve one
861
- capped slot for a strong BM25 hit and two for a structure channel when
862
- such candidates exist, without returning more than ``effective_limit``.
863
- """
864
- channel_minimums = (
865
- ("bm25", 1, 0.0),
866
- ("entity_graph", 2, 0.0),
867
- )
868
- top_ids = {fr.fact_id for fr in top}
869
-
870
- promoted = []
871
- for ch_name, minimum, score_floor in channel_minimums:
872
- ch_items = ch_results.get(ch_name, [])
873
- if not ch_items:
874
- continue
875
-
876
- eligible_ids = {
877
- fid
878
- for fid, score in ch_items
879
- if (
880
- float(score) > score_floor
881
- if ch_name == "bm25"
882
- else float(score) >= score_floor
883
- )
884
- }
885
- if not eligible_ids:
886
- continue
887
-
888
- present = sum(1 for fid in eligible_ids if fid in top_ids)
889
- if present >= minimum:
890
- continue
891
-
892
- needed = minimum - present
893
- for fr in fused:
894
- if fr.fact_id in eligible_ids and fr.fact_id not in top_ids:
895
- promoted.append(fr)
896
- top_ids.add(fr.fact_id)
897
- needed -= 1
898
- if needed <= 0:
899
- break
900
-
901
- if not promoted:
902
- return top
903
-
904
- selected = promoted[:effective_limit]
905
- result = list(top[:effective_limit])
906
- free_slots = max(0, effective_limit - len(result))
907
- result.extend(selected[:free_slots])
908
- remaining = selected[free_slots:]
909
- if remaining:
910
- keep = max(0, effective_limit - len(remaining))
911
- result = result[:keep] + remaining
912
- return result[:effective_limit]
913
-
914
- # -- Channel execution --------------------------------------------------
915
-
916
- def _embed_query(self, query: str) -> list[float] | None:
917
- """Embed query with LRU cache. Avoids redundant Ollama/API calls."""
918
- if self._embedder is None:
919
- return None
920
- cached = self._query_embedding_cache.get(query)
921
- if cached is not None:
922
- return cached
923
- emb = self._embedder.embed(query)
924
- # Evict oldest if cache full
925
- if len(self._query_embedding_cache) >= self._cache_max_size:
926
- oldest = next(iter(self._query_embedding_cache))
927
- del self._query_embedding_cache[oldest]
928
- self._query_embedding_cache[query] = emb
929
- return emb
930
-
931
- def _semantic_rank_for_unenriched(
932
- self, ch_results: dict[str, list[tuple[str, float]]],
933
- ) -> dict[str, list[tuple[str, float]]]:
934
- """Give a candidate whose vector does not exist yet a fair semantic rank.
935
-
936
- Fusion here is rank-based, so a fact the semantic channel did not return
937
- forfeits that channel's entire contribution — the most heavily weighted
938
- one. When the reason for that absence is simply that the vector has not
939
- been computed yet, the absence describes the ingest pipeline and says
940
- nothing about the fact. Left alone, a memory written seconds ago is the
941
- hardest thing in the store to find, which is the worst possible failure
942
- for this product.
943
-
944
- Such candidates are placed at the MEDIAN of the semantic ranking, never
945
- near the top: enough to compete on their other evidence, not enough to
946
- win on freshness alone. A candidate that HAS a vector and still was not
947
- returned is left exactly as it is — that absence is real evidence of
948
- irrelevance, and the two must not be confused.
949
-
950
- Returns a new mapping; the input is not modified.
951
- """
952
- sem = ch_results.get("semantic") or []
953
- if not sem:
954
- return ch_results
955
- if not getattr(self._config, "write_recency_floor_enabled", True):
956
- return ch_results
957
- if os.environ.get("SLM_WRITE_RECENCY_NO_FLOOR", "0") == "1":
958
- return ch_results
959
-
960
- have = {fid for fid, _ in sem}
961
- elsewhere = {
962
- fid
963
- for name, rows in ch_results.items()
964
- if name != "semantic"
965
- for fid, _ in rows
966
- }
967
- candidates = sorted(elsewhere - have)
968
- if not candidates:
969
- return ch_results
970
-
971
- from datetime import UTC, datetime, timedelta
972
-
973
- minutes = float(getattr(self._config, "write_recency_floor_minutes", 60.0))
974
- cutoff = (datetime.now(UTC) - timedelta(minutes=minutes)).isoformat()
975
- placeholders = ",".join("?" for _ in candidates)
976
- try:
977
- # A missing embedding_metadata row means no vector projection exists,
978
- # which is what makes the semantic channel's silence uninformative.
979
- unenriched = [
980
- dict(r)["fact_id"]
981
- for r in self._db.execute(
982
- f"SELECT af.fact_id FROM atomic_facts AS af "
983
- f"LEFT JOIN embedding_metadata AS em ON em.fact_id = af.fact_id "
984
- f"WHERE af.fact_id IN ({placeholders}) "
985
- f" AND em.fact_id IS NULL "
986
- f" AND af.created_at >= ?",
987
- (*candidates, cutoff),
988
- )
989
- ]
990
- except (NameError, AttributeError, TypeError):
991
- # These mean this code is wrong, not that the data is unusual. A bare
992
- # `except Exception` here hid a missing import and left the whole
993
- # feature silently inert while every test still passed.
994
- raise
995
- except Exception as exc:
996
- # A store without this table, or a locked database: ranking must still
997
- # return. Logged at warning, because "silently did nothing" is the
998
- # failure mode this task exists to fix.
999
- logger.warning("recent-unenriched admission skipped: %s: %s",
1000
- type(exc).__name__, exc)
1001
- return ch_results
1002
- if not unenriched:
1003
- return ch_results
1004
-
1005
- scores = sorted(s for _, s in sem)
1006
- mid = len(scores) // 2
1007
- median = (
1008
- scores[mid] if len(scores) % 2 == 1
1009
- else (scores[mid - 1] + scores[mid]) / 2.0
1010
- )
1011
- insert_at = len(sem) // 2
1012
- merged = list(sem[:insert_at]) + [(fid, median) for fid in unenriched] + list(sem[insert_at:])
1013
- logger.debug(
1014
- "admitted %d recent un-enriched candidate(s) at semantic rank %d of %d",
1015
- len(unenriched), insert_at + 1, len(merged),
1016
- )
1017
- return {**ch_results, "semantic": merged}
1018
-
1019
- def _run_channels(
1020
- self,
1021
- query: str,
1022
- profile_id: str,
1023
- strat: QueryStrategy,
1024
- *,
1025
- extra_disabled_channels: set[str] | None = None,
1026
- include_global: bool = False,
1027
- include_shared: bool = False,
1028
- as_of: str | None = None,
1029
- known_as_of: str | None = None,
1030
- valid_at: str | None = None,
1031
- include_unknown: bool = False,
1032
- dropped_channels: set[str] | None = None,
1033
- channel_status: dict[str, str] | None = None,
1034
- ) -> dict[str, list[tuple[str, float]]]:
1035
- """Run active retrieval channels.
1036
-
1037
- v3.4.53: channels run in PARALLEL via ThreadPoolExecutor. Industry
1038
- standard (EverMemOS, szl-recall, ContentPilot 2026): all channels
1039
- are independent after embedding; running them serially wastes time
1040
- equal to the sum of all producer latencies. When multiple producers are
1041
- enabled and healthy, parallel dispatch generally bounds the producer
1042
- phase by the slowest submitted producer, plus serial embedding and
1043
- result-collection overhead.
1044
-
1045
- ``dropped_channels``, when given, receives the name of every channel
1046
- abandoned at ``CHANNEL_HANG_GUARD_SECONDS``. Those channels contributed
1047
- nothing, so the caller needs to know the answer is incomplete rather
1048
- than merely late. It is a caller-owned set passed down per recall and
1049
- deliberately not an attribute of self — two concurrent recalls sharing
1050
- one would report each other's losses (the v3.4.64 race).
1051
-
1052
- ``channel_status``, likewise caller-owned, receives one entry per
1053
- channel saying what became of it. Every channel gets exactly one:
1054
- those that cannot run are recorded before dispatch with the reason, and
1055
- every dispatched channel is recorded by the collection loop below —
1056
- which iterates the futures, so it cannot skip one.
1057
- """
1058
- import os as _os_e
1059
- import time as _time_e
1060
- _et = bool(_os_e.environ.get("SLM_RECALL_TIMING"))
1061
- out: dict[str, list[tuple[str, float]]] = {}
1062
- # Skip channels listed in disabled_channels (ablation support)
1063
- # V3.4.40: union with per-recall extra_disabled set (e.g. --fast skip)
1064
- # V3.4.64: extra_disabled is now a local parameter, not a shared instance
1065
- # attribute — eliminates the concurrent-recall race condition.
1066
- disabled = set(self._config.disabled_channels) | set(extra_disabled_channels or ())
1067
-
1068
- # V3.3.4: Embed query ONCE, reuse for semantic + hopfield channels
1069
- q_emb: list[float] | None = None
1070
- needs_embedding = (
1071
- (self._semantic is not None and "semantic" not in disabled)
1072
- or (self._hopfield is not None and "hopfield" not in disabled)
1073
- or (self._spreading_activation is not None and "spreading_activation" not in disabled)
1074
- )
1075
- if needs_embedding:
1076
- try:
1077
- q_emb = self._embed_query(query)
1078
- if q_emb is None:
1079
- logger.warning(
1080
- "Query embedding returned None — semantic, hopfield, "
1081
- "spreading_activation channels will be skipped this recall"
1082
- )
1083
- except Exception as exc:
1084
- logger.warning("Query embedding failed: %s", exc)
1085
-
1086
- # Why a channel will not run, recorded BEFORE dispatch. An embedding
1087
- # failure silently takes three of the five channels down together, and
1088
- # the answer never said so: it looked exactly like a store with nothing
1089
- # relevant in it. Configuration and ablation are recorded too, so an
1090
- # operator reading a list of absent channels can tell their own choices
1091
- # apart from a fault.
1092
- if channel_status is not None:
1093
- for _name, _obj, _needs_emb in (
1094
- ("semantic", self._semantic, True),
1095
- ("bm25", self._bm25, False),
1096
- ("temporal", self._temporal, False),
1097
- ("hopfield", self._hopfield, True),
1098
- ("spreading_activation", self._spreading_activation, True),
1099
- ):
1100
- if _obj is None:
1101
- channel_status[_name] = chstat.NOT_CONFIGURED
1102
- elif _name in disabled:
1103
- channel_status[_name] = chstat.DISABLED
1104
- elif _needs_emb and q_emb is None:
1105
- channel_status[_name] = chstat.NO_EMBEDDING
1106
-
1107
- # v3.4.53: collect channel callables and run in parallel.
1108
- # Each channel is a standalone search — no shared mutable state,
1109
- # no ordering dependencies. SQLite WAL mode permits concurrent reads.
1110
- futures: dict[str, concurrent.futures.Future] = {}
1111
-
1112
- def _safe_channel(name: str, fn, *args):
1113
- """Run a single channel, returning (name, result_or_None, status).
1114
-
1115
- Returning the status alongside the result is what separates "found
1116
- nothing" from "raised": both used to come back as ``None``.
1117
- """
1118
- _cs = _time_e.monotonic() if _et else 0.0
1119
- try:
1120
- res = fn(*args)
1121
- if _et:
1122
- logger.warning("[RECALL-TIMING] channel.%-16s %.0f ms",
1123
- name, (_time_e.monotonic() - _cs) * 1000.0)
1124
- if res:
1125
- return (name, res, chstat.OK)
1126
- return (name, None, chstat.EMPTY)
1127
- except Exception as exc:
1128
- logger.warning("%s channel: %s", name, exc)
1129
- return (name, None, chstat.ERROR)
1130
-
1131
- executor = self._channel_executor
1132
- if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
1133
- futures["semantic"] = executor.submit(
1134
- _safe_channel, "semantic",
1135
- functools.partial(
1136
- self._semantic.search,
1137
- include_global=include_global, include_shared=include_shared,
1138
- ),
1139
- q_emb, profile_id, self._config.semantic_top_k,
1140
- )
1141
- if self._bm25 is not None and "bm25" not in disabled:
1142
- futures["bm25"] = executor.submit(
1143
- _safe_channel, "bm25",
1144
- functools.partial(
1145
- self._bm25.search,
1146
- include_global=include_global, include_shared=include_shared,
1147
- ),
1148
- query, profile_id, self._config.bm25_top_k,
1149
- )
1150
- if self._temporal is not None and "temporal" not in disabled:
1151
- futures["temporal"] = executor.submit(
1152
- _safe_channel, "temporal",
1153
- functools.partial(
1154
- self._temporal.search,
1155
- include_global=include_global, include_shared=include_shared,
1156
- query_type=strat.query_type,
1157
- ),
1158
- query, profile_id, self._config.bm25_top_k,
1159
- )
1160
- if self._hopfield is not None and q_emb is not None and "hopfield" not in disabled:
1161
- futures["hopfield"] = executor.submit(
1162
- _safe_channel, "hopfield",
1163
- functools.partial(
1164
- self._hopfield.search,
1165
- include_global=include_global, include_shared=include_shared,
1166
- ),
1167
- q_emb, profile_id, self._config.hopfield_top_k,
1168
- )
1169
- if (
1170
- self._spreading_activation is not None
1171
- and q_emb is not None
1172
- and "spreading_activation" not in disabled
1173
- ):
1174
- futures["spreading_activation"] = executor.submit(
1175
- _safe_channel, "spreading_activation",
1176
- functools.partial(
1177
- self._spreading_activation.search,
1178
- include_global=include_global, include_shared=include_shared,
1179
- ),
1180
- q_emb, profile_id, self._config.bm25_top_k,
1181
- )
1182
-
1183
- # One shared limit keeps parallel dispatch genuinely bounded. A
1184
- # per-future timeout here would serialise the wait and turn five slow
1185
- # channels into five seconds of UI latency.
1186
- done, pending = concurrent.futures.wait(
1187
- futures.values(), timeout=CHANNEL_HANG_GUARD_SECONDS,
1188
- )
1189
- for name, fut in futures.items():
1190
- if fut in pending:
1191
- # Not a latency notice: this answer is missing whatever this
1192
- # channel alone could see, so it is logged at the level that
1193
- # says so and recorded for the caller.
1194
- logger.error(
1195
- "Channel %s did not finish within %.1fs; this recall is "
1196
- "answering without it",
1197
- name, CHANNEL_HANG_GUARD_SECONDS,
1198
- )
1199
- if dropped_channels is not None:
1200
- dropped_channels.add(name)
1201
- # Same branch as the dropped set on purpose: two writes in one
1202
- # place cannot disagree about which channels timed out.
1203
- if channel_status is not None:
1204
- channel_status[name] = chstat.TIMEOUT
1205
- fut.cancel() # no-op if already running; prevents queued jobs from starting
1206
- continue
1207
- try:
1208
- ch_name, result, status = fut.result()
1209
- if channel_status is not None:
1210
- channel_status[ch_name] = status
1211
- if result:
1212
- out[ch_name] = result
1213
- except Exception as exc:
1214
- logger.warning("Channel %s failed: %s", name, exc)
1215
- if channel_status is not None:
1216
- channel_status[name] = chstat.ERROR
1217
-
1218
- # Apply registered post-retrieval filters (forgetting filter, etc.).
1219
- # Pass as_of in context dict when set so the bi-temporal validity filter
1220
- # can perform point-in-time demotion. None context preserves the existing
1221
- # behaviour for all callers that don't use time-travel recall.
1222
- _filter_context = {"as_of": as_of} if as_of is not None else None
1223
- if hasattr(self, '_registry') and self._registry._filters:
1224
- for fn in self._registry._filters:
1225
- try:
1226
- out = fn(out, profile_id, _filter_context)
1227
- except Exception as exc:
1228
- logger.warning("Post-retrieval filter failed: %s", exc)
1229
-
1230
- return out
1231
-
1232
- def close(self, *, wait: bool = False) -> None:
1233
- """Release owned channel workers without blocking daemon shutdown.
1234
-
1235
- Active channel calls have their own response deadline. Waiting here
1236
- can still deadlock shutdown when an extension ignores that deadline,
1237
- so the daemon uses the executor's non-blocking cancellation path.
1238
- """
1239
- with self._close_lock:
1240
- if self._closed:
1241
- return
1242
- self._closed = True
1243
- self._channel_executor.shutdown(wait=wait, cancel_futures=True)
1244
-
1245
- # -- Fact loading -------------------------------------------------------
1246
-
1247
- def _load_facts(
1248
- self,
1249
- fused: list[FusionResult],
1250
- profile_id: str,
1251
- *,
1252
- include_global: bool = False,
1253
- include_shared: bool = False,
1254
- ) -> dict[str, AtomicFact]:
1255
- """Load facts by ID — targeted query, not full-table scan.
1256
-
1257
- V3.3.13: Was loading ALL facts (O(n) memory) then filtering.
1258
- Now uses get_facts_by_ids() for O(k) where k = pool size (~60).
1259
- """
1260
- needed = [fr.fact_id for fr in fused]
1261
- if not needed:
1262
- return {}
1263
- facts = self._db.get_facts_by_ids(
1264
- needed, profile_id,
1265
- include_global=include_global,
1266
- include_shared=include_shared,
1267
- )
1268
- return {f.fact_id: f for f in facts}
1269
-
1270
- # -- Cross-encoder rerank -----------------------------------------------
1271
-
1272
- @staticmethod
1273
- def _sigmoid(x: float) -> float:
1274
- """Numerically stable sigmoid."""
1275
- x = max(-500.0, min(500.0, x))
1276
- return 1.0 / (1.0 + math.exp(-x))
1277
-
1278
- def _apply_reranker(
1279
- self, query: str, fused: list[FusionResult],
1280
- fact_map: dict[str, AtomicFact],
1281
- alpha: float = 0.75,
1282
- ) -> tuple[list[FusionResult], bool, str]:
1283
- """Rerank with blended CE + RRF scores (Bug 1 fix).
1284
-
1285
- Blended: alpha * sigmoid(CE_score) + (1 - alpha) * rrf_score.
1286
- Speaker tags stripped before scoring (Bug 3 fix).
1287
- """
1288
- # Bug 2 fix: score ALL candidates, not just top_k. v3.8.5: verified on
1289
- # the real DB that bounding the CE to the top-N fusion candidates both
1290
- # (a) gave NO latency win (the cross-encoder batches all pairs in one
1291
- # forward pass, so 60 vs 184 pairs is within noise) and (b) CHANGED the
1292
- # top-5 on 4/8 queries — the CE legitimately promotes items ranked below
1293
- # the fusion top-N into the answer. So exhaustive reranking stays: it is
1294
- # a quality feature, not the latency bottleneck.
1295
- candidates = [
1296
- (fact_map[fr.fact_id], fr.fused_score)
1297
- for fr in fused if fr.fact_id in fact_map
1298
- ]
1299
- if not candidates:
1300
- return fused, False, "no_candidates"
1301
-
1302
- # V3.3.16: Strip speaker tags WITHOUT copying full AtomicFact objects.
1303
- # Previously created full copies including 768-dim embeddings (~6KB each),
1304
- # which over 304 recalls caused pymalloc arena fragmentation → 25GB.
1305
- # Now: temporarily patch .content on originals, rerank, then restore.
1306
- originals: list[tuple[AtomicFact, str]] = [] # (fact, original_content)
1307
- for fact, _ in candidates:
1308
- orig = fact.content
1309
- fact.content = re.sub(r'^\[[A-Za-z]+\]:\s*', '', orig)
1310
- originals.append((fact, orig))
1311
-
1312
- try:
1313
- rerank_with_status = getattr(
1314
- self._reranker, "rerank_with_status", None,
1315
- )
1316
- # MagicMock fabricates arbitrary attributes; only use the richer
1317
- # contract when it is defined by the reranker type itself.
1318
- if callable(rerank_with_status) and hasattr(
1319
- type(self._reranker), "rerank_with_status",
1320
- ):
1321
- scored, applied, status = rerank_with_status(
1322
- query, candidates, top_k=len(candidates),
1323
- )
1324
- else:
1325
- scored = self._reranker.rerank( # type: ignore[union-attr]
1326
- query, candidates, top_k=len(candidates),
1327
- )
1328
- applied, status = True, "applied"
1329
- except Exception as exc:
1330
- logger.warning("Cross-encoder rerank failed: %s", exc)
1331
- return fused, False, "error"
1332
- finally:
1333
- # Restore original content (with speaker tags)
1334
- for fact, orig_content in originals:
1335
- fact.content = orig_content
1336
-
1337
- if not applied:
1338
- return fused, False, status
1339
-
1340
- # The worker can report applied=True while returning scores=null — the
1341
- # subprocess answers, so the call "succeeded", but there is nothing to
1342
- # score with. Iterating None here raised TypeError from OUTSIDE the
1343
- # try/except above (which only wraps the rerank call itself), so the
1344
- # error escaped into the recall path rather than degrading to the fused
1345
- # ordering. Fail soft: reranking is a quality improvement on top of a
1346
- # correct result set, never a correctness requirement.
1347
- # `not scored` covers None AND an empty sequence. An empty list is the
1348
- # same defect wearing different clothes: the worker says applied=True but
1349
- # supplied nothing to rank with. Guarding only None would let [] through
1350
- # to build an empty score_map, and every candidate would then be scored
1351
- # against a degenerate min/max — silently shrinking the fused component
1352
- # by (1 - alpha) while still reporting the rerank as applied.
1353
- if not scored:
1354
- logger.warning(
1355
- "Cross-encoder worker reported applied=True with %s scores; "
1356
- "falling back to fused ranking for this query.",
1357
- "null" if scored is None else "empty",
1358
- )
1359
- return fused, False, "worker_null_scores"
1360
-
1361
- score_map = {fact.fact_id: score for fact, score in scored}
1362
-
1363
- # Min-max normalize CE scores to [0, 1] within the batch instead of
1364
- # sigmoid (which compresses the useful discrimination range).
1365
- ce_values = list(score_map.values())
1366
- ce_min = min(ce_values) if ce_values else 0.0
1367
- ce_max = max(ce_values) if ce_values else 1.0
1368
- ce_range = ce_max - ce_min if ce_max > ce_min else 1.0
1369
-
1370
- # Also normalize RRF scores so both terms contribute meaningfully
1371
- rrf_values = [fr.fused_score for fr in fused]
1372
- rrf_max = max(rrf_values) if rrf_values else 1.0
1373
- rrf_max = rrf_max if rrf_max > 0 else 1.0
1374
-
1375
- updated = [
1376
- FusionResult(
1377
- fact_id=fr.fact_id,
1378
- fused_score=(
1379
- alpha * ((score_map.get(fr.fact_id, ce_min) - ce_min) / ce_range)
1380
- + (1.0 - alpha) * (fr.fused_score / rrf_max)
1381
- ),
1382
- channel_ranks=fr.channel_ranks,
1383
- channel_scores=fr.channel_scores,
1384
- )
1385
- for fr in fused
1386
- ]
1387
- updated.sort(key=lambda r: (-r.fused_score, r.fact_id))
1388
- return updated, True, "applied"
1389
-
1390
- # -- Agentic adapter -----------------------------------
1391
-
1392
- def recall_facts(
1393
- self, query: str, profile_id: str,
1394
- top_k: int = 20, skip_agentic: bool = True,
1395
- ) -> list[tuple[AtomicFact, float]]:
1396
- """Simplified recall returning (fact, score) tuples.
1397
-
1398
- Used by AgenticRetriever for round-2 re-retrieval.
1399
- skip_agentic is always True here to prevent infinite recursion.
1400
- """
1401
- response = self.recall(query, profile_id, limit=top_k)
1402
- return [(r.fact, r.score) for r in response.results]
1403
-
1404
- # -- Trust weighting ----------------------------------------------------
1405
-
1406
- def _get_trust_weight(self, fact: AtomicFact, profile_id: str) -> tuple[float, float]:
1407
- """Look up Bayesian trust score and convert to a multiplicative weight.
1408
-
1409
- Returns (trust_weight, raw_trust_score).
1410
- trust_weight is clamped to [0.5, 1.5]:
1411
- - trust=0.0 -> weight=0.5 (demote untrusted facts)
1412
- - trust=0.5 -> weight=1.0 (neutral, default prior)
1413
- - trust=1.0 -> weight=1.5 (promote highly trusted facts)
1414
- If trust scoring is disabled or unavailable, returns (1.0, 0.5).
1415
- """
1416
- if not self._config.use_trust_weighting or self._trust_scorer is None:
1417
- return 1.0, 0.5
1418
-
1419
- try:
1420
- raw = self._trust_scorer.get_fact_trust(fact.fact_id, profile_id)
1421
- except Exception:
1422
- return 1.0, 0.5
1423
-
1424
- # Linear map: trust 0.0->0.5, 0.5->1.0, 1.0->1.5
1425
- weight = 0.5 + raw # raw in [0, 1] -> weight in [0.5, 1.5]
1426
- return weight, raw
1427
-
1428
- # -- Response building --------------------------------------------------
1429
-
1430
- def _build_results(
1431
- self, fused: list[FusionResult], fact_map: dict[str, AtomicFact],
1432
- strat: QueryStrategy,
1433
- ) -> list[RetrievalResult]:
1434
- from datetime import UTC, datetime
1435
- now = datetime.now(UTC)
1436
- results: list[RetrievalResult] = []
1437
- profile_id = next(
1438
- (f.profile_id for f in fact_map.values()), "default",
1439
- )
1440
- for fr in fused:
1441
- fact = fact_map.get(fr.fact_id)
1442
- if fact is None:
1443
- continue
1444
- evidence = [
1445
- f"{ch}(rank={rk}, score={fr.channel_scores.get(ch, 0.0):.4f})"
1446
- # Channel name breaks a tie, so the evidence string a caller
1447
- # sees is the same on two runs when two channels agree on rank.
1448
- for ch, rk in sorted(fr.channel_ranks.items(), key=lambda x: (x[1], x[0]))
1449
- if rk < 1000
1450
- ]
1451
- # Recency decay: Ebbinghaus exponential + FSRS stability strengthening (v3.4.51).
1452
- #
1453
- # Base: R = e^(-λt), λ = ln(2)/S, S = effective half-life in days.
1454
- # FSRS v5 (Dae & Jarrett 2024): S grows with successful recall frequency.
1455
- # S_effective = S_base × min(2.0, 1 + 0.1 × access_count)
1456
- # → 0 recalls: S=30d 5 recalls: S=45d 10+ recalls: S=60d (max)
1457
- # Effect: frequently-recalled architectural decisions resist decay naturally;
1458
- # one-off session handoffs and debug notes decay at full rate.
1459
- #
1460
- # Boost range: [0.80×, 1.10×]
1461
- # 0d, 0acc → 1.10× 45d, 0acc → 0.91× 90d, 0acc → 0.84×
1462
- # 45d, 5acc → 0.95× 90d, 10acc → 0.90× (frequently used memories stay relevant)
1463
- age_days = 0.0
1464
- age_known = False
1465
- if fact.created_at:
1466
- try:
1467
- created = datetime.fromisoformat(fact.created_at.replace("Z", "+00:00"))
1468
- age_days = max(0.0, (now - created).total_seconds() / 86400.0)
1469
- age_known = True
1470
- except (ValueError, TypeError):
1471
- pass
1472
- _access = max(0, getattr(fact, "access_count", 0) or 0)
1473
- _S = 30.0 * min(2.0, 1.0 + 0.1 * _access)
1474
- recency_boost = 0.8 + 0.3 * math.exp(-(math.log(2) / _S) * age_days)
1475
-
1476
- # Content quality: penalize short/low-info facts that rank high
1477
- # due to BM25 name-matching (greetings like "Hey Caroline!" score high
1478
- # on BM25 but have zero retrieval value)
1479
- content_len = len(fact.content.strip())
1480
- if content_len < 10:
1481
- quality = 0.3
1482
- elif content_len < 25:
1483
- quality = 0.7
1484
- else:
1485
- quality = 1.0
1486
-
1487
- # Trust weighting: Bayesian trust modulates final ranking
1488
- trust_weight, raw_trust = self._get_trust_weight(fact, profile_id)
1489
-
1490
- boosted_score = fr.fused_score * recency_boost * quality * trust_weight
1491
-
1492
- # Query-type-conditioned recency amplifier.
1493
- # Applied only to "recency" and "temporal" queries; factual, entity,
1494
- # and all other types receive a factor of exactly 1.0 (no change).
1495
- # The amplitude scalar is read from RetrievalConfig so it can be tuned
1496
- # or zeroed at runtime. strength=0.0 is a strict no-op — the if-guard
1497
- # ensures the previous ranking is reproduced byte-for-byte.
1498
- #
1499
- # recency — 7-day half-life, 1.5× maximum (present-activity queries)
1500
- # temporal — 30-day half-life, 1.2× maximum (past-event queries)
1501
- #
1502
- # Hook for the follow-on embedding-lag adjustment (task 2.6): that
1503
- # adjustment also multiplies boosted_score and belongs immediately after
1504
- # this block, conditioned on channel_scores["semantic"] == 0.0 AND
1505
- # age_days < 1.0. Add it as an independent if-block here so the two
1506
- # factors compose cleanly without restructuring what is above or below.
1507
- _prior_strength = getattr(self._config, "recency_prior_strength", 0.5)
1508
- # age_known matters here: the fallback above leaves age_days at 0.0
1509
- # when a fact carries no usable timestamp, which reads as "written
1510
- # moments ago" and would hand an undated fact the largest possible
1511
- # boost for being new. Not knowing when something was written is not
1512
- # evidence that it is fresh.
1513
- if (_prior_strength > 0.0 and age_known
1514
- and strat.query_type in ("recency", "temporal")):
1515
- _half_life = 7.0 if strat.query_type == "recency" else 30.0
1516
- # Both query types use max_amp=1.5 so the decay is visible.
1517
- # With max_amp=1.2 and half_life=30, the raw value at age 0d
1518
- # is 1.5 and at age 30d is 1.25 — both clamp to 1.2. The prior
1519
- # was inert over the first ~39 days, which is the range it was
1520
- # built to discriminate. Raising the cap to 1.5 lets the
1521
- # formula vary from 1.5 (fresh) through 1.25 (30d) toward 1.0
1522
- # (old). This changes ranking: facts from 2 days ago and 30
1523
- # days ago now receive different boosts. The change is a
1524
- # correction to a clamp that made the prior inert, not a
1525
- # measured gain.
1526
- _max_amp = 1.5
1527
- _cond_boost = 1.0 + _prior_strength * math.exp(
1528
- -(math.log(2) / _half_life) * age_days
1529
- )
1530
- _cond_boost = min(_cond_boost, _max_amp)
1531
- boosted_score = boosted_score * _cond_boost
1532
-
1533
- # v3.5.0 (M2): soft-normalize to [0,1]. RRF weights + scene/entity
1534
- # boosts push raw scores well above 1 (observed: 27.97). A sigmoid
1535
- # preserves rank (monotonic) while giving users a readable 0-1 range.
1536
- normalized_score = 1.0 / (1.0 + math.exp(-boosted_score * 0.5))
1537
- results.append(RetrievalResult(
1538
- fact=fact, score=round(normalized_score, 4),
1539
- channel_scores=fr.channel_scores,
1540
- confidence=fact.confidence,
1541
- relevance_score=round(normalized_score, 4),
1542
- ranking_score=boosted_score,
1543
- memory_confidence=fact.confidence,
1544
- evidence_chain=evidence,
1545
- trust_score=raw_trust,
1546
- ))
1547
- # ranking_score incorporates every modifier computed in this loop
1548
- # (Ebbinghaus decay, quality, trust, and the query-type-conditioned
1549
- # recency amplifier). Sort here so RecallResponse.results[0] is
1550
- # always the highest-ranked fact — callers that rely on the returned
1551
- # order get the amplified ranking, not the pre-amplifier fused order.
1552
- # Tie-break on fact_id keeps two runs over an unchanged store stable.
1553
- results.sort(key=lambda r: (-(r.ranking_score or 0.0), r.fact.fact_id))
1554
- return results
1555
-
1556
-
1557
- # ---------------------------------------------------------------------------
1558
- # apply_channel_weights (LLD-03 §5.5 — module-level pure helper)
1559
- # ---------------------------------------------------------------------------
1560
-
1561
-
1562
- _CHANNEL_KEYS: tuple[str, ...] = (
1563
- "semantic", "bm25", "entity_graph", "temporal",
1564
- # hopfield + spreading_activation are real retrieval channels (score
1565
- # contract v2) with bandit-chosen weights; omitting them here silently
1566
- # discarded adaptive reranking for multi-hop relational recall.
1567
- "spreading_activation", "hopfield",
1568
- )
1569
-
1570
-
1571
- def apply_channel_weights(
1572
- candidates: list[RetrievalResult],
1573
- weights: dict[str, float] | None,
1574
- ) -> list[RetrievalResult]:
1575
- """Re-score candidates under a bandit-chosen weight bundle.
1576
-
1577
- Multiplies each candidate's ``channel_scores[ch]`` by ``weights[ch]``
1578
- and applies ``cross_encoder_bias`` to the final score. Preserves order;
1579
- callers reorder via ensemble_rerank.
1580
-
1581
- Returns a NEW list with new ``RetrievalResult`` instances — never mutates
1582
- input. Unknown / missing weights default to 1.0.
1583
-
1584
- Safe against ``weights=None`` (returns input unchanged) and empty lists.
1585
- """
1586
- if not candidates or not weights:
1587
- return list(candidates)
1588
-
1589
- ce_bias = float(weights.get("cross_encoder_bias", 1.0))
1590
- out: list[RetrievalResult] = []
1591
- for c in candidates:
1592
- original_cs = c.channel_scores or {}
1593
- new_cs: dict[str, float] = dict(original_cs)
1594
- base = 0.0
1595
- for ch in _CHANNEL_KEYS:
1596
- raw = float(original_cs.get(ch, 0.0))
1597
- w = float(weights.get(ch, 1.0))
1598
- scaled = raw * w
1599
- new_cs[ch] = scaled
1600
- base += scaled
1601
- new_score = (base if base > 0.0 else float(c.score)) * ce_bias
1602
- out.append(RetrievalResult(
1603
- fact=c.fact,
1604
- score=c.score,
1605
- channel_scores=new_cs,
1606
- confidence=c.confidence,
1607
- relevance_score=c.relevance_score,
1608
- ranking_score=new_score,
1609
- memory_confidence=c.memory_confidence,
1610
- rank_position=c.rank_position,
1611
- evidence_chain=c.evidence_chain,
1612
- trust_score=c.trust_score,
1613
- marker=c.marker,
1614
- ))
1615
- return out