nexo-brain 1.7.0 → 2.1.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 (261) hide show
  1. package/README.md +160 -60
  2. package/bin/nexo-brain.js +680 -381
  3. package/package.json +18 -3
  4. package/scripts/migrate-to-unified.sh +813 -0
  5. package/scripts/migrate-v1.7-to-v1.8.py +214 -0
  6. package/scripts/pre-commit-check.sh +1 -1
  7. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  8. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  9. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  10. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  11. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  12. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  17. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  18. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  21. package/src/auto_close_sessions.py +1 -1
  22. package/src/auto_update.py +634 -0
  23. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  38. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  39. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  40. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  41. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  42. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  43. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  44. package/src/cognitive/_core.py +7 -3
  45. package/src/cognitive/_decay.py +1 -1
  46. package/src/cognitive/_search.py +1 -0
  47. package/src/cognitive/_trust.py +3 -3
  48. package/src/crons/manifest.json +106 -0
  49. package/src/crons/sync.py +217 -0
  50. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  52. package/src/dashboard/app.py +24 -4
  53. package/src/dashboard/templates/dashboard.html +3 -2
  54. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  55. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  57. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  58. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  59. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  60. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  61. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  62. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  63. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  64. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  65. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  66. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  67. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  68. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  69. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  70. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  71. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  72. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  73. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  74. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  75. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  76. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  77. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  78. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  79. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  80. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  90. package/src/db/_core.py +5 -1
  91. package/src/db/_episodic.py +2 -4
  92. package/src/db/_reminders.py +45 -6
  93. package/src/db/_schema.py +31 -0
  94. package/src/evolution_cycle.py +33 -11
  95. package/src/hooks/auto_capture.py +1 -1
  96. package/src/hooks/capture-tool-logs.sh +76 -0
  97. package/src/hooks/inbox-hook.sh +2 -1
  98. package/src/hooks/post-compact.sh +2 -1
  99. package/src/hooks/pre-compact.sh +104 -2
  100. package/src/hooks/session-start.sh +6 -2
  101. package/src/hooks/session-stop.sh +4 -2
  102. package/src/kg_populate.py +4 -1
  103. package/src/migrate_embeddings.py +4 -1
  104. package/src/plugin_loader.py +100 -34
  105. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  106. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  107. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  108. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  109. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  110. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  111. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  112. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  113. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  114. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  115. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  116. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  117. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  118. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  119. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  120. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  121. package/src/plugins/agents.py +2 -2
  122. package/src/plugins/backup.py +5 -4
  123. package/src/plugins/core_rules.py +37 -16
  124. package/src/plugins/episodic_memory.py +14 -5
  125. package/src/plugins/evolution.py +6 -2
  126. package/src/plugins/guard.py +20 -11
  127. package/src/plugins/update.py +256 -0
  128. package/src/requirements.txt +12 -0
  129. package/src/scripts/check-context.py +8 -3
  130. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  131. package/src/scripts/deep-sleep/apply_findings.py +514 -169
  132. package/src/scripts/deep-sleep/collect.py +480 -0
  133. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  134. package/src/scripts/deep-sleep/extract.py +249 -0
  135. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  136. package/src/scripts/deep-sleep/synthesize.py +191 -0
  137. package/src/scripts/nexo-auto-update.py +4 -211
  138. package/src/scripts/nexo-backup.sh +5 -13
  139. package/src/scripts/nexo-brain-activation.sh +26 -26
  140. package/src/scripts/nexo-catchup.py +36 -25
  141. package/src/scripts/nexo-cognitive-decay.py +7 -3
  142. package/src/scripts/nexo-daily-self-audit.py +44 -15
  143. package/src/scripts/nexo-deep-sleep.sh +31 -16
  144. package/src/scripts/nexo-evolution-run.py +21 -11
  145. package/src/scripts/nexo-followup-hygiene.py +6 -4
  146. package/src/scripts/nexo-github-monitor.py +12 -8
  147. package/src/scripts/nexo-immune.py +6 -4
  148. package/src/scripts/nexo-inbox-hook.sh +2 -1
  149. package/src/scripts/nexo-install.py +4 -225
  150. package/src/scripts/nexo-learning-housekeep.py +7 -3
  151. package/src/scripts/nexo-learning-validator.py +1 -22
  152. package/src/scripts/nexo-migrate.py +9 -3
  153. package/src/scripts/nexo-postmortem-consolidator.py +17 -10
  154. package/src/scripts/nexo-pre-commit.py +3 -1
  155. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  156. package/src/scripts/nexo-proactive-dashboard.py +5 -4
  157. package/src/scripts/nexo-runtime-preflight.py +59 -55
  158. package/src/scripts/nexo-send-email.py +1 -1
  159. package/src/scripts/nexo-send-reply.py +3 -1
  160. package/src/scripts/nexo-sleep.py +13 -9
  161. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  162. package/src/scripts/nexo-synthesis.py +11 -7
  163. package/src/scripts/nexo-tcc-approve.sh +79 -0
  164. package/src/scripts/nexo-update.sh +161 -0
  165. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  166. package/src/scripts/nexo-watchdog.sh +22 -13
  167. package/src/server.py +77 -28
  168. package/src/storage_router.py +6 -2
  169. package/src/tools_learnings.py +6 -6
  170. package/src/tools_menu.py +1 -1
  171. package/src/tools_reminders_crud.py +10 -8
  172. package/src/tools_sessions.py +76 -4
  173. package/templates/CLAUDE.md.template +14 -80
  174. package/templates/launchagents/README.md +7 -7
  175. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  176. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  177. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  178. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  179. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  180. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  181. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  182. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  183. package/templates/launchagents/com.nexo.immune.plist +4 -0
  184. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  185. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  186. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  187. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  188. package/templates/openclaw.json +1 -1
  189. package/tests/conftest.py +2 -2
  190. package/tests/test_cognitive.py +7 -6
  191. package/tests/test_migrations.py +26 -0
  192. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  193. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  194. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  195. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  196. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  197. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  198. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  199. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  200. package/src/__pycache__/server.cpython-314.pyc +0 -0
  201. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  202. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  203. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  204. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  205. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  206. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  207. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  208. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  209. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  210. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  212. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  213. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  214. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  215. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  216. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  217. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  218. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  219. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  220. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  221. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  222. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  223. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  224. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  225. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  226. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  244. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  245. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  246. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  247. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  248. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  249. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  250. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  251. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  252. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  253. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  254. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  255. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  256. package/src/scripts/deep-sleep/prompt.md +0 -109
  257. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  259. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  260. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  261. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -11,10 +11,14 @@ from datetime import datetime, timedelta
11
11
  from pathlib import Path
12
12
  from typing import Optional
13
13
 
14
- COGNITIVE_DB = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cognitive.db")
14
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
15
+ _data_dir = os.path.join(NEXO_HOME, "data")
16
+ os.makedirs(_data_dir, exist_ok=True)
17
+
18
+ COGNITIVE_DB = os.path.join(_data_dir, "cognitive.db")
15
19
  EMBEDDING_DIM = 768
16
- LAMBDA_STM = 0.1 # half-life ~7 days
17
- LAMBDA_LTM = 0.012 # half-life ~60 days
20
+ LAMBDA_STM = 0.004126 # half-life = ln(2) / (7 * 24) ≈ 7 days
21
+ LAMBDA_LTM = 0.000481 # half-life = ln(2) / (60 * 24) ≈ 60 days
18
22
 
19
23
  # Prediction Error Gate thresholds
20
24
  PE_GATE_REJECT = 0.85 # similarity > this → reject (not novel enough)
@@ -254,7 +254,7 @@ def dream_cycle(max_insights: int = 50) -> dict:
254
254
  })
255
255
 
256
256
  if len(recent_memories) < 2:
257
- return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories)}
257
+ return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories), "candidates_found": 0}
258
258
 
259
259
  # 2. Get already-dreamed pairs to skip
260
260
  dreamed = set()
@@ -1,5 +1,6 @@
1
1
  """NEXO Cognitive — Search, retrieval, ranking."""
2
2
  import math
3
+ import sqlite3
3
4
  import numpy as np
4
5
  from datetime import datetime
5
6
  from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob, _get_model, _get_reranker, rerank_results, EMBEDDING_DIM
@@ -1,7 +1,7 @@
1
1
  """NEXO Cognitive — Trust scoring, sentiment, dissonance."""
2
2
  import re
3
3
  import numpy as np
4
- from datetime import datetime
4
+ from datetime import datetime, timedelta
5
5
  from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array
6
6
  from cognitive._core import POSITIVE_SIGNALS, NEGATIVE_SIGNALS, URGENCY_SIGNALS
7
7
 
@@ -314,13 +314,13 @@ def detect_sentiment(text: str) -> dict:
314
314
  sentiment = "negative"
315
315
  intensity = min(1.0, 0.3 + neg_score * 0.15)
316
316
  if intensity > 0.7:
317
- guidance = "MODE: Ultra-conciso. Cero explicaciones. Resolver y mostrar resultado."
317
+ guidance = "MODE: Ultra-concise. Zero explanations. Solve and show result."
318
318
  else:
319
319
  guidance = "MODE: Concise. Less context, more direct action."
320
320
  elif pos_score > neg_score and pos_score >= 1:
321
321
  sentiment = "positive"
322
322
  intensity = min(1.0, 0.3 + pos_score * 0.15)
323
- guidance = "MODE: Normal. Buen momento para proponer ideas de backlog o mejoras."
323
+ guidance = "MODE: Normal. Good time to suggest backlog ideas or improvements."
324
324
  elif urgency_hits:
325
325
  sentiment = "urgent"
326
326
  intensity = 0.8
@@ -0,0 +1,106 @@
1
+ {
2
+ "$schema": "NEXO cron manifest — synced by nexo_update to LaunchAgents (macOS) or systemd timers (Linux)",
3
+ "version": 2,
4
+ "crons": [
5
+ {
6
+ "id": "deep-sleep",
7
+ "script": "scripts/nexo-deep-sleep.sh",
8
+ "type": "shell",
9
+ "schedule": {"hour": 4, "minute": 30},
10
+ "description": "Overnight session analysis — 4 phases: collect, extract, synthesize, apply",
11
+ "core": true
12
+ },
13
+ {
14
+ "id": "sleep",
15
+ "script": "scripts/nexo-sleep.py",
16
+ "schedule": {"hour": 4, "minute": 0},
17
+ "description": "Nightly memory consolidation and dream cycle",
18
+ "core": true
19
+ },
20
+ {
21
+ "id": "cognitive-decay",
22
+ "script": "scripts/nexo-cognitive-decay.py",
23
+ "schedule": {"hour": 3, "minute": 0},
24
+ "description": "Memory decay — reduce strength of unaccessed memories",
25
+ "core": true
26
+ },
27
+ {
28
+ "id": "learning-housekeep",
29
+ "script": "scripts/nexo-learning-housekeep.py",
30
+ "schedule": {"hour": 3, "minute": 15},
31
+ "description": "Archive stale learnings, deduplicate, validate",
32
+ "core": true
33
+ },
34
+ {
35
+ "id": "immune",
36
+ "script": "scripts/nexo-immune.py",
37
+ "interval_seconds": 1800,
38
+ "description": "Health monitor — checks MCP, DB, services, auto-repairs",
39
+ "core": true
40
+ },
41
+ {
42
+ "id": "watchdog",
43
+ "script": "scripts/nexo-watchdog.sh",
44
+ "type": "shell",
45
+ "interval_seconds": 1800,
46
+ "description": "System health checks — snapshots, logs, alerts",
47
+ "core": true
48
+ },
49
+ {
50
+ "id": "self-audit",
51
+ "script": "scripts/nexo-daily-self-audit.py",
52
+ "schedule": {"hour": 7, "minute": 0},
53
+ "description": "Daily self-audit — validates learnings, protocols, drift",
54
+ "core": true
55
+ },
56
+ {
57
+ "id": "postmortem",
58
+ "script": "scripts/nexo-postmortem-consolidator.py",
59
+ "schedule": {"hour": 23, "minute": 30},
60
+ "description": "Consolidate session post-mortems into patterns",
61
+ "core": true
62
+ },
63
+ {
64
+ "id": "evolution",
65
+ "script": "scripts/nexo-evolution-run.py",
66
+ "schedule": {"hour": 5, "minute": 0, "weekday": 0},
67
+ "description": "Weekly self-improvement cycle — propose and evaluate changes",
68
+ "core": true
69
+ },
70
+ {
71
+ "id": "followup-hygiene",
72
+ "script": "scripts/nexo-followup-hygiene.py",
73
+ "schedule": {"hour": 5, "minute": 0},
74
+ "description": "Clean stale followups, archive completed, validate dates",
75
+ "core": true
76
+ },
77
+ {
78
+ "id": "synthesis",
79
+ "script": "scripts/nexo-synthesis.py",
80
+ "interval_seconds": 7200,
81
+ "description": "Periodic synthesis — cross-reference learnings, decisions, changes",
82
+ "core": true
83
+ },
84
+ {
85
+ "id": "auto-close-sessions",
86
+ "script": "scripts/nexo-auto-close-sessions.py",
87
+ "interval_seconds": 300,
88
+ "description": "Close stale sessions that lost their parent process",
89
+ "core": true
90
+ },
91
+ {
92
+ "id": "github-monitor",
93
+ "script": "scripts/nexo-github-monitor.py",
94
+ "schedule": {"hour": 8, "minute": 0},
95
+ "description": "Monitor GitHub repo — issues, PRs, stars, auto-respond",
96
+ "core": true
97
+ },
98
+ {
99
+ "id": "catchup",
100
+ "script": "scripts/nexo-catchup.py",
101
+ "schedule": {"hour": 8, "minute": 30},
102
+ "description": "Morning catchup briefing for the user",
103
+ "core": true
104
+ }
105
+ ]
106
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Cron Sync — Synchronize crons/manifest.json with system LaunchAgents (macOS).
4
+
5
+ Called by nexo_update after pulling new code. Ensures:
6
+ - New crons in manifest → installed
7
+ - Removed crons from manifest → unloaded + deleted
8
+ - Changed schedule/interval → plist updated + reloaded
9
+ - Personal (non-core) crons → left untouched
10
+
11
+ Usage:
12
+ python3 crons/sync.py [--dry-run]
13
+
14
+ Environment:
15
+ NEXO_HOME — root of NEXO installation
16
+ NEXO_CODE — path to NEXO source (defaults to script parent's parent)
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import platform
22
+ import plistlib
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
28
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
29
+ MANIFEST = Path(__file__).resolve().parent / "manifest.json"
30
+ LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
31
+ LABEL_PREFIX = "com.nexo."
32
+ LOG_DIR = NEXO_HOME / "logs"
33
+
34
+
35
+ def log(msg: str):
36
+ print(f"[cron-sync] {msg}", flush=True)
37
+
38
+
39
+ def load_manifest() -> list[dict]:
40
+ with open(MANIFEST) as f:
41
+ data = json.load(f)
42
+ return data.get("crons", [])
43
+
44
+
45
+ def build_plist(cron: dict) -> dict:
46
+ """Build a macOS LaunchAgent plist dict from a manifest entry."""
47
+ cron_id = cron["id"]
48
+ label = f"{LABEL_PREFIX}{cron_id}"
49
+ script_path = str(NEXO_CODE / cron["script"])
50
+ script_type = cron.get("type", "python")
51
+
52
+ if script_type == "shell":
53
+ program_args = ["/bin/bash", script_path]
54
+ else:
55
+ # Find python3
56
+ python_candidates = [
57
+ "/opt/homebrew/bin/python3",
58
+ "/usr/local/bin/python3",
59
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
60
+ "/usr/bin/python3",
61
+ ]
62
+ python_bin = "python3"
63
+ for p in python_candidates:
64
+ if Path(p).exists():
65
+ python_bin = p
66
+ break
67
+ program_args = [python_bin, script_path]
68
+
69
+ plist = {
70
+ "Label": label,
71
+ "ProgramArguments": program_args,
72
+ "StandardOutPath": str(LOG_DIR / f"{cron_id}-stdout.log"),
73
+ "StandardErrorPath": str(LOG_DIR / f"{cron_id}-stderr.log"),
74
+ "EnvironmentVariables": {
75
+ "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:"
76
+ + str(Path.home() / ".local" / "bin") + ":"
77
+ + str(Path.home() / ".nvm/versions/node/v22.14.0/bin") + ":"
78
+ + "/Library/Frameworks/Python.framework/Versions/3.12/bin",
79
+ "HOME": str(Path.home()),
80
+ "NEXO_HOME": str(NEXO_HOME),
81
+ "NEXO_CODE": str(NEXO_CODE),
82
+ "PYTHONUNBUFFERED": "1",
83
+ },
84
+ }
85
+
86
+ # Schedule
87
+ if "interval_seconds" in cron:
88
+ plist["StartInterval"] = cron["interval_seconds"]
89
+ elif "schedule" in cron:
90
+ cal = {}
91
+ s = cron["schedule"]
92
+ if "hour" in s:
93
+ cal["Hour"] = s["hour"]
94
+ if "minute" in s:
95
+ cal["Minute"] = s["minute"]
96
+ if "weekday" in s:
97
+ cal["Weekday"] = s["weekday"]
98
+ plist["StartCalendarInterval"] = cal
99
+
100
+ return plist
101
+
102
+
103
+ def get_installed_nexo_crons() -> dict[str, Path]:
104
+ """Return dict of cron_id → plist_path for installed NEXO crons."""
105
+ installed = {}
106
+ if not LAUNCH_AGENTS_DIR.exists():
107
+ return installed
108
+ for f in LAUNCH_AGENTS_DIR.glob(f"{LABEL_PREFIX}*.plist"):
109
+ cron_id = f.stem.replace(LABEL_PREFIX, "")
110
+ installed[cron_id] = f
111
+ return installed
112
+
113
+
114
+ def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
115
+ """Check if the installed plist differs from what we'd generate."""
116
+ try:
117
+ with open(existing_path, "rb") as f:
118
+ existing = plistlib.load(f)
119
+ except Exception:
120
+ return True
121
+
122
+ # Compare key fields
123
+ if existing.get("ProgramArguments") != new_plist.get("ProgramArguments"):
124
+ return True
125
+ if existing.get("StartInterval") != new_plist.get("StartInterval"):
126
+ return True
127
+ if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
128
+ return True
129
+ return False
130
+
131
+
132
+ def install_plist(label: str, plist: dict, plist_path: Path, dry_run: bool):
133
+ """Write plist and load it."""
134
+ if dry_run:
135
+ log(f" DRY-RUN: would install {plist_path.name}")
136
+ return
137
+
138
+ # Unload if already loaded
139
+ subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
140
+
141
+ with open(plist_path, "wb") as f:
142
+ plistlib.dump(plist, f)
143
+
144
+ subprocess.run(["launchctl", "load", str(plist_path)], capture_output=True)
145
+ log(f" Installed + loaded: {plist_path.name}")
146
+
147
+
148
+ def unload_plist(plist_path: Path, dry_run: bool):
149
+ """Unload and remove a plist."""
150
+ if dry_run:
151
+ log(f" DRY-RUN: would remove {plist_path.name}")
152
+ return
153
+
154
+ subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
155
+ plist_path.unlink(missing_ok=True)
156
+ log(f" Removed: {plist_path.name}")
157
+
158
+
159
+ def sync(dry_run: bool = False):
160
+ if platform.system() != "Darwin":
161
+ log("Not macOS — cron sync only supports LaunchAgents. Skipping.")
162
+ return
163
+
164
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
165
+ LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
166
+
167
+ manifest_crons = load_manifest()
168
+ manifest_ids = {c["id"] for c in manifest_crons}
169
+ installed = get_installed_nexo_crons()
170
+
171
+ log(f"Manifest: {len(manifest_crons)} core crons")
172
+ log(f"Installed: {len(installed)} NEXO crons")
173
+
174
+ # 1. Install or update crons from manifest
175
+ for cron in manifest_crons:
176
+ cron_id = cron["id"]
177
+ label = f"{LABEL_PREFIX}{cron_id}"
178
+ plist_path = LAUNCH_AGENTS_DIR / f"{label}.plist"
179
+ new_plist = build_plist(cron)
180
+
181
+ if cron_id not in installed:
182
+ log(f" NEW: {cron_id}")
183
+ install_plist(label, new_plist, plist_path, dry_run)
184
+ elif plist_needs_update(installed[cron_id], new_plist):
185
+ log(f" UPDATE: {cron_id}")
186
+ install_plist(label, new_plist, plist_path, dry_run)
187
+ else:
188
+ log(f" OK: {cron_id}")
189
+
190
+ # 2. Remove crons that are in installed but NOT in manifest and ARE core
191
+ # (personal crons like shopify-backup, email-monitor are left alone)
192
+ for cron_id, plist_path in installed.items():
193
+ if cron_id not in manifest_ids:
194
+ # Check if this was previously a core cron by reading the plist
195
+ # If it points to NEXO_CODE scripts → it's core, safe to remove
196
+ try:
197
+ with open(plist_path, "rb") as f:
198
+ existing = plistlib.load(f)
199
+ args = existing.get("ProgramArguments", [])
200
+ is_core = any(str(NEXO_CODE) in str(a) for a in args)
201
+ except Exception:
202
+ is_core = False
203
+
204
+ if is_core:
205
+ log(f" REMOVE (no longer in manifest): {cron_id}")
206
+ unload_plist(plist_path, dry_run)
207
+ else:
208
+ log(f" SKIP (personal): {cron_id}")
209
+
210
+ log("Sync complete.")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ dry_run = "--dry-run" in sys.argv
215
+ if dry_run:
216
+ log("DRY RUN MODE — no changes will be made")
217
+ sync(dry_run=dry_run)
@@ -10,6 +10,7 @@ Usage:
10
10
  import argparse
11
11
  import json
12
12
  import os
13
+ import platform
13
14
  import subprocess
14
15
  import sys
15
16
  import time
@@ -22,7 +23,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
22
23
  from fastapi.staticfiles import StaticFiles
23
24
  from pydantic import BaseModel
24
25
 
25
- # Add parent dir to path so we can import nexo-mcp modules
26
+ # Add parent dir to path so we can import NEXO modules
26
27
  _PARENT = str(Path(__file__).resolve().parent.parent)
27
28
  if _PARENT not in sys.path:
28
29
  sys.path.insert(0, _PARENT)
@@ -63,7 +64,7 @@ async def create_tables():
63
64
 
64
65
 
65
66
  # ---------------------------------------------------------------------------
66
- # Lazy imports — modules live in the parent nexo-mcp directory
67
+ # Lazy imports — modules live in the parent source directory
67
68
  # ---------------------------------------------------------------------------
68
69
 
69
70
  def _cognitive():
@@ -307,15 +308,29 @@ async def api_adaptive():
307
308
 
308
309
  @app.get("/api/sessions")
309
310
  async def api_sessions(limit: int = Query(10, ge=1, le=50)):
310
- """Recent session diaries."""
311
+ """Recent session diaries + active sessions from sessions table."""
311
312
  db = _db()
312
313
  conn = db.get_db()
314
+ # Active sessions (from sessions table, not diaries)
315
+ active_rows = conn.execute(
316
+ "SELECT sid as session_id, task, last_update_epoch, claude_session_id "
317
+ "FROM sessions WHERE last_update_epoch > (strftime('%s','now') - 900) "
318
+ "ORDER BY last_update_epoch DESC"
319
+ ).fetchall()
320
+ active = [dict(r) for r in active_rows]
321
+ # Add last_heartbeat as ISO string for frontend
322
+ for a in active:
323
+ epoch = a.get("last_update_epoch", 0)
324
+ if epoch:
325
+ import datetime
326
+ a["last_heartbeat"] = datetime.datetime.fromtimestamp(epoch).isoformat()
327
+ # Recent diaries
313
328
  rows = conn.execute(
314
329
  "SELECT * FROM session_diary ORDER BY created_at DESC LIMIT ?",
315
330
  (limit,),
316
331
  ).fetchall()
317
332
  diaries = [dict(r) for r in rows]
318
- return {"count": len(diaries), "sessions": diaries}
333
+ return {"count": len(diaries), "sessions": active, "diaries": diaries}
319
334
 
320
335
 
321
336
  @app.get("/api/kg/nodes")
@@ -602,6 +617,11 @@ async def api_ops_execute(fid: str):
602
617
  return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
603
618
  item = dict(row)
604
619
  description = item["description"].replace('"', '\\"').replace("'", "\\'")
620
+ if platform.system() != "Darwin":
621
+ return JSONResponse(
622
+ {"error": "This operation requires macOS (uses osascript to open Terminal)"},
623
+ status_code=501,
624
+ )
605
625
  script = f'tell application "Terminal" to do script "claude \\"NEXO: execute followup #{fid} — {description}\\""'
606
626
  subprocess.Popen(["osascript", "-e", script])
607
627
  return {"success": True, "followup_id": fid}
@@ -446,11 +446,12 @@
446
446
 
447
447
  // --- Overdue Items ---
448
448
  if (remindersData || followupsData) {
449
+ const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED', 'blocked', 'waiting'];
449
450
  const reminders = (remindersData?.reminders || []).filter(r =>
450
- r.status === 'PENDING' && r.date && r.date < today
451
+ !excludeStatus.includes(r.status) && r.date && r.date <= today
451
452
  );
452
453
  const followups = (followupsData?.followups || []).filter(f =>
453
- f.status === 'PENDING' && f.date && f.date < today
454
+ !excludeStatus.includes(f.status) && f.date && f.date <= today
454
455
  );
455
456
  const total = reminders.length + followups.length;
456
457
  const el = document.getElementById('overdue-count');
package/src/db/_core.py CHANGED
@@ -9,11 +9,15 @@ import datetime
9
9
  import pathlib
10
10
  import threading
11
11
 
12
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
13
+ _data_dir = os.path.join(NEXO_HOME, "data")
14
+ os.makedirs(_data_dir, exist_ok=True)
15
+
12
16
  DB_PATH = os.environ.get(
13
17
  "NEXO_TEST_DB",
14
18
  os.environ.get(
15
19
  "NEXO_DB",
16
- os.path.join(os.path.dirname(os.path.abspath(__file__)), "nexo.db"),
20
+ os.path.join(_data_dir, "nexo.db"),
17
21
  ),
18
22
  )
19
23
 
@@ -82,7 +82,7 @@ def auto_resolve_followups(change: dict) -> list[str]:
82
82
  conn = get_db()
83
83
  open_followups = conn.execute(
84
84
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
85
- "AND status != 'DELETED'"
85
+ "AND status NOT IN ('DELETED','archived','blocked','waiting')"
86
86
  ).fetchall()
87
87
 
88
88
  if not open_followups:
@@ -148,9 +148,7 @@ def update_change_commit(id: int, commit_ref: str) -> dict:
148
148
  fts_upsert("change", str(id), r.get("files",""), body, "change_log", commit=False)
149
149
 
150
150
  # Auto-resolve followups that match this change
151
- resolved = auto_resolve_followups(r)
152
- if resolved:
153
- r["auto_resolved_followups"] = resolved
151
+ r["_auto_resolved"] = auto_resolve_followups(r)
154
152
  return r
155
153
 
156
154