superlocalmemory 3.6.23 → 3.7.1

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 (285) hide show
  1. package/CHANGELOG.md +61 -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 +17 -9
  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/agents/slm-memory-advisor.md +1 -1
  18. package/plugin-src/manifest.json +1 -1
  19. package/plugin-src/requirements.txt +1 -1
  20. package/plugin-src/rules/AGENTS.md +1 -1
  21. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  22. package/pyproject.toml +40 -8
  23. package/scripts/postinstall-interactive.js +17 -94
  24. package/scripts/postinstall.js +185 -258
  25. package/scripts/preuninstall.js +9 -50
  26. package/src/superlocalmemory/__init__.py +2 -2
  27. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  28. package/src/superlocalmemory/attribution/signer.py +34 -19
  29. package/src/superlocalmemory/attribution/watermark.py +1 -1
  30. package/src/superlocalmemory/cli/_lazy_init.py +7 -7
  31. package/src/superlocalmemory/cli/commands.py +453 -191
  32. package/src/superlocalmemory/cli/context_commands.py +5 -4
  33. package/src/superlocalmemory/cli/daemon.py +282 -187
  34. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  35. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  36. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  37. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  38. package/src/superlocalmemory/cli/main.py +128 -31
  39. package/src/superlocalmemory/cli/pending_store.py +54 -38
  40. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  41. package/src/superlocalmemory/cli/service_installer.py +57 -52
  42. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  43. package/src/superlocalmemory/cli/version_banner.py +2 -1
  44. package/src/superlocalmemory/code_graph/config.py +3 -1
  45. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  46. package/src/superlocalmemory/core/config.py +65 -20
  47. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  48. package/src/superlocalmemory/core/context_cache.py +56 -8
  49. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  50. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  51. package/src/superlocalmemory/core/embeddings.py +54 -18
  52. package/src/superlocalmemory/core/engine.py +150 -104
  53. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  54. package/src/superlocalmemory/core/engine_wiring.py +32 -23
  55. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  56. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  57. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  58. package/src/superlocalmemory/core/health_monitor.py +4 -2
  59. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  60. package/src/superlocalmemory/core/injection.py +69 -18
  61. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  62. package/src/superlocalmemory/core/maintenance.py +1 -1
  63. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  64. package/src/superlocalmemory/core/mutations.py +143 -0
  65. package/src/superlocalmemory/core/platform_utils.py +7 -4
  66. package/src/superlocalmemory/core/ram_lock.py +16 -5
  67. package/src/superlocalmemory/core/rate_limit.py +1 -1
  68. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  69. package/src/superlocalmemory/core/recall_worker.py +76 -59
  70. package/src/superlocalmemory/core/registry.py +1 -1
  71. package/src/superlocalmemory/core/scale_engine.py +293 -0
  72. package/src/superlocalmemory/core/score_contract.py +62 -0
  73. package/src/superlocalmemory/core/security_primitives.py +3 -1
  74. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  75. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  76. package/src/superlocalmemory/core/tier_manager.py +32 -20
  77. package/src/superlocalmemory/core/worker_pool.py +13 -4
  78. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  79. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  80. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  81. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  82. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  84. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  85. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  86. package/src/superlocalmemory/encoding/foresight.py +2 -0
  87. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  88. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  89. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  90. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  91. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  92. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  93. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  94. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  95. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  96. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  97. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  98. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  100. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  101. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  102. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  103. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  104. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  105. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  106. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  107. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  108. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  109. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  110. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  111. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  112. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  113. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  114. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  115. package/src/superlocalmemory/infra/backup.py +7 -5
  116. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  117. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  118. package/src/superlocalmemory/infra/data_root.py +199 -0
  119. package/src/superlocalmemory/infra/event_bus.py +3 -1
  120. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  121. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  122. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  123. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  124. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  125. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  126. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  127. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  128. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  129. package/src/superlocalmemory/learning/ensemble.py +11 -0
  130. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  131. package/src/superlocalmemory/learning/feedback.py +1 -1
  132. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  133. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  134. package/src/superlocalmemory/learning/ranker.py +4 -1
  135. package/src/superlocalmemory/learning/source_quality.py +1 -1
  136. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  137. package/src/superlocalmemory/llm/backbone.py +1 -1
  138. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  139. package/src/superlocalmemory/math/fisher.py +1 -1
  140. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  141. package/src/superlocalmemory/math/hopfield.py +1 -1
  142. package/src/superlocalmemory/math/langevin.py +1 -1
  143. package/src/superlocalmemory/math/polar_quant.py +3 -4
  144. package/src/superlocalmemory/math/qjl.py +1 -1
  145. package/src/superlocalmemory/math/sheaf.py +1 -1
  146. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  147. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  148. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  149. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  150. package/src/superlocalmemory/mcp/server.py +39 -13
  151. package/src/superlocalmemory/mcp/shared.py +69 -3
  152. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  153. package/src/superlocalmemory/mcp/tools_core.py +130 -31
  154. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  155. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  156. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  157. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  158. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  159. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  160. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  161. package/src/superlocalmemory/mesh/broker.py +2 -0
  162. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  163. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  164. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  165. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  166. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  167. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  168. package/src/superlocalmemory/optimize/config/store.py +6 -4
  169. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  170. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  171. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  172. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  173. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  174. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  175. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  176. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  177. package/src/superlocalmemory/retrieval/engine.py +169 -79
  178. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  179. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  180. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  181. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  182. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  183. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  184. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  185. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  186. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  187. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  188. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  189. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  190. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  191. package/src/superlocalmemory/server/api.py +10 -7
  192. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  193. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  194. package/src/superlocalmemory/server/route_mutations.py +84 -0
  195. package/src/superlocalmemory/server/routes/agents.py +8 -6
  196. package/src/superlocalmemory/server/routes/brain.py +14 -12
  197. package/src/superlocalmemory/server/routes/chat.py +29 -12
  198. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  199. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  200. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  201. package/src/superlocalmemory/server/routes/memories.py +104 -43
  202. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  203. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  204. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  205. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  206. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  207. package/src/superlocalmemory/server/security_middleware.py +1 -1
  208. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  209. package/src/superlocalmemory/server/write_identity.py +147 -0
  210. package/src/superlocalmemory/storage/access_log.py +4 -3
  211. package/src/superlocalmemory/storage/database.py +118 -25
  212. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  213. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  214. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  215. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  216. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  217. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  218. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  219. package/src/superlocalmemory/storage/models.py +16 -0
  220. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  221. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  222. package/src/superlocalmemory/ui/favicon.svg +5 -0
  223. package/src/superlocalmemory/ui/index.html +1 -0
  224. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  225. package/src/superlocalmemory/ui/js/core.js +49 -8
  226. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  227. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  228. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  229. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  230. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  231. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  232. package/src/superlocalmemory/ui/js/settings.js +4 -2
  233. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  234. package/bin/slm +0 -59
  235. package/bin/slm.bat +0 -77
  236. package/bin/slm.cmd +0 -5
  237. package/ide/integrations/langchain/README.md +0 -106
  238. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  239. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  240. package/ide/integrations/langchain/pyproject.toml +0 -38
  241. package/ide/integrations/langchain/tests/__init__.py +0 -3
  242. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  243. package/ide/integrations/langchain/tests/test_security.py +0 -117
  244. package/ide/integrations/llamaindex/README.md +0 -81
  245. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  246. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  247. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  248. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  249. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  250. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  251. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  252. package/scripts/_savings_math.py +0 -270
  253. package/scripts/build-dmg.sh +0 -417
  254. package/scripts/build-plugin.js +0 -742
  255. package/scripts/build-slm-hook.ps1 +0 -40
  256. package/scripts/build-slm-hook.sh +0 -45
  257. package/scripts/build_entry.py +0 -452
  258. package/scripts/ci/stage5b_gate.sh +0 -50
  259. package/scripts/dogfood_savings.py +0 -490
  260. package/scripts/generate-thumbnails.py +0 -218
  261. package/scripts/install-skills.ps1 +0 -4
  262. package/scripts/install-skills.sh +0 -5
  263. package/scripts/install.ps1 +0 -701
  264. package/scripts/install.sh +0 -1015
  265. package/scripts/postinstall_binary.js +0 -287
  266. package/scripts/prepack.js +0 -33
  267. package/scripts/release_manifest.py +0 -273
  268. package/scripts/slm-hook.spec +0 -56
  269. package/scripts/start-dashboard.ps1 +0 -52
  270. package/scripts/start-dashboard.sh +0 -41
  271. package/scripts/sync-wiki.ps1 +0 -127
  272. package/scripts/sync-wiki.sh +0 -82
  273. package/scripts/test-dmg.sh +0 -161
  274. package/scripts/test-npm-package.ps1 +0 -252
  275. package/scripts/test-npm-package.sh +0 -207
  276. package/scripts/verify-install.ps1 +0 -294
  277. package/scripts/verify-install.sh +0 -266
  278. package/scripts/verify-v27.ps1 +0 -301
  279. package/scripts/verify-v27.sh +0 -233
  280. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  281. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  282. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  283. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  284. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  285. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -17,15 +17,14 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
17
17
  from __future__ import annotations
18
18
 
19
19
  import logging
20
- from pathlib import Path
21
20
  from typing import Callable
22
21
 
23
22
  from mcp.types import ToolAnnotations
24
23
 
25
24
  logger = logging.getLogger(__name__)
26
25
 
27
- MEMORY_DIR = Path.home() / ".superlocalmemory"
28
- DB_PATH = MEMORY_DIR / "memory.db"
26
+ from superlocalmemory.infra.data_root import state_path
27
+ from superlocalmemory.mcp.shared import authorize_mcp_mutation
29
28
 
30
29
 
31
30
  def _try_daemon_post(path: str, body: dict, timeout_s: float = 60.0) -> dict | None:
@@ -62,7 +61,7 @@ def _emit_event(event_type: str, payload: dict | None = None,
62
61
  """Emit an event to the EventBus (best-effort, never raises)."""
63
62
  try:
64
63
  from superlocalmemory.infra.event_bus import EventBus
65
- bus = EventBus.get_instance(str(DB_PATH))
64
+ bus = EventBus.get_instance(str(state_path("memory.db")))
66
65
  bus.emit(event_type, payload=payload, source_agent=source_agent,
67
66
  source_protocol="mcp")
68
67
  except Exception:
@@ -121,14 +120,22 @@ def register_v33_tools(server, get_engine: Callable) -> None:
121
120
  total += int(r["cnt"])
122
121
  result = {"total": total, "transitions": 0, "dry_run_zones": zones}
123
122
  else:
123
+ authorization = authorize_mcp_mutation(
124
+ engine,
125
+ "delete",
126
+ mutation_source="mcp-forgetting-cycle",
127
+ profile_id=pid,
128
+ )
124
129
  result = scheduler.run_decay_cycle(pid, force=True)
130
+ authorization.complete()
125
131
 
126
- _emit_event("forgetting.cycle_complete", {
127
- "profile_id": pid,
128
- "dry_run": dry_run,
129
- "total": result.get("total", 0),
130
- "transitions": result.get("transitions", 0),
131
- })
132
+ if not dry_run:
133
+ _emit_event("forgetting.cycle_complete", {
134
+ "profile_id": pid,
135
+ "dry_run": False,
136
+ "total": result.get("total", 0),
137
+ "transitions": result.get("transitions", 0),
138
+ })
132
139
 
133
140
  return {"success": True, "dry_run": dry_run, **result}
134
141
 
@@ -183,15 +190,23 @@ def register_v33_tools(server, get_engine: Callable) -> None:
183
190
  facts = engine._db.get_all_facts(pid)
184
191
  result = {"total": len(facts), "would_quantize": 0, "dry_run": True}
185
192
  else:
193
+ authorization = authorize_mcp_mutation(
194
+ engine,
195
+ "update",
196
+ mutation_source="mcp-quantization-cycle",
197
+ profile_id=pid,
198
+ )
186
199
  result = scheduler.run_eap_cycle(pid)
200
+ authorization.complete()
187
201
 
188
- _emit_event("eap.cycle_complete", {
189
- "profile_id": pid,
190
- "dry_run": dry_run,
191
- "total": result.get("total", 0),
192
- "downgrades": result.get("downgrades", 0),
193
- "upgrades": result.get("upgrades", 0),
194
- })
202
+ if not dry_run:
203
+ _emit_event("eap.cycle_complete", {
204
+ "profile_id": pid,
205
+ "dry_run": False,
206
+ "total": result.get("total", 0),
207
+ "downgrades": result.get("downgrades", 0),
208
+ "upgrades": result.get("upgrades", 0),
209
+ })
195
210
 
196
211
  return {"success": True, "dry_run": dry_run, **result}
197
212
 
@@ -245,8 +260,15 @@ def register_v33_tools(server, get_engine: Callable) -> None:
245
260
  CognitiveConsolidator,
246
261
  )
247
262
 
263
+ authorization = authorize_mcp_mutation(
264
+ engine,
265
+ "update",
266
+ mutation_source="mcp-cognitive-consolidation",
267
+ profile_id=pid,
268
+ )
248
269
  consolidator = CognitiveConsolidator(db=engine._db)
249
270
  result = consolidator.run_pipeline(pid)
271
+ authorization.complete()
250
272
 
251
273
  _emit_event("ccq.consolidation_complete", {
252
274
  "profile_id": pid,
@@ -338,6 +360,14 @@ def register_v33_tools(server, get_engine: Callable) -> None:
338
360
  dry_run: If True, report orphans but don't kill them.
339
361
  """
340
362
  try:
363
+ engine = get_engine()
364
+ authorization = None
365
+ if not dry_run:
366
+ authorization = authorize_mcp_mutation(
367
+ engine,
368
+ "delete",
369
+ mutation_source="mcp-process-reaper",
370
+ )
341
371
  from superlocalmemory.infra.process_reaper import (
342
372
  cleanup_all_orphans,
343
373
  ReaperConfig,
@@ -345,6 +375,8 @@ def register_v33_tools(server, get_engine: Callable) -> None:
345
375
 
346
376
  config = ReaperConfig()
347
377
  result = cleanup_all_orphans(config, dry_run=dry_run)
378
+ if authorization is not None:
379
+ authorization.complete()
348
380
 
349
381
  return {
350
382
  "success": True,
@@ -446,6 +478,12 @@ def register_v33_tools(server, get_engine: Callable) -> None:
446
478
  daemon_result["via"] = "daemon"
447
479
  return daemon_result
448
480
 
481
+ authorization = authorize_mcp_mutation(
482
+ engine,
483
+ "update",
484
+ mutation_source="mcp-maintenance-cycle",
485
+ profile_id=pid,
486
+ )
449
487
  results = {}
450
488
 
451
489
  # 1. Langevin dynamics step (lifecycle evolution)
@@ -476,6 +514,7 @@ def register_v33_tools(server, get_engine: Callable) -> None:
476
514
  except Exception as exc:
477
515
  results["behavioral"] = {"error": str(exc)}
478
516
 
517
+ authorization.complete()
479
518
  return {"success": True, "profile": pid, **results}
480
519
 
481
520
  except Exception as exc:
@@ -301,6 +301,7 @@ class MeshBroker:
301
301
  "FROM mesh_messages m "
302
302
  "LEFT JOIN mesh_reads r ON m.id = r.message_id AND r.peer_id = ? "
303
303
  "WHERE m.target_type='broadcast' AND m.from_peer != ? "
304
+ "AND r.peer_id IS NULL "
304
305
  "AND (m.expires_at IS NULL OR m.expires_at > ?) "
305
306
  "ORDER BY m.created_at DESC LIMIT 50",
306
307
  (peer_id, peer_id, now),
@@ -316,6 +317,7 @@ class MeshBroker:
316
317
  "FROM mesh_messages m "
317
318
  "LEFT JOIN mesh_reads r ON m.id = r.message_id AND r.peer_id = ? "
318
319
  "WHERE m.target_type='project' AND m.project_path=? AND m.from_peer != ? "
320
+ "AND r.peer_id IS NULL "
319
321
  "AND (m.expires_at IS NULL OR m.expires_at > ?) "
320
322
  "ORDER BY m.created_at DESC LIMIT 50",
321
323
  (peer_id, project_path, peer_id, now),
@@ -21,12 +21,52 @@ import logging
21
21
  import os
22
22
  import threading
23
23
  import time
24
+ import ipaddress
24
25
  from typing import Any
25
26
 
26
27
  import httpx
27
28
 
28
29
  logger = logging.getLogger("superlocalmemory.mesh.remote_sync")
29
30
 
31
+
32
+ def _service_ip_addresses(info: Any) -> list[str]:
33
+ """Return validated textual IPs from current and older Zeroconf APIs."""
34
+ candidates: list[Any] = []
35
+ parsed = getattr(info, "parsed_addresses", None)
36
+ if callable(parsed):
37
+ try:
38
+ candidates.extend(parsed())
39
+ except (OSError, TypeError, ValueError):
40
+ pass
41
+ candidates.extend(getattr(info, "addresses", None) or [])
42
+
43
+ addresses: list[str] = []
44
+ for candidate in candidates:
45
+ try:
46
+ address = ipaddress.ip_address(candidate)
47
+ except (TypeError, ValueError):
48
+ continue
49
+ # An mDNS peer must name a routable endpoint, never a wildcard or
50
+ # multicast destination. Authentication is still enforced by mesh.
51
+ if address.is_unspecified or address.is_multicast:
52
+ continue
53
+ rendered = str(address)
54
+ if rendered not in addresses:
55
+ addresses.append(rendered)
56
+ return addresses
57
+
58
+
59
+ def _peer_url(host: str, port: int) -> str:
60
+ """Format an IP literal safely for an HTTP authority."""
61
+ address = ipaddress.ip_address(host)
62
+ rendered = str(address)
63
+ if address.version == 6:
64
+ # RFC 6874 requires a percent sign in an IPv6 zone identifier to be
65
+ # escaped when the literal appears inside a URI authority.
66
+ rendered = rendered.replace("%", "%25")
67
+ rendered = f"[{rendered}]"
68
+ return f"http://{rendered}:{int(port)}"
69
+
30
70
  # Optional zeroconf for mDNS discovery
31
71
  try:
32
72
  from zeroconf import ServiceBrowser, ServiceInfo, Zeroconf
@@ -236,17 +276,15 @@ class RemoteSyncClient:
236
276
  if not ZEROCONF_AVAILABLE:
237
277
  return
238
278
  info = zeroconf.get_service_info(service_type, name)
239
- if info and info.addresses:
240
- # Get first IPv4 address
241
- for addr in info.addresses:
242
- if isinstance(addr, str) and "." in addr: # IPv4
243
- port = info.port or 8765
244
- peer_url = f"http://{addr}:{port}"
245
- self._update_peer_url(addr, port)
246
- logger.info(
247
- "RemoteSyncClient: discovered SLM at %s", peer_url
248
- )
249
- return
279
+ if info:
280
+ for addr in _service_ip_addresses(info):
281
+ port = info.port or 8765
282
+ peer_url = _peer_url(addr, port)
283
+ self._update_peer_url(addr, port)
284
+ logger.info(
285
+ "RemoteSyncClient: discovered SLM at %s", peer_url
286
+ )
287
+ return
250
288
  except Exception as e:
251
289
  logger.debug("RemoteSyncClient: mDNS add_service error: %s", e)
252
290
 
@@ -260,7 +298,7 @@ class RemoteSyncClient:
260
298
 
261
299
  def _update_peer_url(self, host: str, port: int) -> None:
262
300
  """Update peer URL from discovery."""
263
- new_url = f"http://{host}:{port}"
301
+ new_url = _peer_url(host, port)
264
302
  if self._peer_url != new_url:
265
303
  self._peer_url = new_url
266
304
  logger.info("RemoteSyncClient: updated peer URL to %s", new_url)
@@ -79,6 +79,33 @@ class NoOpSemantic(SemanticTier):
79
79
  return False
80
80
 
81
81
 
82
+ class _LazySemanticEmbedder:
83
+ """Start the canonical embedding service only after an opted-in lookup."""
84
+
85
+ def __init__(self) -> None:
86
+ self._lock = threading.Lock()
87
+ self._service = None
88
+
89
+ def __call__(self, text: str) -> list[float] | None:
90
+ if self._service is None:
91
+ with self._lock:
92
+ if self._service is None:
93
+ from superlocalmemory.core.config import SLMConfig
94
+ from superlocalmemory.core.embeddings import EmbeddingService
95
+
96
+ self._service = EmbeddingService(SLMConfig.load().embedding)
97
+ return self._service.embed(text)
98
+
99
+ def close(self) -> None:
100
+ service = self._service
101
+ self._service = None
102
+ if service is not None:
103
+ try:
104
+ service.unload()
105
+ except Exception:
106
+ pass
107
+
108
+
82
109
  # ---------------------------------------------------------------------------
83
110
  # Metrics
84
111
  # ---------------------------------------------------------------------------
@@ -130,13 +157,23 @@ class CacheManager:
130
157
  self._metrics = CacheMetrics()
131
158
 
132
159
  @classmethod
133
- def get_instance(cls) -> "CacheManager":
160
+ def get_instance(
161
+ cls,
162
+ *,
163
+ optimize_config: Any | None = None,
164
+ semantic_embedder: Callable[[str], list[float] | None] | None = None,
165
+ ) -> "CacheManager":
134
166
  if cls._instance is None:
135
167
  with cls._instance_lock:
136
168
  if cls._instance is None:
137
169
  from superlocalmemory.optimize.storage.db import CacheDB as _CacheDB
138
170
  _db = _CacheDB.get_default()
139
171
  cls._instance = cls(db=_db)
172
+ if optimize_config is not None:
173
+ cls._instance.configure_semantic(
174
+ optimize_config,
175
+ embedder=semantic_embedder,
176
+ )
140
177
  return cls._instance
141
178
 
142
179
  @classmethod
@@ -149,6 +186,7 @@ class CacheManager:
149
186
  """Reset the singleton (testing only)."""
150
187
  with cls._instance_lock:
151
188
  if cls._instance is not None:
189
+ cls._instance._close_semantic()
152
190
  try:
153
191
  cls._instance._db.close() # type: ignore[attr-defined]
154
192
  except Exception:
@@ -416,8 +454,46 @@ class CacheManager:
416
454
  MetricsCollector.get_instance().on_miss()
417
455
 
418
456
  def set_semantic_tier(self, tier: SemanticTier) -> None:
457
+ self._close_semantic()
419
458
  self._semantic = tier
420
459
 
460
+ def _close_semantic(self) -> None:
461
+ close = getattr(self._semantic, "close", None)
462
+ if callable(close):
463
+ try:
464
+ close()
465
+ except Exception:
466
+ pass
467
+
468
+ def configure_semantic(
469
+ self,
470
+ optimize_config: Any,
471
+ *,
472
+ embedder: Callable[[str], list[float] | None] | None = None,
473
+ ) -> None:
474
+ """Wire or disable the real semantic tier from live proxy config."""
475
+ enabled = bool(getattr(optimize_config, "semantic_enabled", False))
476
+ if not enabled:
477
+ if not isinstance(self._semantic, NoOpSemantic):
478
+ self.set_semantic_tier(NoOpSemantic())
479
+ return
480
+
481
+ from superlocalmemory.optimize.cache.semantic import VCacheSemantic
482
+
483
+ desired_embedder = embedder or _LazySemanticEmbedder()
484
+ current = self._semantic
485
+ if (
486
+ isinstance(current, VCacheSemantic)
487
+ and current._config == optimize_config
488
+ and (embedder is None or current._embedder is embedder)
489
+ ):
490
+ return
491
+ self.set_semantic_tier(VCacheSemantic(
492
+ db=self._db,
493
+ config=optimize_config,
494
+ embedder=desired_embedder,
495
+ ))
496
+
421
497
  # ---- core request path ----
422
498
 
423
499
  def get_or_call(
@@ -22,7 +22,7 @@ import logging
22
22
  import random
23
23
  import threading
24
24
  import time
25
- from typing import TYPE_CHECKING, Any
25
+ from typing import TYPE_CHECKING, Any, Callable
26
26
 
27
27
  import numpy as np
28
28
 
@@ -77,9 +77,12 @@ class VCacheSemantic(SemanticTier):
77
77
  self,
78
78
  db: "CacheDB",
79
79
  config: "OptimizeConfig",
80
+ *,
81
+ embedder: Callable[[str], list[float] | np.ndarray | None] | None = None,
80
82
  ) -> None:
81
83
  self._db = db
82
84
  self._config = config
85
+ self._embedder = embedder
83
86
  # TODO(v3.7): when entry_count > 10_000, promote to sqlite-vec. Config flag: semantic_use_vec.
84
87
 
85
88
  self._boundary_store = BoundaryStore(
@@ -134,7 +137,13 @@ class VCacheSemantic(SemanticTier):
134
137
  return None
135
138
  try:
136
139
  if embed is None:
137
- return None
140
+ if self._embedder is None:
141
+ return None
142
+ messages = _extract_messages(req)
143
+ system = _extract_system(req)
144
+ embed = self._embedder(self._build_query_text(messages, system))
145
+ if embed is None:
146
+ return None
138
147
  vec = np.asarray(embed, dtype=np.float32)
139
148
  if vec.shape[0] != _EMBED_DIM:
140
149
  logger.debug(
@@ -190,7 +199,13 @@ class VCacheSemantic(SemanticTier):
190
199
  return
191
200
  try:
192
201
  if embed is None:
193
- return
202
+ if self._embedder is None:
203
+ return
204
+ messages = _extract_messages(req)
205
+ system = _extract_system(req)
206
+ embed = self._embedder(self._build_query_text(messages, system))
207
+ if embed is None:
208
+ return
194
209
  messages = _extract_messages(req)
195
210
  system = _extract_system(req)
196
211
  query_text = self._build_query_text(messages, system)
@@ -247,6 +262,11 @@ class VCacheSemantic(SemanticTier):
247
262
  tenant_id, exc, exc_info=True,
248
263
  )
249
264
 
265
+ def close(self) -> None:
266
+ close = getattr(self._embedder, "close", None)
267
+ if callable(close):
268
+ close()
269
+
250
270
  # ------------------------------------------------------------------
251
271
  # Internal lookup
252
272
  # ------------------------------------------------------------------
@@ -17,6 +17,10 @@ from __future__ import annotations
17
17
  import logging
18
18
  import re
19
19
  import threading
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from superlocalmemory.optimize.storage.db import CacheDB
20
24
 
21
25
  logger = logging.getLogger("slm.optimize.compress.ccr")
22
26
 
@@ -18,11 +18,16 @@ import json
18
18
  import logging
19
19
  import threading
20
20
  from dataclasses import dataclass
21
- from typing import Any
21
+ from typing import TYPE_CHECKING, Any
22
22
 
23
23
  from superlocalmemory.optimize.proxy.lifecycle import ProxyRequest, CompressHook
24
24
  from superlocalmemory.optimize.config.store import ConfigStore
25
25
 
26
+ if TYPE_CHECKING:
27
+ from superlocalmemory.optimize.compress.align import CacheAligner
28
+ from superlocalmemory.optimize.compress.ccr import CCRStore
29
+ from superlocalmemory.optimize.compress.prose_llmlingua import LLMLinguaCompressor
30
+
26
31
  logger = logging.getLogger("slm.optimize.compress.router")
27
32
 
28
33
  _MIN_CHARS_FOR_COMPRESSION: int = 500
@@ -6,8 +6,13 @@ module. They NEVER construct ConfigStore themselves or read optimize.json direct
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ from typing import TYPE_CHECKING
10
+
9
11
  from superlocalmemory.optimize.config.schema import OptimizeConfig
10
12
 
13
+ if TYPE_CHECKING:
14
+ from superlocalmemory.optimize.config.store import ConfigStore
15
+
11
16
  _store: "ConfigStore | None" = None
12
17
 
13
18
 
@@ -17,16 +17,16 @@ import json
17
17
  import logging
18
18
  import os
19
19
  import threading
20
- import time
21
20
  from pathlib import Path
22
21
  from typing import Any, Callable
23
22
 
24
- from superlocalmemory.optimize.config.schema import OptimizeConfig
23
+ from superlocalmemory.infra.data_root import DynamicStatePath
25
24
  from superlocalmemory.optimize.config.defaults import DEFAULT_OPTIMIZE_CONFIG
25
+ from superlocalmemory.optimize.config.schema import OptimizeConfig
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
- _DEFAULT_CONFIG_PATH: Path = Path.home() / ".superlocalmemory" / "optimize.json"
29
+ _DEFAULT_CONFIG_PATH = DynamicStatePath("optimize.json")
30
30
  _POLL_INTERVAL_SECONDS: float = 2.0
31
31
 
32
32
 
@@ -38,7 +38,9 @@ class ConfigStore:
38
38
  config_path: Path | None = None,
39
39
  poll_interval: float = _POLL_INTERVAL_SECONDS,
40
40
  ) -> None:
41
- self._config_path = Path(config_path) if config_path else _DEFAULT_CONFIG_PATH
41
+ self._config_path = Path(
42
+ config_path if config_path is not None else _DEFAULT_CONFIG_PATH
43
+ )
42
44
  self._poll_interval_seconds = float(poll_interval)
43
45
  self._lock = threading.RLock()
44
46
  self._change_callbacks: list[Callable[[OptimizeConfig], None]] = []
@@ -7,6 +7,7 @@ import inspect
7
7
  import json
8
8
  import logging
9
9
  from typing import Any, AsyncIterator, Callable
10
+ from weakref import WeakKeyDictionary
10
11
 
11
12
  import httpx
12
13
  from fastapi.requests import Request
@@ -24,8 +25,10 @@ _get_running_loop = asyncio.get_running_loop
24
25
  logger = logging.getLogger("slm.optimize.proxy.helpers")
25
26
 
26
27
  # Per-callable cache of "does this hook method accept a tenant_id kwarg?".
27
- # Keyed by id() of the bound method's __func__ so it is stable per hook class.
28
- _HOOK_TENANT_SUPPORT: dict[int, bool] = {}
28
+ # Keep the callable itself as the key. Integer id() values can be reused after
29
+ # a hook class is collected, which can apply a stale legacy signature result
30
+ # to a new tenant-aware hook and silently drop tenant isolation.
31
+ _HOOK_TENANT_SUPPORT: WeakKeyDictionary[object, bool] = WeakKeyDictionary()
29
32
 
30
33
 
31
34
  def _accepts_tenant_id(fn: Callable) -> bool:
@@ -42,8 +45,12 @@ def _accepts_tenant_id(fn: Callable) -> bool:
42
45
  raises fails open to a cache MISS, never the shared namespace.
43
46
  """
44
47
  target = getattr(fn, "__func__", fn)
45
- key = id(target)
46
- cached = _HOOK_TENANT_SUPPORT.get(key)
48
+ try:
49
+ cached = _HOOK_TENANT_SUPPORT.get(target)
50
+ except TypeError:
51
+ # Some extension callables cannot be weak-referenced. Inspect them on
52
+ # every use instead of falling back to an unsafe integer identity.
53
+ cached = None
47
54
  if cached is None:
48
55
  try:
49
56
  params = inspect.signature(fn).parameters
@@ -53,7 +60,10 @@ def _accepts_tenant_id(fn: Callable) -> bool:
53
60
  except (ValueError, TypeError):
54
61
  # Builtins / C callables without a signature — assume legacy.
55
62
  cached = False
56
- _HOOK_TENANT_SUPPORT[key] = cached
63
+ try:
64
+ _HOOK_TENANT_SUPPORT[target] = cached
65
+ except TypeError:
66
+ pass
57
67
  return cached
58
68
 
59
69
  # SEC-M-02 (CWE-400): reject oversized bodies to prevent compression-bomb DoS.
@@ -28,9 +28,10 @@ import threading
28
28
  from pathlib import Path
29
29
  from typing import Any
30
30
 
31
+ from superlocalmemory.infra.data_root import state_path
32
+
31
33
  logger = logging.getLogger("slm.optimize.proxy.capture")
32
34
 
33
- _CAPTURE_DIRNAME = ".superlocalmemory"
34
35
  _CAPTURE_FILENAME = "optimize_capture.jsonl"
35
36
  _CAPTURE_ENV = "SLM_OPTIMIZE_CAPTURE"
36
37
  _TRUTHY = frozenset({"1", "true", "yes", "on"})
@@ -51,7 +52,7 @@ def capture_enabled() -> bool:
51
52
 
52
53
 
53
54
  def _capture_path() -> Path:
54
- return Path.home() / _CAPTURE_DIRNAME / _CAPTURE_FILENAME
55
+ return state_path(_CAPTURE_FILENAME)
55
56
 
56
57
 
57
58
  class ShadowCapture:
@@ -17,7 +17,7 @@ from superlocalmemory.optimize.proxy.lifecycle import HookChain
17
17
 
18
18
  logger = logging.getLogger("slm.optimize.proxy")
19
19
 
20
- _PROXY_VERSION = "3.6.3"
20
+ _PROXY_VERSION = "3.7.1"
21
21
  _REQUEST_TIMEOUT_S = 300.0
22
22
  _CONNECT_TIMEOUT_S = 10.0
23
23
  _MAX_CONNECTIONS = 100
@@ -178,7 +178,7 @@ def _load_hooks(config: OptimizeConfig) -> HookChain:
178
178
  if config.cache_enabled:
179
179
  try:
180
180
  from superlocalmemory.optimize.cache.manager import CacheManager
181
- cache_hook = CacheManager.get_instance()
181
+ cache_hook = CacheManager.get_instance(optimize_config=config)
182
182
  except Exception as exc:
183
183
  logger.warning(
184
184
  "cache hook load failed (proxy continues without cache): %s", exc
@@ -41,7 +41,6 @@ import os
41
41
  import platform
42
42
  import re
43
43
  import sqlite3
44
- import struct
45
44
  import time
46
45
  import uuid
47
46
  import zlib
@@ -50,12 +49,13 @@ from pathlib import Path
50
49
  from typing import Any
51
50
 
52
51
  from cryptography.exceptions import InvalidTag
52
+ from cryptography.hazmat.primitives import hashes
53
53
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
54
54
  from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
55
- from cryptography.hazmat.primitives import hashes
56
55
 
57
- from superlocalmemory.storage.database import DatabaseManager
56
+ from superlocalmemory.infra.data_root import DynamicStatePath, state_path
58
57
  from superlocalmemory.optimize.storage import schema as _schema
58
+ from superlocalmemory.storage.database import DatabaseManager
59
59
 
60
60
  logger = logging.getLogger(__name__)
61
61
 
@@ -89,7 +89,7 @@ MID_FILENAME: str = ".llmcache_key"
89
89
  SALT_PREFIX: str = "salt:"
90
90
 
91
91
  # C-06: persisted AES key — survives machine-id changes after first run
92
- _KEY_FILE: Path = Path.home() / LLMCACHE_DIRNAME / "opt-key.bin"
92
+ _KEY_FILE = DynamicStatePath("opt-key.bin")
93
93
 
94
94
  _FORBIDDEN_MEMORY_TABLES: frozenset[str] = frozenset({
95
95
  "memories", "atomic_facts", "profiles", "canonical_entities",
@@ -199,7 +199,7 @@ class CacheDB:
199
199
 
200
200
  def __init__(self, db_path: Path | None = None) -> None:
201
201
  if db_path is None:
202
- db_path = Path.home() / LLMCACHE_DIRNAME / LLMCACHE_DBNAME
202
+ db_path = state_path(LLMCACHE_DBNAME)
203
203
  self._db_path = Path(db_path)
204
204
  self._db_path.parent.mkdir(parents=True, exist_ok=True)
205
205
  # If the file exists but is not a valid SQLite database (e.g. user
@@ -290,7 +290,6 @@ class CacheDB:
290
290
  mid: str | None = None
291
291
  if system == "Darwin":
292
292
  try:
293
- import plistlib
294
293
  import subprocess
295
294
  out = subprocess.run(
296
295
  ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
@@ -311,7 +310,7 @@ class CacheDB:
311
310
  except OSError:
312
311
  mid = None
313
312
  if not mid:
314
- mid_file = Path.home() / LLMCACHE_DIRNAME / MID_FILENAME
313
+ mid_file = state_path(MID_FILENAME)
315
314
  if mid_file.exists():
316
315
  try:
317
316
  mid = mid_file.read_text(encoding="utf-8").strip()
@@ -335,9 +334,10 @@ class CacheDB:
335
334
  so changing the underlying machine-id string cannot invalidate existing
336
335
  cache entries.
337
336
  """
337
+ key_file = Path(_KEY_FILE)
338
338
  try:
339
- if _KEY_FILE.exists():
340
- key = _KEY_FILE.read_bytes()
339
+ if key_file.exists():
340
+ key = key_file.read_bytes()
341
341
  if len(key) == 32:
342
342
  return key
343
343
  except Exception as exc:
@@ -347,9 +347,9 @@ class CacheDB:
347
347
  machine_id = self._get_machine_id()
348
348
  key = self._derive_aes_key(machine_id, salt)
349
349
  try:
350
- _KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
351
- _KEY_FILE.write_bytes(key)
352
- os.chmod(_KEY_FILE, 0o600)
350
+ key_file.parent.mkdir(parents=True, exist_ok=True)
351
+ key_file.write_bytes(key)
352
+ os.chmod(key_file, 0o600)
353
353
  except Exception as exc:
354
354
  logger.warning("CacheDB: could not persist AES key (fail-open): %s", exc)
355
355
  return key