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
@@ -41,8 +41,12 @@ record = {
41
41
  print(json.dumps(record))
42
42
  " >> "$LOG_FILE" 2>/dev/null
43
43
 
44
- # ── Layer 1: Auto-diary every 10 tool calls ─────────────────────────
45
- COUNTER_FILE="$NEXO_HOME/operations/.tool-call-count"
44
+ # ── Layer 1: Auto-diary every 10 tool calls (session-scoped) ─────────
45
+ # Extract session_id for per-session counters (prevents cross-terminal contamination)
46
+ SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id','global'))" 2>/dev/null || echo "global")
47
+ COUNTER_DIR="$NEXO_HOME/operations/counters"
48
+ mkdir -p "$COUNTER_DIR"
49
+ COUNTER_FILE="$COUNTER_DIR/.tool-call-count-${SESSION_ID}"
46
50
  NEXO_DB="$NEXO_HOME/data/nexo.db"
47
51
 
48
52
  # Increment counter (atomic: read+write in one step)
@@ -86,12 +90,25 @@ if not entries:
86
90
 
87
91
  tools_summary = ', '.join(entries[-10:])
88
92
 
89
- # Get current session and task from sessions table
93
+ # Get session by claude session_id (scoped), fallback to most recent
94
+ session_id = '$SESSION_ID'
90
95
  conn = sqlite3.connect(db_path, timeout=2)
91
96
  conn.row_factory = sqlite3.Row
92
- row = conn.execute(
93
- 'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
94
- ).fetchone()
97
+
98
+ # Try to find NEXO SID mapped to this claude session_id
99
+ row = None
100
+ if session_id and session_id != 'global':
101
+ row = conn.execute(
102
+ 'SELECT sid, task FROM sessions WHERE claude_session_id = ? LIMIT 1',
103
+ (session_id,)
104
+ ).fetchone()
105
+
106
+ # Fallback: most recent active session
107
+ if not row:
108
+ row = conn.execute(
109
+ 'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
110
+ ).fetchone()
111
+
95
112
  if not row:
96
113
  conn.close()
97
114
  sys.exit(0)
@@ -92,10 +92,11 @@ try:
92
92
 
93
93
  try:
94
94
  rows = db.execute(
95
- 'SELECT sid, task, started FROM sessions '
96
- 'WHERE completed=0 AND (strftime(\"%s\",\"now\") - last_update) < 900'
95
+ 'SELECT sid, task, started_epoch FROM sessions '
96
+ 'WHERE (strftime(\"%s\",\"now\") - last_update_epoch) < 900'
97
97
  ).fetchall()
98
- sessions = [{'sid': r['sid'], 'task': r['task'], 'started': r['started'][:16]} for r in rows]
98
+ from datetime import datetime as _dt
99
+ sessions = [{'sid': r['sid'], 'task': r['task'], 'started': _dt.fromtimestamp(r['started_epoch']).strftime('%Y-%m-%d %H:%M') if r['started_epoch'] else '?'} for r in rows]
99
100
  except Exception:
100
101
  pass
101
102
 
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """Dynamic plugin loader for NEXO MCP server."""
2
3
 
3
4
  import importlib
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """Update plugin — pull latest code, backup DBs, run migrations, verify."""
2
3
  import json
3
4
  import os
@@ -12,19 +13,71 @@ from pathlib import Path
12
13
  _THIS_DIR = Path(__file__).resolve().parent
13
14
  REPO_DIR = _THIS_DIR.parent.parent
14
15
  PACKAGE_JSON = REPO_DIR / "package.json"
15
- SRC_DIR = REPO_DIR / "src"
16
16
 
17
17
  NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
18
  DATA_DIR = NEXO_HOME / "data"
19
19
  BACKUP_BASE = NEXO_HOME / "backups"
20
20
 
21
+ # In packaged installs, update.py lives at ~/.nexo/plugins/update.py
22
+ # so REPO_DIR would be ~/ (wrong). Detect this and fix paths.
23
+ _PACKAGED_INSTALL = not (REPO_DIR / ".git").exists() and not (REPO_DIR / ".git").is_file()
24
+
25
+ if _PACKAGED_INSTALL:
26
+ # In packaged mode, core .py files live directly in NEXO_HOME
27
+ SRC_DIR = NEXO_HOME
28
+ else:
29
+ SRC_DIR = REPO_DIR / "src"
30
+
31
+
32
+ def _find_npm_pkg_src() -> Path | None:
33
+ """Locate the nexo-brain npm package's src/ directory for requirements.txt."""
34
+ try:
35
+ result = subprocess.run(
36
+ ["npm", "root", "-g"],
37
+ capture_output=True, text=True, timeout=10,
38
+ )
39
+ if result.returncode == 0:
40
+ npm_src = Path(result.stdout.strip()) / "nexo-brain" / "src"
41
+ if npm_src.is_dir():
42
+ return npm_src
43
+ except Exception:
44
+ pass
45
+ return None
46
+
47
+ def _is_git_repo() -> bool:
48
+ """Check if REPO_DIR is a valid git repository."""
49
+ return (REPO_DIR / ".git").exists() or (REPO_DIR / ".git").is_file()
50
+
51
+
52
+ def _refresh_installed_manifest():
53
+ """Copy source crons/ to NEXO_HOME/crons/ so catchup & watchdog stay current."""
54
+ try:
55
+ src_crons = SRC_DIR / "crons"
56
+ dst_crons = NEXO_HOME / "crons"
57
+ if src_crons.exists():
58
+ dst_crons.mkdir(parents=True, exist_ok=True)
59
+ for f in src_crons.iterdir():
60
+ if f.is_file():
61
+ shutil.copy2(str(f), str(dst_crons / f.name))
62
+ except Exception:
63
+ pass
64
+
21
65
 
22
66
  def _read_version() -> str:
23
- """Read version from package.json."""
67
+ """Read version from package.json or NEXO_HOME/version.json (packaged installs)."""
24
68
  try:
25
- return json.loads(PACKAGE_JSON.read_text()).get("version", "unknown")
69
+ if PACKAGE_JSON.exists():
70
+ return json.loads(PACKAGE_JSON.read_text()).get("version", "unknown")
26
71
  except Exception:
27
- return "unknown"
72
+ pass
73
+ # Packaged installs don't ship package.json — check version.json in NEXO_HOME
74
+ try:
75
+ version_file = NEXO_HOME / "version.json"
76
+ if version_file.exists():
77
+ return json.loads(version_file.read_text()).get("version", "unknown")
78
+ except Exception:
79
+ pass
80
+ return "unknown"
28
81
 
29
82
 
30
83
  def _git(*args, cwd=None) -> tuple[int, str, str]:
@@ -39,13 +92,28 @@ def _git(*args, cwd=None) -> tuple[int, str, str]:
39
92
  return result.returncode, result.stdout.strip(), result.stderr.strip()
40
93
 
41
94
 
95
+ def _requirements_hash() -> str:
96
+ """Return a content hash of requirements.txt, or empty string if missing."""
97
+ import hashlib
98
+ req_file = SRC_DIR / "requirements.txt"
99
+ if not req_file.exists() and _PACKAGED_INSTALL:
100
+ npm_src = _find_npm_pkg_src()
101
+ if npm_src:
102
+ req_file = npm_src / "requirements.txt"
103
+ if req_file.exists():
104
+ return hashlib.sha256(req_file.read_bytes()).hexdigest()
105
+ return ""
106
+
107
+
42
108
  def _check_dirty() -> str | None:
43
- """Return error message if src/ has uncommitted changes, else None."""
44
- rc, out, _ = _git("status", "--porcelain", "--", "src/")
109
+ """Return error message if worktree has uncommitted changes, else None."""
110
+ if not _is_git_repo():
111
+ return None # Not a git repo, skip dirty check
112
+ rc, out, _ = _git("status", "--porcelain")
45
113
  if rc != 0:
46
114
  return "Failed to check git status."
47
115
  if out:
48
- return f"Uncommitted changes in src/:\n{out}\nCommit or stash before updating."
116
+ return f"Uncommitted changes:\n{out}\nCommit or stash before updating."
49
117
  return None
50
118
 
51
119
 
@@ -101,12 +169,51 @@ def _restore_databases(backup_dir: str):
101
169
  break
102
170
 
103
171
 
172
+ def _reinstall_pip_deps() -> str | None:
173
+ """Reinstall Python dependencies from requirements.txt into the managed venv."""
174
+ req_file = SRC_DIR / "requirements.txt"
175
+ if not req_file.exists() and _PACKAGED_INSTALL:
176
+ # In packaged mode, requirements.txt lives in the npm package's src/ dir
177
+ npm_src = _find_npm_pkg_src()
178
+ if npm_src:
179
+ req_file = npm_src / "requirements.txt"
180
+ if not req_file.exists():
181
+ return None # No requirements file, skip
182
+ venv_pip = NEXO_HOME / ".venv" / "bin" / "pip"
183
+ if not venv_pip.exists():
184
+ venv_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
185
+ if not venv_pip.exists():
186
+ # No venv, try system pip with --break-system-packages
187
+ try:
188
+ result = subprocess.run(
189
+ [sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
190
+ capture_output=True, text=True, timeout=120,
191
+ )
192
+ if result.returncode != 0:
193
+ return f"pip install failed: {result.stderr or result.stdout}"
194
+ except Exception as e:
195
+ return f"pip install error: {e}"
196
+ return None
197
+ try:
198
+ result = subprocess.run(
199
+ [str(venv_pip), "install", "--quiet", "-r", str(req_file)],
200
+ capture_output=True, text=True, timeout=120,
201
+ )
202
+ if result.returncode != 0:
203
+ return f"pip install failed: {result.stderr or result.stdout}"
204
+ except Exception as e:
205
+ return f"pip install error: {e}"
206
+ return None
207
+
208
+
104
209
  def _run_migrations() -> str | None:
105
210
  """Run init_db() to apply pending migrations. Returns error or None."""
211
+ # In packaged mode, db/ lives in NEXO_HOME; in dev mode, in SRC_DIR
212
+ cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
106
213
  try:
107
214
  result = subprocess.run(
108
215
  [sys.executable, "-c", "import db; db.init_db()"],
109
- cwd=str(SRC_DIR),
216
+ cwd=cwd,
110
217
  capture_output=True,
111
218
  text=True,
112
219
  timeout=30,
@@ -120,10 +227,12 @@ def _run_migrations() -> str | None:
120
227
 
121
228
  def _verify_import() -> str | None:
122
229
  """Verify server.py can be imported successfully."""
230
+ # In packaged mode, server.py lives in NEXO_HOME; in dev mode, in SRC_DIR
231
+ cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
123
232
  try:
124
233
  result = subprocess.run(
125
234
  [sys.executable, "-c", "import server"],
126
- cwd=str(SRC_DIR),
235
+ cwd=cwd,
127
236
  capture_output=True,
128
237
  text=True,
129
238
  timeout=15,
@@ -135,27 +244,235 @@ def _verify_import() -> str | None:
135
244
  return None
136
245
 
137
246
 
247
+ def _sync_hooks_to_home():
248
+ """Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after update."""
249
+ import shutil
250
+ hooks_src = SRC_DIR / "hooks"
251
+ hooks_dest = NEXO_HOME / "hooks"
252
+ if not hooks_src.is_dir():
253
+ return
254
+ hooks_dest.mkdir(parents=True, exist_ok=True)
255
+ synced = 0
256
+ for f in hooks_src.iterdir():
257
+ if f.is_file() and f.suffix == ".sh":
258
+ dest = hooks_dest / f.name
259
+ shutil.copy2(str(f), str(dest))
260
+ os.chmod(str(dest), 0o755)
261
+ synced += 1
262
+ if synced:
263
+ print(f"[NEXO update] Synced {synced} hook(s) to {hooks_dest}", file=sys.stderr)
264
+
265
+
266
+ def _backup_code_tree() -> tuple[str | None, str | None]:
267
+ """Snapshot NEXO_HOME code dirs before npm update. Returns (backup_dir, error)."""
268
+ timestamp = time.strftime("%Y-%m-%d-%H%M%S")
269
+ backup_dir = BACKUP_BASE / f"code-tree-{timestamp}"
270
+ # Directories and flat files that postinstall copies into NEXO_HOME
271
+ code_dirs = ["hooks", "plugins", "db", "cognitive", "dashboard", "rules", "crons", "scripts"]
272
+ code_files_glob = ["*.py", "requirements.txt"]
273
+ try:
274
+ backup_dir.mkdir(parents=True, exist_ok=True)
275
+ # Backup directories
276
+ for d in code_dirs:
277
+ src = NEXO_HOME / d
278
+ if src.is_dir():
279
+ shutil.copytree(src, backup_dir / d, dirs_exist_ok=True)
280
+ # Backup flat code files in NEXO_HOME root
281
+ for pattern in code_files_glob:
282
+ for f in NEXO_HOME.glob(pattern):
283
+ if f.is_file():
284
+ shutil.copy2(f, backup_dir / f.name)
285
+ # Backup version.json
286
+ vf = NEXO_HOME / "version.json"
287
+ if vf.is_file():
288
+ shutil.copy2(vf, backup_dir / "version.json")
289
+ except Exception as e:
290
+ return None, f"Code tree backup failed: {e}"
291
+ return str(backup_dir), None
292
+
293
+
294
+ def _restore_code_tree(backup_dir: str) -> str | None:
295
+ """Restore NEXO_HOME code dirs from a backup snapshot. Returns error or None."""
296
+ bdir = Path(backup_dir)
297
+ if not bdir.is_dir():
298
+ return f"Code tree backup dir not found: {backup_dir}"
299
+ try:
300
+ for item in bdir.iterdir():
301
+ dest = NEXO_HOME / item.name
302
+ if item.is_dir():
303
+ if dest.is_dir():
304
+ shutil.rmtree(dest)
305
+ shutil.copytree(item, dest)
306
+ elif item.is_file():
307
+ shutil.copy2(item, dest)
308
+ except Exception as e:
309
+ return f"Code tree restore failed: {e}"
310
+ return None
311
+
312
+
313
+ def _rollback_npm_package(target_version: str) -> str | None:
314
+ """Rollback nexo-brain npm package to a specific version.
315
+
316
+ Uses NEXO_SKIP_POSTINSTALL because we restore the code tree
317
+ from our own pre-update backup — no need for postinstall migration.
318
+ """
319
+ try:
320
+ result = subprocess.run(
321
+ ["npm", "install", "-g", f"nexo-brain@{target_version}"],
322
+ capture_output=True, text=True, timeout=120,
323
+ env={**os.environ, "NEXO_SKIP_POSTINSTALL": "1", "NEXO_HOME": str(NEXO_HOME)},
324
+ )
325
+ if result.returncode != 0:
326
+ return f"npm rollback failed: {result.stderr or result.stdout}"
327
+ except Exception as e:
328
+ return f"npm rollback error: {e}"
329
+ return None
330
+
331
+
332
+ def _handle_packaged_update() -> str:
333
+ """Update a packaged (npm) install — no git repo available."""
334
+ old_version = _read_version()
335
+
336
+ # 1. Backup databases BEFORE any changes
337
+ backup_dir, backup_err = _backup_databases()
338
+ if backup_err:
339
+ return f"ABORTED at backup: {backup_err}"
340
+
341
+ # 2. Backup NEXO_HOME code tree BEFORE npm update
342
+ # postinstall copies hooks/core/plugins/scripts into NEXO_HOME,
343
+ # so we need a full snapshot to restore on failure.
344
+ code_backup_dir, code_err = _backup_code_tree()
345
+ if code_err:
346
+ return f"ABORTED at code tree backup: {code_err}"
347
+
348
+ # 3. Run npm update (postinstall.js will migrate NEXO_HOME in-place)
349
+ try:
350
+ result = subprocess.run(
351
+ ["npm", "update", "-g", "nexo-brain"],
352
+ capture_output=True, text=True, timeout=120,
353
+ env={**os.environ, "NEXO_HOME": str(NEXO_HOME)},
354
+ )
355
+ if result.returncode != 0:
356
+ # npm failed (including postinstall failures) — full rollback
357
+ if backup_dir:
358
+ _restore_databases(backup_dir)
359
+ if code_backup_dir:
360
+ _restore_code_tree(code_backup_dir)
361
+ # Reinstall pip deps from restored old requirements.txt
362
+ _reinstall_pip_deps()
363
+ rollback_err = _rollback_npm_package(old_version)
364
+ msg = f"ABORTED: npm update failed: {result.stderr or result.stdout}"
365
+ if rollback_err:
366
+ msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
367
+ msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
368
+ return msg
369
+ except FileNotFoundError:
370
+ return "ABORTED: npm not found. Install Node.js to update packaged installs."
371
+ except Exception as e:
372
+ if backup_dir:
373
+ _restore_databases(backup_dir)
374
+ if code_backup_dir:
375
+ _restore_code_tree(code_backup_dir)
376
+ # Reinstall pip deps from restored old requirements.txt
377
+ _reinstall_pip_deps()
378
+ rollback_err = _rollback_npm_package(old_version)
379
+ msg = f"ABORTED: npm update error: {e}"
380
+ if rollback_err:
381
+ msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
382
+ msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
383
+ return msg
384
+
385
+ new_version = _read_version()
386
+ if old_version == new_version:
387
+ return f"Already up to date (v{old_version}). No changes."
388
+
389
+ # 4. Post-npm verification steps
390
+ errors = []
391
+
392
+ # Reinstall pip deps for new version
393
+ pip_err = _reinstall_pip_deps()
394
+ if pip_err:
395
+ errors.append(f"pip deps: {pip_err}")
396
+
397
+ # Run migrations
398
+ mig_err = _run_migrations()
399
+ if mig_err:
400
+ errors.append(f"migrations: {mig_err}")
401
+
402
+ # Verify server can still import
403
+ verify_err = _verify_import()
404
+ if verify_err:
405
+ errors.append(f"verification: {verify_err}")
406
+
407
+ if errors:
408
+ # 5. Full rollback: restore code tree + DBs + pip deps + rollback npm package
409
+ if code_backup_dir:
410
+ tree_err = _restore_code_tree(code_backup_dir)
411
+ else:
412
+ tree_err = "no code tree backup available"
413
+ if backup_dir:
414
+ _restore_databases(backup_dir)
415
+ # Reinstall pip deps from the restored (old) requirements.txt
416
+ # so the venv matches the rolled-back code tree
417
+ pip_rollback_err = _reinstall_pip_deps() if not tree_err else None
418
+ rollback_err = _rollback_npm_package(old_version)
419
+ lines = [f"UPDATE FAILED (packaged install, v{old_version} -> v{new_version})"]
420
+ for err in errors:
421
+ lines.append(f" ERROR: {err}")
422
+ lines.append(f" Databases restored from: {backup_dir}")
423
+ if tree_err:
424
+ lines.append(f" WARNING: code tree restore failed: {tree_err}")
425
+ else:
426
+ lines.append(f" Code tree restored from: {code_backup_dir}")
427
+ if pip_rollback_err:
428
+ lines.append(f" WARNING: pip deps rollback failed: {pip_rollback_err}")
429
+ elif not tree_err:
430
+ lines.append(" Python deps: reinstalled from old requirements.txt")
431
+ if rollback_err:
432
+ lines.append(f" WARNING: npm rollback failed: {rollback_err}")
433
+ lines.append(f" Manual rollback: npm install -g nexo-brain@{old_version}")
434
+ else:
435
+ lines.append(f" npm package rolled back to v{old_version}")
436
+ lines.append("")
437
+ lines.append("Fix the errors above, then run nexo_update again.")
438
+ return "\n".join(lines)
439
+
440
+ lines = ["UPDATE SUCCESSFUL (packaged install)"]
441
+ lines.append(f" Version: {old_version} -> {new_version}")
442
+ lines.append(f" Backup: {backup_dir}")
443
+ lines.append("")
444
+ lines.append("MCP server restart needed to load new code.")
445
+ return "\n".join(lines)
446
+
447
+
138
448
  def handle_update(remote: str = "origin", branch: str = "main") -> str:
139
449
  """Pull latest NEXO code, backup databases, run migrations, and verify.
140
450
 
141
- Full update flow:
142
- 1. Check for uncommitted changes in src/
451
+ Supports both git checkouts and packaged (npm) installs.
452
+
453
+ Full update flow (git):
454
+ 1. Check for uncommitted changes in entire worktree
143
455
  2. Backup all .db files
144
456
  3. git pull
145
- 4. Run migrations if version changed
146
- 5. Verify server.py imports
147
- 6. Rollback on failure
457
+ 4. Reinstall Python dependencies if version changed
458
+ 5. Run migrations if version changed
459
+ 6. Verify server.py imports
460
+ 7. Rollback on failure (git reset --hard to saved commit)
148
461
 
149
462
  Args:
150
463
  remote: Git remote name (default: origin)
151
464
  branch: Git branch to pull (default: main)
152
465
  """
466
+ # Packaged install — no git repo
467
+ if not _is_git_repo():
468
+ return _handle_packaged_update()
469
+
153
470
  steps_done = []
154
471
  old_commit = None
155
472
  backup_dir = None
156
473
 
157
474
  try:
158
- # Step 1: Check dirty
475
+ # Step 1: Check dirty (full worktree)
159
476
  dirty_err = _check_dirty()
160
477
  if dirty_err:
161
478
  return f"ABORTED: {dirty_err}"
@@ -163,6 +480,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
163
480
 
164
481
  # Record current state
165
482
  old_version = _read_version()
483
+ old_req_hash = _requirements_hash()
166
484
  rc, old_commit, _ = _git("rev-parse", "HEAD")
167
485
  if rc != 0:
168
486
  return "ABORTED: Not a git repository or git not available."
@@ -179,39 +497,59 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
179
497
  return f"ABORTED at git pull: {pull_err or pull_out}"
180
498
  steps_done.append("git-pull")
181
499
 
182
- # Step 4: Check version change
500
+ # Step 4: Check version and dependency changes
183
501
  new_version = _read_version()
184
502
  version_changed = old_version != new_version
503
+ new_req_hash = _requirements_hash()
504
+ deps_changed = old_req_hash != new_req_hash
185
505
 
186
- # Step 5: Run migrations if version changed
506
+ # Step 5: Reinstall pip dependencies if requirements.txt changed
507
+ if deps_changed or version_changed:
508
+ pip_err = _reinstall_pip_deps()
509
+ if pip_err:
510
+ raise RuntimeError(f"Pip install failed: {pip_err}")
511
+ steps_done.append("pip-deps")
512
+
513
+ # Step 6: Run migrations if version changed
187
514
  if version_changed:
188
515
  mig_err = _run_migrations()
189
516
  if mig_err:
190
517
  raise RuntimeError(f"Migration failed: {mig_err}")
191
518
  steps_done.append("migrations")
192
519
 
193
- # Step 6: Verify import
520
+ # Step 7: Verify import
194
521
  verify_err = _verify_import()
195
522
  if verify_err:
196
523
  raise RuntimeError(f"Verification failed: {verify_err}")
197
524
  steps_done.append("verify")
198
525
 
199
- # Step 7: Sync crons with manifest
526
+ # Step 8: Sync crons with manifest
200
527
  cron_sync_result = ""
201
528
  try:
202
- cron_sync_path = NEXO_CODE / "crons" / "sync.py"
529
+ cron_sync_path = SRC_DIR / "crons" / "sync.py"
203
530
  if cron_sync_path.exists():
204
- import subprocess as _sp
205
- r = _sp.run(
531
+ r = subprocess.run(
206
532
  [sys.executable, str(cron_sync_path)],
207
533
  capture_output=True, text=True, timeout=30,
208
- env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(NEXO_CODE)},
534
+ env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(SRC_DIR)},
209
535
  )
210
536
  cron_sync_result = r.stdout.strip()
211
- steps_done.append("cron-sync")
537
+ if r.returncode == 0:
538
+ steps_done.append("cron-sync")
539
+ # Refresh installed manifest only after successful sync
540
+ _refresh_installed_manifest()
541
+ else:
542
+ cron_sync_result = f"Cron sync failed (exit {r.returncode}): {r.stderr or r.stdout}"
212
543
  except Exception as e:
213
544
  cron_sync_result = f"Cron sync warning: {e}"
214
545
 
546
+ # Step 9: Sync hooks to NEXO_HOME
547
+ try:
548
+ _sync_hooks_to_home()
549
+ steps_done.append("hook-sync")
550
+ except Exception as e:
551
+ pass # Non-critical, log in function
552
+
215
553
  # Build result
216
554
  if pull_out == "Already up to date.":
217
555
  return f"Already up to date (v{old_version}). No changes pulled."
@@ -223,22 +561,35 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
223
561
  lines.append(f" Version: {old_version} (unchanged)")
224
562
  lines.append(f" Branch: {remote}/{branch}")
225
563
  lines.append(f" Backup: {backup_dir}")
564
+ if "pip-deps" in steps_done:
565
+ lines.append(" Python deps: reinstalled")
226
566
  if version_changed:
227
567
  lines.append(" Migrations: applied")
228
568
  if "cron-sync" in steps_done:
229
569
  lines.append(" Crons: synced with manifest")
570
+ if "hook-sync" in steps_done:
571
+ lines.append(" Hooks: synced to NEXO_HOME")
230
572
  lines.append("")
231
573
  lines.append("MCP server restart needed to load new code.")
232
574
  return "\n".join(lines)
233
575
 
234
576
  except Exception as e:
235
- # Rollback
577
+ # Rollback — use git checkout to saved commit (safer than reset --hard)
236
578
  rollback_lines = [f"UPDATE FAILED: {e}", "", "Rolling back..."]
237
579
 
238
580
  if old_commit and "git-pull" in steps_done:
581
+ # Full rollback: reset HEAD + index + worktree to old commit
239
582
  rc, _, err = _git("reset", "--hard", old_commit)
240
583
  if rc == 0:
241
- rollback_lines.append(f" Git: reset to {old_commit[:8]}")
584
+ rollback_lines.append(f" Git: restored files to {old_commit[:8]}")
585
+ # Reinstall pip deps from the restored old requirements.txt
586
+ # so the venv matches the rolled-back code
587
+ if "pip-deps" in steps_done:
588
+ pip_rb_err = _reinstall_pip_deps()
589
+ if pip_rb_err:
590
+ rollback_lines.append(f" WARNING: pip deps rollback failed: {pip_rb_err}")
591
+ else:
592
+ rollback_lines.append(" Python deps: reinstalled from old requirements.txt")
242
593
  else:
243
594
  rollback_lines.append(f" Git rollback FAILED: {err}")
244
595
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 4: Apply synthesized findings.
4
5
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
4
5
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 2: Extract findings from each session using Claude CLI.
4
5
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
4
5
 
@@ -18,10 +18,32 @@ import sys
18
18
  from datetime import datetime, timedelta
19
19
  from pathlib import Path
20
20
 
21
- CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
22
-
23
21
  HOME = Path.home()
24
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
22
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(HOME / ".nexo")))
23
+
24
+
25
+ def _resolve_claude_cli() -> Path:
26
+ """Find claude CLI: saved path > PATH > common locations."""
27
+ saved = NEXO_HOME / "config" / "claude-cli-path"
28
+ if saved.exists():
29
+ p = Path(saved.read_text().strip())
30
+ if p.exists():
31
+ return p
32
+ import shutil
33
+ found = shutil.which("claude")
34
+ if found:
35
+ return Path(found)
36
+ for candidate in [
37
+ HOME / ".local" / "bin" / "claude",
38
+ HOME / ".npm-global" / "bin" / "claude",
39
+ Path("/usr/local/bin/claude"),
40
+ ]:
41
+ if candidate.exists():
42
+ return candidate
43
+ return HOME / ".local" / "bin" / "claude" # last resort
44
+
45
+
46
+ CLAUDE_CLI = _resolve_claude_cli()
25
47
  LOG_DIR = NEXO_HOME / "logs"
26
48
  LOG_DIR.mkdir(parents=True, exist_ok=True)
27
49
  LOG_FILE = LOG_DIR / "catchup.log"
@@ -47,7 +69,10 @@ def _resolve_python() -> str:
47
69
 
48
70
  NEXO_PYTHON = _resolve_python()
49
71
  NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
50
- MANIFEST = NEXO_CODE / "crons" / "manifest.json"
72
+ # Look for manifest in NEXO_HOME first (packaged install), then NEXO_CODE (dev/repo)
73
+ _manifest_home = NEXO_HOME / "crons" / "manifest.json"
74
+ _manifest_code = NEXO_CODE / "crons" / "manifest.json"
75
+ MANIFEST = _manifest_home if _manifest_home.exists() else _manifest_code
51
76
 
52
77
 
53
78
  def _load_tasks_from_manifest() -> list[tuple]: