superlocalmemory 3.6.23 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +18 -10
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/rules/AGENTS.md +1 -1
  18. package/pyproject.toml +40 -8
  19. package/scripts/postinstall-interactive.js +17 -94
  20. package/scripts/postinstall.js +185 -258
  21. package/scripts/preuninstall.js +9 -50
  22. package/src/superlocalmemory/__init__.py +2 -2
  23. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  24. package/src/superlocalmemory/attribution/signer.py +34 -19
  25. package/src/superlocalmemory/attribution/watermark.py +1 -1
  26. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  27. package/src/superlocalmemory/cli/commands.py +453 -191
  28. package/src/superlocalmemory/cli/context_commands.py +5 -4
  29. package/src/superlocalmemory/cli/daemon.py +282 -187
  30. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  31. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  32. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  33. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  34. package/src/superlocalmemory/cli/main.py +128 -31
  35. package/src/superlocalmemory/cli/pending_store.py +54 -38
  36. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  37. package/src/superlocalmemory/cli/service_installer.py +57 -52
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  39. package/src/superlocalmemory/cli/version_banner.py +2 -1
  40. package/src/superlocalmemory/code_graph/config.py +3 -1
  41. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  42. package/src/superlocalmemory/core/config.py +65 -20
  43. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  44. package/src/superlocalmemory/core/context_cache.py +56 -8
  45. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  46. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  47. package/src/superlocalmemory/core/embeddings.py +54 -18
  48. package/src/superlocalmemory/core/engine.py +150 -104
  49. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  50. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  51. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  52. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  53. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  54. package/src/superlocalmemory/core/health_monitor.py +4 -2
  55. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  56. package/src/superlocalmemory/core/injection.py +69 -18
  57. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  58. package/src/superlocalmemory/core/maintenance.py +1 -1
  59. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  60. package/src/superlocalmemory/core/mutations.py +143 -0
  61. package/src/superlocalmemory/core/platform_utils.py +7 -4
  62. package/src/superlocalmemory/core/ram_lock.py +16 -5
  63. package/src/superlocalmemory/core/rate_limit.py +1 -1
  64. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  65. package/src/superlocalmemory/core/recall_worker.py +76 -59
  66. package/src/superlocalmemory/core/registry.py +1 -1
  67. package/src/superlocalmemory/core/scale_engine.py +293 -0
  68. package/src/superlocalmemory/core/score_contract.py +62 -0
  69. package/src/superlocalmemory/core/security_primitives.py +3 -1
  70. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  72. package/src/superlocalmemory/core/tier_manager.py +32 -20
  73. package/src/superlocalmemory/core/worker_pool.py +13 -4
  74. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  75. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  76. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  77. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  79. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  80. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  81. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  82. package/src/superlocalmemory/encoding/foresight.py +2 -0
  83. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  84. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  85. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  86. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  87. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  88. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  89. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  90. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  91. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  92. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  93. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  94. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  95. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  96. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  97. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  98. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  99. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  100. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  101. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  102. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  103. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  104. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  105. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  106. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  107. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  108. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  109. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  110. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  111. package/src/superlocalmemory/infra/backup.py +7 -5
  112. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  113. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  114. package/src/superlocalmemory/infra/data_root.py +199 -0
  115. package/src/superlocalmemory/infra/event_bus.py +3 -1
  116. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  117. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  118. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  119. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  120. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  121. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  122. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  123. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  124. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  125. package/src/superlocalmemory/learning/ensemble.py +11 -0
  126. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  127. package/src/superlocalmemory/learning/feedback.py +1 -1
  128. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  129. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  130. package/src/superlocalmemory/learning/ranker.py +4 -1
  131. package/src/superlocalmemory/learning/source_quality.py +1 -1
  132. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  133. package/src/superlocalmemory/llm/backbone.py +1 -1
  134. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  135. package/src/superlocalmemory/math/fisher.py +1 -1
  136. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  137. package/src/superlocalmemory/math/hopfield.py +1 -1
  138. package/src/superlocalmemory/math/langevin.py +1 -1
  139. package/src/superlocalmemory/math/polar_quant.py +3 -4
  140. package/src/superlocalmemory/math/qjl.py +1 -1
  141. package/src/superlocalmemory/math/sheaf.py +1 -1
  142. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  143. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  144. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  145. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  146. package/src/superlocalmemory/mcp/server.py +39 -13
  147. package/src/superlocalmemory/mcp/shared.py +69 -3
  148. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  149. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  150. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  151. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  152. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  153. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  154. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  155. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  156. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  157. package/src/superlocalmemory/mesh/broker.py +2 -0
  158. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  159. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  160. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  161. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  162. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  163. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  164. package/src/superlocalmemory/optimize/config/store.py +6 -4
  165. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  166. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  167. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  168. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  169. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  170. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  171. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  172. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  173. package/src/superlocalmemory/retrieval/engine.py +169 -79
  174. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  175. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  176. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  177. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  178. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  179. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  180. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  181. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  182. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  183. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  184. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  185. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  186. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  187. package/src/superlocalmemory/server/api.py +10 -7
  188. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  189. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  190. package/src/superlocalmemory/server/route_mutations.py +84 -0
  191. package/src/superlocalmemory/server/routes/agents.py +8 -6
  192. package/src/superlocalmemory/server/routes/brain.py +14 -12
  193. package/src/superlocalmemory/server/routes/chat.py +29 -12
  194. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  195. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  196. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  197. package/src/superlocalmemory/server/routes/memories.py +104 -43
  198. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  199. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  200. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  201. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  202. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  203. package/src/superlocalmemory/server/security_middleware.py +1 -1
  204. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  205. package/src/superlocalmemory/server/write_identity.py +147 -0
  206. package/src/superlocalmemory/storage/access_log.py +4 -3
  207. package/src/superlocalmemory/storage/database.py +118 -25
  208. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  209. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  210. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  211. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  212. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  213. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  214. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  215. package/src/superlocalmemory/storage/models.py +16 -0
  216. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  217. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  218. package/src/superlocalmemory/ui/favicon.svg +5 -0
  219. package/src/superlocalmemory/ui/index.html +1 -0
  220. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  221. package/src/superlocalmemory/ui/js/core.js +49 -8
  222. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  223. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  224. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  225. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  226. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  227. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  228. package/src/superlocalmemory/ui/js/settings.js +4 -2
  229. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  230. package/bin/slm +0 -59
  231. package/bin/slm.bat +0 -77
  232. package/bin/slm.cmd +0 -5
  233. package/ide/integrations/langchain/README.md +0 -106
  234. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  235. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  236. package/ide/integrations/langchain/pyproject.toml +0 -38
  237. package/ide/integrations/langchain/tests/__init__.py +0 -3
  238. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  239. package/ide/integrations/langchain/tests/test_security.py +0 -117
  240. package/ide/integrations/llamaindex/README.md +0 -81
  241. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  242. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  243. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  244. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  245. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  246. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  247. package/plugin-src/.mcp.json +0 -12
  248. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  249. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  250. package/plugin-src/hooks/.gitkeep +0 -0
  251. package/plugin-src/hooks/hooks.json +0 -23
  252. package/plugin-src/manifest.json +0 -25
  253. package/plugin-src/requirements.txt +0 -1
  254. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  255. package/plugin-src/scripts/ensure-venv.bat +0 -122
  256. package/plugin-src/scripts/ensure-venv.sh +0 -105
  257. package/plugin-src/scripts/slm-launch +0 -15
  258. package/plugin-src/scripts/slm-launch.bat +0 -17
  259. package/plugin-src/settings.json +0 -16
  260. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  261. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  262. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  263. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  264. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  265. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  266. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  267. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  268. package/scripts/_savings_math.py +0 -270
  269. package/scripts/build-dmg.sh +0 -417
  270. package/scripts/build-plugin.js +0 -742
  271. package/scripts/build-slm-hook.ps1 +0 -40
  272. package/scripts/build-slm-hook.sh +0 -45
  273. package/scripts/build_entry.py +0 -452
  274. package/scripts/ci/stage5b_gate.sh +0 -50
  275. package/scripts/dogfood_savings.py +0 -490
  276. package/scripts/generate-thumbnails.py +0 -218
  277. package/scripts/install-skills.ps1 +0 -4
  278. package/scripts/install-skills.sh +0 -5
  279. package/scripts/install.ps1 +0 -701
  280. package/scripts/install.sh +0 -1015
  281. package/scripts/postinstall_binary.js +0 -287
  282. package/scripts/prepack.js +0 -33
  283. package/scripts/release_manifest.py +0 -273
  284. package/scripts/slm-hook.spec +0 -56
  285. package/scripts/start-dashboard.ps1 +0 -52
  286. package/scripts/start-dashboard.sh +0 -41
  287. package/scripts/sync-wiki.ps1 +0 -127
  288. package/scripts/sync-wiki.sh +0 -82
  289. package/scripts/test-dmg.sh +0 -161
  290. package/scripts/test-npm-package.ps1 +0 -252
  291. package/scripts/test-npm-package.sh +0 -207
  292. package/scripts/verify-install.ps1 +0 -294
  293. package/scripts/verify-install.sh +0 -266
  294. package/scripts/verify-v27.ps1 +0 -301
  295. package/scripts/verify-v27.sh +0 -233
  296. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  297. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  298. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  299. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  300. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  301. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -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:
@@ -0,0 +1,327 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Bounded, content-free operational diagnostics stored on this machine.
5
+
6
+ The store accepts only enumerated operations and normalized client families.
7
+ It persists daily counters, latency buckets, coarse error classes, and client
8
+ family transitions. Export is a deliberate local file operation; this module
9
+ has no automatic reporting mechanism.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import math
16
+ import os
17
+ import re
18
+ import sqlite3
19
+ import tempfile
20
+ from collections.abc import Callable, Iterable
21
+ from datetime import UTC, datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from superlocalmemory.infra.data_root import state_path
26
+
27
+ SCHEMA_VERSION = 1
28
+ RETENTION_DAYS = 31
29
+ MAX_COUNTER = 2**63 - 1
30
+
31
+ _OPERATIONS = frozenset(
32
+ {
33
+ "activation",
34
+ "remember",
35
+ "recall",
36
+ "observe",
37
+ "session_open",
38
+ "session_close",
39
+ }
40
+ )
41
+ _CLIENT_FAMILIES = (
42
+ "claude",
43
+ "codex",
44
+ "cursor",
45
+ "windsurf",
46
+ "copilot",
47
+ "vscode",
48
+ "jetbrains",
49
+ "dashboard",
50
+ "hook",
51
+ "mcp",
52
+ "api",
53
+ "cli",
54
+ )
55
+ _LATENCY_BOUNDS_MS = (5, 25, 100, 500, 2_000, 10_000, 60_000)
56
+ _ERROR_CLASSES = frozenset(
57
+ {"timeout", "authorization", "validation", "unavailable", "internal"}
58
+ )
59
+
60
+
61
+ def normalize_client(value: str | None) -> str:
62
+ """Collapse an arbitrary actor label to a fixed non-identifying family."""
63
+ tokens = frozenset(re.findall(r"[a-z0-9]+", str(value or "").lower()))
64
+ for family in _CLIENT_FAMILIES:
65
+ if family in tokens:
66
+ return family
67
+ return "other"
68
+
69
+
70
+ def classify_error(error: BaseException | None) -> str | None:
71
+ """Map an exception type to a fixed class without inspecting its message."""
72
+ if error is None:
73
+ return None
74
+ if isinstance(error, TimeoutError):
75
+ return "timeout"
76
+ if isinstance(error, PermissionError):
77
+ return "authorization"
78
+ if isinstance(error, (TypeError, ValueError)):
79
+ return "validation"
80
+ if isinstance(error, (ConnectionError, FileNotFoundError)):
81
+ return "unavailable"
82
+ return "internal"
83
+
84
+
85
+ def _latency_bucket(duration_ms: float | int | None) -> str | None:
86
+ if duration_ms is None:
87
+ return None
88
+ try:
89
+ value = float(duration_ms)
90
+ except (TypeError, ValueError, OverflowError):
91
+ return "invalid"
92
+ if not math.isfinite(value) or value < 0:
93
+ return "invalid"
94
+ for bound in _LATENCY_BOUNDS_MS:
95
+ if value <= bound:
96
+ return f"le_{bound}"
97
+ return "gt_60000"
98
+
99
+
100
+ class LocalDiagnostics:
101
+ """SQLite-backed bounded aggregate diagnostics.
102
+
103
+ The schema intentionally has no free-text value column. Every persisted
104
+ dimension comes from a finite allow-list, so caller payloads cannot enter
105
+ the database or an export by accident.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ db_file: str | Path,
111
+ *,
112
+ clock: Callable[[], datetime] | None = None,
113
+ retention_days: int = RETENTION_DAYS,
114
+ ) -> None:
115
+ if retention_days < 1 or retention_days > RETENTION_DAYS:
116
+ raise ValueError(f"retention_days must be between 1 and {RETENTION_DAYS}")
117
+ self._db_file = Path(db_file)
118
+ self._clock = clock or (lambda: datetime.now(UTC))
119
+ self._retention_days = retention_days
120
+
121
+ def _connect(self) -> sqlite3.Connection:
122
+ self._db_file.parent.mkdir(parents=True, exist_ok=True)
123
+ if os.name == "posix":
124
+ os.chmod(self._db_file.parent, 0o700)
125
+ conn = sqlite3.connect(str(self._db_file), timeout=5.0)
126
+ conn.execute(
127
+ "CREATE TABLE IF NOT EXISTS diagnostic_aggregates ("
128
+ "day TEXT NOT NULL, group_name TEXT NOT NULL, "
129
+ "dimension TEXT NOT NULL, bucket TEXT NOT NULL, "
130
+ "value INTEGER NOT NULL CHECK(value >= 0), "
131
+ "PRIMARY KEY(day, group_name, dimension, bucket))"
132
+ )
133
+ conn.commit()
134
+ if os.name == "posix":
135
+ os.chmod(self._db_file, 0o600)
136
+ return conn
137
+
138
+ def _today(self) -> datetime:
139
+ current = self._clock()
140
+ if current.tzinfo is None:
141
+ current = current.replace(tzinfo=UTC)
142
+ return current.astimezone(UTC)
143
+
144
+ @staticmethod
145
+ def _increment(
146
+ conn: sqlite3.Connection,
147
+ day: str,
148
+ group_name: str,
149
+ dimension: str,
150
+ bucket: str,
151
+ ) -> None:
152
+ conn.execute(
153
+ "INSERT INTO diagnostic_aggregates "
154
+ "(day, group_name, dimension, bucket, value) VALUES (?,?,?,?,1) "
155
+ "ON CONFLICT(day, group_name, dimension, bucket) DO UPDATE SET "
156
+ "value=MIN(diagnostic_aggregates.value + 1, ?)",
157
+ (day, group_name, dimension, bucket, MAX_COUNTER),
158
+ )
159
+
160
+ def record(
161
+ self,
162
+ operation: str,
163
+ *,
164
+ client: str | None = None,
165
+ duration_ms: float | int | None = None,
166
+ error: BaseException | None = None,
167
+ source_clients: Iterable[str] = (),
168
+ ) -> None:
169
+ """Record one operation using only fixed aggregate dimensions."""
170
+ if operation not in _OPERATIONS:
171
+ raise ValueError(f"unsupported diagnostic operation: {operation}")
172
+ current = self._today()
173
+ day = current.date().isoformat()
174
+ cutoff = (current.date() - timedelta(days=self._retention_days - 1)).isoformat()
175
+ client_family = normalize_client(client)
176
+ status = "error" if error is not None else "success"
177
+ duration_bucket = _latency_bucket(duration_ms)
178
+ error_class = classify_error(error)
179
+ transitions = {
180
+ f"{normalize_client(source)}_to_{client_family}"
181
+ for source in source_clients
182
+ if normalize_client(source) != client_family
183
+ }
184
+
185
+ conn = self._connect()
186
+ try:
187
+ conn.execute("BEGIN IMMEDIATE")
188
+ conn.execute("DELETE FROM diagnostic_aggregates WHERE day < ?", (cutoff,))
189
+ self._increment(conn, day, "operations", operation, status)
190
+ self._increment(conn, day, "clients", client_family, operation)
191
+ if duration_bucket is not None:
192
+ self._increment(conn, day, "latency_ms", operation, duration_bucket)
193
+ if error_class is not None:
194
+ self._increment(conn, day, "errors", operation, error_class)
195
+ if operation == "activation":
196
+ self._increment(conn, day, "activations", client_family, "count")
197
+ for transition in sorted(transitions):
198
+ self._increment(conn, day, "handoffs", transition, "count")
199
+ conn.commit()
200
+ except BaseException:
201
+ conn.rollback()
202
+ raise
203
+ finally:
204
+ conn.close()
205
+
206
+ def export_payload(self) -> dict[str, Any]:
207
+ """Return a deterministic, sanitized aggregate document."""
208
+ days: dict[str, dict[str, Any]] = {}
209
+ if self._db_file.is_file():
210
+ conn = self._connect()
211
+ try:
212
+ rows = conn.execute(
213
+ "SELECT day, group_name, dimension, bucket, value "
214
+ "FROM diagnostic_aggregates "
215
+ "ORDER BY day, group_name, dimension, bucket"
216
+ ).fetchall()
217
+ finally:
218
+ conn.close()
219
+ for day, group_name, dimension, bucket, value in rows:
220
+ entry = days.setdefault(str(day), {"day": str(day)})
221
+ group = entry.setdefault(str(group_name), {})
222
+ dimension_values = group.setdefault(str(dimension), {})
223
+ dimension_values[str(bucket)] = int(value)
224
+ return {
225
+ "schema_version": SCHEMA_VERSION,
226
+ "retention_days": self._retention_days,
227
+ "privacy": {
228
+ "local_only": True,
229
+ "reporting": "manual_export_only",
230
+ "content_values": False,
231
+ "query_values": False,
232
+ "fact_identifiers": False,
233
+ "filesystem_locations": False,
234
+ "identity_values": False,
235
+ },
236
+ "days": [days[key] for key in sorted(days)],
237
+ }
238
+
239
+ def export_json(self, destination: str | Path) -> dict[str, Any]:
240
+ """Atomically write a deterministic manual export with mode 0600."""
241
+ payload = self.export_payload()
242
+ output = Path(destination)
243
+ output.parent.mkdir(parents=True, exist_ok=True)
244
+ data = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8")
245
+ temporary_name = ""
246
+ try:
247
+ with tempfile.NamedTemporaryFile(
248
+ mode="wb", dir=output.parent, prefix=f".{output.name}.", delete=False,
249
+ ) as temporary:
250
+ temporary_name = temporary.name
251
+ os.chmod(temporary_name, 0o600)
252
+ temporary.write(data)
253
+ temporary.flush()
254
+ os.fsync(temporary.fileno())
255
+ os.replace(temporary_name, output)
256
+ if os.name == "posix":
257
+ os.chmod(output, 0o600)
258
+ finally:
259
+ if temporary_name:
260
+ Path(temporary_name).unlink(missing_ok=True)
261
+ return payload
262
+
263
+
264
+ def default_diagnostics() -> LocalDiagnostics:
265
+ """Return the diagnostics store for the active local state root."""
266
+ return LocalDiagnostics(state_path("diagnostics.db"))
267
+
268
+
269
+ def record_operation(
270
+ operation: str,
271
+ *,
272
+ client: str | None = None,
273
+ duration_ms: float | int | None = None,
274
+ error: BaseException | None = None,
275
+ source_clients: Iterable[str] = (),
276
+ ) -> None:
277
+ """Best-effort hot-path adapter; diagnostics never affect correctness."""
278
+ try:
279
+ default_diagnostics().record(
280
+ operation,
281
+ client=client,
282
+ duration_ms=duration_ms,
283
+ error=error,
284
+ source_clients=source_clients,
285
+ )
286
+ except Exception:
287
+ return
288
+
289
+
290
+ def record_recall(db: Any, response: Any, *, client: str | None) -> None:
291
+ """Record recall and aggregate cross-client provenance transitions."""
292
+ source_clients: list[str] = []
293
+ fact_ids = [
294
+ str(result.fact.fact_id)
295
+ for result in getattr(response, "results", ())
296
+ if getattr(getattr(result, "fact", None), "fact_id", None)
297
+ ]
298
+ profile_id = ""
299
+ if getattr(response, "results", None):
300
+ profile_id = str(getattr(response.results[0].fact, "profile_id", ""))
301
+ if fact_ids and profile_id:
302
+ try:
303
+ marks = ",".join("?" for _ in fact_ids)
304
+ rows = db.execute(
305
+ f"SELECT DISTINCT created_by FROM provenance "
306
+ f"WHERE profile_id=? AND fact_id IN ({marks})",
307
+ (profile_id, *fact_ids),
308
+ )
309
+ source_clients = [str(dict(row).get("created_by", "")) for row in rows]
310
+ except Exception:
311
+ source_clients = []
312
+ record_operation(
313
+ "recall",
314
+ client=client,
315
+ duration_ms=getattr(response, "retrieval_time_ms", None),
316
+ source_clients=source_clients,
317
+ )
318
+
319
+
320
+ __all__ = [
321
+ "LocalDiagnostics",
322
+ "classify_error",
323
+ "default_diagnostics",
324
+ "normalize_client",
325
+ "record_operation",
326
+ "record_recall",
327
+ ]
@@ -79,6 +79,29 @@ class SlmProcessInfo:
79
79
  age_hours: float
80
80
 
81
81
 
82
+ def is_mcp_server_process(process: SlmProcessInfo) -> bool:
83
+ """Return whether *process* is an MCP stdio server, not an SLM daemon.
84
+
85
+ The startup reaper is deliberately aggressive (zero age threshold), but
86
+ only an orphaned MCP process is safe to reap from ``slm mcp``. A unified
87
+ daemon is intentionally detached from the shell that launched it, so on
88
+ some hosts it has PPID 1 and looks like an orphan. Reaping that daemon at
89
+ MCP startup drops in-flight CLI and dashboard requests.
90
+
91
+ Keep this predicate narrow and based on the command line recorded by the
92
+ OS: every supported MCP entry point includes the standalone ``mcp``
93
+ argument or imports the MCP server module. The daemon's command does
94
+ neither.
95
+ """
96
+ command = process.command.lower()
97
+ return (
98
+ "/slm mcp" in command
99
+ or " slm mcp" in command
100
+ or "superlocalmemory.cli.main mcp" in command
101
+ or "superlocalmemory.mcp.server" in command
102
+ )
103
+
104
+
82
105
  # ---------------------------------------------------------------------------
83
106
  # Windows no-op stubs (AUDIT FIX H0-HIGH-02)
84
107
  # ---------------------------------------------------------------------------
@@ -8,7 +8,7 @@ All adapters run as separate subprocesses managed via PID files.
8
8
  Config stored in ~/.superlocalmemory/adapters.json.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -20,10 +20,12 @@ import subprocess
20
20
  import sys
21
21
  from pathlib import Path
22
22
 
23
+ from superlocalmemory.infra.data_root import canonical_data_root, state_path
24
+
23
25
  logger = logging.getLogger("superlocalmemory.ingestion.manager")
24
26
 
25
- _SLM_HOME = Path.home() / ".superlocalmemory"
26
- _ADAPTERS_CONFIG = _SLM_HOME / "adapters.json"
27
+ _SLM_HOME = None # test-only compatibility override
28
+ _ADAPTERS_CONFIG = None # test-only compatibility override
27
29
  _VALID_ADAPTERS = ("gmail", "calendar", "transcript")
28
30
 
29
31
  # Module paths for each adapter
@@ -34,19 +36,35 @@ _ADAPTER_MODULES = {
34
36
  }
35
37
 
36
38
 
39
+ def _slm_home() -> Path:
40
+ return Path(_SLM_HOME) if _SLM_HOME is not None else canonical_data_root()
41
+
42
+
43
+ def _adapters_config_path() -> Path:
44
+ if _ADAPTERS_CONFIG is not None:
45
+ return Path(_ADAPTERS_CONFIG)
46
+ return state_path("adapters.json")
47
+
48
+
49
+ def _adapter_log_dir() -> Path:
50
+ return _slm_home() / "logs"
51
+
52
+
37
53
  def _load_config() -> dict:
38
- if _ADAPTERS_CONFIG.exists():
39
- return json.loads(_ADAPTERS_CONFIG.read_text())
54
+ config_path = _adapters_config_path()
55
+ if config_path.exists():
56
+ return json.loads(config_path.read_text())
40
57
  return {name: {"enabled": False} for name in _VALID_ADAPTERS}
41
58
 
42
59
 
43
60
  def _save_config(config: dict) -> None:
44
- _ADAPTERS_CONFIG.parent.mkdir(parents=True, exist_ok=True)
45
- _ADAPTERS_CONFIG.write_text(json.dumps(config, indent=2))
61
+ config_path = _adapters_config_path()
62
+ config_path.parent.mkdir(parents=True, exist_ok=True)
63
+ config_path.write_text(json.dumps(config, indent=2))
46
64
 
47
65
 
48
66
  def _pid_file(name: str) -> Path:
49
- return _SLM_HOME / f"adapter-{name}.pid"
67
+ return _slm_home() / f"adapter-{name}.pid"
50
68
 
51
69
 
52
70
  def _is_running(name: str) -> tuple[bool, int | None]:
@@ -128,7 +146,7 @@ def start_adapter(name: str) -> dict:
128
146
  return {"ok": False, "error": f"No module for {name}"}
129
147
 
130
148
  cmd = [sys.executable, "-m", module]
131
- log_dir = _SLM_HOME / "logs"
149
+ log_dir = _adapter_log_dir()
132
150
  log_dir.mkdir(parents=True, exist_ok=True)
133
151
  log_path = log_dir / f"adapter-{name}.log"
134
152