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
@@ -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":