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
@@ -0,0 +1,248 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Namespace-aware identity for the local SuperLocalMemory daemon.
5
+
6
+ PID files and listening ports are liveness hints, not ownership credentials.
7
+ This module binds a daemon process to one canonical SLM data root and one
8
+ random process instance through a private, atomically written descriptor.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import getpass
14
+ import hashlib
15
+ import hmac
16
+ import json
17
+ import os
18
+ import secrets
19
+ import time
20
+ import uuid
21
+ from dataclasses import asdict, dataclass
22
+ from pathlib import Path
23
+ from typing import Any, Mapping
24
+
25
+ from superlocalmemory.infra.data_root import canonical_data_root
26
+
27
+ DAEMON_DESCRIPTOR_SCHEMA = 1
28
+ DAEMON_PROTOCOL = 1
29
+ DAEMON_SERVICE = "superlocalmemory-daemon"
30
+ _NAMESPACE_DOMAIN = b"superlocalmemory-daemon-namespace-v1\0"
31
+ _CAPABILITY_DOMAIN = b"superlocalmemory-daemon-capability-v1\0"
32
+
33
+
34
+ def _canonical_path(value: str | Path) -> Path:
35
+ expanded = Path(value).expanduser().resolve(strict=False)
36
+ return Path(os.path.normcase(str(expanded)))
37
+
38
+
39
+ def namespace_id_for(data_root: str | Path) -> str:
40
+ """Return a stable, non-reversible identifier for a canonical data root."""
41
+ canonical = _canonical_path(data_root)
42
+ normalized = os.path.normcase(str(canonical)).encode("utf-8")
43
+ return hashlib.sha256(_NAMESPACE_DOMAIN + normalized).hexdigest()
44
+
45
+
46
+ def capability_fingerprint(capability: str) -> str:
47
+ """Hash the private local capability before exposing it through health."""
48
+ return hashlib.sha256(_CAPABILITY_DOMAIN + capability.encode("utf-8")).hexdigest()
49
+
50
+
51
+ def owner_id() -> str:
52
+ """Cross-platform local owner identifier used for accidental isolation."""
53
+ getuid = getattr(os, "getuid", None)
54
+ if getuid is not None:
55
+ return f"uid:{getuid()}"
56
+ return f"user:{getpass.getuser()}"
57
+
58
+
59
+ def process_create_time_for(pid: int) -> float:
60
+ """Return OS process creation time, falling back only when unavailable."""
61
+ try:
62
+ import psutil
63
+
64
+ return float(psutil.Process(pid).create_time())
65
+ except Exception:
66
+ return time.time()
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class DaemonDescriptor:
71
+ schema: int
72
+ service: str
73
+ daemon_protocol: int
74
+ namespace_id: str
75
+ instance_id: str
76
+ capability: str
77
+ capability_fingerprint: str
78
+ owner_id: str
79
+ data_root: str
80
+ pid: int
81
+ process_create_time: float
82
+ port: int
83
+ state: str
84
+ version: str
85
+ started_at: float
86
+
87
+ def public_health_fields(self) -> dict[str, Any]:
88
+ """Identity fields safe to expose on the loopback health endpoint."""
89
+ return {
90
+ "service": self.service,
91
+ "daemon_protocol": self.daemon_protocol,
92
+ "namespace_id": self.namespace_id,
93
+ "instance_id": self.instance_id,
94
+ "capability_fingerprint": self.capability_fingerprint,
95
+ "owner_id": self.owner_id,
96
+ "pid": self.pid,
97
+ "port": self.port,
98
+ "state": self.state,
99
+ "version": self.version,
100
+ }
101
+
102
+
103
+ def build_descriptor(
104
+ *,
105
+ data_root: str | Path | None = None,
106
+ port: int,
107
+ version: str,
108
+ pid: int | None = None,
109
+ process_create_time: float | None = None,
110
+ instance_id: str | None = None,
111
+ capability: str | None = None,
112
+ state: str = "starting",
113
+ started_at: float | None = None,
114
+ ) -> DaemonDescriptor:
115
+ """Build one daemon-process descriptor for the selected namespace."""
116
+ root = _canonical_path(data_root) if data_root is not None else canonical_data_root()
117
+ actual_pid = int(pid if pid is not None else os.getpid())
118
+ actual_capability = capability or secrets.token_urlsafe(32)
119
+ return DaemonDescriptor(
120
+ schema=DAEMON_DESCRIPTOR_SCHEMA,
121
+ service=DAEMON_SERVICE,
122
+ daemon_protocol=DAEMON_PROTOCOL,
123
+ namespace_id=namespace_id_for(root),
124
+ instance_id=instance_id or str(uuid.uuid4()),
125
+ capability=actual_capability,
126
+ capability_fingerprint=capability_fingerprint(actual_capability),
127
+ owner_id=owner_id(),
128
+ data_root=str(root),
129
+ pid=actual_pid,
130
+ process_create_time=float(
131
+ process_create_time
132
+ if process_create_time is not None
133
+ else process_create_time_for(actual_pid)
134
+ ),
135
+ port=int(port),
136
+ state=state,
137
+ version=version,
138
+ started_at=float(started_at if started_at is not None else time.time()),
139
+ )
140
+
141
+
142
+ def descriptor_path(data_root: str | Path | None = None) -> Path:
143
+ root = _canonical_path(data_root) if data_root is not None else canonical_data_root()
144
+ return root / "daemon.json"
145
+
146
+
147
+ def write_descriptor(
148
+ descriptor: DaemonDescriptor,
149
+ *,
150
+ data_root: str | Path | None = None,
151
+ ) -> Path:
152
+ """Atomically publish a mode-0600 descriptor in its owned namespace."""
153
+ root = _canonical_path(data_root or descriptor.data_root)
154
+ root.mkdir(parents=True, exist_ok=True)
155
+ destination = root / "daemon.json"
156
+ temporary = root / f".daemon.{os.getpid()}.{secrets.token_hex(8)}.tmp"
157
+ payload = json.dumps(asdict(descriptor), sort_keys=True, separators=(",", ":"))
158
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
159
+ try:
160
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
161
+ stream.write(payload)
162
+ stream.flush()
163
+ os.fsync(stream.fileno())
164
+ os.replace(temporary, destination)
165
+ try:
166
+ os.chmod(destination, 0o600)
167
+ except OSError:
168
+ pass
169
+ finally:
170
+ temporary.unlink(missing_ok=True)
171
+ return destination
172
+
173
+
174
+ def read_descriptor(
175
+ *, data_root: str | Path | None = None,
176
+ ) -> DaemonDescriptor | None:
177
+ """Read and validate the local descriptor; malformed state fails closed."""
178
+ path = descriptor_path(data_root)
179
+ try:
180
+ raw = json.loads(path.read_text(encoding="utf-8"))
181
+ descriptor = DaemonDescriptor(**raw)
182
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
183
+ return None
184
+ if descriptor.schema != DAEMON_DESCRIPTOR_SCHEMA:
185
+ return None
186
+ if descriptor.service != DAEMON_SERVICE:
187
+ return None
188
+ if descriptor.daemon_protocol != DAEMON_PROTOCOL:
189
+ return None
190
+ expected_root = (
191
+ _canonical_path(data_root) if data_root is not None else canonical_data_root()
192
+ )
193
+ if Path(descriptor.data_root).resolve(strict=False) != expected_root:
194
+ return None
195
+ if descriptor.namespace_id != namespace_id_for(expected_root):
196
+ return None
197
+ if descriptor.capability_fingerprint != capability_fingerprint(
198
+ descriptor.capability
199
+ ):
200
+ return None
201
+ if descriptor.owner_id != owner_id():
202
+ return None
203
+ if not (1 <= descriptor.port <= 65535) or descriptor.pid <= 0:
204
+ return None
205
+ return descriptor
206
+
207
+
208
+ def descriptor_matches_health(
209
+ descriptor: DaemonDescriptor,
210
+ health: Mapping[str, Any],
211
+ ) -> bool:
212
+ """Require exact owned-daemon identity before a client attaches."""
213
+ expected = descriptor.public_health_fields()
214
+ identity_keys = (
215
+ "service",
216
+ "daemon_protocol",
217
+ "namespace_id",
218
+ "instance_id",
219
+ "capability_fingerprint",
220
+ "owner_id",
221
+ "version",
222
+ "pid",
223
+ "port",
224
+ )
225
+ for key in identity_keys:
226
+ left = str(expected[key])
227
+ right = str(health.get(key, ""))
228
+ if not hmac.compare_digest(left, right):
229
+ return False
230
+ return True
231
+
232
+
233
+ def clear_descriptor(
234
+ instance_id: str,
235
+ *,
236
+ data_root: str | Path | None = None,
237
+ ) -> bool:
238
+ """Delete the descriptor only when the caller still owns that instance."""
239
+ descriptor = read_descriptor(data_root=data_root)
240
+ if descriptor is None or not hmac.compare_digest(
241
+ descriptor.instance_id, instance_id
242
+ ):
243
+ return False
244
+ try:
245
+ descriptor_path(data_root).unlink()
246
+ return True
247
+ except OSError:
248
+ return False
@@ -0,0 +1,199 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Single authority for SuperLocalMemory runtime-state paths.
5
+
6
+ The resolver is intentionally stdlib-only so CLI, daemon, MCP, and hook entry
7
+ points can use it before importing the engine. Environment selection is an
8
+ explicit process contract and always wins over persisted configuration.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from pathlib import Path
16
+
17
+ DATA_ROOT_ALIASES = ("SLM_DATA_DIR", "SL_MEMORY_PATH", "SLM_HOME")
18
+ _DURABLE_IDENTITY_NAMES = frozenset(
19
+ {
20
+ ".install_token",
21
+ ".signer_key",
22
+ "api_key",
23
+ "credentials.json",
24
+ },
25
+ )
26
+
27
+
28
+ class DataRootConflictError(RuntimeError):
29
+ """Raised when two state-bearing roots make startup ambiguous."""
30
+
31
+
32
+ def _canonical_path(value: str | Path) -> Path:
33
+ expanded = Path(value).expanduser().resolve(strict=False)
34
+ return Path(os.path.normcase(str(expanded)))
35
+
36
+
37
+ def environment_data_root() -> Path | None:
38
+ """Return the winning environment root, or ``None`` when none is set."""
39
+ for name in DATA_ROOT_ALIASES:
40
+ value = os.environ.get(name, "").strip()
41
+ if value:
42
+ return _canonical_path(value)
43
+ return None
44
+
45
+
46
+ def _legacy_configured_root(default_root: Path) -> Path | None:
47
+ """Read the bounded legacy ``config.json:base_dir`` relocation hint.
48
+
49
+ Only an absolute path is accepted. Relative paths historically depended on
50
+ the caller's working directory and cannot safely identify a process
51
+ namespace. Corrupt or unreadable configuration never changes the root.
52
+ """
53
+ config_path = default_root / "config.json"
54
+ try:
55
+ payload = json.loads(config_path.read_text(encoding="utf-8"))
56
+ value = payload.get("base_dir") if isinstance(payload, dict) else None
57
+ if not isinstance(value, str) or not value.strip():
58
+ return None
59
+ candidate = Path(value).expanduser()
60
+ if not candidate.is_absolute():
61
+ return None
62
+ return _canonical_path(candidate)
63
+ except (OSError, ValueError, TypeError):
64
+ return None
65
+
66
+
67
+ def canonical_data_root(
68
+ configured_base_dir: str | Path | None = None,
69
+ *,
70
+ home: str | Path | None = None,
71
+ ) -> Path:
72
+ """Resolve the one runtime-state namespace for the current process.
73
+
74
+ Precedence:
75
+ 1. ``SLM_DATA_DIR``
76
+ 2. ``SL_MEMORY_PATH`` (legacy input shim)
77
+ 3. ``SLM_HOME`` (legacy input shim)
78
+ 4. explicit configured base directory
79
+ 5. legacy default-root ``config.json:base_dir``
80
+ 6. ``~/.superlocalmemory``
81
+
82
+ The function performs no writes and never moves data.
83
+ """
84
+ selected = environment_data_root()
85
+ if selected is not None:
86
+ return selected
87
+ if configured_base_dir is not None:
88
+ return _canonical_path(configured_base_dir)
89
+ home_path = _canonical_path(home if home is not None else Path.home())
90
+ default_root = _canonical_path(home_path / ".superlocalmemory")
91
+ return _legacy_configured_root(default_root) or default_root
92
+
93
+
94
+ def _durable_markers(root: Path) -> tuple[Path, ...]:
95
+ """Return bounded evidence that ``root`` contains durable SLM state.
96
+
97
+ The startup guard deliberately ignores ``config.json`` on its own because
98
+ the default-root file is the compatibility locator for legacy custom
99
+ roots. Database sidecars count even if the base file was lost.
100
+ """
101
+ try:
102
+ children = tuple(root.iterdir())
103
+ except FileNotFoundError:
104
+ return ()
105
+ except OSError as exc:
106
+ raise DataRootConflictError(
107
+ f"SuperLocalMemory cannot inspect possible state root {root}: {exc}. "
108
+ "Startup is blocked until the root is readable or explicitly resolved."
109
+ ) from exc
110
+ markers = []
111
+ for path in children:
112
+ name = path.name
113
+ if (
114
+ name in _DURABLE_IDENTITY_NAMES
115
+ or name.endswith((".db", ".db-wal", ".db-shm", ".sqlite", ".sqlite3"))
116
+ ):
117
+ markers.append(path)
118
+ return tuple(sorted(markers))
119
+
120
+
121
+ def assert_no_durable_root_conflict(
122
+ *,
123
+ home: str | Path | None = None,
124
+ ) -> None:
125
+ """Refuse ambiguous startup when selected and default roots hold state.
126
+
127
+ This check never writes, copies, or deletes data. A default root that only
128
+ contains the legacy relocation config is safe and does not trigger it.
129
+ """
130
+ home_path = _canonical_path(home if home is not None else Path.home())
131
+ default_root = _canonical_path(home_path / ".superlocalmemory")
132
+ selected_root = canonical_data_root(home=home_path)
133
+ if selected_root == default_root:
134
+ return
135
+ selected_markers = _durable_markers(selected_root)
136
+ default_markers = _durable_markers(default_root)
137
+ if not selected_markers or not default_markers:
138
+ return
139
+ raise DataRootConflictError(
140
+ "Conflicting SuperLocalMemory state roots detected: "
141
+ f"selected={selected_root} and default={default_root}. "
142
+ "SuperLocalMemory will not merge or move either root automatically. "
143
+ "Stop both namespaces, back them up, and choose an explicit migration "
144
+ "or one SLM_DATA_DIR before starting the daemon."
145
+ )
146
+
147
+
148
+ def state_path(
149
+ *parts: str | Path,
150
+ configured_base_dir: str | Path | None = None,
151
+ home: str | Path | None = None,
152
+ ) -> Path:
153
+ """Return a canonical child path without permitting root escape."""
154
+ relative = Path()
155
+ for raw_part in parts:
156
+ part = Path(raw_part)
157
+ if part.is_absolute() or ".." in part.parts:
158
+ raise ValueError(f"state path must remain relative: {raw_part!s}")
159
+ relative /= part
160
+ root = canonical_data_root(configured_base_dir, home=home)
161
+ destination = _canonical_path(root / relative)
162
+ try:
163
+ destination.relative_to(root)
164
+ except ValueError as exc:
165
+ raise ValueError("state path escapes the canonical data root") from exc
166
+ return destination
167
+
168
+
169
+ class DynamicStatePath(os.PathLike[str]):
170
+ """Backward-compatible path proxy that never freezes the active root."""
171
+
172
+ __slots__ = ("_parts",)
173
+
174
+ def __init__(self, *parts: str | Path) -> None:
175
+ self._parts = parts
176
+
177
+ def _resolve(self) -> Path:
178
+ return state_path(*self._parts)
179
+
180
+ def __fspath__(self) -> str:
181
+ return str(self._resolve())
182
+
183
+ def __truediv__(self, other: str | Path) -> Path:
184
+ return self._resolve() / other
185
+
186
+ def __str__(self) -> str:
187
+ return str(self._resolve())
188
+
189
+ def __repr__(self) -> str:
190
+ return f"DynamicStatePath({self._parts!r})"
191
+
192
+ def __eq__(self, other: object) -> bool:
193
+ try:
194
+ return self._resolve() == Path(other) # type: ignore[arg-type]
195
+ except TypeError:
196
+ return False
197
+
198
+ def __getattr__(self, name: str):
199
+ return getattr(self._resolve(), name)
@@ -16,6 +16,8 @@ from datetime import datetime, timedelta, timezone
16
16
  from pathlib import Path
17
17
  from typing import Any, Callable, Dict, List, Optional
18
18
 
19
+ from superlocalmemory.infra.data_root import state_path
20
+
19
21
  logger = logging.getLogger("superlocalmemory.events")
20
22
 
21
23
  # Default retention windows (hours)
@@ -66,7 +68,7 @@ class EventBus:
66
68
  def get_instance(cls, db_path: Optional[Path] = None) -> "EventBus":
67
69
  """Get or create the singleton EventBus for a database path."""
68
70
  if db_path is None:
69
- db_path = Path.home() / ".superlocalmemory" / "memory.db"
71
+ db_path = state_path("memory.db")
70
72
 
71
73
  key = str(db_path)
72
74
  with cls._instances_lock: