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
@@ -19,22 +19,20 @@ Key features:
19
19
  - Returns [] on any error (HR-06)
20
20
 
21
21
  Part of Qualixar | Author: Varun Pratap Bhardwaj
22
- License: Elastic-2.0
22
+ License: AGPL-3.0-or-later
23
23
  """
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
27
  import logging
28
+ import threading
28
29
  import time
29
- from typing import TYPE_CHECKING, Any
30
+ from typing import Any
30
31
 
31
32
  import numpy as np
32
33
 
33
34
  from superlocalmemory.math.hopfield import HopfieldConfig, ModernHopfieldNetwork
34
-
35
- if TYPE_CHECKING:
36
- from superlocalmemory.retrieval.vector_store import VectorStore
37
- from superlocalmemory.storage.database import DatabaseManager
35
+ from superlocalmemory.retrieval.scope_policy import filter_authorized_results
38
36
 
39
37
  logger = logging.getLogger(__name__)
40
38
 
@@ -73,11 +71,13 @@ class HopfieldChannel:
73
71
  self._vector_store = vector_store
74
72
  self._config = config or HopfieldConfig()
75
73
  self._hopfield = ModernHopfieldNetwork(self._config)
74
+ self._cache_lock = threading.RLock()
76
75
 
77
76
  # Memory matrix cache (per LLD Section 2.2, HR-09)
78
77
  self._cached_matrix: np.ndarray | None = None
79
78
  self._cached_fact_ids: list[str] = []
80
79
  self._cached_profile: str = ""
80
+ self._cached_scope_key: tuple[str, bool, bool] | None = None
81
81
  self._cached_count: int = 0
82
82
  self._cache_timestamp: float = 0.0
83
83
 
@@ -104,8 +104,17 @@ class HopfieldChannel:
104
104
  if not self._config.enabled:
105
105
  return []
106
106
 
107
+ include_global = bool(getattr(self, "include_global", False))
108
+ include_shared = bool(getattr(self, "include_shared", False))
107
109
  try:
108
- return self._search_inner(query, profile_id, top_k)
110
+ with self._cache_lock:
111
+ return self._search_inner(
112
+ query,
113
+ profile_id,
114
+ top_k,
115
+ include_global=include_global,
116
+ include_shared=include_shared,
117
+ )
109
118
  except Exception as exc:
110
119
  # HR-06: Return [] on any error
111
120
  logger.warning("Hopfield channel error: %s", exc)
@@ -118,6 +127,9 @@ class HopfieldChannel:
118
127
  query: Any,
119
128
  profile_id: str,
120
129
  top_k: int,
130
+ *,
131
+ include_global: bool = False,
132
+ include_shared: bool = False,
121
133
  ) -> list[tuple[str, float]]:
122
134
  """Core search logic, separated for clean error handling."""
123
135
  # Step 2: Convert query to numpy
@@ -132,11 +144,18 @@ class HopfieldChannel:
132
144
  return []
133
145
 
134
146
  # Step 3b (AUDIT FIX G-MEDIUM-02): Check skip_threshold BEFORE loading matrix
135
- total_count = (
136
- self._vector_store.count(profile_id)
137
- if self._vector_store and getattr(self._vector_store, "available", False)
138
- else 0
139
- )
147
+ try:
148
+ total_count = self._db.get_fact_count(
149
+ profile_id,
150
+ include_global=include_global,
151
+ include_shared=include_shared,
152
+ )
153
+ except (AttributeError, TypeError):
154
+ total_count = (
155
+ self._vector_store.count(profile_id)
156
+ if self._vector_store and getattr(self._vector_store, "available", False)
157
+ else 0
158
+ )
140
159
  # Step 3c: Skip for very large stores
141
160
  if total_count > self._config.skip_threshold:
142
161
  logger.debug(
@@ -162,17 +181,42 @@ class HopfieldChannel:
162
181
  # VS exists. Routing on prefilter_candidates (not prefilter_threshold)
163
182
  # ensures the matrix is always bounded to ~prefilter_candidates rows.
164
183
  if vs_ok and total_count > self._config.prefilter_candidates:
165
- return self._search_with_prefilter(q_vec, profile_id, [], top_k)
184
+ return self._search_with_prefilter(
185
+ q_vec,
186
+ profile_id,
187
+ [],
188
+ top_k,
189
+ include_global=include_global,
190
+ include_shared=include_shared,
191
+ )
166
192
 
167
193
  # Tiny store (or no VS): build (cached) full matrix.
168
- memory_matrix, fact_ids = self._get_memory_matrix(profile_id)
194
+ memory_matrix, fact_ids = self._get_memory_matrix(
195
+ profile_id,
196
+ include_global=include_global,
197
+ include_shared=include_shared,
198
+ )
169
199
  if memory_matrix is None or len(fact_ids) == 0:
170
200
  return []
171
201
  if vs_ok and len(fact_ids) > self._config.prefilter_candidates:
172
- return self._search_with_prefilter(q_vec, profile_id, fact_ids, top_k)
173
- return self._search_full_matrix(
202
+ return self._search_with_prefilter(
203
+ q_vec,
204
+ profile_id,
205
+ fact_ids,
206
+ top_k,
207
+ include_global=include_global,
208
+ include_shared=include_shared,
209
+ )
210
+ results = self._search_full_matrix(
174
211
  q_vec, memory_matrix, fact_ids, top_k,
175
212
  )
213
+ return filter_authorized_results(
214
+ self._db,
215
+ results,
216
+ profile_id,
217
+ include_global=include_global,
218
+ include_shared=include_shared,
219
+ )
176
220
 
177
221
  def _search_full_matrix(
178
222
  self,
@@ -220,6 +264,9 @@ class HopfieldChannel:
220
264
  profile_id: str,
221
265
  all_fact_ids: list[str],
222
266
  top_k: int,
267
+ *,
268
+ include_global: bool = False,
269
+ include_shared: bool = False,
223
270
  ) -> list[tuple[str, float]]:
224
271
  """Two-stage retrieval for large stores (>prefilter_threshold facts).
225
272
 
@@ -243,15 +290,35 @@ class HopfieldChannel:
243
290
  top_k=self._config.prefilter_candidates,
244
291
  profile_id=profile_id,
245
292
  )
246
- if not knn_results:
293
+ # The ANN index is owner-profile partitioned. Supplement it with
294
+ # opted-in cross-profile facts, then authorize the combined candidates
295
+ # through the canonical DB predicate below.
296
+ external_facts = self._db.get_external_visible_facts(
297
+ profile_id,
298
+ include_global=include_global,
299
+ include_shared=include_shared,
300
+ )
301
+ combined = {fact_id: score for fact_id, score in knn_results}
302
+ query_norm = float(np.linalg.norm(query))
303
+ for fact in external_facts:
304
+ embedding = getattr(fact, "embedding", None)
305
+ if embedding is None or len(embedding) != self._config.dimension:
306
+ continue
307
+ vector = np.array(embedding, dtype=np.float32)
308
+ denominator = query_norm * float(np.linalg.norm(vector))
309
+ if denominator <= 1e-8:
310
+ continue
311
+ score = (float(np.dot(query, vector) / denominator) + 1.0) / 2.0
312
+ combined[fact.fact_id] = max(combined.get(fact.fact_id, 0.0), score)
313
+ if not combined:
247
314
  return []
248
315
 
249
316
  # Stage 2: Load candidate facts
250
- candidate_ids = [fid for fid, _ in knn_results]
317
+ candidate_ids = list(combined)
251
318
  candidates = self._db.get_facts_by_ids(
252
319
  candidate_ids, profile_id,
253
- include_global=getattr(self, 'include_global', False),
254
- include_shared=getattr(self, 'include_shared', False),
320
+ include_global=include_global,
321
+ include_shared=include_shared,
255
322
  )
256
323
  if not candidates:
257
324
  return []
@@ -276,10 +343,21 @@ class HopfieldChannel:
276
343
  sub_matrix = sub_matrix / norms
277
344
 
278
345
  # Stage 4: Hopfield on subset
279
- return self._search_full_matrix(query, sub_matrix, sub_ids, top_k)
346
+ results = self._search_full_matrix(query, sub_matrix, sub_ids, top_k)
347
+ return filter_authorized_results(
348
+ self._db,
349
+ results,
350
+ profile_id,
351
+ include_global=include_global,
352
+ include_shared=include_shared,
353
+ )
280
354
 
281
355
  def _get_memory_matrix(
282
- self, profile_id: str,
356
+ self,
357
+ profile_id: str,
358
+ *,
359
+ include_global: bool = False,
360
+ include_shared: bool = False,
283
361
  ) -> tuple[np.ndarray | None, list[str]]:
284
362
  """Build or retrieve cached memory matrix X (n x d).
285
363
 
@@ -290,14 +368,22 @@ class HopfieldChannel:
290
368
  (memory_matrix, fact_ids) or (None, []) if no valid facts.
291
369
  """
292
370
  # Step 1: Check cache validity
293
- current_count = (
294
- self._vector_store.count(profile_id)
295
- if self._vector_store and getattr(self._vector_store, "available", False)
296
- else 0
297
- )
371
+ scope_key = (profile_id, bool(include_global), bool(include_shared))
372
+ try:
373
+ current_count = self._db.get_fact_count(
374
+ profile_id,
375
+ include_global=include_global,
376
+ include_shared=include_shared,
377
+ )
378
+ except (AttributeError, TypeError):
379
+ current_count = (
380
+ self._vector_store.count(profile_id)
381
+ if self._vector_store and getattr(self._vector_store, "available", False)
382
+ else 0
383
+ )
298
384
 
299
385
  if (
300
- self._cached_profile == profile_id
386
+ self._cached_scope_key == scope_key
301
387
  and self._cached_count == current_count
302
388
  and self._cached_matrix is not None
303
389
  and (time.monotonic() - self._cache_timestamp)
@@ -310,8 +396,8 @@ class HopfieldChannel:
310
396
  # deserialize the whole table just to slice it.
311
397
  facts = self._db.get_all_facts(
312
398
  profile_id, limit=5000,
313
- include_global=getattr(self, 'include_global', False),
314
- include_shared=getattr(self, 'include_shared', False),
399
+ include_global=include_global,
400
+ include_shared=include_shared,
315
401
  )
316
402
  if not facts:
317
403
  return (None, [])
@@ -341,6 +427,7 @@ class HopfieldChannel:
341
427
  self._cached_matrix = matrix
342
428
  self._cached_fact_ids = fact_ids
343
429
  self._cached_profile = profile_id
430
+ self._cached_scope_key = scope_key
344
431
  self._cached_count = current_count
345
432
  self._cache_timestamp = time.monotonic()
346
433
 
@@ -354,5 +441,6 @@ class HopfieldChannel:
354
441
  """
355
442
  self._cached_matrix = None
356
443
  self._cached_fact_ids = []
444
+ self._cached_scope_key = None
357
445
  self._cached_count = 0
358
446
  self._cache_timestamp = 0.0
@@ -14,7 +14,7 @@ retrieval pool with high scores.
14
14
  Competitor reference: EverMemOS profile synthesis (~+15-20% SH).
15
15
 
16
16
  Part of Qualixar | Author: Varun Pratap Bhardwaj
17
- License: Elastic-2.0
17
+ License: AGPL-3.0-or-later
18
18
  """
19
19
  from __future__ import annotations
20
20
 
@@ -6,8 +6,8 @@
6
6
 
7
7
  Merges results from:
8
8
  Tier 1: float32 (VectorStore.search -- exact cosine)
9
- Tier 2: int8 (VectorStore.search_int8 -- sqlite-vec native)
10
- Tier 3: polar (QuantizedEmbeddingStore.search -- PolarQuant)
9
+ Tier 2: int8 (QuantizedEmbeddingStore -- 8-bit PolarQuant rows)
10
+ Tier 3: polar (QuantizedEmbeddingStore -- 2/4-bit PolarQuant rows)
11
11
 
12
12
  Deduplicates by keeping the highest score per fact_id.
13
13
  Applies precision-dependent score penalties:
@@ -16,7 +16,7 @@ Applies precision-dependent score penalties:
16
16
  - polar: config.polar_search_penalty (default 0.95x)
17
17
 
18
18
  Part of Qualixar | Author: Varun Pratap Bhardwaj
19
- License: Elastic-2.0
19
+ License: AGPL-3.0-or-later
20
20
  """
21
21
 
22
22
  from __future__ import annotations
@@ -110,16 +110,17 @@ class QuantizationAwareSearch:
110
110
  def _search_int8(
111
111
  self, query: NDArray, profile_id: str, top_k: int,
112
112
  ) -> list[tuple[str, float]]:
113
- """Tier 2: int8 approximate via VectorStore.search_int8.
113
+ """Tier 2: persisted 8-bit quantized embeddings.
114
114
 
115
115
  Applies 0.98x penalty to account for int8 quantization error.
116
- Gracefully returns [] if VectorStore lacks search_int8 method.
117
116
  """
118
- fn = getattr(self._vector_store, "search_int8", None)
119
- if fn is None:
120
- return []
121
117
  try:
122
- raw = fn(query, profile_id=profile_id, top_k=top_k)
118
+ raw = self._quantized_store.search(
119
+ query,
120
+ profile_id,
121
+ top_k,
122
+ bit_widths=(8,),
123
+ )
123
124
  return [(fid, score * _INT8_PENALTY) for fid, score in raw]
124
125
  except Exception as exc:
125
126
  logger.debug("int8 search failed: %s", exc)
@@ -133,7 +134,12 @@ class QuantizationAwareSearch:
133
134
  Applies polar_search_penalty from config.
134
135
  """
135
136
  try:
136
- raw = self._quantized_store.search(query, profile_id, top_k)
137
+ raw = self._quantized_store.search(
138
+ query,
139
+ profile_id,
140
+ top_k,
141
+ bit_widths=(2, 4),
142
+ )
137
143
  penalty = self._config.polar_search_penalty
138
144
  return [(fid, score * penalty) for fid, score in raw]
139
145
  except Exception as exc:
@@ -11,7 +11,7 @@ at ~60 MB. Same isolation pattern as EmbeddingService.
11
11
  The worker subprocess auto-kills after 2 minutes idle.
12
12
 
13
13
  Part of Qualixar | Author: Varun Pratap Bhardwaj
14
- License: Elastic-2.0
14
+ License: AGPL-3.0-or-later
15
15
  """
16
16
 
17
17
  from __future__ import annotations
@@ -29,21 +29,27 @@ from typing import Any
29
29
 
30
30
  from pathlib import Path
31
31
 
32
+ from superlocalmemory.infra.data_root import state_path
32
33
  from superlocalmemory.storage.models import AtomicFact
33
34
 
34
- _RERANKER_PID_FILE = Path.home() / ".superlocalmemory" / ".reranker-worker.pid"
35
+ _RERANKER_PID_FILE = None # test-only override
36
+
37
+
38
+ def _reranker_pid_file() -> Path:
39
+ return _RERANKER_PID_FILE or state_path(".reranker-worker.pid")
35
40
 
36
41
 
37
42
  def _is_reranker_worker_alive() -> bool:
38
43
  """Check if a reranker worker PID is already alive (machine-wide singleton)."""
39
44
  try:
40
- if not _RERANKER_PID_FILE.exists():
45
+ pid_file = _reranker_pid_file()
46
+ if not pid_file.exists():
41
47
  return False
42
- pid = int(_RERANKER_PID_FILE.read_text().strip())
48
+ pid = int(pid_file.read_text().strip())
43
49
  os.kill(pid, 0)
44
50
  return True
45
51
  except (ValueError, OSError, ProcessLookupError):
46
- _RERANKER_PID_FILE.unlink(missing_ok=True)
52
+ _reranker_pid_file().unlink(missing_ok=True)
47
53
  return False
48
54
 
49
55
  # Track all live reranker instances for atexit cleanup
@@ -215,8 +221,9 @@ class CrossEncoderReranker:
215
221
  **popen_platform_kwargs(),
216
222
  )
217
223
  # v3.4.13: Register PID for machine-wide singleton
218
- _RERANKER_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
219
- _RERANKER_PID_FILE.write_text(str(self._worker_proc.pid))
224
+ pid_file = _reranker_pid_file()
225
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
226
+ pid_file.write_text(str(self._worker_proc.pid))
220
227
  logger.info(
221
228
  "Reranker worker spawned (PID %d)", self._worker_proc.pid,
222
229
  )
@@ -321,22 +328,41 @@ class CrossEncoderReranker:
321
328
  return result_container[0] if result_container else ""
322
329
 
323
330
  def _kill_worker(self) -> None:
324
- """Terminate worker subprocess."""
331
+ """Terminate the worker and close every owned pipe exactly once."""
325
332
  if self._idle_timer is not None:
326
333
  self._idle_timer.cancel()
327
334
  self._idle_timer = None
328
- if self._worker_proc is not None:
335
+
336
+ proc = self._worker_proc
337
+ if proc is not None:
338
+ # Detach first so re-entrant/finalizer cleanup is idempotent.
339
+ self._worker_proc = None
340
+ self._worker_ready = False
329
341
  try:
330
- self._worker_proc.stdin.write('{"cmd":"quit"}\n')
331
- self._worker_proc.stdin.flush()
332
- self._worker_proc.wait(timeout=3)
342
+ proc.stdin.write('{"cmd":"quit"}\n')
343
+ proc.stdin.flush()
344
+ proc.wait(timeout=3)
333
345
  except Exception:
334
346
  try:
335
- self._worker_proc.kill()
347
+ returncode = proc.poll()
336
348
  except Exception:
337
- pass
338
- self._worker_proc = None
339
- self._worker_ready = False
349
+ returncode = None
350
+ if returncode is None or not isinstance(returncode, int):
351
+ try:
352
+ proc.kill()
353
+ proc.wait(timeout=3)
354
+ except Exception:
355
+ pass
356
+ finally:
357
+ # Explicit close prevents TextIOWrapper from flushing a dead
358
+ # child's stdin later from an unraisable object finalizer.
359
+ for stream_name in ("stdin", "stdout", "stderr"):
360
+ stream = getattr(proc, stream_name, None)
361
+ if stream is not None:
362
+ try:
363
+ stream.close()
364
+ except (BrokenPipeError, OSError, ValueError):
365
+ pass
340
366
 
341
367
  def _reset_idle_timer(self) -> None:
342
368
  """Reset idle timer — kills worker after 2 min inactivity."""
@@ -373,13 +399,23 @@ class CrossEncoderReranker:
373
399
  results (without reranking), and MCP gets reranked results
374
400
  (worker stays warm between calls).
375
401
  """
402
+ results, _, _ = self.rerank_with_status(query, candidates, top_k=top_k)
403
+ return results
404
+
405
+ def rerank_with_status(
406
+ self,
407
+ query: str,
408
+ candidates: list[tuple[AtomicFact, float]],
409
+ top_k: int = 10,
410
+ ) -> tuple[list[tuple[AtomicFact, float]], bool, str]:
411
+ """Return results plus whether cross-encoder inference actually ran."""
376
412
  if not candidates:
377
- return []
413
+ return [], False, "no_candidates"
378
414
 
379
415
  # Non-blocking: if model isn't loaded yet, return fallback
380
416
  if not self._model_loaded:
381
417
  sorted_cands = sorted(candidates, key=lambda x: x[1], reverse=True)
382
- return sorted_cands[:top_k]
418
+ return sorted_cands[:top_k], False, "fallback_not_ready"
383
419
 
384
420
  documents = [fact.content for fact, _ in candidates]
385
421
 
@@ -397,7 +433,7 @@ class CrossEncoderReranker:
397
433
  if resp is None or not resp.get("ok"):
398
434
  # Fallback: return by existing score
399
435
  sorted_cands = sorted(candidates, key=lambda x: x[1], reverse=True)
400
- return sorted_cands[:top_k]
436
+ return sorted_cands[:top_k], False, "fallback_busy_or_unavailable"
401
437
 
402
438
  scores = resp["scores"]
403
439
  scored: list[tuple[AtomicFact, float]] = [
@@ -405,7 +441,7 @@ class CrossEncoderReranker:
405
441
  for (fact, _), score in zip(candidates, scores)
406
442
  ]
407
443
  scored.sort(key=lambda x: x[1], reverse=True)
408
- return scored[:top_k]
444
+ return scored[:top_k], True, "applied"
409
445
 
410
446
  def score_pair(self, query: str, document: str) -> float:
411
447
  """Score a single (query, document) pair."""
@@ -0,0 +1,85 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Fail-closed authorization helpers for retrieval candidate paths.
5
+
6
+ Candidate generators may use caches, approximate indexes, or graph stores that
7
+ are not the authorization source of truth. Every such path must therefore
8
+ re-authorize fact IDs through ``DatabaseManager.get_facts_by_ids()``, whose SQL
9
+ is built by the canonical ``_scope_where`` predicate.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Iterable
15
+
16
+ from superlocalmemory.storage.database import _scope_where
17
+
18
+
19
+ def authorized_fact_ids(
20
+ db: Any,
21
+ fact_ids: Iterable[str],
22
+ profile_id: str,
23
+ *,
24
+ include_global: bool = False,
25
+ include_shared: bool = False,
26
+ ) -> set[str]:
27
+ """Return only IDs visible under the canonical scope predicate.
28
+
29
+ Authorization errors fail closed. The stable de-duplication avoids SQLite
30
+ parameter waste without changing candidate order at the caller boundary.
31
+ """
32
+ unique_ids = list(dict.fromkeys(fact_ids))
33
+ if not unique_ids:
34
+ return set()
35
+ try:
36
+ facts = db.get_facts_by_ids(
37
+ unique_ids,
38
+ profile_id,
39
+ include_global=bool(include_global),
40
+ include_shared=bool(include_shared),
41
+ )
42
+ if isinstance(facts, list):
43
+ return {fact.fact_id for fact in facts}
44
+ except Exception:
45
+ pass
46
+
47
+ # Lightweight DB wrappers used by maintenance paths may expose execute()
48
+ # without the higher-level method. Keep the same canonical predicate.
49
+ try:
50
+ where, params = _scope_where(
51
+ profile_id,
52
+ include_global=include_global,
53
+ include_shared=include_shared,
54
+ )
55
+ placeholders = ",".join("?" for _ in unique_ids)
56
+ rows = db.execute(
57
+ f"SELECT fact_id FROM atomic_facts WHERE fact_id IN ({placeholders}) "
58
+ f"AND {where}",
59
+ (*unique_ids, *params),
60
+ )
61
+ if not isinstance(rows, list):
62
+ rows = list(rows)
63
+ return {dict(row)["fact_id"] for row in rows}
64
+ except Exception:
65
+ return set()
66
+
67
+
68
+ def filter_authorized_results(
69
+ db: Any,
70
+ results: Iterable[tuple[str, float]],
71
+ profile_id: str,
72
+ *,
73
+ include_global: bool = False,
74
+ include_shared: bool = False,
75
+ ) -> list[tuple[str, float]]:
76
+ """Preserve result order/scores while removing unauthorized fact IDs."""
77
+ materialized = list(results)
78
+ allowed = authorized_fact_ids(
79
+ db,
80
+ (fact_id for fact_id, _score in materialized),
81
+ profile_id,
82
+ include_global=include_global,
83
+ include_shared=include_shared,
84
+ )
85
+ return [item for item in materialized if item[0] in allowed]