superlocalmemory 3.6.22 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -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()
@@ -1687,6 +1799,38 @@ def _gather_optimize_surface_b() -> dict:
1687
1799
  return result
1688
1800
 
1689
1801
 
1802
+ def _readline_with_timeout(
1803
+ stream, timeout_sec: float,
1804
+ ) -> tuple[str | None, Exception | None]:
1805
+ """Read one line from a pipe-like stream without POSIX-only select().
1806
+
1807
+ Windows select() only accepts sockets, not subprocess pipes. A bounded
1808
+ helper thread keeps the embedding-worker probe cross-platform while
1809
+ preserving the existing timeout behavior.
1810
+ """
1811
+ import threading
1812
+
1813
+ result: dict[str, object] = {}
1814
+
1815
+ def _read() -> None:
1816
+ try:
1817
+ result["line"] = stream.readline()
1818
+ except Exception as exc: # noqa: BLE001 - returned as probe failure
1819
+ result["exc"] = exc
1820
+
1821
+ reader = threading.Thread(target=_read, daemon=True)
1822
+ reader.start()
1823
+ reader.join(timeout_sec)
1824
+ if reader.is_alive():
1825
+ return None, None
1826
+ line = result.get("line")
1827
+ exc = result.get("exc")
1828
+ return (
1829
+ line if isinstance(line, str) else None,
1830
+ exc if isinstance(exc, Exception) else None,
1831
+ )
1832
+
1833
+
1690
1834
  def cmd_doctor(args: Namespace) -> None:
1691
1835
  """Comprehensive pre-flight check — verify everything works.
1692
1836
 
@@ -1746,7 +1890,7 @@ def cmd_doctor(args: Namespace) -> None:
1746
1890
  ver = getattr(m, "__version__", "?")
1747
1891
  core_ok.append(mod)
1748
1892
  core_versions.append(f"{mod} {ver}")
1749
- except ImportError:
1893
+ except Exception: # dependency import may fail after module discovery
1750
1894
  pass
1751
1895
  if len(core_ok) == len(core_modules):
1752
1896
  _check("Core deps", "PASS", ", ".join(core_versions[:4]) + "...")
@@ -1763,7 +1907,7 @@ def cmd_doctor(args: Namespace) -> None:
1763
1907
  try:
1764
1908
  __import__(mod)
1765
1909
  search_ok.append(mod)
1766
- except ImportError:
1910
+ except Exception: # dependency import may fail after module discovery
1767
1911
  pass
1768
1912
  if len(search_ok) == len(search_mods):
1769
1913
  _check("Search deps", "PASS", "sentence-transformers, torch, sklearn, geoopt")
@@ -1777,7 +1921,7 @@ def cmd_doctor(args: Namespace) -> None:
1777
1921
  for mod in ["fastapi", "uvicorn", "websockets"]:
1778
1922
  try:
1779
1923
  __import__(mod)
1780
- except ImportError:
1924
+ except Exception: # dependency import may fail after module discovery
1781
1925
  dash_ok = False
1782
1926
  break
1783
1927
  if dash_ok:
@@ -1790,7 +1934,7 @@ def cmd_doctor(args: Namespace) -> None:
1790
1934
  try:
1791
1935
  import lightgbm
1792
1936
  _check("Learning deps", "PASS", f"lightgbm {lightgbm.__version__}")
1793
- except ImportError:
1937
+ except Exception: # dependency import may fail after module discovery
1794
1938
  _check("Learning deps", "WARN", "lightgbm not installed",
1795
1939
  "pip install lightgbm")
1796
1940
  except OSError as exc:
@@ -1807,7 +1951,7 @@ def cmd_doctor(args: Namespace) -> None:
1807
1951
  try:
1808
1952
  __import__(mod)
1809
1953
  perf_ok.append(mod)
1810
- except ImportError:
1954
+ except Exception: # dependency import may fail after module discovery
1811
1955
  pass
1812
1956
  if perf_ok:
1813
1957
  _check("Performance deps", "PASS", "orjson")
@@ -1839,10 +1983,11 @@ def cmd_doctor(args: Namespace) -> None:
1839
1983
  proc.stdin.write(_json.dumps({"cmd": "ping"}) + "\n")
1840
1984
  proc.stdin.flush()
1841
1985
 
1842
- import select as _sel
1843
- ready, _, _ = _sel.select([proc.stdout], [], [], 30)
1844
- if ready:
1845
- resp = _json.loads(proc.stdout.readline())
1986
+ line, read_exc = _readline_with_timeout(proc.stdout, 30)
1987
+ if read_exc is not None:
1988
+ _check("Embedding worker", "FAIL", str(read_exc), "slm warmup")
1989
+ elif line is not None:
1990
+ resp = _json.loads(line or "{}")
1846
1991
  if resp.get("ok"):
1847
1992
  _check(
1848
1993
  "Embedding worker", "PASS",
@@ -1930,7 +2075,8 @@ def cmd_doctor(args: Namespace) -> None:
1930
2075
  pass # Config load failed — already caught above
1931
2076
 
1932
2077
  # 9. Disk space
1933
- slm_home = Path.home() / ".superlocalmemory"
2078
+ from superlocalmemory.infra.data_root import canonical_data_root
2079
+ slm_home = canonical_data_root()
1934
2080
  try:
1935
2081
  usage = shutil.disk_usage(slm_home if slm_home.exists() else Path.home())
1936
2082
  free_gb = usage.free / (1024 ** 3)
@@ -1975,8 +2121,8 @@ def cmd_doctor(args: Namespace) -> None:
1975
2121
  "WARN",
1976
2122
  "System Python is externally managed (EXTERNALLY-MANAGED marker found). "
1977
2123
  "pip install may fail with PEP 668 error.",
1978
- "Use pipx for an isolated install: pipx install superlocalmemory "
1979
- "(last-resort only: pip install --break-system-packages superlocalmemory)",
2124
+ "Use an isolated install: pipx install superlocalmemory "
2125
+ "or uv tool install superlocalmemory",
1980
2126
  )
1981
2127
  else:
1982
2128
  _check(
@@ -1998,7 +2144,7 @@ def cmd_doctor(args: Namespace) -> None:
1998
2144
  "disabled (optimize.json enabled=false) — caching/compression not active"
1999
2145
  + (f" [{_error}]" if _error else ""),
2000
2146
  fix="Enable via dashboard Optimize tab or set enabled=true"
2001
- " in ~/.superlocalmemory/optimize.json",
2147
+ f" in {slm_home / 'optimize.json'}",
2002
2148
  )
2003
2149
  else:
2004
2150
  _surfaces = []
@@ -2063,14 +2209,85 @@ def cmd_doctor(args: Namespace) -> None:
2063
2209
 
2064
2210
  def cmd_trace(args: Namespace) -> None:
2065
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
+
2066
2285
  from superlocalmemory.core.engine import MemoryEngine
2067
2286
  from superlocalmemory.core.config import SLMConfig
2068
-
2069
- use_json = getattr(args, 'json', False)
2070
2287
  try:
2071
2288
  config = SLMConfig.load()
2072
2289
  engine = MemoryEngine(config)
2073
- limit = getattr(args, 'limit', 10)
2290
+ engine.initialize()
2074
2291
  response = engine.recall(args.query, limit=limit)
2075
2292
  except Exception as exc:
2076
2293
  if use_json:
@@ -2081,22 +2298,23 @@ def cmd_trace(args: Namespace) -> None:
2081
2298
 
2082
2299
  if use_json:
2083
2300
  from superlocalmemory.cli.json_output import json_print
2084
- items = []
2085
- for r in response.results:
2086
- item = {
2087
- "fact_id": r.fact.fact_id, "content": r.fact.content[:200],
2088
- "score": round(r.score, 3),
2089
- }
2090
- if hasattr(r, "channel_scores") and r.channel_scores:
2091
- item["channel_scores"] = {
2092
- k: round(v, 3) for k, v in r.channel_scores.items()
2093
- }
2094
- 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
+ )
2095
2311
  json_print("trace", data={
2096
2312
  "query": args.query,
2097
2313
  "query_type": getattr(response, "query_type", "unknown"),
2098
2314
  "retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
2099
2315
  "results": items, "count": len(items),
2316
+ "no_confident_match": no_confident_match,
2317
+ **recall_response_metadata(response),
2100
2318
  }, next_actions=[
2101
2319
  {"command": "slm recall '<query>' --json", "description": "Standard recall"},
2102
2320
  ])
@@ -2106,7 +2324,9 @@ def cmd_trace(args: Namespace) -> None:
2106
2324
  print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
2107
2325
  print(f"Results: {len(response.results)}")
2108
2326
  for i, r in enumerate(response.results, 1):
2109
- 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}")
2110
2330
  if hasattr(r, "channel_scores") and r.channel_scores:
2111
2331
  for ch, sc in r.channel_scores.items():
2112
2332
  print(f" {ch}: {sc:.3f}")
@@ -2129,11 +2349,16 @@ def cmd_mcp(_args: Namespace) -> None:
2129
2349
  from superlocalmemory.infra.process_reaper import (
2130
2350
  ReaperConfig,
2131
2351
  find_orphans,
2352
+ is_mcp_server_process,
2132
2353
  kill_orphan,
2133
2354
  )
2134
2355
  _reaper_cfg = ReaperConfig(orphan_age_threshold_hours=0.0)
2135
2356
  for _orphan in find_orphans(_reaper_cfg):
2136
- 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)
2137
2362
  except Exception:
2138
2363
  pass # Never block MCP startup on cleanup failure
2139
2364
 
@@ -2381,7 +2606,6 @@ def _cmd_init_auto(
2381
2606
  from pathlib import Path
2382
2607
  from superlocalmemory.core.config import SLMConfig
2383
2608
  from superlocalmemory.storage.models import Mode
2384
- from superlocalmemory.cli.setup_wizard import _mark_complete
2385
2609
 
2386
2610
  # Step 1: write mode-A config (create-if-absent or --force).
2387
2611
  # Pass slm_data_dir explicitly so env-overridden paths are respected even
@@ -2395,8 +2619,7 @@ def _cmd_init_auto(
2395
2619
  sys.exit(1)
2396
2620
 
2397
2621
  # Step 2: mark complete (write .setup-complete sentinel).
2398
- # Write the sentinel directly using slm_data_dir to avoid the module-level
2399
- # _SLM_HOME resolved at import time (tests set the env var after import).
2622
+ # Write the sentinel directly using the already-selected namespace.
2400
2623
  try:
2401
2624
  import platform
2402
2625
  import time as _time
@@ -2428,7 +2651,6 @@ def _cmd_init_auto(
2428
2651
 
2429
2652
  def cmd_init(args: Namespace) -> None:
2430
2653
  """One-command setup: mode + hooks + IDE connect + warmup."""
2431
- from pathlib import Path
2432
2654
  from superlocalmemory.cli._lazy_init import slm_home
2433
2655
  from superlocalmemory.core.config import SLMConfig
2434
2656
 
@@ -2529,12 +2751,49 @@ def cmd_init(args: Namespace) -> None:
2529
2751
 
2530
2752
 
2531
2753
  def cmd_hooks(args: Namespace) -> None:
2532
- """Manage Claude Code hooks for invisible memory injection."""
2754
+ """Manage additive Claude Code or Codex memory lifecycle hooks."""
2533
2755
  from superlocalmemory.hooks.claude_code_hooks import (
2534
2756
  install_hooks, remove_hooks, check_status,
2535
2757
  )
2536
2758
 
2537
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
2538
2797
  # Gate is OFF by default. --gate opts in (for brave users).
2539
2798
  include_gate = getattr(args, "gate", False)
2540
2799
 
@@ -2570,6 +2829,34 @@ def cmd_hooks(args: Namespace) -> None:
2570
2829
  print(" Or: slm init (full setup)")
2571
2830
 
2572
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
+
2573
2860
  def cmd_session_context(args: Namespace) -> None:
2574
2861
  """Print session context (for hook scripts and piping).
2575
2862
 
@@ -2795,7 +3082,15 @@ def cmd_observe(args: Namespace) -> None:
2795
3082
  engine = MemoryEngine(config)
2796
3083
  engine.initialize()
2797
3084
 
2798
- 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
+ ))
2799
3094
  decision = auto.evaluate(content)
2800
3095
 
2801
3096
  if decision.capture:
@@ -2840,7 +3135,7 @@ def cmd_decay(args: Namespace) -> None:
2840
3135
  scheduler = ForgettingScheduler(
2841
3136
  engine._db, ebbinghaus, config.forgetting,
2842
3137
  )
2843
- result = scheduler.run_decay_cycle(pid, force=True)
3138
+ result = scheduler.run_decay_cycle(pid, force=True, dry_run=dry_run)
2844
3139
  except Exception as exc:
2845
3140
  if use_json:
2846
3141
  from superlocalmemory.cli.json_output import json_print
@@ -2905,7 +3200,7 @@ def cmd_quantize(args: Namespace) -> None:
2905
3200
  scheduler = EAPScheduler(
2906
3201
  engine._db, ebbinghaus, qstore, config.quantization,
2907
3202
  )
2908
- result = scheduler.run_eap_cycle(pid)
3203
+ result = scheduler.run_eap_cycle(pid, dry_run=dry_run)
2909
3204
  except Exception as exc:
2910
3205
  if use_json:
2911
3206
  from superlocalmemory.cli.json_output import json_print