superlocalmemory 3.6.23 → 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 (301) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +18 -10
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/rules/AGENTS.md +1 -1
  18. package/pyproject.toml +40 -8
  19. package/scripts/postinstall-interactive.js +17 -94
  20. package/scripts/postinstall.js +185 -258
  21. package/scripts/preuninstall.js +9 -50
  22. package/src/superlocalmemory/__init__.py +2 -2
  23. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  24. package/src/superlocalmemory/attribution/signer.py +34 -19
  25. package/src/superlocalmemory/attribution/watermark.py +1 -1
  26. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  27. package/src/superlocalmemory/cli/commands.py +453 -191
  28. package/src/superlocalmemory/cli/context_commands.py +5 -4
  29. package/src/superlocalmemory/cli/daemon.py +282 -187
  30. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  31. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  32. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  33. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  34. package/src/superlocalmemory/cli/main.py +128 -31
  35. package/src/superlocalmemory/cli/pending_store.py +54 -38
  36. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  37. package/src/superlocalmemory/cli/service_installer.py +57 -52
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  39. package/src/superlocalmemory/cli/version_banner.py +2 -1
  40. package/src/superlocalmemory/code_graph/config.py +3 -1
  41. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  42. package/src/superlocalmemory/core/config.py +65 -20
  43. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  44. package/src/superlocalmemory/core/context_cache.py +56 -8
  45. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  46. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  47. package/src/superlocalmemory/core/embeddings.py +54 -18
  48. package/src/superlocalmemory/core/engine.py +150 -104
  49. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  50. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  51. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  52. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  53. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  54. package/src/superlocalmemory/core/health_monitor.py +4 -2
  55. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  56. package/src/superlocalmemory/core/injection.py +69 -18
  57. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  58. package/src/superlocalmemory/core/maintenance.py +1 -1
  59. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  60. package/src/superlocalmemory/core/mutations.py +143 -0
  61. package/src/superlocalmemory/core/platform_utils.py +7 -4
  62. package/src/superlocalmemory/core/ram_lock.py +16 -5
  63. package/src/superlocalmemory/core/rate_limit.py +1 -1
  64. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  65. package/src/superlocalmemory/core/recall_worker.py +76 -59
  66. package/src/superlocalmemory/core/registry.py +1 -1
  67. package/src/superlocalmemory/core/scale_engine.py +293 -0
  68. package/src/superlocalmemory/core/score_contract.py +62 -0
  69. package/src/superlocalmemory/core/security_primitives.py +3 -1
  70. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  72. package/src/superlocalmemory/core/tier_manager.py +32 -20
  73. package/src/superlocalmemory/core/worker_pool.py +13 -4
  74. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  75. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  76. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  77. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  79. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  80. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  81. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  82. package/src/superlocalmemory/encoding/foresight.py +2 -0
  83. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  84. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  85. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  86. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  87. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  88. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  89. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  90. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  91. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  92. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  93. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  94. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  95. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  96. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  97. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  98. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  99. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  100. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  101. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  102. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  103. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  104. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  105. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  106. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  107. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  108. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  109. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  110. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  111. package/src/superlocalmemory/infra/backup.py +7 -5
  112. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  113. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  114. package/src/superlocalmemory/infra/data_root.py +199 -0
  115. package/src/superlocalmemory/infra/event_bus.py +3 -1
  116. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  117. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  118. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  119. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  120. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  121. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  122. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  123. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  124. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  125. package/src/superlocalmemory/learning/ensemble.py +11 -0
  126. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  127. package/src/superlocalmemory/learning/feedback.py +1 -1
  128. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  129. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  130. package/src/superlocalmemory/learning/ranker.py +4 -1
  131. package/src/superlocalmemory/learning/source_quality.py +1 -1
  132. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  133. package/src/superlocalmemory/llm/backbone.py +1 -1
  134. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  135. package/src/superlocalmemory/math/fisher.py +1 -1
  136. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  137. package/src/superlocalmemory/math/hopfield.py +1 -1
  138. package/src/superlocalmemory/math/langevin.py +1 -1
  139. package/src/superlocalmemory/math/polar_quant.py +3 -4
  140. package/src/superlocalmemory/math/qjl.py +1 -1
  141. package/src/superlocalmemory/math/sheaf.py +1 -1
  142. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  143. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  144. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  145. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  146. package/src/superlocalmemory/mcp/server.py +39 -13
  147. package/src/superlocalmemory/mcp/shared.py +69 -3
  148. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  149. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  150. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  151. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  152. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  153. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  154. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  155. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  156. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  157. package/src/superlocalmemory/mesh/broker.py +2 -0
  158. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  159. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  160. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  161. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  162. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  163. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  164. package/src/superlocalmemory/optimize/config/store.py +6 -4
  165. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  166. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  167. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  168. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  169. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  170. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  171. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  172. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  173. package/src/superlocalmemory/retrieval/engine.py +169 -79
  174. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  175. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  176. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  177. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  178. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  179. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  180. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  181. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  182. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  183. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  184. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  185. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  186. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  187. package/src/superlocalmemory/server/api.py +10 -7
  188. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  189. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  190. package/src/superlocalmemory/server/route_mutations.py +84 -0
  191. package/src/superlocalmemory/server/routes/agents.py +8 -6
  192. package/src/superlocalmemory/server/routes/brain.py +14 -12
  193. package/src/superlocalmemory/server/routes/chat.py +29 -12
  194. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  195. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  196. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  197. package/src/superlocalmemory/server/routes/memories.py +104 -43
  198. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  199. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  200. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  201. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  202. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  203. package/src/superlocalmemory/server/security_middleware.py +1 -1
  204. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  205. package/src/superlocalmemory/server/write_identity.py +147 -0
  206. package/src/superlocalmemory/storage/access_log.py +4 -3
  207. package/src/superlocalmemory/storage/database.py +118 -25
  208. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  209. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  210. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  211. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  212. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  213. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  214. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  215. package/src/superlocalmemory/storage/models.py +16 -0
  216. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  217. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  218. package/src/superlocalmemory/ui/favicon.svg +5 -0
  219. package/src/superlocalmemory/ui/index.html +1 -0
  220. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  221. package/src/superlocalmemory/ui/js/core.js +49 -8
  222. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  223. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  224. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  225. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  226. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  227. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  228. package/src/superlocalmemory/ui/js/settings.js +4 -2
  229. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  230. package/bin/slm +0 -59
  231. package/bin/slm.bat +0 -77
  232. package/bin/slm.cmd +0 -5
  233. package/ide/integrations/langchain/README.md +0 -106
  234. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  235. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  236. package/ide/integrations/langchain/pyproject.toml +0 -38
  237. package/ide/integrations/langchain/tests/__init__.py +0 -3
  238. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  239. package/ide/integrations/langchain/tests/test_security.py +0 -117
  240. package/ide/integrations/llamaindex/README.md +0 -81
  241. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  242. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  243. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  244. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  245. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  246. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  247. package/plugin-src/.mcp.json +0 -12
  248. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  249. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  250. package/plugin-src/hooks/.gitkeep +0 -0
  251. package/plugin-src/hooks/hooks.json +0 -23
  252. package/plugin-src/manifest.json +0 -25
  253. package/plugin-src/requirements.txt +0 -1
  254. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  255. package/plugin-src/scripts/ensure-venv.bat +0 -122
  256. package/plugin-src/scripts/ensure-venv.sh +0 -105
  257. package/plugin-src/scripts/slm-launch +0 -15
  258. package/plugin-src/scripts/slm-launch.bat +0 -17
  259. package/plugin-src/settings.json +0 -16
  260. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  261. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  262. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  263. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  264. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  265. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  266. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  267. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  268. package/scripts/_savings_math.py +0 -270
  269. package/scripts/build-dmg.sh +0 -417
  270. package/scripts/build-plugin.js +0 -742
  271. package/scripts/build-slm-hook.ps1 +0 -40
  272. package/scripts/build-slm-hook.sh +0 -45
  273. package/scripts/build_entry.py +0 -452
  274. package/scripts/ci/stage5b_gate.sh +0 -50
  275. package/scripts/dogfood_savings.py +0 -490
  276. package/scripts/generate-thumbnails.py +0 -218
  277. package/scripts/install-skills.ps1 +0 -4
  278. package/scripts/install-skills.sh +0 -5
  279. package/scripts/install.ps1 +0 -701
  280. package/scripts/install.sh +0 -1015
  281. package/scripts/postinstall_binary.js +0 -287
  282. package/scripts/prepack.js +0 -33
  283. package/scripts/release_manifest.py +0 -273
  284. package/scripts/slm-hook.spec +0 -56
  285. package/scripts/start-dashboard.ps1 +0 -52
  286. package/scripts/start-dashboard.sh +0 -41
  287. package/scripts/sync-wiki.ps1 +0 -127
  288. package/scripts/sync-wiki.sh +0 -82
  289. package/scripts/test-dmg.sh +0 -161
  290. package/scripts/test-npm-package.ps1 +0 -252
  291. package/scripts/test-npm-package.sh +0 -207
  292. package/scripts/verify-install.ps1 +0 -294
  293. package/scripts/verify-install.sh +0 -266
  294. package/scripts/verify-v27.ps1 +0 -301
  295. package/scripts/verify-v27.sh +0 -233
  296. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  297. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  298. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  299. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  300. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  301. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -34,6 +34,10 @@ class PoolRecallItem:
34
34
  fact: PoolFact = field(default_factory=PoolFact)
35
35
  score: float = 0.0
36
36
  confidence: float = 0.0
37
+ relevance_score: float = 0.0
38
+ ranking_score: float | None = None
39
+ memory_confidence: float = 0.0
40
+ rank_position: int = 0
37
41
  trust_score: float = 0.0
38
42
  channel_scores: dict[str, float] = field(default_factory=dict)
39
43
  evidence_chain: list[Any] = field(default_factory=list)
@@ -46,6 +50,12 @@ class PoolRecallResponse:
46
50
  retrieval_time_ms: float = 0.0
47
51
  channel_weights: dict[str, float] = field(default_factory=dict)
48
52
  total_candidates: int = 0
53
+ score_contract_version: str = "2"
54
+ calibration_status: str = "uncalibrated"
55
+ calibration_id: str | None = None
56
+ answer_confidence: float | None = None
57
+ abstained: bool = False
58
+ abstention_reason: str | None = None
49
59
 
50
60
 
51
61
  class PoolError(RuntimeError):
@@ -107,6 +117,15 @@ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallRespons
107
117
  ),
108
118
  score=float(item.get("score", 0.0)),
109
119
  confidence=float(item.get("confidence", 0.0)),
120
+ relevance_score=float(item.get("relevance_score", item.get("score", 0.0))),
121
+ ranking_score=(
122
+ float(item["ranking_score"])
123
+ if item.get("ranking_score") is not None else None
124
+ ),
125
+ memory_confidence=float(
126
+ item.get("memory_confidence", item.get("confidence", 0.0))
127
+ ),
128
+ rank_position=int(item.get("rank_position", 0)),
110
129
  trust_score=float(item.get("trust_score", 0.0)),
111
130
  channel_scores=item.get("channel_scores", {}) or {},
112
131
  evidence_chain=list(item.get("evidence_chain", []) or []),
@@ -122,6 +141,14 @@ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallRespons
122
141
  if isinstance(raw, dict) else {},
123
142
  total_candidates=int(raw.get("total_candidates", 0))
124
143
  if isinstance(raw, dict) else 0,
144
+ score_contract_version=str(raw.get("score_contract_version", "2"))
145
+ if isinstance(raw, dict) else "2",
146
+ calibration_status=str(raw.get("calibration_status", "uncalibrated"))
147
+ if isinstance(raw, dict) else "uncalibrated",
148
+ calibration_id=raw.get("calibration_id") if isinstance(raw, dict) else None,
149
+ answer_confidence=raw.get("answer_confidence") if isinstance(raw, dict) else None,
150
+ abstained=bool(raw.get("abstained", False)) if isinstance(raw, dict) else False,
151
+ abstention_reason=raw.get("abstention_reason") if isinstance(raw, dict) else None,
125
152
  )
126
153
 
127
154
 
@@ -0,0 +1,53 @@
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
+ """Resource-safe FastMCP Streamable-HTTP integration.
6
+
7
+ MCP SDK 1.27.1 passes an AnyIO ``MemoryObjectReceiveStream`` directly to
8
+ ``EventSourceResponse`` for every JSON-RPC POST. The response consumes that
9
+ stream but does not close it after normal iteration, leaving one receive
10
+ endpoint per request for the garbage collector. The response is the owner of
11
+ that per-request iterator, so SLM closes it at the response boundary.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+ from sse_starlette.sse import EventSourceResponse
18
+ from starlette.types import Receive, Scope, Send
19
+
20
+
21
+ class ClosingEventSourceResponse(EventSourceResponse):
22
+ """EventSourceResponse that closes the async iterator it consumes."""
23
+
24
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
25
+ try:
26
+ await super().__call__(scope, receive, send)
27
+ finally:
28
+ close = getattr(self.body_iterator, "aclose", None)
29
+ if close is not None:
30
+ await close()
31
+
32
+
33
+ def install_streamable_http_resource_guard() -> None:
34
+ """Install the response owner used by MCP's Streamable-HTTP transport."""
35
+ from mcp.server import streamable_http
36
+
37
+ streamable_http.EventSourceResponse = ClosingEventSourceResponse
38
+
39
+
40
+ class SLMFastMCP(FastMCP):
41
+ """FastMCP with SLM release identity and deterministic SSE cleanup."""
42
+
43
+ def __init__(self, *args, product_version: str = "3.7.0", **kwargs) -> None:
44
+ super().__init__(*args, **kwargs)
45
+ # FastMCP delegates the initialize response to the low-level MCP
46
+ # server. Without an explicit value it reports the installed ``mcp``
47
+ # library version, which makes an SLM 3.7 server identify itself as
48
+ # (for example) 1.27.1 to every IDE client.
49
+ self._mcp_server.version = product_version
50
+
51
+ def streamable_http_app(self):
52
+ install_streamable_http_resource_guard()
53
+ return super().streamable_http_app()
@@ -27,11 +27,11 @@ _os.environ.setdefault('SLM_SKIP_DEP_CHECK', '1')
27
27
  import logging
28
28
  import sys
29
29
 
30
- from mcp.server.fastmcp import FastMCP
30
+ from superlocalmemory.mcp.http_transport import SLMFastMCP
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
- server = FastMCP("SuperLocalMemory V3")
34
+ server = SLMFastMCP("SuperLocalMemory V3")
35
35
 
36
36
  # Lazy engine singleton -------------------------------------------------------
37
37
 
@@ -175,6 +175,18 @@ _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
175
175
  "mesh": _PROFILE_MESH,
176
176
  } # "whole" intentionally absent — maps to raw server (D-2 LOCKED)
177
177
 
178
+ # Compatibility aliases published by the v3.6 README. Keep these explicit so
179
+ # a stale client configuration has one deterministic meaning and emits a
180
+ # migration warning. Any other value is a configuration error (fail closed).
181
+ _PROFILE_ALIASES: dict[str, str] = {
182
+ "core14": "core",
183
+ "code20": "code",
184
+ "full38": "full",
185
+ "power50": "power",
186
+ "mesh8": "mesh",
187
+ "whole81": "whole",
188
+ }
189
+
178
190
  _profile = _os_reg.environ.get("SLM_MCP_PROFILE", "").strip().lower()
179
191
 
180
192
 
@@ -188,19 +200,26 @@ def _resolve_profile_allowed(
188
200
  Returns:
189
201
  None — for "" (no selection) or "whole" (raw server, all tools).
190
202
  frozenset — the profile's tool set for known profiles.
191
- essential — fail-open fallback for unknown non-empty profiles (+ warning).
203
+
204
+ Raises:
205
+ ValueError: if ``profile`` is neither canonical nor an explicitly
206
+ supported compatibility alias.
192
207
  """
193
- if not profile or profile == "whole":
208
+ canonical = _PROFILE_ALIASES.get(profile, profile)
209
+ if canonical != profile:
210
+ logger.warning(
211
+ "SLM_MCP_PROFILE=%r is deprecated; use %r instead.",
212
+ profile,
213
+ canonical,
214
+ )
215
+ if not canonical or canonical == "whole":
194
216
  return None
195
- if profile in definitions:
196
- return definitions[profile]
197
- logger.warning(
198
- "SLM_MCP_PROFILE=%r is not a recognised profile "
199
- "(valid: %s, whole); falling back to essential tools.",
200
- profile,
201
- ", ".join(sorted(definitions)),
217
+ if canonical in definitions:
218
+ return definitions[canonical]
219
+ valid = ", ".join((*sorted(definitions), "whole"))
220
+ raise ValueError(
221
+ f"SLM_MCP_PROFILE={profile!r} is not recognised; valid profiles: {valid}"
202
222
  )
203
- return essential
204
223
 
205
224
 
206
225
  class _FilteredServer:
@@ -237,7 +256,14 @@ elif _user_allowlist_str:
237
256
  elif _profile == "whole":
238
257
  _target = server # NEW raw-server
239
258
  elif _profile:
240
- _target = _FilteredServer(server, _resolve_profile_allowed(_profile, _PROFILE_DEFINITIONS, _ESSENTIAL_TOOLS)) # NEW
259
+ _profile_allowed = _resolve_profile_allowed(
260
+ _profile, _PROFILE_DEFINITIONS, _ESSENTIAL_TOOLS
261
+ )
262
+ _target = (
263
+ server
264
+ if _profile_allowed is None
265
+ else _FilteredServer(server, _profile_allowed)
266
+ )
241
267
  else:
242
268
  _target = _FilteredServer(server, _ESSENTIAL_TOOLS) # default (unchanged)
243
269
 
@@ -10,9 +10,10 @@ V3.3.12: Extracted _emit_event to eliminate code duplication.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from pathlib import Path
13
+ from dataclasses import dataclass
14
+ from typing import Any
14
15
 
15
- _DB_PATH = Path.home() / ".superlocalmemory" / "memory.db"
16
+ from superlocalmemory.infra.data_root import state_path
16
17
 
17
18
 
18
19
  def emit_event(event_type: str, payload: dict | None = None,
@@ -20,8 +21,73 @@ def emit_event(event_type: str, payload: dict | None = None,
20
21
  """Emit an event to the EventBus (best-effort, never raises)."""
21
22
  try:
22
23
  from superlocalmemory.infra.event_bus import EventBus
23
- bus = EventBus.get_instance(_DB_PATH)
24
+ bus = EventBus.get_instance(state_path("memory.db"))
24
25
  bus.emit(event_type, payload=payload, source_agent=source_agent,
25
26
  source_protocol="mcp")
26
27
  except Exception:
27
28
  pass
29
+
30
+
31
+ @dataclass
32
+ class MCPMutationAuthorization:
33
+ """One capability-owned MCP mutation authorized by engine policy."""
34
+
35
+ engine: Any
36
+ operation: str
37
+ context: dict[str, str]
38
+ _completed: bool = False
39
+
40
+ @property
41
+ def actor_id(self) -> str:
42
+ return self.context["agent_id"]
43
+
44
+ def complete(self) -> None:
45
+ """Emit the matching post hook once the mutation succeeds."""
46
+ if self._completed:
47
+ return
48
+ self.engine._hooks.run_post(self.operation, self.context)
49
+ self._completed = True
50
+
51
+
52
+ def authorize_mcp_mutation(
53
+ engine: Any,
54
+ operation: str,
55
+ *,
56
+ mutation_source: str,
57
+ profile_id: str | None = None,
58
+ fact_id: str = "",
59
+ content_preview: str = "",
60
+ ) -> MCPMutationAuthorization:
61
+ """Derive local authority and run policy before a direct MCP write.
62
+
63
+ MCP URL/env agent identifiers remain audit attribution only. The trusted
64
+ principal is derived from the private local install capability, matching
65
+ the canonical ingestion and recall-worker mutation boundaries.
66
+ """
67
+ if operation not in {"update", "delete"}:
68
+ raise ValueError("MCP mutations must use update or delete policy")
69
+
70
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
71
+ from superlocalmemory.mcp.agent_context import get_current_agent_id
72
+
73
+ source_agent_id = get_current_agent_id(env_fallback=True)
74
+ context = {
75
+ "operation": operation,
76
+ "agent_id": local_trusted_actor_id("mcp"),
77
+ "source_agent_id": source_agent_id,
78
+ "mutation_source": mutation_source,
79
+ "profile_id": profile_id or engine.profile_id,
80
+ }
81
+ if fact_id:
82
+ context["fact_id"] = fact_id
83
+ if content_preview:
84
+ context["content_preview"] = content_preview[:100]
85
+ engine._hooks.run_pre(operation, context)
86
+ return MCPMutationAuthorization(engine, operation, context)
87
+
88
+
89
+ __all__ = [
90
+ "MCPMutationAuthorization",
91
+ "authorize_mcp_mutation",
92
+ "emit_event",
93
+ ]
@@ -19,16 +19,17 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  import datetime
21
21
  import logging
22
- import os
23
22
  import sqlite3
24
23
  import uuid
25
- from pathlib import Path
26
- from typing import Callable
24
+ from typing import TYPE_CHECKING, Callable
27
25
 
28
- logger = logging.getLogger(__name__)
26
+ from superlocalmemory.infra.data_root import canonical_data_root, state_path
27
+ from superlocalmemory.mcp.shared import authorize_mcp_mutation
28
+
29
+ if TYPE_CHECKING:
30
+ from superlocalmemory.mcp._pool_adapter import PoolRecallResponse
29
31
 
30
- MEMORY_DIR = Path.home() / ".superlocalmemory"
31
- DB_PATH = MEMORY_DIR / "memory.db"
32
+ logger = logging.getLogger(__name__)
32
33
 
33
34
 
34
35
  def _sqlite_emergency_recall(
@@ -48,8 +49,9 @@ def _sqlite_emergency_recall(
48
49
  Used ONLY when Tier-1 (full daemon recall) fails completely. Normal
49
50
  path is full 6-channel; this is the fire-alarm.
50
51
  """
51
- from superlocalmemory.mcp._pool_adapter import PoolFact, PoolRecallItem, PoolRecallResponse
52
52
  import re
53
+
54
+ from superlocalmemory.mcp._pool_adapter import PoolFact, PoolRecallItem, PoolRecallResponse
53
55
  try:
54
56
  # FTS5 MATCH syntax: tokenize the query, drop special characters
55
57
  # that confuse the parser (/, :, ., etc), and join with OR for
@@ -64,7 +66,7 @@ def _sqlite_emergency_recall(
64
66
  f"AND f.created_at >= datetime('now', '-{int(max_age_days)} days') "
65
67
  if max_age_days > 0 else ""
66
68
  )
67
- conn = sqlite3.connect(str(DB_PATH), timeout=5.0)
69
+ conn = sqlite3.connect(str(state_path("memory.db")), timeout=5.0)
68
70
  try:
69
71
  rows = conn.execute(
70
72
  f"""SELECT f.fact_id, f.content, f.memory_id, f.created_at,
@@ -129,24 +131,26 @@ def _emit_event(event_type: str, payload: dict | None = None,
129
131
  resolved_agent = source_agent if source_agent is not None else _get_agent_id()
130
132
  try:
131
133
  from superlocalmemory.infra.event_bus import EventBus
132
- bus = EventBus.get_instance(str(DB_PATH))
134
+ bus = EventBus.get_instance(str(state_path("memory.db")))
133
135
  bus.emit(event_type, payload=payload, source_agent=resolved_agent,
134
136
  source_protocol="mcp")
135
137
  except Exception as exc:
136
138
  logger.warning("event emit failed: type=%s err=%s", event_type, exc)
137
139
 
138
140
 
139
- def _register_agent(agent_id: str, profile_id: str) -> None:
141
+ def _register_agent(agent_id: str, profile_id: str) -> bool:
140
142
  """Register an agent in the AgentRegistry (best-effort)."""
141
143
  try:
142
144
  from superlocalmemory.core.registry import AgentRegistry
143
- registry_path = MEMORY_DIR / "agents.json"
145
+ registry_path = canonical_data_root() / "agents.json"
144
146
  registry = AgentRegistry(persist_path=registry_path)
145
147
  registry.register_agent(agent_id, profile_id)
148
+ return True
146
149
  except Exception as exc:
147
150
  logger.warning(
148
151
  "agent registry write failed: agent=%s err=%s", agent_id, exc,
149
152
  )
153
+ return False
150
154
 
151
155
 
152
156
  def register_active_tools(server, get_engine: Callable) -> None:
@@ -194,7 +198,12 @@ def register_active_tools(server, get_engine: Callable) -> None:
194
198
  rules = RulesEngine()
195
199
 
196
200
  if not rules.should_recall("session_start"):
197
- return {"success": True, "context": "", "memories": [], "message": "Auto-recall disabled"}
201
+ return {
202
+ "success": True,
203
+ "context": "",
204
+ "memories": [],
205
+ "message": "Auto-recall disabled",
206
+ }
198
207
 
199
208
  recall_config = rules.get_recall_config()
200
209
  relevance_threshold = recall_config.get("relevance_threshold", 0.3)
@@ -239,7 +248,8 @@ def register_active_tools(server, get_engine: Callable) -> None:
239
248
  # Memories older than max_age_days are excluded unless their score
240
249
  # exceeds 0.7 (high-relevance architectural decisions always surface).
241
250
  # max_age_days=0 disables the gate entirely.
242
- from datetime import UTC, datetime as _dt
251
+ from datetime import UTC
252
+ from datetime import datetime as _dt
243
253
  _now = _dt.now(UTC)
244
254
 
245
255
  def _age_days(created_at_str: str) -> float:
@@ -272,6 +282,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
272
282
  clamp_content,
273
283
  is_low_quality,
274
284
  render_context,
285
+ sanitize_untrusted_content,
275
286
  )
276
287
 
277
288
  pid = engine.profile_id
@@ -282,7 +293,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
282
293
  pinned_facts = engine.db.get_pinned(pid)
283
294
  except Exception:
284
295
  pinned_facts = []
285
- pinned_ids = {f.fact_id for f in pinned_facts}
286
296
  pinned_seen = set()
287
297
 
288
298
  cfg_inj = getattr(getattr(engine, "config", None), "injection", None)
@@ -304,6 +314,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
304
314
  importance=getattr(pf, "importance", 0.0) or 0.0,
305
315
  access_count=getattr(pf, "access_count", 0) or 0,
306
316
  pinned=True,
317
+ source_type="pinned-fact",
307
318
  ))
308
319
  pinned_seen.add(pf.fact_id)
309
320
 
@@ -317,17 +328,18 @@ def register_active_tools(server, get_engine: Callable) -> None:
317
328
  fact_id=r.fact.fact_id,
318
329
  importance=getattr(r.fact, "importance", 0.0) or 0.0,
319
330
  access_count=getattr(r.fact, "access_count", 0) or 0,
331
+ source_type="recall",
320
332
  ))
321
333
 
322
334
  mode_str = str(getattr(engine, "mode", "B")).upper()
323
335
  try:
324
- context = render_context(inj_mems, mode=mode_str, cfg=cfg_inj, wrap=False)
336
+ context = render_context(
337
+ inj_mems, mode=mode_str, cfg=cfg_inj, wrap=True,
338
+ )
325
339
  except Exception:
326
- # Fall back to legacy content building on any formatter failure
327
- lines = ["# Relevant Memory Context", ""]
328
- for m in inj_mems[:max_results]:
329
- lines.append(f"- {m.content[:200]}")
330
- context = "\n".join(lines)
340
+ # Fail closed: never serialize retrieved content through a
341
+ # weaker ad-hoc path when the mandatory renderer fails.
342
+ context = ""
331
343
 
332
344
  # GAP-FIX (v3.4.65 delivery-lead): the memories[] array is part of
333
345
  # the MCP response Claude Code ingests — it MUST be bounded too, not
@@ -335,16 +347,43 @@ def register_active_tools(server, get_engine: Callable) -> None:
335
347
  # content shipped here (one fact was 131K chars → ~124K-token
336
348
  # response, defeating the whole token budget). Clamp each content
337
349
  # to per_memory_max_tokens, drop junk, and honour max_results.
338
- memories = [
339
- {
350
+ contract_by_id = {
351
+ r.fact.fact_id: r for r in relevant[:max_results]
352
+ }
353
+ memories = []
354
+ for position, m in enumerate(inj_mems[:max_results], start=1):
355
+ if is_low_quality(m.content):
356
+ continue
357
+ result_contract = contract_by_id.get(m.fact_id)
358
+ relevance = (
359
+ getattr(result_contract, "relevance_score", m.score)
360
+ if result_contract is not None else m.score
361
+ )
362
+ memory_confidence = (
363
+ getattr(result_contract, "memory_confidence", None)
364
+ if result_contract is not None else None
365
+ )
366
+ memories.append({
340
367
  "fact_id": m.fact_id,
341
- "content": clamp_content(m.content, cfg_inj),
368
+ "content": clamp_content(
369
+ sanitize_untrusted_content(m.content), cfg_inj,
370
+ ),
342
371
  "score": m.score,
372
+ "relevance_score": relevance,
373
+ "ranking_score": (
374
+ getattr(result_contract, "ranking_score", None)
375
+ if result_contract is not None else None
376
+ ),
377
+ "confidence": memory_confidence,
378
+ "memory_confidence": memory_confidence,
379
+ "rank_position": (
380
+ getattr(result_contract, "rank_position", position)
381
+ if result_contract is not None else position
382
+ ),
343
383
  "is_core": m.is_core,
344
- }
345
- for m in inj_mems[:max_results]
346
- if not is_low_quality(m.content)
347
- ]
384
+ "untrusted": True,
385
+ "source_type": m.source_type,
386
+ })
348
387
 
349
388
  # Get learning status
350
389
  feedback_count = 0
@@ -366,7 +405,21 @@ def register_active_tools(server, get_engine: Callable) -> None:
366
405
 
367
406
  # Register agent + emit event (v3.4.39: SLM_AGENT_ID env support)
368
407
  agent_id = _get_agent_id()
369
- _register_agent(agent_id, pid)
408
+ if hasattr(engine, "_hooks"):
409
+ registration_auth = authorize_mcp_mutation(
410
+ engine,
411
+ "update",
412
+ mutation_source="mcp-agent-registration",
413
+ profile_id=pid,
414
+ )
415
+ if _register_agent(agent_id, pid):
416
+ registration_auth.complete()
417
+ else:
418
+ # A LIGHT client without the policy registry is read-capable,
419
+ # but must not fall back to an unauthorised registry write.
420
+ logger.info(
421
+ "agent registration skipped: policy hooks unavailable"
422
+ )
370
423
  _emit_event("agent.connected", {
371
424
  "agent_id": agent_id,
372
425
  "project_path": project_path,
@@ -381,11 +434,29 @@ def register_active_tools(server, get_engine: Callable) -> None:
381
434
  "memory_count": len(memories),
382
435
  "core_memory": [m["content"] for m in memories if m.get("is_core")],
383
436
  "degraded_mode": degraded_mode,
384
- "retrieval_mode": "emergency_fts5_bm25" if degraded_mode else "full_6_channel",
437
+ "retrieval_mode": (
438
+ "emergency_fts5_bm25"
439
+ if degraded_mode
440
+ else "hybrid_candidate_fusion"
441
+ ),
442
+ "score_contract_version": getattr(
443
+ response, "score_contract_version", "2"
444
+ ),
445
+ "calibration_status": getattr(
446
+ response, "calibration_status", "uncalibrated"
447
+ ),
448
+ "calibration_id": getattr(response, "calibration_id", None),
449
+ "answer_confidence": getattr(response, "answer_confidence", None),
450
+ "abstained": getattr(response, "abstained", not bool(relevant)),
451
+ "abstention_reason": getattr(response, "abstention_reason", None),
385
452
  "learning": {
386
453
  "feedback_signals": feedback_count,
387
454
  "phase": 1 if feedback_count < 50 else (2 if feedback_count < 200 else 3),
388
- "status": "collecting" if feedback_count < 50 else ("learning" if feedback_count < 200 else "trained"),
455
+ "status": (
456
+ "collecting"
457
+ if feedback_count < 50
458
+ else "learning" if feedback_count < 200 else "trained"
459
+ ),
389
460
  },
390
461
  }
391
462
  except Exception as exc:
@@ -496,8 +567,22 @@ def register_active_tools(server, get_engine: Callable) -> None:
496
567
  pid = engine.profile_id
497
568
 
498
569
  if feedback not in ("relevant", "irrelevant", "partial"):
499
- return {"success": False, "error": f"Invalid feedback: {feedback}. Use relevant/irrelevant/partial"}
570
+ return {
571
+ "success": False,
572
+ "error": (
573
+ f"Invalid feedback: {feedback}. "
574
+ "Use relevant/irrelevant/partial"
575
+ ),
576
+ }
500
577
 
578
+ authorization = authorize_mcp_mutation(
579
+ engine,
580
+ "update",
581
+ mutation_source="mcp-recall-feedback",
582
+ profile_id=pid,
583
+ fact_id=fact_id,
584
+ content_preview=feedback,
585
+ )
501
586
  record = engine._adaptive_learner.record_feedback(
502
587
  query=query,
503
588
  fact_id=fact_id,
@@ -506,6 +591,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
506
591
  )
507
592
 
508
593
  count = engine._adaptive_learner.get_feedback_count(pid)
594
+ authorization.complete()
509
595
 
510
596
  _emit_event("pattern.learned", {
511
597
  "fact_id": fact_id,
@@ -562,7 +648,15 @@ def register_active_tools(server, get_engine: Callable) -> None:
562
648
  pass
563
649
  if not sid:
564
650
  return {"success": False, "error": "No session_id provided or found"}
651
+ authorization = authorize_mcp_mutation(
652
+ engine,
653
+ "update",
654
+ mutation_source="mcp-session-close",
655
+ profile_id=engine.profile_id,
656
+ content_preview=sid,
657
+ )
565
658
  count = engine.close_session(sid)
659
+ authorization.complete()
566
660
  return {
567
661
  "success": True,
568
662
  "session_id": sid,
@@ -596,13 +690,29 @@ def register_active_tools(server, get_engine: Callable) -> None:
596
690
  if action == "pin":
597
691
  if not fact_id:
598
692
  return {"success": False, "error": "fact_id required for pin"}
693
+ authorization = authorize_mcp_mutation(
694
+ engine,
695
+ "update",
696
+ mutation_source="mcp-core-memory-pin",
697
+ profile_id=pid,
698
+ fact_id=fact_id,
699
+ )
599
700
  db.set_pinned(fact_id, True)
701
+ authorization.complete()
600
702
  return {"success": True, "action": "pin", "fact_id": fact_id}
601
703
 
602
704
  if action == "unpin":
603
705
  if not fact_id:
604
706
  return {"success": False, "error": "fact_id required for unpin"}
707
+ authorization = authorize_mcp_mutation(
708
+ engine,
709
+ "update",
710
+ mutation_source="mcp-core-memory-unpin",
711
+ profile_id=pid,
712
+ fact_id=fact_id,
713
+ )
605
714
  db.set_pinned(fact_id, False)
715
+ authorization.complete()
606
716
  return {"success": True, "action": "unpin", "fact_id": fact_id}
607
717
 
608
718
  if action == "list":