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
@@ -16,6 +16,7 @@ import logging
16
16
  import os
17
17
  import sys
18
18
  from argparse import Namespace
19
+ from pathlib import Path
19
20
 
20
21
  logger = logging.getLogger(__name__)
21
22
 
@@ -29,7 +30,13 @@ def _cmd_db_dispatch(args: Namespace) -> None:
29
30
  if rc:
30
31
  sys.exit(rc)
31
32
  return
32
- print("Usage: slm db migrate [--status] [--dry-run]")
33
+ if sub == "scale":
34
+ from superlocalmemory.cli.scale_engine_cmd import cmd_db_scale
35
+ rc = cmd_db_scale(args)
36
+ if rc:
37
+ sys.exit(rc)
38
+ return
39
+ print("Usage: slm db migrate [--status] [--dry-run] | slm db scale <action>")
33
40
  sys.exit(2)
34
41
 
35
42
 
@@ -139,7 +146,7 @@ def cmd_session(args: Namespace) -> None:
139
146
  def dispatch(args: Namespace) -> None:
140
147
  """Route CLI command to the appropriate handler."""
141
148
  # Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
142
- if args.command not in ("hooks", "init", "mcp"):
149
+ if args.command not in ("hooks", "codex", "init", "mcp"):
143
150
  try:
144
151
  from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
145
152
  auto_install_if_needed()
@@ -169,6 +176,7 @@ def dispatch(args: Namespace) -> None:
169
176
  "dashboard": cmd_dashboard,
170
177
  "profile": cmd_profile,
171
178
  "hooks": cmd_hooks,
179
+ "codex": cmd_codex,
172
180
  "session-context": cmd_session_context,
173
181
  "session": cmd_session, # #49: local session open/close for hooks
174
182
  "observe": cmd_observe,
@@ -200,6 +208,8 @@ def dispatch(args: Namespace) -> None:
200
208
  "reconfigure": _cmd_escape_reconfigure,
201
209
  "benchmark": _cmd_escape_benchmark,
202
210
  "rotate-token": _cmd_escape_rotate_token,
211
+ "evidence": _cmd_evidence,
212
+ "diagnostics": _cmd_diagnostics,
203
213
  # LLD-06 — `slm wrap <agent> [args...]` activates the Optimize proxy.
204
214
  "wrap": _cmd_wrap,
205
215
  # V3.6 Optimize subcommands (additive)
@@ -217,6 +227,20 @@ def dispatch(args: Namespace) -> None:
217
227
  sys.exit(1)
218
228
 
219
229
 
230
+ def _cmd_evidence(args: Namespace) -> None:
231
+ """Lazy-load the evidence/rebuild command surface."""
232
+ from superlocalmemory.cli.evidence_cmd import cmd_evidence
233
+
234
+ cmd_evidence(args)
235
+
236
+
237
+ def _cmd_diagnostics(args: Namespace) -> None:
238
+ """Lazy-load the local aggregate diagnostics export surface."""
239
+ from superlocalmemory.cli.diagnostics_cmd import cmd_diagnostics
240
+
241
+ cmd_diagnostics(args)
242
+
243
+
220
244
  def _cmd_wrap(args: Namespace) -> None:
221
245
  """LLD-06 §6.6 — `slm wrap <agent> [args...]` activates the Optimize proxy.
222
246
 
@@ -319,29 +343,29 @@ def cmd_serve(args: Namespace) -> None:
319
343
  print(" slm serve status — check daemon status")
320
344
  print(" slm serve stop — stop daemon and free RAM")
321
345
  else:
322
- print("Failed to start daemon. Check ~/.superlocalmemory/logs/daemon.log")
346
+ from superlocalmemory.infra.data_root import state_path
347
+ print(f"Failed to start daemon. Check {state_path('logs', 'daemon.log')}")
323
348
 
324
349
 
325
350
  # -- Ingestion Adapters (V3.4.3) ------------------------------------------
326
351
 
327
352
 
328
353
  def cmd_restart(args: Namespace) -> None:
329
- """Nuclear restart: kill ALL orphans, clean state, start fresh, verify health.
354
+ """Restart the one daemon owned by the current SLM data namespace.
330
355
 
331
356
  5-step pipeline:
332
- 1. Kill ALL SLM processes (daemon + workers + orphans)
333
- 2. Clean stale PID/port/lock files
357
+ 1. Capability-stop the owned daemon and its children
358
+ 2. Acquire the namespace start lock
334
359
  3. Start fresh daemon
335
360
  4. Wait for engine warmup + verify health
336
361
  5. Optionally open dashboard
337
362
  """
338
- import os
339
363
  import time
340
- from pathlib import Path
364
+ from superlocalmemory.infra.daemon_identity import canonical_data_root
341
365
 
342
366
  use_json = getattr(args, "json", False)
343
367
  open_dashboard = getattr(args, "dashboard", False)
344
- slm_dir = Path.home() / ".superlocalmemory"
368
+ slm_dir = canonical_data_root()
345
369
  steps: list[dict] = []
346
370
 
347
371
  def _log(step: int, name: str, status: str, detail: str = ""):
@@ -357,65 +381,9 @@ def cmd_restart(args: Namespace) -> None:
357
381
  print(" " + "=" * 40)
358
382
  print()
359
383
 
360
- # Step 1: Kill ALL SLM processes
361
- killed = 0
362
- try:
363
- import psutil
364
- my_pid = os.getpid()
365
- targets = [
366
- "superlocalmemory.server.unified_daemon",
367
- "superlocalmemory.core.embedding_worker",
368
- "superlocalmemory.core.recall_worker",
369
- "superlocalmemory.core.reranker_worker",
370
- "superlocalmemory.cli.daemon",
371
- ]
372
- for proc in psutil.process_iter(["pid", "cmdline"]):
373
- try:
374
- if proc.pid == my_pid:
375
- continue
376
- cmdline = " ".join(proc.info.get("cmdline") or [])
377
- if any(t in cmdline for t in targets):
378
- for child in proc.children(recursive=True):
379
- try:
380
- child.kill()
381
- killed += 1
382
- except (psutil.NoSuchProcess, psutil.AccessDenied):
383
- pass
384
- proc.kill()
385
- killed += 1
386
- except (psutil.NoSuchProcess, psutil.AccessDenied):
387
- pass
388
- except ImportError:
389
- # Fallback: pkill
390
- import subprocess as _sp
391
- for pattern in [
392
- "superlocalmemory.server.unified_daemon",
393
- "superlocalmemory.core.embedding_worker",
394
- "superlocalmemory.core.recall_worker",
395
- "superlocalmemory.core.reranker_worker",
396
- ]:
397
- try:
398
- r = _sp.run(["pkill", "-9", "-f", pattern], capture_output=True, timeout=5)
399
- if r.returncode == 0:
400
- killed += 1
401
- except Exception:
402
- pass
403
-
404
- _log(1, "Kill all SLM processes", "ok", f"{killed} processes killed")
405
- time.sleep(3)
406
-
407
- # Step 2: Clean stale files + HOLD the lock to prevent races
408
- # v3.4.13: Do NOT delete daemon.lock — HOLD it instead.
409
- # If we delete it, `slm mcp` (still running in Claude) will see no lock,
410
- # acquire a NEW lock, and start a second daemon during our restart.
411
- cleaned = []
412
- for fname in ("daemon.pid", "daemon.port", ".embedding-worker.pid", ".reranker-worker.pid"):
413
- fpath = slm_dir / fname
414
- if fpath.exists():
415
- fpath.unlink(missing_ok=True)
416
- cleaned.append(fname)
417
-
418
- # Hold the lock file to block other processes from starting a daemon
384
+ # Acquire the namespace lock before requesting shutdown. Otherwise an
385
+ # auto-starting hook can observe the brief offline window and start a
386
+ # second daemon while this command is still waiting for the old process.
419
387
  _LOCK_FILE = slm_dir / "daemon.lock"
420
388
  _LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
421
389
  restart_lock_fd = None
@@ -427,8 +395,49 @@ def cmd_restart(args: Namespace) -> None:
427
395
  except Exception:
428
396
  pass # Best-effort — don't block restart if lock fails
429
397
 
430
- _log(2, "Clean stale state files", "ok",
431
- f"removed: {', '.join(cleaned)}" if cleaned else "already clean")
398
+ # Step 1: stop only the descriptor-owned daemon. Its graceful shutdown
399
+ # owns worker termination; process-name-wide scans are forbidden.
400
+ from superlocalmemory.cli.daemon import (
401
+ is_daemon_running,
402
+ read_descriptor,
403
+ stop_daemon,
404
+ wait_for_owned_daemon_shutdown,
405
+ )
406
+
407
+ was_running = is_daemon_running()
408
+ owned_descriptor = read_descriptor() if was_running else None
409
+ stopped = stop_daemon() if was_running else True
410
+ if stopped and was_running:
411
+ stopped = wait_for_owned_daemon_shutdown(owned_descriptor)
412
+ killed = 1 if was_running and stopped else 0
413
+ _log(
414
+ 1,
415
+ "Stop owned SLM daemon",
416
+ "ok" if stopped else "fail",
417
+ "owned daemon stopped" if killed else (
418
+ "already stopped" if stopped else "owned daemon did not stop"
419
+ ),
420
+ )
421
+ if not stopped:
422
+ if restart_lock_fd:
423
+ restart_lock_fd.close()
424
+ if use_json:
425
+ from superlocalmemory.cli.json_output import json_print
426
+ json_print(
427
+ "restart",
428
+ data={"steps": steps, "success": False},
429
+ next_actions=[{
430
+ "command": "slm doctor",
431
+ "description": "Diagnose the owned daemon",
432
+ }],
433
+ )
434
+ else:
435
+ print("\n Restart FAILED at step 1. The owned daemon was not stopped.")
436
+ return
437
+
438
+ # Step 2: the namespace lock was acquired before shutdown so hooks cannot
439
+ # auto-start another daemon inside the offline transition.
440
+ _log(2, "Acquire namespace start lock", "ok", str(_LOCK_FILE))
432
441
 
433
442
  # Step 3: Start fresh daemon (lock still held — no races)
434
443
  # v3.4.42: Call _start_daemon_subprocess() directly instead of
@@ -438,7 +447,6 @@ def cmd_restart(args: Namespace) -> None:
438
447
  # fall into its lock-fail branch and time out after 60s while the
439
448
  # actual daemon never gets started. Calling the helper directly
440
449
  # bypasses that self-deadlock and starts the daemon as intended.
441
- time.sleep(1)
442
450
  from superlocalmemory.cli.daemon import _start_daemon_subprocess
443
451
  started = _start_daemon_subprocess()
444
452
 
@@ -561,14 +569,15 @@ def cmd_config(args: Namespace) -> None:
561
569
  slm config get evolution.backend
562
570
  """
563
571
  import json
564
- from pathlib import Path
572
+
573
+ from superlocalmemory.infra.data_root import state_path
565
574
 
566
575
  use_json = getattr(args, "json", False)
567
576
  action = getattr(args, "action", "get")
568
577
  key = getattr(args, "key", "")
569
578
  value = getattr(args, "value", None)
570
579
 
571
- config_path = Path.home() / ".superlocalmemory" / "config.json"
580
+ config_path = state_path("config.json")
572
581
 
573
582
  # Read existing config
574
583
  cfg: dict = {}
@@ -602,6 +611,7 @@ def cmd_config(args: Namespace) -> None:
602
611
  _ALLOWED_CONFIG_KEYS = {
603
612
  "evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
604
613
  "mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
614
+ "graph_backend", "vector_backend", "scale_engine_state",
605
615
  }
606
616
  if key not in _ALLOWED_CONFIG_KEYS:
607
617
  if use_json:
@@ -682,7 +692,8 @@ def cmd_evolve(args: Namespace) -> None:
682
692
  If disabled, exits silently (zero output for fire-and-forget).
683
693
  """
684
694
  import json
685
- from pathlib import Path
695
+
696
+ from superlocalmemory.infra.data_root import state_path
686
697
 
687
698
  session_id = getattr(args, "session", "") or ""
688
699
  profile = getattr(args, "profile", "default") or "default"
@@ -691,7 +702,7 @@ def cmd_evolve(args: Namespace) -> None:
691
702
  return # Silent exit — nothing to do without a session
692
703
 
693
704
  # Check if evolution is enabled via config.json
694
- config_path = Path.home() / ".superlocalmemory" / "config.json"
705
+ config_path = state_path("config.json")
695
706
  try:
696
707
  cfg = json.loads(config_path.read_text()) if config_path.exists() else {}
697
708
  except (json.JSONDecodeError, OSError):
@@ -705,7 +716,7 @@ def cmd_evolve(args: Namespace) -> None:
705
716
  try:
706
717
  from superlocalmemory.evolution.skill_evolver import SkillEvolver
707
718
 
708
- db_path = Path.home() / ".superlocalmemory" / "memory.db"
719
+ db_path = state_path("memory.db")
709
720
  if not db_path.exists():
710
721
  return
711
722
 
@@ -810,7 +821,7 @@ def cmd_provider(args: Namespace) -> None:
810
821
  if args.action == "set":
811
822
  from superlocalmemory.cli.setup_wizard import configure_provider
812
823
 
813
- configure_provider(config)
824
+ configure_provider(config, provider_name=getattr(args, "provider", None))
814
825
  else:
815
826
  print(f"Provider: {config.llm.provider or 'none (Mode A)'}")
816
827
  if config.llm.model:
@@ -882,6 +893,11 @@ def cmd_connect(args: Namespace) -> None:
882
893
  agents_md_source=_agents_md_source_factory(),
883
894
  )
884
895
 
896
+ if not result.get("error"):
897
+ from superlocalmemory.infra.local_diagnostics import record_operation
898
+
899
+ record_operation("activation", client=ide_arg)
900
+
885
901
  if getattr(args, "json", False):
886
902
  from superlocalmemory.cli.json_output import json_print
887
903
  json_print("connect", data=result)
@@ -1050,57 +1066,67 @@ def cmd_remember(args: Namespace) -> None:
1050
1066
  if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
1051
1067
  )
1052
1068
 
1053
- # V3.3.21: Route through daemon for instant remember (no cold start).
1054
- # If daemon is running, send request directly (~0.1s).
1055
- # If not, use store-first pattern (pending.db) as fallback.
1056
- if not sync_mode:
1057
- # Try daemon first
1058
- try:
1059
- from superlocalmemory.cli.daemon import is_daemon_running, daemon_request, ensure_daemon
1060
- if is_daemon_running() or ensure_daemon():
1061
- result = daemon_request("POST", "/remember", {
1069
+ # Both paths use the one owned daemon. A second local engine for --sync
1070
+ # duplicates heavyweight workers and can block for minutes on cold models.
1071
+ daemon_owned = False
1072
+ try:
1073
+ from superlocalmemory.cli.daemon import (
1074
+ daemon_request, ensure_daemon, is_daemon_running,
1075
+ )
1076
+ daemon_owned = is_daemon_running() or ensure_daemon()
1077
+ if daemon_owned:
1078
+ path = "/remember?wait=true" if sync_mode else "/remember"
1079
+ result = daemon_request(
1080
+ "POST", path, {
1062
1081
  "content": args.content,
1063
1082
  "tags": args.tags or "",
1064
1083
  "scope": scope,
1065
1084
  "shared_with": shared_with,
1085
+ },
1086
+ timeout_seconds=30,
1087
+ )
1088
+ if result and "fact_ids" in result:
1089
+ if use_json:
1090
+ from superlocalmemory.cli.json_output import json_print
1091
+ json_print("remember", data=result)
1092
+ else:
1093
+ state = result.get("materialization_state", "queryable")
1094
+ operation_id = result.get("operation_id", "unknown")
1095
+ print(
1096
+ f"{state.capitalize()} \u2713 {result['count']} facts "
1097
+ f"(operation={operation_id})."
1098
+ )
1099
+ return
1100
+ if sync_mode:
1101
+ if use_json:
1102
+ from superlocalmemory.cli.json_output import json_print
1103
+ json_print("remember", error={
1104
+ "code": "SYNC_TIMEOUT",
1105
+ "message": (
1106
+ "Canonical ingestion did not complete within 30s; "
1107
+ "the durable operation remains available for retry."
1108
+ ),
1109
+ })
1110
+ else:
1111
+ print(
1112
+ "Synchronous ingestion did not complete within 30s; "
1113
+ "the durable operation remains queued.",
1114
+ file=sys.stderr,
1115
+ )
1116
+ sys.exit(1)
1117
+ except SystemExit:
1118
+ raise
1119
+ except Exception:
1120
+ if sync_mode and daemon_owned:
1121
+ if use_json:
1122
+ from superlocalmemory.cli.json_output import json_print
1123
+ json_print("remember", error={
1124
+ "code": "SYNC_TIMEOUT",
1125
+ "message": "Owned daemon request failed before completion.",
1066
1126
  })
1067
- if result and "fact_ids" in result:
1068
- if use_json:
1069
- from superlocalmemory.cli.json_output import json_print
1070
- json_print("remember", data=result)
1071
- else:
1072
- print(f"Stored \u2713 {result['count']} facts (via daemon).")
1073
- return
1074
- except Exception:
1075
- pass # Fall through to pending store
1076
-
1077
- # v3.4.13: Store to pending DB (zero data loss) — daemon processes in background.
1078
- # NO subprocess spawn. Daemon's background loop picks up pending memories.
1079
- from superlocalmemory.cli.pending_store import store_pending
1080
-
1081
- # v3.6.15 multi-scope: carry an explicit non-personal scope into the
1082
- # pending row's metadata so the materializer replays the right
1083
- # visibility. Unset / personal carries nothing — byte-identical to
1084
- # pre-3.6.15 pending rows.
1085
- _pending_meta = None
1086
- if scope and scope != "personal":
1087
- _pending_meta = {"scope": scope}
1088
- if shared_with:
1089
- _pending_meta["shared_with"] = shared_with
1090
-
1091
- row_id = store_pending(
1092
- content=args.content,
1093
- tags=args.tags or "",
1094
- metadata=_pending_meta,
1095
- )
1096
-
1097
- if use_json:
1098
- from superlocalmemory.cli.json_output import json_print
1099
- json_print("remember", data={"queued": True, "async": True,
1100
- "pending_id": row_id, "safe": True})
1101
- else:
1102
- print(f"Stored \u2713 (pending_id={row_id}) \u2014 processing in background.")
1103
- return
1127
+ sys.exit(1)
1128
+ # Receipt-first writes may use the authenticated local fallback when
1129
+ # no owned daemon exists.
1104
1130
 
1105
1131
  from superlocalmemory.core.engine import MemoryEngine
1106
1132
 
@@ -1111,10 +1137,21 @@ def cmd_remember(args: Namespace) -> None:
1111
1137
 
1112
1138
  # v3.6.15: resolve an unset scope to the configured default_scope.
1113
1139
  _scope = scope or getattr(getattr(config, "scope", None), "default_scope", "personal")
1140
+ from superlocalmemory.core.engine_ingestion import (
1141
+ canonical_store,
1142
+ local_trusted_actor_id,
1143
+ )
1144
+
1114
1145
  metadata = {"tags": args.tags} if args.tags else {}
1115
- fact_ids = engine.store(
1116
- args.content, metadata=metadata,
1117
- scope=_scope, shared_with=shared_with,
1146
+ operation = canonical_store(
1147
+ engine,
1148
+ args.content,
1149
+ source_type="cli-sync" if sync_mode else "cli-offline-canonical",
1150
+ trusted_actor_id=local_trusted_actor_id("cli"),
1151
+ metadata=metadata,
1152
+ scope=_scope,
1153
+ shared_with=shared_with,
1154
+ return_receipt=True,
1118
1155
  )
1119
1156
  except Exception as exc:
1120
1157
  if use_json:
@@ -1123,16 +1160,30 @@ def cmd_remember(args: Namespace) -> None:
1123
1160
  sys.exit(1)
1124
1161
  raise
1125
1162
 
1163
+ fact_ids = list(operation.fact_ids) if hasattr(operation, "fact_ids") else list(operation)
1164
+ operation_data = {
1165
+ "fact_ids": fact_ids,
1166
+ "count": len(fact_ids),
1167
+ "materialization_state": getattr(
1168
+ getattr(operation, "state", None), "value", "complete"
1169
+ ),
1170
+ }
1171
+ if getattr(operation, "operation_id", None):
1172
+ operation_data["operation_id"] = operation.operation_id
1173
+
1126
1174
  if use_json:
1127
1175
  from superlocalmemory.cli.json_output import json_print
1128
- json_print("remember", data={"fact_ids": fact_ids, "count": len(fact_ids)},
1176
+ json_print("remember", data=operation_data,
1129
1177
  next_actions=[
1130
1178
  {"command": "slm recall '<query>' --json", "description": "Search your memories"},
1131
1179
  {"command": "slm list --json -n 5", "description": "See recent memories"},
1132
1180
  ])
1133
1181
  return
1134
1182
 
1135
- print(f"Stored {len(fact_ids)} facts.")
1183
+ print(
1184
+ f"Complete \u2713 {len(fact_ids)} facts "
1185
+ f"(operation={operation_data.get('operation_id', 'none')})."
1186
+ )
1136
1187
 
1137
1188
 
1138
1189
  def cmd_recall(args: Namespace) -> None:
@@ -1214,7 +1265,10 @@ def cmd_recall(args: Namespace) -> None:
1214
1265
  # v3.6.6: route the direct-fallback path through the SAME shared
1215
1266
  # serializer the daemon uses, so CLI-without-daemon output is identical
1216
1267
  # to CLI/MCP-with-daemon (budget + source discipline + no_confident_match).
1217
- from superlocalmemory.server.recall_serializer import serialize_recall_response
1268
+ from superlocalmemory.server.recall_serializer import (
1269
+ recall_response_metadata,
1270
+ serialize_recall_response,
1271
+ )
1218
1272
  _rc = getattr(config, "retrieval", None)
1219
1273
  _ser, _no_match = serialize_recall_response(
1220
1274
  response,
@@ -1228,21 +1282,12 @@ def cmd_recall(args: Namespace) -> None:
1228
1282
  from superlocalmemory.cli.json_output import json_print
1229
1283
  items = []
1230
1284
  for d in _ser:
1231
- item = {
1232
- "fact_id": d["fact_id"], "content": d["content"],
1233
- "score": round(d["score"], 3),
1234
- }
1235
- if d.get("channel_scores"):
1236
- item["channel_scores"] = {k: round(v, 3) for k, v in d["channel_scores"].items()}
1237
- if d.get("truncated"):
1238
- item["truncated"] = True
1239
- if d.get("stub"):
1240
- item["stub"] = True
1241
- items.append(item)
1285
+ items.append(dict(d))
1242
1286
  json_print("recall", data={
1243
1287
  "results": items, "count": len(items),
1244
1288
  "query_type": getattr(response, "query_type", "unknown"),
1245
1289
  "no_confident_match": _no_match,
1290
+ **recall_response_metadata(response),
1246
1291
  }, next_actions=[
1247
1292
  {"command": "slm list --json", "description": "List recent memories"},
1248
1293
  ])
@@ -1258,15 +1303,21 @@ def cmd_recall(args: Namespace) -> None:
1258
1303
  print("No confident match." if _no_match else "No memories found.")
1259
1304
  return
1260
1305
  for i, d in enumerate(_ser, 1):
1261
- print(f" {i}. [{d['score']:.2f}] {d['content']}")
1306
+ print(f" {i}. [relevance {d['relevance_score']:.2f}] {d['content']}")
1262
1307
 
1263
1308
 
1264
1309
  def _cli_record_signals(config, query, results):
1265
1310
  """Record learning signals from CLI recall (no MCP dependency)."""
1266
1311
  from pathlib import Path
1312
+
1267
1313
  from superlocalmemory.learning.feedback import FeedbackCollector
1268
1314
  from superlocalmemory.learning.signals import LearningSignals
1269
- slm_dir = Path.home() / ".superlocalmemory"
1315
+ configured_root = getattr(config, "base_dir", None)
1316
+ if configured_root is not None:
1317
+ slm_dir = Path(configured_root)
1318
+ else:
1319
+ from superlocalmemory.infra.data_root import canonical_data_root
1320
+ slm_dir = canonical_data_root()
1270
1321
  pid = config.active_profile
1271
1322
  fact_ids = [r.fact.fact_id for r in results[:10]]
1272
1323
  if not fact_ids:
@@ -1303,6 +1354,19 @@ def cmd_forget(args: Namespace) -> None:
1303
1354
 
1304
1355
  dry_run = getattr(args, 'dry_run', False)
1305
1356
 
1357
+ def delete_fact_authorized_for_cli(fact_id: str) -> None:
1358
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1359
+ from superlocalmemory.core.mutations import delete_fact_authorized
1360
+
1361
+ result = delete_fact_authorized(
1362
+ engine,
1363
+ fact_id,
1364
+ trusted_actor_id=local_trusted_actor_id("cli"),
1365
+ source_agent_id="cli",
1366
+ )
1367
+ if not result.get("ok"):
1368
+ raise RuntimeError(result.get("error", "delete failed"))
1369
+
1306
1370
  if use_json:
1307
1371
  from superlocalmemory.cli.json_output import json_print
1308
1372
  if not matches:
@@ -1317,7 +1381,7 @@ def cmd_forget(args: Namespace) -> None:
1317
1381
  return
1318
1382
  if getattr(args, 'yes', False):
1319
1383
  for f in matches:
1320
- engine._db.delete_fact(f.fact_id)
1384
+ delete_fact_authorized_for_cli(f.fact_id)
1321
1385
  json_print("forget", data={
1322
1386
  "matched_count": len(matches), "deleted_count": len(matches),
1323
1387
  "deleted": [f.fact_id for f in matches],
@@ -1345,13 +1409,13 @@ def cmd_forget(args: Namespace) -> None:
1345
1409
  return
1346
1410
  if getattr(args, 'yes', False):
1347
1411
  for f in matches:
1348
- engine._db.delete_fact(f.fact_id)
1412
+ delete_fact_authorized_for_cli(f.fact_id)
1349
1413
  print(f"Deleted {len(matches)} memories.")
1350
1414
  return
1351
1415
  confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
1352
1416
  if confirm in ("y", "yes"):
1353
1417
  for f in matches:
1354
- engine._db.delete_fact(f.fact_id)
1418
+ delete_fact_authorized_for_cli(f.fact_id)
1355
1419
  print(f"Deleted {len(matches)} memories.")
1356
1420
  else:
1357
1421
  print("Cancelled.")
@@ -1389,7 +1453,15 @@ def cmd_delete(args: Namespace) -> None:
1389
1453
  sys.exit(1)
1390
1454
  content = dict(rows[0]).get("content", "")
1391
1455
  if getattr(args, "yes", False):
1392
- engine._db.delete_fact(fact_id)
1456
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1457
+ from superlocalmemory.core.mutations import delete_fact_authorized
1458
+
1459
+ delete_fact_authorized(
1460
+ engine,
1461
+ fact_id,
1462
+ trusted_actor_id=local_trusted_actor_id("cli"),
1463
+ source_agent_id="cli",
1464
+ )
1393
1465
  json_print("delete", data={"deleted": fact_id, "content": content[:120]},
1394
1466
  next_actions=[
1395
1467
  {"command": "slm list --json", "description": "Verify remaining memories"},
@@ -1416,7 +1488,15 @@ def cmd_delete(args: Namespace) -> None:
1416
1488
  print("Cancelled.")
1417
1489
  return
1418
1490
 
1419
- engine._db.delete_fact(fact_id)
1491
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1492
+ from superlocalmemory.core.mutations import delete_fact_authorized
1493
+
1494
+ delete_fact_authorized(
1495
+ engine,
1496
+ fact_id,
1497
+ trusted_actor_id=local_trusted_actor_id("cli"),
1498
+ source_agent_id="cli",
1499
+ )
1420
1500
  print(f"Deleted: {fact_id}")
1421
1501
 
1422
1502
 
@@ -1464,9 +1544,15 @@ def cmd_update(args: Namespace) -> None:
1464
1544
  return
1465
1545
 
1466
1546
  old_content = dict(rows[0]).get("content", "")
1467
- engine._db.execute(
1468
- "UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
1469
- (new_content, fact_id),
1547
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1548
+ from superlocalmemory.core.mutations import update_fact_authorized
1549
+
1550
+ update_fact_authorized(
1551
+ engine,
1552
+ fact_id,
1553
+ new_content,
1554
+ trusted_actor_id=local_trusted_actor_id("cli"),
1555
+ source_agent_id="cli",
1470
1556
  )
1471
1557
 
1472
1558
  if use_json:
@@ -1592,17 +1678,43 @@ def cmd_status(args: Namespace) -> None:
1592
1678
 
1593
1679
  def cmd_health(args: Namespace) -> None:
1594
1680
  """Show math layer health status."""
1595
- from superlocalmemory.core.engine import MemoryEngine
1596
1681
  from superlocalmemory.core.config import SLMConfig
1597
1682
 
1598
1683
  use_json = getattr(args, 'json', False)
1599
1684
  try:
1600
1685
  config = SLMConfig.load()
1601
- engine = MemoryEngine(config)
1602
- engine.initialize()
1603
- facts = engine._db.get_all_facts(engine.profile_id)
1604
- fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
1605
- langevin_count = sum(1 for f in facts if f.langevin_position is not None)
1686
+ from superlocalmemory.cli.daemon import is_daemon_running
1687
+ if is_daemon_running():
1688
+ # A running daemon owns the writable SQLite connections. Opening a
1689
+ # second MemoryEngine re-runs schema initialization and can lock
1690
+ # the user's database. Health only needs aggregate counts, so use
1691
+ # a read-only snapshot connection instead.
1692
+ import sqlite3
1693
+ db_path = config.db_path
1694
+ conn = sqlite3.connect(
1695
+ f"file:{db_path}?mode=ro", uri=True, timeout=5,
1696
+ )
1697
+ try:
1698
+ row = conn.execute(
1699
+ "SELECT COUNT(*), "
1700
+ "SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
1701
+ "SUM(CASE WHEN langevin_position IS NOT NULL THEN 1 ELSE 0 END) "
1702
+ "FROM atomic_facts WHERE profile_id = ?",
1703
+ (config.active_profile,),
1704
+ ).fetchone()
1705
+ total_facts, fisher_count, langevin_count = row or (0, 0, 0)
1706
+ finally:
1707
+ conn.close()
1708
+ facts = [None] * int(total_facts or 0)
1709
+ fisher_count = int(fisher_count or 0)
1710
+ langevin_count = int(langevin_count or 0)
1711
+ else:
1712
+ from superlocalmemory.core.engine import MemoryEngine
1713
+ engine = MemoryEngine(config)
1714
+ engine.initialize()
1715
+ facts = engine._db.get_all_facts(engine.profile_id)
1716
+ fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
1717
+ langevin_count = sum(1 for f in facts if f.langevin_position is not None)
1606
1718
  except Exception as exc:
1607
1719
  if use_json:
1608
1720
  from superlocalmemory.cli.json_output import json_print
@@ -1642,7 +1754,7 @@ def _gather_optimize_surface_b() -> dict:
1642
1754
  compress_runs, tokens_saved, cache_hits, cache_misses,
1643
1755
  db_present, error
1644
1756
  """
1645
- from pathlib import Path
1757
+ from superlocalmemory.infra.data_root import state_path
1646
1758
  from superlocalmemory.optimize.storage.db import CacheDB
1647
1759
 
1648
1760
  result: dict = {
@@ -1672,7 +1784,7 @@ def _gather_optimize_surface_b() -> dict:
1672
1784
 
1673
1785
  # Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
1674
1786
  try:
1675
- db_path = Path.home() / ".superlocalmemory" / "llmcache.db"
1787
+ db_path = state_path("llmcache.db")
1676
1788
  result["db_present"] = db_path.exists()
1677
1789
  if result["db_present"]:
1678
1790
  snap = CacheDB.get_default().metrics_load()
@@ -1778,7 +1890,7 @@ def cmd_doctor(args: Namespace) -> None:
1778
1890
  ver = getattr(m, "__version__", "?")
1779
1891
  core_ok.append(mod)
1780
1892
  core_versions.append(f"{mod} {ver}")
1781
- except ImportError:
1893
+ except Exception: # dependency import may fail after module discovery
1782
1894
  pass
1783
1895
  if len(core_ok) == len(core_modules):
1784
1896
  _check("Core deps", "PASS", ", ".join(core_versions[:4]) + "...")
@@ -1795,7 +1907,7 @@ def cmd_doctor(args: Namespace) -> None:
1795
1907
  try:
1796
1908
  __import__(mod)
1797
1909
  search_ok.append(mod)
1798
- except ImportError:
1910
+ except Exception: # dependency import may fail after module discovery
1799
1911
  pass
1800
1912
  if len(search_ok) == len(search_mods):
1801
1913
  _check("Search deps", "PASS", "sentence-transformers, torch, sklearn, geoopt")
@@ -1809,7 +1921,7 @@ def cmd_doctor(args: Namespace) -> None:
1809
1921
  for mod in ["fastapi", "uvicorn", "websockets"]:
1810
1922
  try:
1811
1923
  __import__(mod)
1812
- except ImportError:
1924
+ except Exception: # dependency import may fail after module discovery
1813
1925
  dash_ok = False
1814
1926
  break
1815
1927
  if dash_ok:
@@ -1822,7 +1934,7 @@ def cmd_doctor(args: Namespace) -> None:
1822
1934
  try:
1823
1935
  import lightgbm
1824
1936
  _check("Learning deps", "PASS", f"lightgbm {lightgbm.__version__}")
1825
- except ImportError:
1937
+ except Exception: # dependency import may fail after module discovery
1826
1938
  _check("Learning deps", "WARN", "lightgbm not installed",
1827
1939
  "pip install lightgbm")
1828
1940
  except OSError as exc:
@@ -1839,7 +1951,7 @@ def cmd_doctor(args: Namespace) -> None:
1839
1951
  try:
1840
1952
  __import__(mod)
1841
1953
  perf_ok.append(mod)
1842
- except ImportError:
1954
+ except Exception: # dependency import may fail after module discovery
1843
1955
  pass
1844
1956
  if perf_ok:
1845
1957
  _check("Performance deps", "PASS", "orjson")
@@ -1963,7 +2075,8 @@ def cmd_doctor(args: Namespace) -> None:
1963
2075
  pass # Config load failed — already caught above
1964
2076
 
1965
2077
  # 9. Disk space
1966
- slm_home = Path.home() / ".superlocalmemory"
2078
+ from superlocalmemory.infra.data_root import canonical_data_root
2079
+ slm_home = canonical_data_root()
1967
2080
  try:
1968
2081
  usage = shutil.disk_usage(slm_home if slm_home.exists() else Path.home())
1969
2082
  free_gb = usage.free / (1024 ** 3)
@@ -2008,8 +2121,8 @@ def cmd_doctor(args: Namespace) -> None:
2008
2121
  "WARN",
2009
2122
  "System Python is externally managed (EXTERNALLY-MANAGED marker found). "
2010
2123
  "pip install may fail with PEP 668 error.",
2011
- "Use pipx for an isolated install: pipx install superlocalmemory "
2012
- "(last-resort only: pip install --break-system-packages superlocalmemory)",
2124
+ "Use an isolated install: pipx install superlocalmemory "
2125
+ "or uv tool install superlocalmemory",
2013
2126
  )
2014
2127
  else:
2015
2128
  _check(
@@ -2031,7 +2144,7 @@ def cmd_doctor(args: Namespace) -> None:
2031
2144
  "disabled (optimize.json enabled=false) — caching/compression not active"
2032
2145
  + (f" [{_error}]" if _error else ""),
2033
2146
  fix="Enable via dashboard Optimize tab or set enabled=true"
2034
- " in ~/.superlocalmemory/optimize.json",
2147
+ f" in {slm_home / 'optimize.json'}",
2035
2148
  )
2036
2149
  else:
2037
2150
  _surfaces = []
@@ -2096,14 +2209,85 @@ def cmd_doctor(args: Namespace) -> None:
2096
2209
 
2097
2210
  def cmd_trace(args: Namespace) -> None:
2098
2211
  """Recall with per-channel score breakdown."""
2212
+ use_json = getattr(args, 'json', False)
2213
+ limit = getattr(args, 'limit', 10)
2214
+
2215
+ # Trace must use the same daemon-owned engine as recall. A direct CLI
2216
+ # engine cannot attach to the machine-wide embedding worker already owned
2217
+ # by the daemon; it then silently loses semantic, Hopfield, and spreading
2218
+ # activation channels. The daemon trace route keeps the loaded model,
2219
+ # graph, and retrieval state intact while returning the same score detail.
2220
+ try:
2221
+ from superlocalmemory.cli.daemon import (
2222
+ daemon_request, ensure_daemon, is_daemon_running,
2223
+ )
2224
+ if is_daemon_running() or ensure_daemon():
2225
+ result = daemon_request(
2226
+ "POST", "/api/v3/recall/trace",
2227
+ {"query": args.query, "limit": limit},
2228
+ )
2229
+ if result and "results" in result:
2230
+ if use_json:
2231
+ from superlocalmemory.cli.json_output import json_print
2232
+ json_print("trace", data={
2233
+ "query": result.get("query", args.query),
2234
+ "query_type": result.get("query_type", "unknown"),
2235
+ "retrieval_time_ms": round(
2236
+ float(result.get("retrieval_time_ms", 0)), 1,
2237
+ ),
2238
+ "results": result["results"],
2239
+ "count": len(result["results"]),
2240
+ "no_confident_match": bool(
2241
+ result.get("no_confident_match", False),
2242
+ ),
2243
+ "score_contract_version": result.get(
2244
+ "score_contract_version", "2",
2245
+ ),
2246
+ "calibration_status": result.get(
2247
+ "calibration_status", "uncalibrated",
2248
+ ),
2249
+ "calibration_id": result.get("calibration_id"),
2250
+ "answer_confidence": result.get("answer_confidence"),
2251
+ "abstained": bool(result.get("abstained", False)),
2252
+ "abstention_reason": result.get("abstention_reason"),
2253
+ }, next_actions=[
2254
+ {
2255
+ "command": "slm recall '<query>' --json",
2256
+ "description": "Standard recall",
2257
+ },
2258
+ ])
2259
+ return
2260
+ print(f"Query: {result.get('query', args.query)}")
2261
+ print(
2262
+ f"Type: {result.get('query_type', 'unknown')} | Time: "
2263
+ f"{float(result.get('retrieval_time_ms', 0)):.0f}ms"
2264
+ )
2265
+ print(f"Results: {len(result['results'])}")
2266
+ for i, item in enumerate(result["results"], 1):
2267
+ print(
2268
+ f"\n {i}. [relevance "
2269
+ f"{float(item.get('relevance_score', item.get('score', 0))):.3f}] "
2270
+ f"{str(item.get('content', ''))[:100]}"
2271
+ )
2272
+ if item.get("ranking_score") is not None:
2273
+ print(
2274
+ " ranking utility: "
2275
+ f"{float(item['ranking_score']):.6f}"
2276
+ )
2277
+ for channel, score in (item.get("channel_scores") or {}).items():
2278
+ print(f" {channel}: {float(score):.3f}")
2279
+ return
2280
+ except Exception:
2281
+ # The direct path remains the offline escape hatch when a daemon is
2282
+ # unavailable or a local transport error occurs.
2283
+ pass
2284
+
2099
2285
  from superlocalmemory.core.engine import MemoryEngine
2100
2286
  from superlocalmemory.core.config import SLMConfig
2101
-
2102
- use_json = getattr(args, 'json', False)
2103
2287
  try:
2104
2288
  config = SLMConfig.load()
2105
2289
  engine = MemoryEngine(config)
2106
- limit = getattr(args, 'limit', 10)
2290
+ engine.initialize()
2107
2291
  response = engine.recall(args.query, limit=limit)
2108
2292
  except Exception as exc:
2109
2293
  if use_json:
@@ -2114,22 +2298,23 @@ def cmd_trace(args: Namespace) -> None:
2114
2298
 
2115
2299
  if use_json:
2116
2300
  from superlocalmemory.cli.json_output import json_print
2117
- items = []
2118
- for r in response.results:
2119
- item = {
2120
- "fact_id": r.fact.fact_id, "content": r.fact.content[:200],
2121
- "score": round(r.score, 3),
2122
- }
2123
- if hasattr(r, "channel_scores") and r.channel_scores:
2124
- item["channel_scores"] = {
2125
- k: round(v, 3) for k, v in r.channel_scores.items()
2126
- }
2127
- items.append(item)
2301
+ from superlocalmemory.server.recall_serializer import (
2302
+ recall_response_metadata,
2303
+ serialize_recall_response,
2304
+ )
2305
+ items, no_confident_match = serialize_recall_response(
2306
+ response,
2307
+ limit=limit,
2308
+ per_fact_max=200,
2309
+ total_max=max(200, limit * 200),
2310
+ )
2128
2311
  json_print("trace", data={
2129
2312
  "query": args.query,
2130
2313
  "query_type": getattr(response, "query_type", "unknown"),
2131
2314
  "retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
2132
2315
  "results": items, "count": len(items),
2316
+ "no_confident_match": no_confident_match,
2317
+ **recall_response_metadata(response),
2133
2318
  }, next_actions=[
2134
2319
  {"command": "slm recall '<query>' --json", "description": "Standard recall"},
2135
2320
  ])
@@ -2139,7 +2324,9 @@ def cmd_trace(args: Namespace) -> None:
2139
2324
  print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
2140
2325
  print(f"Results: {len(response.results)}")
2141
2326
  for i, r in enumerate(response.results, 1):
2142
- print(f"\n {i}. [{r.score:.3f}] {r.fact.content[:100]}")
2327
+ print(f"\n {i}. [relevance {r.relevance_score:.3f}] {r.fact.content[:100]}")
2328
+ if r.ranking_score is not None:
2329
+ print(f" ranking utility: {r.ranking_score:.6f}")
2143
2330
  if hasattr(r, "channel_scores") and r.channel_scores:
2144
2331
  for ch, sc in r.channel_scores.items():
2145
2332
  print(f" {ch}: {sc:.3f}")
@@ -2162,11 +2349,16 @@ def cmd_mcp(_args: Namespace) -> None:
2162
2349
  from superlocalmemory.infra.process_reaper import (
2163
2350
  ReaperConfig,
2164
2351
  find_orphans,
2352
+ is_mcp_server_process,
2165
2353
  kill_orphan,
2166
2354
  )
2167
2355
  _reaper_cfg = ReaperConfig(orphan_age_threshold_hours=0.0)
2168
2356
  for _orphan in find_orphans(_reaper_cfg):
2169
- kill_orphan(_orphan.pid, graceful_timeout_seconds=1.0)
2357
+ # A unified daemon is expected to be detached from the launching
2358
+ # shell and can therefore have PPID 1. The MCP reaper must never
2359
+ # treat that healthy shared daemon as an orphaned stdio server.
2360
+ if is_mcp_server_process(_orphan):
2361
+ kill_orphan(_orphan.pid, graceful_timeout_seconds=1.0)
2170
2362
  except Exception:
2171
2363
  pass # Never block MCP startup on cleanup failure
2172
2364
 
@@ -2414,7 +2606,6 @@ def _cmd_init_auto(
2414
2606
  from pathlib import Path
2415
2607
  from superlocalmemory.core.config import SLMConfig
2416
2608
  from superlocalmemory.storage.models import Mode
2417
- from superlocalmemory.cli.setup_wizard import _mark_complete
2418
2609
 
2419
2610
  # Step 1: write mode-A config (create-if-absent or --force).
2420
2611
  # Pass slm_data_dir explicitly so env-overridden paths are respected even
@@ -2428,8 +2619,7 @@ def _cmd_init_auto(
2428
2619
  sys.exit(1)
2429
2620
 
2430
2621
  # Step 2: mark complete (write .setup-complete sentinel).
2431
- # Write the sentinel directly using slm_data_dir to avoid the module-level
2432
- # _SLM_HOME resolved at import time (tests set the env var after import).
2622
+ # Write the sentinel directly using the already-selected namespace.
2433
2623
  try:
2434
2624
  import platform
2435
2625
  import time as _time
@@ -2461,7 +2651,6 @@ def _cmd_init_auto(
2461
2651
 
2462
2652
  def cmd_init(args: Namespace) -> None:
2463
2653
  """One-command setup: mode + hooks + IDE connect + warmup."""
2464
- from pathlib import Path
2465
2654
  from superlocalmemory.cli._lazy_init import slm_home
2466
2655
  from superlocalmemory.core.config import SLMConfig
2467
2656
 
@@ -2562,12 +2751,49 @@ def cmd_init(args: Namespace) -> None:
2562
2751
 
2563
2752
 
2564
2753
  def cmd_hooks(args: Namespace) -> None:
2565
- """Manage Claude Code hooks for invisible memory injection."""
2754
+ """Manage additive Claude Code or Codex memory lifecycle hooks."""
2566
2755
  from superlocalmemory.hooks.claude_code_hooks import (
2567
2756
  install_hooks, remove_hooks, check_status,
2568
2757
  )
2569
2758
 
2570
2759
  action = getattr(args, "action", "status")
2760
+ agent = getattr(args, "agent", "claude")
2761
+ dry_run = getattr(args, "dry_run", False)
2762
+ if agent == "codex":
2763
+ from superlocalmemory.hooks.codex_hooks import (
2764
+ install_hooks as install_codex_hooks,
2765
+ remove_hooks as remove_codex_hooks,
2766
+ check_status as check_codex_hooks,
2767
+ )
2768
+ if action == "install":
2769
+ result = install_codex_hooks(dry_run=dry_run)
2770
+ if result["success"]:
2771
+ prefix = "would be installed" if dry_run else "installed"
2772
+ print(f"SLM hooks {prefix} in Codex: {result['path']}")
2773
+ if result.get("hooks_added"):
2774
+ print(f" Hook types: {', '.join(result['hooks_added'])}")
2775
+ print(" Review and trust the new hooks in Codex with /hooks.")
2776
+ else:
2777
+ print(f"Installation failed: {result['errors']}")
2778
+ return
2779
+ if action == "remove":
2780
+ result = remove_codex_hooks(dry_run=dry_run)
2781
+ if result["success"]:
2782
+ prefix = "would be removed" if dry_run else "removed"
2783
+ print(f"SLM hooks {prefix} from Codex: {result['path']}")
2784
+ else:
2785
+ print(f"Removal failed: {result['errors']}")
2786
+ return
2787
+ result = check_codex_hooks()
2788
+ if result["installed"] is None:
2789
+ print(f"SLM Codex hooks: INDETERMINATE ({result['error']})")
2790
+ elif result["installed"]:
2791
+ print("SLM Codex hooks: INSTALLED")
2792
+ print(f" Hook types: {', '.join(result['hook_types'])}")
2793
+ else:
2794
+ print("SLM Codex hooks: NOT INSTALLED")
2795
+ print(" Run: slm hooks install --agent codex")
2796
+ return
2571
2797
  # Gate is OFF by default. --gate opts in (for brave users).
2572
2798
  include_gate = getattr(args, "gate", False)
2573
2799
 
@@ -2603,6 +2829,34 @@ def cmd_hooks(args: Namespace) -> None:
2603
2829
  print(" Or: slm init (full setup)")
2604
2830
 
2605
2831
 
2832
+ def cmd_codex(args: Namespace) -> None:
2833
+ """Manage explicit, SLM-owned Codex skills, agents, and lifecycle hooks."""
2834
+ from superlocalmemory.hooks.codex_assets import install_assets, remove_assets, status_assets
2835
+ from superlocalmemory.hooks.codex_hooks import install_hooks, remove_hooks, check_status
2836
+
2837
+ action, dry_run = getattr(args, "action", "status"), getattr(args, "dry_run", False)
2838
+ if action == "install":
2839
+ assets, hooks = install_assets(dry_run=dry_run), install_hooks(dry_run=dry_run)
2840
+ if assets.get("success") and hooks.get("success"):
2841
+ print(f"SLM Codex add-ons {'would be installed' if dry_run else 'installed'}: 7 skills, 2 subagents, 4 lifecycle hooks.")
2842
+ print("MCP wiring remains explicit: run `slm connect codex` if it is not already configured.")
2843
+ print("Review and trust newly installed hooks in Codex with /hooks.")
2844
+ else:
2845
+ print(f"Codex integration failed: {assets.get('errors', []) + hooks.get('errors', [])}")
2846
+ return
2847
+ if action == "remove":
2848
+ assets, hooks = remove_assets(dry_run=dry_run), remove_hooks(dry_run=dry_run)
2849
+ if assets.get("success") and hooks.get("success"):
2850
+ print("SLM-owned Codex add-ons removed; your MCP and non-SLM settings were left intact.")
2851
+ else:
2852
+ print(f"Codex removal failed: {assets.get('errors', []) + hooks.get('errors', [])}")
2853
+ return
2854
+ assets, hooks = status_assets(), check_status()
2855
+ print(f"SLM Codex add-ons: {'INSTALLED' if assets['installed'] and hooks['installed'] else 'NOT INSTALLED'}")
2856
+ print(f" Skills: {len(assets['skills'])}/7; subagents: {len(assets['agents'])}/2")
2857
+ print(f" Hooks: {', '.join(hooks.get('hook_types', [])) or 'none'}")
2858
+
2859
+
2606
2860
  def cmd_session_context(args: Namespace) -> None:
2607
2861
  """Print session context (for hook scripts and piping).
2608
2862
 
@@ -2828,7 +3082,15 @@ def cmd_observe(args: Namespace) -> None:
2828
3082
  engine = MemoryEngine(config)
2829
3083
  engine.initialize()
2830
3084
 
2831
- auto = AutoCapture(engine=engine)
3085
+ from superlocalmemory.core.engine_ingestion import (
3086
+ canonical_store_fn,
3087
+ local_trusted_actor_id,
3088
+ )
3089
+ auto = AutoCapture(store_fn=canonical_store_fn(
3090
+ engine,
3091
+ source_type="cli-observe",
3092
+ trusted_actor_id=local_trusted_actor_id("cli"),
3093
+ ))
2832
3094
  decision = auto.evaluate(content)
2833
3095
 
2834
3096
  if decision.capture:
@@ -2873,7 +3135,7 @@ def cmd_decay(args: Namespace) -> None:
2873
3135
  scheduler = ForgettingScheduler(
2874
3136
  engine._db, ebbinghaus, config.forgetting,
2875
3137
  )
2876
- result = scheduler.run_decay_cycle(pid, force=True)
3138
+ result = scheduler.run_decay_cycle(pid, force=True, dry_run=dry_run)
2877
3139
  except Exception as exc:
2878
3140
  if use_json:
2879
3141
  from superlocalmemory.cli.json_output import json_print
@@ -2938,7 +3200,7 @@ def cmd_quantize(args: Namespace) -> None:
2938
3200
  scheduler = EAPScheduler(
2939
3201
  engine._db, ebbinghaus, qstore, config.quantization,
2940
3202
  )
2941
- result = scheduler.run_eap_cycle(pid)
3203
+ result = scheduler.run_eap_cycle(pid, dry_run=dry_run)
2942
3204
  except Exception as exc:
2943
3205
  if use_json:
2944
3206
  from superlocalmemory.cli.json_output import json_print