superlocalmemory 3.6.22 → 3.7.0

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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -13,7 +13,7 @@ where all variances are identical, Fisher distance degenerates to a
13
13
  monotonic transform of Euclidean distance — same ranking as cosine.
14
14
 
15
15
  Part of Qualixar | Author: Varun Pratap Bhardwaj
16
- License: Elastic-2.0
16
+ License: AGPL-3.0-or-later
17
17
  """
18
18
 
19
19
  from __future__ import annotations
@@ -34,6 +34,20 @@ logger = logging.getLogger(__name__)
34
34
  _VARIANCE_FLOOR: float = 1e-6
35
35
 
36
36
 
37
+ class _LanceCandidateSource:
38
+ """Adapt the promoted Lance projection to the existing candidate contract."""
39
+
40
+ available = True
41
+
42
+ def __init__(self, backend: Any) -> None:
43
+ self._backend = backend
44
+
45
+ def search(self, query_embedding: list[float], *, top_k: int, profile_id: str) -> list[tuple[str, float]]:
46
+ return self._backend.similarity_search(
47
+ query_embedding, top_k=top_k, profile_id=profile_id,
48
+ )
49
+
50
+
37
51
  def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
38
52
  """Cosine similarity in [-1, 1]. Returns 0.0 on zero vectors."""
39
53
  norm_a = np.linalg.norm(a)
@@ -95,6 +109,9 @@ class SemanticChannel:
95
109
  # V3.3.26: Lazily instantiated FRQAD metric for mixed-precision scoring
96
110
  self._frqad_metric: object | None = None
97
111
  self._vector_store = vector_store
112
+ self._scale_vector_backend: Any | None = None
113
+ self._scale_shadow_checks = 0
114
+ self._scale_shadow_mismatches = 0
98
115
  # V3.3.19: TurboQuant 3-tier search (stateless, optional)
99
116
  self._qas = quantization_aware_search
100
117
 
@@ -123,6 +140,23 @@ class SemanticChannel:
123
140
 
124
141
  q_vec = np.array(query_embedding, dtype=np.float32)
125
142
 
143
+ # Lance is a derived projection. It is never an authorization source
144
+ # and it never silently replaces the canonical sqlite-vec path: every
145
+ # promoted query is shadowed and falls back if membership/order differs.
146
+ if (
147
+ self._scale_vector_backend is not None
148
+ and not bool(getattr(self, "include_global", False))
149
+ and not bool(getattr(self, "include_shared", False))
150
+ ):
151
+ projected = self._search_via_lance(query_embedding, q_vec, profile_id, top_k)
152
+ canonical = self._search_without_lance(query_embedding, q_vec, profile_id, top_k)
153
+ self._scale_shadow_checks += 1
154
+ if [fid for fid, _ in projected] == [fid for fid, _ in canonical]:
155
+ return projected
156
+ self._scale_shadow_mismatches += 1
157
+ logger.warning("Lance semantic projection diverged from SQLite; using SQLite")
158
+ return canonical
159
+
126
160
  # --- FAST PATH: sqlite-vec KNN ---
127
161
  if self._vector_store and self._vector_store.available:
128
162
  results = self._search_via_vector_store(
@@ -135,6 +169,44 @@ class SemanticChannel:
135
169
  # --- FALLBACK: full-table scan (original code, unchanged) ---
136
170
  return self._search_full_scan(query_embedding, q_vec, profile_id, top_k)
137
171
 
172
+ def set_scale_vector_backend(self, backend: Any | None) -> None:
173
+ """Attach a parity-verified Lance projection without replacing SQLite."""
174
+ self._scale_vector_backend = backend
175
+
176
+ def scale_projection_telemetry(self) -> dict[str, int]:
177
+ return {
178
+ "shadow_checks": self._scale_shadow_checks,
179
+ "shadow_mismatches": self._scale_shadow_mismatches,
180
+ }
181
+
182
+ def _search_via_lance(
183
+ self, query_embedding: list[float], q_vec: np.ndarray, profile_id: str, top_k: int,
184
+ ) -> list[tuple[str, float]]:
185
+ original_store, original_qas = self._vector_store, self._qas
186
+ try:
187
+ self._vector_store = _LanceCandidateSource(self._scale_vector_backend)
188
+ # QAS indexes SQLite/quantized records and cannot represent Lance.
189
+ self._qas = None
190
+ return self._search_via_vector_store(query_embedding, q_vec, profile_id, top_k)
191
+ except Exception as exc:
192
+ logger.warning("Lance semantic projection failed closed to SQLite: %s", exc)
193
+ return []
194
+ finally:
195
+ self._vector_store, self._qas = original_store, original_qas
196
+
197
+ def _search_without_lance(
198
+ self, query_embedding: list[float], q_vec: np.ndarray, profile_id: str, top_k: int,
199
+ ) -> list[tuple[str, float]]:
200
+ backend, self._scale_vector_backend = self._scale_vector_backend, None
201
+ try:
202
+ if self._vector_store and self._vector_store.available:
203
+ results = self._search_via_vector_store(query_embedding, q_vec, profile_id, top_k)
204
+ if results:
205
+ return results
206
+ return self._search_full_scan(query_embedding, q_vec, profile_id, top_k)
207
+ finally:
208
+ self._scale_vector_backend = backend
209
+
138
210
  def _search_via_vector_store(
139
211
  self,
140
212
  query_embedding: list[float],
@@ -162,6 +234,36 @@ class SemanticChannel:
162
234
  knn_results = self._vector_store.search(
163
235
  query_embedding, top_k=top_k * 2, profile_id=profile_id,
164
236
  )
237
+
238
+ # The vector index is partitioned by owner profile. An opted-in global
239
+ # or authorized shared fact owned by another profile cannot enter the
240
+ # local KNN candidate set, so merge the bounded cross-profile visible
241
+ # supplement using the same canonical DB scope predicate as fallback.
242
+ include_global = bool(getattr(self, "include_global", False))
243
+ include_shared = bool(getattr(self, "include_shared", False))
244
+ external_facts = self._db.get_external_visible_facts(
245
+ profile_id,
246
+ include_global=include_global,
247
+ include_shared=include_shared,
248
+ )
249
+ external_scores: list[tuple[str, float]] = []
250
+ for fact in external_facts:
251
+ if fact.embedding is None:
252
+ continue
253
+ fact_vec = np.array(fact.embedding, dtype=np.float32)
254
+ if fact_vec.shape != q_vec.shape:
255
+ continue
256
+ score = (_cosine_similarity(q_vec, fact_vec) + 1.0) / 2.0
257
+ if score > 0.05:
258
+ external_scores.append((fact.fact_id, score))
259
+
260
+ if external_scores:
261
+ combined = {fid: score for fid, score in knn_results}
262
+ for fact_id, score in external_scores:
263
+ combined[fact_id] = max(combined.get(fact_id, 0.0), score)
264
+ knn_results = sorted(
265
+ combined.items(), key=lambda item: item[1], reverse=True,
266
+ )[:top_k * 2]
165
267
  if not knn_results:
166
268
  return [] # Caller falls through to full scan
167
269
 
@@ -175,7 +277,12 @@ class SemanticChannel:
175
277
  )
176
278
 
177
279
  if not facts:
178
- return [(fid, score) for fid, score in knn_results[:top_k]]
280
+ # The vector/QAS indexes are candidate sources, never an
281
+ # authorization source. Returning their raw IDs here leaked an
282
+ # owner-private fact precisely when canonical scope filtering
283
+ # rejected the entire candidate set. Empty means "no authorized
284
+ # fast-path hits" so the caller may use the scoped full scan.
285
+ return []
179
286
 
180
287
  # Step 3: Fisher-Rao re-scoring on the subset
181
288
  q_mean: np.ndarray | None = None
@@ -189,12 +296,7 @@ class SemanticChannel:
189
296
  for fact in facts:
190
297
  cos_sim = knn_scores.get(fact.fact_id, 0.0)
191
298
 
192
- # V3.3.21: Fisher-Rao ramp with minimum floor.
193
- # Bug fix: access_count=0 for fresh facts → Fisher weight=0 → metric DEAD.
194
- # Paper 2's +12pp on multi-hop came from Fisher-Rao. A 0.3 floor ensures
195
- # fresh facts still benefit from variance-weighted similarity, while
196
- # frequently accessed facts get progressively stronger Fisher influence.
197
- fisher_weight = max(0.15, min(1.2, (fact.access_count or 0) / 10.0 * 1.2))
299
+ fisher_weight = self._fisher_weight(fact.access_count)
198
300
 
199
301
  if (fisher_weight > 0.01
200
302
  and fact.fisher_variance is not None
@@ -205,8 +307,7 @@ class SemanticChannel:
205
307
  f_sim = self._compute_fisher_sim(
206
308
  q_vec, f_vec, var_vec, fact, q_mean, q_var,
207
309
  )
208
- capped_w = min(1.0, fisher_weight)
209
- sim = capped_w * f_sim + (1.0 - capped_w) * cos_sim
310
+ sim = fisher_weight * f_sim + (1.0 - fisher_weight) * cos_sim
210
311
  else:
211
312
  sim = cos_sim
212
313
 
@@ -252,8 +353,8 @@ class SemanticChannel:
252
353
  # Cosine baseline (always computed)
253
354
  cos_sim = (_cosine_similarity(q_vec, f_vec) + 1.0) / 2.0
254
355
 
255
- # Graduated Fisher-Rao ramp (F37, F108)
256
- fisher_weight = min(1.2, (fact.access_count or 0) / 10.0 * 1.2)
356
+ # The weighting contract is identical to the sqlite-vec path.
357
+ fisher_weight = self._fisher_weight(fact.access_count)
257
358
 
258
359
  if (fisher_weight > 0.01
259
360
  and fact.fisher_variance is not None
@@ -262,8 +363,7 @@ class SemanticChannel:
262
363
  f_sim = self._compute_fisher_sim(
263
364
  q_vec, f_vec, var_vec, fact, q_mean, q_var,
264
365
  )
265
- capped_w = min(1.0, fisher_weight)
266
- sim = capped_w * f_sim + (1.0 - capped_w) * cos_sim
366
+ sim = fisher_weight * f_sim + (1.0 - fisher_weight) * cos_sim
267
367
  else:
268
368
  sim = cos_sim
269
369
 
@@ -273,6 +373,14 @@ class SemanticChannel:
273
373
  scored.sort(key=lambda x: x[1], reverse=True)
274
374
  return scored[:top_k]
275
375
 
376
+ @staticmethod
377
+ def _fisher_weight(access_count: int | None) -> float:
378
+ """Canonical Fisher blend shared by every semantic candidate path."""
379
+ graduated = (access_count or 0) / 10.0 * 1.2
380
+ # A small floor keeps Fisher variance active for new facts. The cap
381
+ # prevents the blend from extrapolating beyond its two score inputs.
382
+ return min(1.0, max(0.15, graduated))
383
+
276
384
  # ------------------------------------------------------------------
277
385
  # Fisher similarity dispatch
278
386
  # ------------------------------------------------------------------
@@ -12,7 +12,7 @@ Reads BOTH graph_edges + association_edges via UNION query (Rule 13).
12
12
  Registered as 5th channel via ChannelRegistry (needs_embedding=True).
13
13
 
14
14
  Part of Qualixar | Author: Varun Pratap Bhardwaj
15
- License: Elastic-2.0
15
+ License: AGPL-3.0-or-later
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -25,6 +25,11 @@ from typing import Any
25
25
 
26
26
  import numpy as np
27
27
 
28
+ from superlocalmemory.retrieval.scope_policy import (
29
+ authorized_fact_ids,
30
+ filter_authorized_results,
31
+ )
32
+ from superlocalmemory.storage.database import _scope_where
28
33
  from superlocalmemory.storage.models import _new_id
29
34
 
30
35
  logger = logging.getLogger(__name__)
@@ -113,22 +118,77 @@ class SpreadingActivation:
113
118
  if not self._config.enabled:
114
119
  return []
115
120
 
121
+ include_global = bool(getattr(self, "include_global", False))
122
+ include_shared = bool(getattr(self, "include_shared", False))
116
123
  try:
117
124
  # Step 0: Get seed nodes from VectorStore KNN
118
125
  seed_results = self._vector_store.search(
119
126
  query, top_k=self._config.top_m, profile_id=profile_id,
120
127
  )
128
+ # Owner-partitioned vector indexes cannot discover opted-in peers.
129
+ # Add visible external embeddings with the same cosine seed signal.
130
+ try:
131
+ external_facts = self._db.get_external_visible_facts(
132
+ profile_id,
133
+ include_global=include_global,
134
+ include_shared=include_shared,
135
+ )
136
+ except Exception:
137
+ external_facts = []
138
+ q_vec = np.array(query, dtype=np.float32)
139
+ q_norm = float(np.linalg.norm(q_vec))
140
+ combined = {fact_id: score for fact_id, score in seed_results}
141
+ for fact in external_facts:
142
+ embedding = getattr(fact, "embedding", None)
143
+ if embedding is None:
144
+ continue
145
+ fact_vec = np.array(embedding, dtype=np.float32)
146
+ if fact_vec.shape != q_vec.shape:
147
+ continue
148
+ denominator = q_norm * float(np.linalg.norm(fact_vec))
149
+ if denominator <= 1e-8:
150
+ continue
151
+ score = (float(np.dot(q_vec, fact_vec) / denominator) + 1.0) / 2.0
152
+ combined[fact.fact_id] = max(combined.get(fact.fact_id, 0.0), score)
153
+ allowed_seeds = authorized_fact_ids(
154
+ self._db,
155
+ combined,
156
+ profile_id,
157
+ include_global=include_global,
158
+ include_shared=include_shared,
159
+ )
160
+ seed_results = [
161
+ (fact_id, score)
162
+ for fact_id, score in combined.items()
163
+ if fact_id in allowed_seeds
164
+ ]
121
165
  if not seed_results:
122
166
  return []
123
167
 
124
168
  # Check cache first
125
- query_hash = self._compute_query_hash(query, profile_id)
169
+ query_hash = self._compute_query_hash(
170
+ query,
171
+ profile_id,
172
+ include_global=include_global,
173
+ include_shared=include_shared,
174
+ )
126
175
  cached = self._get_cached_results(query_hash, profile_id)
127
176
  if cached:
128
- return cached[:top_k]
177
+ return filter_authorized_results(
178
+ self._db,
179
+ cached,
180
+ profile_id,
181
+ include_global=include_global,
182
+ include_shared=include_shared,
183
+ )[:top_k]
129
184
 
130
185
  # Run 5-step spreading activation
131
- activations = self._propagate(seed_results, profile_id)
186
+ activations = self._propagate(
187
+ seed_results,
188
+ profile_id,
189
+ include_global=include_global,
190
+ include_shared=include_shared,
191
+ )
132
192
 
133
193
  # FOK gating
134
194
  if not self._fok_check(activations):
@@ -141,7 +201,13 @@ class SpreadingActivation:
141
201
  results = sorted(
142
202
  activations.items(), key=lambda x: x[1], reverse=True,
143
203
  )
144
- return results[:top_k]
204
+ return filter_authorized_results(
205
+ self._db,
206
+ results,
207
+ profile_id,
208
+ include_global=include_global,
209
+ include_shared=include_shared,
210
+ )[:top_k]
145
211
 
146
212
  except Exception as exc:
147
213
  logger.warning(
@@ -154,6 +220,9 @@ class SpreadingActivation:
154
220
  self,
155
221
  seeds: list[tuple[str, float]],
156
222
  profile_id: str,
223
+ *,
224
+ include_global: bool = False,
225
+ include_shared: bool = False,
157
226
  ) -> dict[str, float]:
158
227
  """Execute the 5-step SYNAPSE algorithm.
159
228
 
@@ -186,7 +255,22 @@ class SpreadingActivation:
186
255
 
187
256
  # Get neighbors from BOTH tables (Rule 13) — cached per node
188
257
  if node_id not in neighbor_cache:
189
- neighbor_cache[node_id] = self._get_unified_neighbors(node_id, profile_id)
258
+ raw_neighbors = self._get_unified_neighbors(
259
+ node_id,
260
+ profile_id,
261
+ include_global=include_global,
262
+ include_shared=include_shared,
263
+ )
264
+ allowed_neighbors = authorized_fact_ids(
265
+ self._db,
266
+ (neighbor_id for neighbor_id, _weight in raw_neighbors),
267
+ profile_id,
268
+ include_global=include_global,
269
+ include_shared=include_shared,
270
+ )
271
+ neighbor_cache[node_id] = [
272
+ item for item in raw_neighbors if item[0] in allowed_neighbors
273
+ ]
190
274
  neighbors = neighbor_cache[node_id]
191
275
 
192
276
  # Out-degree for fan effect normalization
@@ -226,7 +310,12 @@ class SpreadingActivation:
226
310
  return activations
227
311
 
228
312
  def _get_unified_neighbors(
229
- self, node_id: str, profile_id: str,
313
+ self,
314
+ node_id: str,
315
+ profile_id: str,
316
+ *,
317
+ include_global: bool = False,
318
+ include_shared: bool = False,
230
319
  ) -> list[tuple[str, float]]:
231
320
  """Get neighbors from BOTH graph_edges and association_edges.
232
321
 
@@ -245,41 +334,56 @@ class SpreadingActivation:
245
334
  # all 2.1M edges then sorting. Each branch wrapped in SELECT * FROM (...)
246
335
  # because SQLite requires parentheses for ORDER BY+LIMIT in compound SELECTs.
247
336
  lim = self._config.max_neighbors_per_node
337
+ graph_where, graph_params = _scope_where(
338
+ profile_id,
339
+ include_global=include_global,
340
+ include_shared=include_shared,
341
+ prefix="ge",
342
+ )
343
+ # association_edges has no scope/shared_with columns in the current
344
+ # schema, so it remains owner-profile-only. Endpoint authorization
345
+ # below still prevents a private candidate from entering results.
346
+ assoc_where = "ae.profile_id = ?"
347
+ assoc_params = [profile_id]
248
348
  rows = self._db.execute(
249
- """
349
+ f"""
250
350
  SELECT neighbor_id, weight FROM (
251
351
  SELECT * FROM (
252
- SELECT target_id AS neighbor_id, weight FROM graph_edges
253
- WHERE source_id = ? AND profile_id = ?
352
+ SELECT target_id AS neighbor_id, weight FROM graph_edges AS ge
353
+ WHERE source_id = ? AND {graph_where}
254
354
  ORDER BY weight DESC LIMIT ?
255
355
  )
256
356
  UNION ALL
257
357
  SELECT * FROM (
258
- SELECT target_fact_id AS neighbor_id, weight FROM association_edges
259
- WHERE source_fact_id = ? AND profile_id = ?
358
+ SELECT target_fact_id AS neighbor_id, weight
359
+ FROM association_edges AS ae
360
+ WHERE source_fact_id = ? AND {assoc_where}
260
361
  ORDER BY weight DESC LIMIT ?
261
362
  )
262
363
  UNION ALL
263
364
  SELECT * FROM (
264
- SELECT source_id AS neighbor_id, weight FROM graph_edges
265
- WHERE target_id = ? AND profile_id = ?
365
+ SELECT source_id AS neighbor_id, weight FROM graph_edges AS ge
366
+ WHERE target_id = ? AND {graph_where}
266
367
  ORDER BY weight DESC LIMIT ?
267
368
  )
268
369
  UNION ALL
269
370
  SELECT * FROM (
270
- SELECT source_fact_id AS neighbor_id, weight FROM association_edges
271
- WHERE target_fact_id = ? AND profile_id = ?
371
+ SELECT source_fact_id AS neighbor_id, weight
372
+ FROM association_edges AS ae
373
+ WHERE target_fact_id = ? AND {assoc_where}
272
374
  ORDER BY weight DESC LIMIT ?
273
375
  )
274
376
  )
275
377
  ORDER BY weight DESC
276
378
  LIMIT ?
277
379
  """,
278
- (node_id, profile_id, lim,
279
- node_id, profile_id, lim,
280
- node_id, profile_id, lim,
281
- node_id, profile_id, lim,
282
- lim),
380
+ (
381
+ node_id, *graph_params, lim,
382
+ node_id, *assoc_params, lim,
383
+ node_id, *graph_params, lim,
384
+ node_id, *assoc_params, lim,
385
+ lim,
386
+ ),
283
387
  )
284
388
  return [
285
389
  (dict(r)["neighbor_id"], dict(r)["weight"]) for r in rows
@@ -301,14 +405,26 @@ class SpreadingActivation:
301
405
  return False
302
406
  return max(activations.values()) >= self._config.tau_gate
303
407
 
304
- def _compute_query_hash(self, query: Any, profile_id: str) -> str:
408
+ def _compute_query_hash(
409
+ self,
410
+ query: Any,
411
+ profile_id: str,
412
+ *,
413
+ include_global: bool = False,
414
+ include_shared: bool = False,
415
+ ) -> str:
305
416
  """Deterministic hash for cache key."""
417
+ scope_bytes = f"|g={int(include_global)}|s={int(include_shared)}".encode()
306
418
  if isinstance(query, np.ndarray):
307
- data = query.tobytes() + profile_id.encode()
419
+ data = query.tobytes() + profile_id.encode() + scope_bytes
308
420
  elif isinstance(query, list):
309
- data = np.array(query, dtype=np.float32).tobytes() + profile_id.encode()
421
+ data = (
422
+ np.array(query, dtype=np.float32).tobytes()
423
+ + profile_id.encode()
424
+ + scope_bytes
425
+ )
310
426
  else:
311
- data = str(query).encode() + profile_id.encode()
427
+ data = str(query).encode() + profile_id.encode() + scope_bytes
312
428
  return hashlib.sha256(data).hexdigest()[:16]
313
429
 
314
430
  def _get_cached_results(
@@ -8,7 +8,7 @@ Classifies query type and returns per-type channel weights.
8
8
  V1 had this code (strategy_learner.py) but never wired it in.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
  from __future__ import annotations
14
14
 
@@ -8,7 +8,7 @@ Searches by referenced_date (NOT just created_at like V1).
8
8
  Returns empty when query has no temporal signal (no recency noise).
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
  from __future__ import annotations
14
14
 
@@ -17,9 +17,10 @@ import math
17
17
  from datetime import datetime
18
18
  from typing import TYPE_CHECKING
19
19
 
20
- from dateutil.parser import parse as dateutil_parse, ParserError
20
+ from dateutil.parser import ParserError, parse as dateutil_parse
21
21
 
22
22
  from superlocalmemory.encoding.temporal_parser import TemporalParser
23
+ from superlocalmemory.storage.database import _scope_where
23
24
 
24
25
  if TYPE_CHECKING:
25
26
  from superlocalmemory.storage.database import DatabaseManager
@@ -148,18 +149,24 @@ class TemporalChannel:
148
149
 
149
150
  results: list[tuple[str, float]] = []
150
151
  seen: set[str] = set()
152
+ where, params = _scope_where(
153
+ profile_id,
154
+ include_global=bool(getattr(self, "include_global", False)),
155
+ include_shared=bool(getattr(self, "include_shared", False)),
156
+ prefix="af",
157
+ )
151
158
 
152
159
  for name in names[:3]: # Limit to first 3 entity mentions
153
- # Look up entity ID
154
- entity = self._db.get_entity_by_name(name, profile_id)
155
- if entity is None:
156
- continue
157
-
158
- # Find all temporal events for this entity
160
+ # Resolve the entity and event in one scope-filtered query. Looking
161
+ # up the entity only in the requester's profile made global events
162
+ # owned by another profile undiscoverable before authorization was
163
+ # even evaluated.
159
164
  rows = self._db.execute(
160
- "SELECT fact_id FROM temporal_events "
161
- "WHERE profile_id = ? AND entity_id = ?",
162
- (profile_id, entity.entity_id),
165
+ "SELECT te.fact_id FROM temporal_events AS te "
166
+ "JOIN canonical_entities AS ce ON ce.entity_id = te.entity_id "
167
+ "JOIN atomic_facts AS af ON af.fact_id = te.fact_id "
168
+ f"WHERE {where} AND LOWER(ce.canonical_name) = LOWER(?)",
169
+ (*params, name),
163
170
  )
164
171
  for row in rows:
165
172
  fid = dict(row)["fact_id"]
@@ -173,11 +180,19 @@ class TemporalChannel:
173
180
  return results
174
181
 
175
182
  def _load_events(self, profile_id: str) -> list[dict]:
183
+ where, params = _scope_where(
184
+ profile_id,
185
+ include_global=bool(getattr(self, "include_global", False)),
186
+ include_shared=bool(getattr(self, "include_shared", False)),
187
+ prefix="af",
188
+ )
176
189
  rows = self._db.execute(
177
- "SELECT fact_id, observation_date, referenced_date, "
178
- "interval_start, interval_end "
179
- "FROM temporal_events WHERE profile_id = ?",
180
- (profile_id,),
190
+ "SELECT te.fact_id, te.observation_date, te.referenced_date, "
191
+ "te.interval_start, te.interval_end "
192
+ "FROM temporal_events AS te "
193
+ "JOIN atomic_facts AS af ON af.fact_id = te.fact_id "
194
+ f"WHERE {where}",
195
+ (*params,),
181
196
  )
182
197
  return [dict(r) for r in rows]
183
198
 
@@ -9,7 +9,7 @@ Falls back to ANNIndex if sqlite-vec is unavailable (Rule 03).
9
9
  Implements ANNSearchable protocol for GraphBuilder compatibility (Rule 07).
10
10
 
11
11
  Part of Qualixar | Author: Varun Pratap Bhardwaj
12
- License: Elastic-2.0
12
+ License: AGPL-3.0-or-later
13
13
  """
14
14
 
15
15
  from __future__ import annotations
@@ -24,12 +24,13 @@ import uvicorn
24
24
 
25
25
  from superlocalmemory.server.security_middleware import SecurityHeadersMiddleware
26
26
  from superlocalmemory.server.routes.helpers import SLM_VERSION
27
+ from superlocalmemory.infra.data_root import DynamicStatePath
27
28
 
28
29
  logger = logging.getLogger("superlocalmemory.api_server")
29
30
 
30
31
  # V3 paths
31
- MEMORY_DIR = Path.home() / ".superlocalmemory"
32
- DB_PATH = MEMORY_DIR / "memory.db"
32
+ MEMORY_DIR = DynamicStatePath()
33
+ DB_PATH = DynamicStatePath("memory.db")
33
34
  # V3.3.21: UI shipped inside the package for pip/npm installs.
34
35
  _PKG_UI = Path(__file__).resolve().parent.parent / "ui"
35
36
  _REPO_UI = Path(__file__).resolve().parent.parent.parent.parent / "ui"
@@ -75,6 +76,12 @@ async def lifespan(application: FastAPI):
75
76
  application.state.engine = None
76
77
  application.state.config = None
77
78
 
79
+ # Event fan-out belongs to the same application lifecycle as the engine.
80
+ # Registering it through FastAPI.on_event created a second, deprecated
81
+ # startup path and made TestClient initialization emit warnings.
82
+ from superlocalmemory.server.routes.events import register_event_listener
83
+
84
+ register_event_listener()
78
85
  yield
79
86
 
80
87
  # Cleanup
@@ -169,7 +176,7 @@ def create_app() -> FastAPI:
169
176
  from superlocalmemory.server.routes.profiles import router as profiles_router
170
177
  from superlocalmemory.server.routes.backup import router as backup_router
171
178
  from superlocalmemory.server.routes.data_io import router as data_io_router
172
- from superlocalmemory.server.routes.events import router as events_router, register_event_listener
179
+ from superlocalmemory.server.routes.events import router as events_router
173
180
  from superlocalmemory.server.routes.agents import router as agents_router
174
181
  from superlocalmemory.server.routes.ws import router as ws_router, manager as ws_manager
175
182
  from superlocalmemory.server.routes.v3_api import router as v3_router
@@ -237,10 +244,6 @@ def create_app() -> FastAPI:
237
244
  "timestamp": datetime.now(timezone.utc).isoformat(),
238
245
  }
239
246
 
240
- @application.on_event("startup")
241
- async def startup_event():
242
- register_event_listener()
243
-
244
247
  return application
245
248
 
246
249
 
@@ -24,6 +24,8 @@ import os
24
24
  from pathlib import Path
25
25
  from typing import Any
26
26
 
27
+ from superlocalmemory.infra.data_root import state_path
28
+
27
29
  logger = logging.getLogger(__name__)
28
30
 
29
31
  _REWARD_INTERVAL = float(
@@ -42,7 +44,7 @@ def _learning_db(config: Any) -> Path:
42
44
  cand = getattr(config, "learning_db_path", None)
43
45
  if cand is not None:
44
46
  return Path(cand)
45
- return Path.home() / ".superlocalmemory" / "learning.db"
47
+ return state_path("learning.db")
46
48
 
47
49
 
48
50
  def _memory_db(config: Any) -> Path:
@@ -50,7 +52,7 @@ def _memory_db(config: Any) -> Path:
50
52
  cand = getattr(config, "db_path", None)
51
53
  if cand is not None:
52
54
  return Path(cand)
53
- return Path.home() / ".superlocalmemory" / "memory.db"
55
+ return state_path("memory.db")
54
56
 
55
57
 
56
58
  def _profile_id(config: Any) -> str: