nexo-brain 2.3.0 → 2.3.2

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 (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. package/tests/test_migrations.py +0 -137
@@ -1,592 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Sleep System v2 — The brain dreams.
4
-
5
- Before: 834 lines with word-overlap "intelligence" for learning consolidation.
6
- Now: Stage A (mechanical cleanup) stays pure Python. Stage B (dreaming) uses
7
- Claude CLI (opus) to understand, deduplicate, and prune with real intelligence.
8
-
9
- Triggered hourly via LaunchAgent. Runs ONCE per day, first time Mac is awake.
10
- If interrupted (power loss, crash), resumes on next trigger.
11
-
12
- Stage A — Housekeeping (Python pure):
13
- Delete old logs, rotate files, trim JSON. No intelligence needed.
14
-
15
- Stage B — Dreaming (Claude CLI opus):
16
- Review learnings for duplicates and contradictions with UNDERSTANDING.
17
- Prune MEMORY.md if over limit. Clean preferences. Compress old observations.
18
- One CLI call that does what 500 lines of word-overlap couldn't.
19
- """
20
-
21
- import fcntl
22
- import json
23
- import os
24
- import re
25
- import shutil
26
- import sqlite3
27
- import subprocess
28
- import sys
29
- from datetime import datetime, date, timedelta
30
- from pathlib import Path
31
-
32
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
33
-
34
- # ─── Paths ────────────────────────────────────────────────────────────────────
35
- CLAUDE_DIR = NEXO_HOME
36
- BRAIN_DIR = CLAUDE_DIR / "brain"
37
- COORD_DIR = CLAUDE_DIR / "coordination"
38
- MEMORY_DIR = CLAUDE_DIR / "memory"
39
- DAEMON_LOGS_DIR = CLAUDE_DIR / "daemon" / "logs"
40
-
41
- DAILY_SUMMARIES_DIR = BRAIN_DIR / "daily_summaries"
42
- SESSION_ARCHIVE_DIR = BRAIN_DIR / "session_archive"
43
- COMPRESSED_MEMORIES_DIR = BRAIN_DIR / "compressed_memories"
44
-
45
- HEARTBEAT_LOG = COORD_DIR / "heartbeat-log.json"
46
- REFLECTION_LOG = COORD_DIR / "reflection-log.json"
47
- SLEEP_LOG = COORD_DIR / "sleep-log.json"
48
-
49
- MEMORY_MD = NEXO_HOME / "memory" / "MEMORY.md"
50
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
51
- CLAUDE_MEM_DB = Path.home() / ".claude-mem" / "claude-mem.db"
52
- CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
53
-
54
- LAST_RUN_FILE = COORD_DIR / "sleep-last-run"
55
- LOCK_FILE = COORD_DIR / "sleep.lock"
56
- PROCESS_LOCK = COORD_DIR / "sleep-process.lock"
57
-
58
- TODAY = date.today()
59
- NOW = datetime.now()
60
- TIMESTAMP = NOW.strftime("%Y-%m-%d %H:%M")
61
-
62
-
63
- # ─── Run-once & resume logic (unchanged from v1) ──────────────────────────────
64
-
65
- def already_ran_today() -> bool:
66
- if not LAST_RUN_FILE.exists():
67
- return False
68
- try:
69
- return LAST_RUN_FILE.read_text().strip() == str(TODAY)
70
- except Exception:
71
- return False
72
-
73
-
74
- def was_interrupted() -> bool:
75
- if not LOCK_FILE.exists():
76
- return False
77
- try:
78
- lock_data = json.loads(LOCK_FILE.read_text())
79
- if lock_data.get("date") != str(TODAY):
80
- LOCK_FILE.unlink()
81
- return False
82
- lock_pid = lock_data.get("pid")
83
- if lock_pid:
84
- try:
85
- os.kill(lock_pid, 0)
86
- log(f"Another instance running (PID {lock_pid}). Exiting.")
87
- return False
88
- except ProcessLookupError:
89
- log(f"Interrupted run (phase: {lock_data.get('phase', '?')}). Resuming.")
90
- return True
91
- except PermissionError:
92
- return False
93
- LOCK_FILE.unlink()
94
- return False
95
- except Exception:
96
- LOCK_FILE.unlink(missing_ok=True)
97
- return False
98
-
99
-
100
- def get_interrupted_phase() -> str:
101
- try:
102
- return json.loads(LOCK_FILE.read_text()).get("phase", "stage_a")
103
- except Exception:
104
- return "stage_a"
105
-
106
-
107
- def set_lock(phase: str):
108
- save_json(LOCK_FILE, {"date": str(TODAY), "phase": phase, "started": TIMESTAMP, "pid": os.getpid()})
109
-
110
-
111
- def mark_complete():
112
- LAST_RUN_FILE.write_text(str(TODAY))
113
- LOCK_FILE.unlink(missing_ok=True)
114
-
115
-
116
- # ─── Helpers ──────────────────────────────────────────────────────────────────
117
-
118
- def log(msg: str):
119
- ts = datetime.now().strftime("%Y-%m-%d %H:%M")
120
- print(f"[{ts}] {msg}")
121
-
122
-
123
- def load_json(path: Path, default=None):
124
- if not path.exists():
125
- return default if default is not None else {}
126
- try:
127
- return json.loads(path.read_text())
128
- except Exception:
129
- return default if default is not None else {}
130
-
131
-
132
- def save_json(path: Path, data):
133
- path.parent.mkdir(parents=True, exist_ok=True)
134
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
135
-
136
-
137
- def parse_date_from_stem(stem: str):
138
- m = re.search(r'(\d{4}-\d{2}-\d{2})', stem)
139
- if m:
140
- try:
141
- return date.fromisoformat(m.group(1))
142
- except ValueError:
143
- return None
144
- return None
145
-
146
-
147
- def append_sleep_log(entry: dict):
148
- entries = load_json(SLEEP_LOG, [])
149
- if not isinstance(entries, list):
150
- entries = []
151
- entries.append(entry)
152
- if len(entries) > 90:
153
- entries = entries[-90:]
154
- save_json(SLEEP_LOG, entries)
155
-
156
-
157
- # ─── Stage A: Mechanical cleanup (UNCHANGED from v1) ─────────────────────────
158
-
159
- def stage_a_cleanup() -> dict:
160
- """Pure Python cleanup. No LLM calls."""
161
- stats = {
162
- "a1_daily_summaries_deleted": 0,
163
- "a2_session_archives_deleted": 0,
164
- "a3_logs_rotated": 0,
165
- "a4_compressed_memories_deleted": 0,
166
- "a5_heartbeat_trimmed": False,
167
- "a6_reflection_trimmed": False,
168
- "a7_daemon_logs_deleted": 0,
169
- }
170
-
171
- # A1: Delete daily_summaries/*.md >90 days
172
- cutoff_90 = TODAY - timedelta(days=90)
173
- if DAILY_SUMMARIES_DIR.exists():
174
- for f in DAILY_SUMMARIES_DIR.glob("*.md"):
175
- d = parse_date_from_stem(f.stem)
176
- if d and d < cutoff_90:
177
- try:
178
- f.unlink()
179
- stats["a1_daily_summaries_deleted"] += 1
180
- except Exception:
181
- pass
182
-
183
- # A2: Delete session_archive/*.jsonl >30 days
184
- cutoff_30 = TODAY - timedelta(days=30)
185
- if SESSION_ARCHIVE_DIR.exists():
186
- for f in SESSION_ARCHIVE_DIR.glob("*.jsonl"):
187
- d = parse_date_from_stem(f.stem)
188
- if d and d < cutoff_30:
189
- try:
190
- f.unlink()
191
- stats["a2_session_archives_deleted"] += 1
192
- except Exception:
193
- pass
194
-
195
- # A3: Rotate coordination/*-stdout.log if >5MB
196
- if COORD_DIR.exists():
197
- for f in COORD_DIR.glob("*-stdout.log"):
198
- try:
199
- if f.stat().st_size > 5 * 1024 * 1024:
200
- lines = f.read_text().splitlines()
201
- keep = lines[-500:]
202
- f.write_text("\n".join(keep) + "\n")
203
- stats["a3_logs_rotated"] += 1
204
- except Exception:
205
- pass
206
-
207
- # A4: Delete compressed_memories/week_*.md >180 days
208
- cutoff_180 = TODAY - timedelta(days=180)
209
- if COMPRESSED_MEMORIES_DIR.exists():
210
- for f in COMPRESSED_MEMORIES_DIR.glob("week_*.md"):
211
- d = parse_date_from_stem(f.stem)
212
- if d and d < cutoff_180:
213
- try:
214
- f.unlink()
215
- stats["a4_compressed_memories_deleted"] += 1
216
- except Exception:
217
- pass
218
-
219
- # A5: Trim heartbeat-log.json to 200 entries
220
- if HEARTBEAT_LOG.exists():
221
- try:
222
- data = load_json(HEARTBEAT_LOG, [])
223
- if isinstance(data, list) and len(data) > 200:
224
- save_json(HEARTBEAT_LOG, data[-200:])
225
- stats["a5_heartbeat_trimmed"] = True
226
- except Exception:
227
- pass
228
-
229
- # A6: Trim reflection-log.json to 60 entries
230
- if REFLECTION_LOG.exists():
231
- try:
232
- data = load_json(REFLECTION_LOG, [])
233
- if isinstance(data, list) and len(data) > 60:
234
- save_json(REFLECTION_LOG, data[-60:])
235
- stats["a6_reflection_trimmed"] = True
236
- except Exception:
237
- pass
238
-
239
- # A7: Delete daemon/logs/ dirs >14 days
240
- cutoff_14 = TODAY - timedelta(days=14)
241
- if DAEMON_LOGS_DIR.exists():
242
- for d_path in sorted(DAEMON_LOGS_DIR.iterdir()):
243
- if not d_path.is_dir():
244
- continue
245
- d = parse_date_from_stem(d_path.name)
246
- if d and d < cutoff_14:
247
- try:
248
- shutil.rmtree(d_path)
249
- stats["a7_daemon_logs_deleted"] += 1
250
- except Exception:
251
- pass
252
-
253
- # A8: Delete cortex/logs/*.log >7 days, truncate launchd >5MB
254
- cutoff_7 = TODAY - timedelta(days=7)
255
- cortex_logs = NEXO_HOME / "cortex" / "logs"
256
- if cortex_logs.exists():
257
- for f in cortex_logs.glob("*.log"):
258
- if f.name.startswith("launchd-"):
259
- try:
260
- if f.stat().st_size > 5 * 1024 * 1024:
261
- lines = f.read_text().splitlines()
262
- f.write_text("\n".join(lines[-500:]) + "\n")
263
- stats["a3_logs_rotated"] += 1
264
- except Exception:
265
- pass
266
- continue
267
- d = parse_date_from_stem(f.stem)
268
- if d and d < cutoff_7:
269
- try:
270
- f.unlink()
271
- except Exception:
272
- pass
273
-
274
- return stats
275
-
276
-
277
- # ─── Stage B: Dreaming (Claude CLI) ─────────────────────────────────────────
278
-
279
- def collect_brain_state() -> dict:
280
- """Collect all data the CLI needs to dream."""
281
- state = {"learnings": [], "preferences": [], "memory_md_lines": 0,
282
- "claude_mem_old": 0, "feedback_count": 0}
283
-
284
- if NEXO_DB.exists():
285
- try:
286
- conn = sqlite3.connect(str(NEXO_DB))
287
- conn.row_factory = sqlite3.Row
288
-
289
- # Learnings
290
- rows = conn.execute(
291
- "SELECT id, title, content, category, created_at FROM learnings "
292
- "WHERE status='active' ORDER BY id"
293
- ).fetchall()
294
- state["learnings"] = [dict(r) for r in rows]
295
-
296
- # Preferences
297
- rows = conn.execute("SELECT key, value, category, updated_at FROM preferences").fetchall()
298
- state["preferences"] = [dict(r) for r in rows]
299
-
300
- conn.close()
301
- except Exception as e:
302
- log(f"DB error: {e}")
303
-
304
- # MEMORY.md
305
- if MEMORY_MD.exists():
306
- state["memory_md_lines"] = len(MEMORY_MD.read_text().splitlines())
307
-
308
- # claude-mem.db old observations
309
- if CLAUDE_MEM_DB.exists():
310
- try:
311
- cutoff = int((datetime.now() - timedelta(days=60)).timestamp() * 1000)
312
- conn = sqlite3.connect(str(CLAUDE_MEM_DB))
313
- state["claude_mem_old"] = conn.execute(
314
- "SELECT COUNT(*) FROM observations WHERE created_at_epoch < ?", (cutoff,)
315
- ).fetchone()[0]
316
- conn.close()
317
- except Exception:
318
- pass
319
-
320
- # Feedback count
321
- state["feedback_count"] = len(list(MEMORY_MD.parent.glob("feedback_*.md")))
322
-
323
- return state
324
-
325
-
326
- def should_dream(state: dict) -> bool:
327
- """Check if there's enough to justify a CLI call."""
328
- return (
329
- len(state["learnings"]) > 10
330
- or state["memory_md_lines"] > 170
331
- or len(state["preferences"]) > 5
332
- or state["claude_mem_old"] > 500
333
- )
334
-
335
-
336
- def dream(state: dict) -> dict:
337
- """The brain dreams — CLI does the intelligent work."""
338
-
339
- # Truncate learnings JSON if too large
340
- learnings_json = json.dumps(state["learnings"], ensure_ascii=False, indent=1)
341
- if len(learnings_json) > 15000:
342
- learnings_json = learnings_json[:15000] + "\n... (truncated)"
343
-
344
- tasks = []
345
-
346
- tasks.append(f"""TASK 1: LEARNING CONSOLIDATION ({len(state['learnings'])} active)
347
- Review these learnings and identify:
348
- a) DUPLICATES: learnings that say the same thing differently.
349
- b) CONTRADICTIONS: learnings that contradict each other.
350
- c) STALE: learnings about bugs/issues fixed >60 days ago that are never referenced.
351
-
352
- Write your findings to {COORD_DIR}/sleep-report.md with sections:
353
- - "## Duplicates to archive" — list learning IDs to archive and why
354
- - "## Contradictions" — pairs of conflicting learnings
355
- - "## Stale candidates" — IDs of learnings that may be obsolete
356
-
357
- Also write a machine-readable file {COORD_DIR}/sleep-actions.json:
358
- {{"archive_ids": [1, 2, 3], "contradiction_pairs": [[4, 5]], "stale_ids": [6, 7]}}
359
-
360
- The wrapper will execute the actual DB operations based on this JSON.
361
-
362
- LEARNINGS:
363
- {learnings_json}""")
364
-
365
- if state["memory_md_lines"] > 170:
366
- tasks.append(f"""TASK 2: MEMORY.MD COMPRESSION ({state['memory_md_lines']} lines, limit 200)
367
- File: {MEMORY_MD}
368
- Read it, compress resolved incidents >21 days, merge duplicates.
369
- NEVER delete: credentials, legal entity info, CRITICAL rules, infrastructure.
370
- Target: <180 lines.""")
371
-
372
- if len(state["preferences"]) > 5:
373
- tasks.append(f"""TASK 3: PREFERENCES CLEANUP ({len(state['preferences'])} entries)
374
- Review the preferences and identify duplicate keys.
375
- Add to sleep-actions.json: "duplicate_preference_keys": ["key1", "key2", ...]
376
- The wrapper will handle the actual DB cleanup safely.""")
377
-
378
- if state["claude_mem_old"] > 500:
379
- tasks.append(f"""TASK 4: OLD OBSERVATIONS ({state['claude_mem_old']} entries >60d)
380
- Note in sleep-report.md that old observations should be cleaned.
381
- Add to sleep-actions.json: "clean_old_observations": true
382
- The wrapper will handle the actual DB cleanup safely.""")
383
-
384
- tasks_str = "\n\n".join(tasks)
385
-
386
- prompt = f"""FIRST: Call nexo_startup(task='deep-sleep nightly maintenance') to register this session.
387
-
388
- You are NEXO Sleep — the nightly brain maintenance process.
389
- Like a human brain during sleep: consolidate important memories, discard noise,
390
- detect conflicts, prepare state for tomorrow.
391
- Use nexo_learning_add, nexo_followup_create, nexo_session_diary_write and other MCP tools directly.
392
-
393
- BRAIN STATE:
394
- - {len(state['learnings'])} active learnings
395
- - {state['memory_md_lines']} lines in MEMORY.md (limit: 200)
396
- - {len(state['preferences'])} preferences
397
- - {state['feedback_count']} feedback files
398
- - {state['claude_mem_old']} old observations (>60d)
399
-
400
- {tasks_str}
401
-
402
- ABSOLUTE RULES:
403
- - NEVER delete legal entity info (LLC, SLU, EIN, NIF, project)
404
- - NEVER delete credentials, tokens, API keys, secrets
405
- - NEVER delete rules marked CRITICAL or MAX PRIORITY
406
- - NEVER delete infrastructure info (servers, repos, deploys)
407
- - When in doubt, DON'T delete
408
-
409
- Write a summary to {COORD_DIR}/sleep-report.md when done.
410
- Execute without asking."""
411
-
412
- log("Stage B: Invoking Claude CLI (opus) — dreaming...")
413
- env = os.environ.copy()
414
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
415
- env.pop("CLAUDECODE", None)
416
- env.pop("CLAUDE_CODE", None)
417
-
418
- try:
419
- result = subprocess.run(
420
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
421
- "--output-format", "text",
422
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
423
- capture_output=True, text=True, timeout=21600, env=env
424
- )
425
-
426
- if result.returncode != 0:
427
- log(f"Stage B: CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
428
- return {"error": result.returncode}
429
-
430
- log(f"Stage B: Dreaming complete. Output: {len(result.stdout or '')} chars")
431
- return {"ok": True, "output_len": len(result.stdout or "")}
432
-
433
- except subprocess.TimeoutExpired:
434
- log("Stage B: CLI timed out (600s)")
435
- return {"error": "timeout"}
436
- except Exception as e:
437
- log(f"Stage B: Exception: {e}")
438
- return {"error": str(e)}
439
-
440
-
441
- def execute_dream_actions(actions: dict, state: dict):
442
- """Execute the DB actions decided by CLI, safely in Python."""
443
- log("Stage B2: Executing dream actions...")
444
-
445
- # Archive duplicate/stale learnings
446
- archive_ids = actions.get("archive_ids", []) + actions.get("stale_ids", [])
447
- if archive_ids and NEXO_DB.exists():
448
- try:
449
- conn = sqlite3.connect(str(NEXO_DB))
450
- for lid in archive_ids:
451
- if isinstance(lid, int):
452
- conn.execute(
453
- "UPDATE learnings SET status='archived' WHERE id=? AND status='active'",
454
- (lid,)
455
- )
456
- conn.commit()
457
- conn.close()
458
- log(f" Archived {len(archive_ids)} learnings: {archive_ids}")
459
- except Exception as e:
460
- log(f" Error archiving learnings: {e}")
461
-
462
- # Clean duplicate preferences
463
- dup_keys = actions.get("duplicate_preference_keys", [])
464
- if dup_keys and NEXO_DB.exists():
465
- try:
466
- conn = sqlite3.connect(str(NEXO_DB))
467
- for key in dup_keys:
468
- if isinstance(key, str):
469
- # Keep newest, delete older duplicates
470
- conn.execute(
471
- "DELETE FROM preferences WHERE key = ? AND rowid NOT IN "
472
- "(SELECT rowid FROM preferences WHERE key = ? ORDER BY updated_at DESC LIMIT 1)",
473
- (key, key)
474
- )
475
- conn.commit()
476
- conn.close()
477
- log(f" Cleaned {len(dup_keys)} duplicate preference keys")
478
- except Exception as e:
479
- log(f" Error cleaning preferences: {e}")
480
-
481
- # Clean old observations
482
- if actions.get("clean_old_observations") and CLAUDE_MEM_DB.exists():
483
- try:
484
- cutoff_ms = int((datetime.now() - timedelta(days=60)).timestamp() * 1000)
485
- conn = sqlite3.connect(str(CLAUDE_MEM_DB))
486
- deleted = conn.execute(
487
- "DELETE FROM observations WHERE created_at_epoch < ? "
488
- "AND discovery_tokens < 300 "
489
- "AND id NOT IN (SELECT id FROM observations WHERE "
490
- "title LIKE '%CRITICO%' OR title LIKE '%credential%' "
491
- "OR title LIKE '%token%' OR title LIKE '%API%' "
492
- "OR title LIKE '%LLC%' OR title LIKE '%SLU%') "
493
- "LIMIT 200",
494
- (cutoff_ms,)
495
- ).rowcount
496
- conn.execute(
497
- "DELETE FROM observations_fts WHERE rowid NOT IN "
498
- "(SELECT id FROM observations)"
499
- )
500
- conn.execute("VACUUM")
501
- conn.commit()
502
- conn.close()
503
- log(f" Cleaned {deleted} old observations")
504
- except Exception as e:
505
- log(f" Error cleaning observations: {e}")
506
-
507
- log("Stage B2: Actions complete.")
508
-
509
-
510
- # ─── Main ────────────────────────────────────────────────────────────────────
511
-
512
- def main():
513
- log("=" * 60)
514
- log("NEXO Sleep System v2 starting")
515
-
516
- # Process lock
517
- try:
518
- lock_fd = open(PROCESS_LOCK, "w")
519
- fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
520
- lock_fd.write(str(os.getpid()))
521
- lock_fd.flush()
522
- except (IOError, OSError):
523
- log("Another sleep instance running. Exiting.")
524
- sys.exit(0)
525
-
526
- try:
527
- if already_ran_today():
528
- log("Already ran today. Exiting.")
529
- sys.exit(0)
530
-
531
- start_phase = "stage_a"
532
- if was_interrupted():
533
- start_phase = get_interrupted_phase()
534
-
535
- run_log = {"date": str(TODAY), "started": TIMESTAMP,
536
- "stage_a": None, "stage_b": None, "completed": None}
537
-
538
- # Stage A: Housekeeping (mechanical)
539
- if start_phase == "stage_a":
540
- set_lock("stage_a")
541
- log("─── Stage A: Housekeeping ───")
542
- run_log["stage_a"] = stage_a_cleanup()
543
-
544
- # Stage B: Dreaming (intelligent)
545
- set_lock("stage_b")
546
- log("─── Stage B: Dreaming ───")
547
- state = collect_brain_state()
548
-
549
- if should_dream(state):
550
- log(f"Brain state: {len(state['learnings'])} learnings, "
551
- f"{state['memory_md_lines']} MEMORY lines, "
552
- f"{state['claude_mem_old']} old observations")
553
- run_log["stage_b"] = dream(state)
554
-
555
- # Stage B2: Execute actions from CLI output
556
- actions_file = COORD_DIR / "sleep-actions.json"
557
- if actions_file.exists():
558
- try:
559
- actions = json.loads(actions_file.read_text())
560
- execute_dream_actions(actions, state)
561
- except Exception as e:
562
- log(f"Stage B2: Error executing actions: {e}")
563
- else:
564
- log("Brain is clean — no dreaming needed.")
565
- run_log["stage_b"] = {"skipped": True}
566
-
567
- # Done
568
- run_log["completed"] = datetime.now().strftime("%Y-%m-%d %H:%M")
569
- mark_complete()
570
- append_sleep_log(run_log)
571
- log(f"NEXO Sleep v2 complete at {run_log['completed']}")
572
-
573
- # Register for catch-up
574
- try:
575
- state_file = NEXO_HOME / "operations" / ".catchup-state.json"
576
- st = json.loads(state_file.read_text()) if state_file.exists() else {}
577
- st["sleep"] = datetime.now().isoformat()
578
- state_file.write_text(json.dumps(st, indent=2))
579
- except Exception:
580
- pass
581
-
582
- finally:
583
- try:
584
- fcntl.flock(lock_fd, fcntl.LOCK_UN)
585
- lock_fd.close()
586
- PROCESS_LOCK.unlink(missing_ok=True)
587
- except Exception:
588
- pass
589
-
590
-
591
- if __name__ == "__main__":
592
- main()
@@ -1,35 +0,0 @@
1
- #!/bin/bash
2
- # NEXO Snapshot Restore — restores files from a snapshot directory.
3
- # Usage: nexo-snapshot-restore.sh <snapshot-dir>
4
- set -euo pipefail
5
-
6
- SNAP_DIR="${1:?Usage: nexo-snapshot-restore.sh <snapshot-dir>}"
7
- MANIFEST="$SNAP_DIR/manifest.json"
8
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
9
- RESTORE_LOG="$NEXO_HOME/logs/snapshot-restores.log"
10
-
11
- if [ ! -f "$MANIFEST" ]; then
12
- echo "ERROR: No manifest.json in $SNAP_DIR" >&2
13
- exit 1
14
- fi
15
-
16
- TS=$(date "+%Y-%m-%d %H:%M:%S")
17
- mkdir -p "$(dirname "$RESTORE_LOG")"
18
- echo "[$TS] Restoring from $SNAP_DIR" >> "$RESTORE_LOG"
19
-
20
- python3 -c "
21
- import json, shutil, os
22
- manifest = json.load(open('$MANIFEST'))
23
- for rel_path in manifest.get('files', []):
24
- src = os.path.join('$SNAP_DIR', 'files', rel_path)
25
- dst = os.path.expanduser('~/' + rel_path)
26
- if os.path.exists(src):
27
- os.makedirs(os.path.dirname(dst), exist_ok=True)
28
- shutil.copy2(src, dst)
29
- print(f' Restored: {rel_path}')
30
- else:
31
- print(f' SKIP (not in snapshot): {rel_path}')
32
- print('Restore complete.')
33
- "
34
-
35
- echo "[$TS] Restore complete from $SNAP_DIR" >> "$RESTORE_LOG"