nexo-brain 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +102 -79
  2. package/bin/nexo-brain.js +681 -303
  3. package/bin/postinstall.js +46 -0
  4. package/package.json +14 -2
  5. package/scripts/migrate-to-unified.sh +813 -0
  6. package/scripts/migrate-v1.7-to-v1.8.py +214 -0
  7. package/scripts/pre-commit-check.sh +1 -1
  8. package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
  9. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  10. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  12. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  13. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  14. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  15. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  16. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  17. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  18. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  19. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  20. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  21. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  22. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  23. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  24. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  25. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  26. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  27. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  28. package/src/__pycache__/server.cpython-310.pyc +0 -0
  29. package/src/__pycache__/server.cpython-314.pyc +0 -0
  30. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  31. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  32. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  33. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  34. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  35. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  36. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  37. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  38. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  39. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  40. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  41. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  42. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  43. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  44. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  45. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  46. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  47. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  48. package/src/auto_close_sessions.py +4 -3
  49. package/src/auto_update.py +634 -0
  50. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  53. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  54. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  55. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  56. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  57. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  58. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  59. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  60. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  61. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  62. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  63. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  64. package/src/cognitive/_core.py +7 -3
  65. package/src/cognitive/_decay.py +1 -1
  66. package/src/cognitive/_memory.py +7 -3
  67. package/src/cognitive/_search.py +12 -10
  68. package/src/cognitive/_trust.py +3 -3
  69. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  70. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  71. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  72. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  73. package/src/dashboard/app.py +9 -3
  74. package/src/dashboard/templates/dashboard.html +4 -4
  75. package/src/dashboard/templates/operations.html +6 -6
  76. package/src/db/__init__.py +2 -1
  77. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  80. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  81. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  82. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  83. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  84. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  85. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  86. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  87. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  88. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  89. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  90. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  91. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  92. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  93. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  94. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  95. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  96. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  97. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  98. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  99. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  100. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  101. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  102. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  103. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  104. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  105. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  106. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  107. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  108. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  109. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  113. package/src/db/_core.py +7 -3
  114. package/src/db/_episodic.py +69 -1
  115. package/src/db/_fts.py +12 -12
  116. package/src/db/_reminders.py +89 -15
  117. package/src/db/_schema.py +41 -0
  118. package/src/evolution_cycle.py +33 -11
  119. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  120. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  121. package/src/hooks/auto_capture.py +1 -1
  122. package/src/hooks/capture-tool-logs.sh +76 -0
  123. package/src/hooks/inbox-hook.sh +2 -1
  124. package/src/hooks/post-compact.sh +2 -1
  125. package/src/hooks/pre-compact.sh +104 -2
  126. package/src/hooks/session-start.sh +6 -2
  127. package/src/hooks/session-stop.sh +2 -1
  128. package/src/kg_populate.py +4 -1
  129. package/src/migrate_embeddings.py +4 -1
  130. package/src/plugin_loader.py +100 -34
  131. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  132. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  133. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  134. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  135. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  136. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  137. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  138. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  139. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  140. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  141. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  142. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  143. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  144. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  145. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  146. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  147. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  148. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  149. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  150. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  151. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  153. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  155. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  159. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  160. package/src/plugins/agents.py +2 -2
  161. package/src/plugins/backup.py +5 -4
  162. package/src/plugins/cognitive_memory.py +1 -1
  163. package/src/plugins/core_rules.py +5 -1
  164. package/src/plugins/episodic_memory.py +43 -16
  165. package/src/plugins/evolution.py +7 -2
  166. package/src/plugins/guard.py +45 -17
  167. package/src/plugins/update.py +238 -0
  168. package/src/requirements.txt +12 -0
  169. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  170. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  171. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  172. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  173. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  174. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  175. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  176. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  177. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  178. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  179. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  180. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  181. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  182. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  183. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  184. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  185. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  186. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  187. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  188. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  189. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  190. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  191. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  192. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  193. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  194. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  195. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  196. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  197. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  198. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  199. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  200. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  201. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  202. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  203. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  204. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  205. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  206. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  207. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  208. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  219. package/src/scripts/check-context.py +13 -3
  220. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  221. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  222. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  223. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  224. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  225. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  226. package/src/scripts/deep-sleep/analyze_session.py +3 -1
  227. package/src/scripts/deep-sleep/apply_findings.py +7 -4
  228. package/src/scripts/deep-sleep/collect_transcripts.py +3 -1
  229. package/src/scripts/nexo-auto-update.py +4 -211
  230. package/src/scripts/nexo-backup.sh +25 -0
  231. package/src/scripts/nexo-brain-activation.sh +26 -26
  232. package/src/scripts/nexo-catchup.py +39 -25
  233. package/src/scripts/nexo-cognitive-decay.py +12 -5
  234. package/src/scripts/nexo-daily-self-audit.py +36 -14
  235. package/src/scripts/nexo-deep-sleep.sh +4 -3
  236. package/src/scripts/nexo-evolution-run.py +40 -12
  237. package/src/scripts/nexo-followup-hygiene.py +6 -3
  238. package/src/scripts/nexo-github-monitor.py +11 -4
  239. package/src/scripts/nexo-immune.py +20 -3
  240. package/src/scripts/nexo-inbox-hook.sh +2 -1
  241. package/src/scripts/nexo-install.py +6 -0
  242. package/src/scripts/nexo-learning-housekeep.py +245 -0
  243. package/src/scripts/nexo-learning-validator.py +12 -2
  244. package/src/scripts/nexo-migrate.py +232 -0
  245. package/src/scripts/nexo-postmortem-consolidator.py +38 -19
  246. package/src/scripts/nexo-pre-commit.py +3 -1
  247. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  248. package/src/scripts/nexo-proactive-dashboard.py +8 -6
  249. package/src/scripts/nexo-runtime-preflight.py +59 -55
  250. package/src/scripts/nexo-send-email.py +2 -2
  251. package/src/scripts/nexo-send-reply.py +3 -1
  252. package/src/scripts/nexo-sleep.py +21 -5
  253. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  254. package/src/scripts/nexo-synthesis.py +19 -4
  255. package/src/scripts/nexo-tcc-approve.sh +79 -0
  256. package/src/scripts/nexo-update.sh +161 -0
  257. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  258. package/src/scripts/nexo-watchdog.sh +41 -31
  259. package/src/server.py +107 -44
  260. package/src/storage_router.py +6 -2
  261. package/src/tools_coordination.py +14 -14
  262. package/src/tools_credentials.py +11 -11
  263. package/src/tools_learnings.py +36 -27
  264. package/src/tools_menu.py +7 -6
  265. package/src/tools_reminders.py +11 -5
  266. package/src/tools_reminders_crud.py +11 -9
  267. package/src/tools_sessions.py +62 -187
  268. package/src/tools_task_history.py +7 -7
  269. package/templates/CLAUDE.md.template +49 -469
  270. package/templates/launchagents/README.md +7 -7
  271. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  272. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  273. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  274. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  275. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  276. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  277. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  278. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  279. package/templates/launchagents/com.nexo.immune.plist +4 -0
  280. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  281. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  282. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  283. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  284. package/templates/openclaw.json +1 -1
  285. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  286. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  287. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  288. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  289. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  290. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  291. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  292. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  293. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  294. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  295. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  296. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  297. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  298. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
  299. package/tests/conftest.py +2 -2
  300. package/tests/test_cognitive.py +7 -6
  301. package/tests/test_migrations.py +29 -3
@@ -7,7 +7,7 @@ from db._fts import fts_upsert
7
7
  # ── Reminders ──────────────────────────────────────────────────────
8
8
 
9
9
  def create_reminder(id: str, description: str, date: str = None,
10
- status: str = 'PENDIENTE', category: str = 'general') -> dict:
10
+ status: str = 'PENDING', category: str = 'general') -> dict:
11
11
  """Create a new reminder."""
12
12
  conn = get_db()
13
13
  now = now_epoch()
@@ -46,7 +46,7 @@ def update_reminder(id: str, **kwargs) -> dict:
46
46
  def complete_reminder(id: str) -> dict:
47
47
  """Mark a reminder as completed with today's date."""
48
48
  today = datetime.date.today().isoformat()
49
- return update_reminder(id, status="COMPLETADO")
49
+ return update_reminder(id, status="COMPLETED")
50
50
 
51
51
 
52
52
  def delete_reminder(id: str) -> bool:
@@ -64,19 +64,19 @@ def get_reminders(filter_type: str = 'all') -> list[dict]:
64
64
  today = datetime.date.today().isoformat()
65
65
  if filter_type == 'completed':
66
66
  rows = conn.execute(
67
- "SELECT * FROM reminders WHERE status LIKE 'COMPLETADO%' ORDER BY updated_at DESC"
67
+ "SELECT * FROM reminders WHERE status LIKE 'COMPLETED%' ORDER BY updated_at DESC"
68
68
  ).fetchall()
69
69
  elif filter_type == 'due':
70
70
  rows = conn.execute(
71
- "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETADO%' "
72
- "AND status != 'ELIMINADO' AND date IS NOT NULL AND date <= ? "
71
+ "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
72
+ "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
73
73
  "ORDER BY date ASC",
74
74
  (today,)
75
75
  ).fetchall()
76
76
  else: # 'all' — active only
77
77
  rows = conn.execute(
78
- "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETADO%' "
79
- "AND status != 'ELIMINADO' ORDER BY date ASC NULLS LAST"
78
+ "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
79
+ "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
80
80
  ).fetchall()
81
81
  return [dict(r) for r in rows]
82
82
 
@@ -88,18 +88,68 @@ def get_reminder(id: str) -> dict | None:
88
88
  return dict(row) if row else None
89
89
 
90
90
 
91
+ def find_similar_followups(description: str, threshold: float = 0.3) -> list[dict]:
92
+ """Find open followups similar to a description using keyword overlap.
93
+
94
+ Uses asymmetric scoring: what fraction of the SMALLER token set overlaps
95
+ with the larger. This handles different-length texts better than Jaccard.
96
+
97
+ Returns matches sorted by similarity score (highest first).
98
+ threshold: minimum overlap ratio (0.0-1.0) to consider a match.
99
+ """
100
+ conn = get_db()
101
+ rows = conn.execute(
102
+ "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
103
+ "AND status != 'DELETED'"
104
+ ).fetchall()
105
+
106
+ def tokenize(text: str) -> set:
107
+ return {w.lower() for w in text.split() if len(w) > 3}
108
+
109
+ query_tokens = tokenize(description)
110
+ if not query_tokens:
111
+ return []
112
+
113
+ matches = []
114
+ for row in rows:
115
+ existing_tokens = tokenize(f"{row['id']} {row['description']} {row['verification'] or ''}")
116
+ if not existing_tokens:
117
+ continue
118
+ intersection = query_tokens & existing_tokens
119
+ if not intersection:
120
+ continue
121
+ smaller = min(len(query_tokens), len(existing_tokens))
122
+ score = len(intersection) / smaller if smaller else 0
123
+ if score >= threshold:
124
+ matches.append({**dict(row), "_similarity": round(score, 2)})
125
+
126
+ matches.sort(key=lambda x: x["_similarity"], reverse=True)
127
+ return matches[:5]
128
+
129
+
91
130
  # ── Followups ──────────────────────────────────────────────────────
92
131
 
93
132
  def create_followup(id: str, description: str, date: str = None,
94
- verification: str = '', status: str = 'PENDIENTE',
133
+ verification: str = '', status: str = 'PENDING',
95
134
  reasoning: str = '', recurrence: str = None) -> dict:
96
135
  """Create a new followup with optional reasoning and recurrence.
97
136
 
137
+ Checks for similar open followups before creating. If a match is found,
138
+ returns a warning with the existing followup ID (still creates the new one).
139
+
98
140
  recurrence format: 'weekly:monday', 'monthly:1', 'monthly:10', 'quarterly', etc.
99
141
  When a recurring followup is completed, a new one is auto-created with the next date.
100
142
  """
101
143
  conn = get_db()
102
144
  now = now_epoch()
145
+
146
+ # Anti-duplicate check
147
+ similar = find_similar_followups(description)
148
+ warning = ""
149
+ if similar:
150
+ ids = ", ".join(s["id"] for s in similar[:3])
151
+ warning = f" ⚠ SIMILAR FOLLOWUPS EXIST: {ids} (scores: {', '.join(str(s['_similarity']) for s in similar[:3])}). Consider updating instead."
152
+
103
153
  try:
104
154
  conn.execute(
105
155
  "INSERT INTO followups (id, date, description, verification, status, reasoning, recurrence, created_at, updated_at) "
@@ -111,7 +161,10 @@ def create_followup(id: str, description: str, date: str = None,
111
161
  except sqlite3.IntegrityError:
112
162
  return {"error": f"Followup {id} already exists. Use update instead."}
113
163
  row = conn.execute("SELECT * FROM followups WHERE id = ?", (id,)).fetchone()
114
- return dict(row)
164
+ result = dict(row)
165
+ if warning:
166
+ result["warning"] = warning
167
+ return result
115
168
 
116
169
 
117
170
  def update_followup(id: str, **kwargs) -> dict:
@@ -190,7 +243,7 @@ def complete_followup(id: str, result: str = '') -> dict:
190
243
  return {"error": f"Followup {id} not found"}
191
244
 
192
245
  today = datetime.date.today().isoformat()
193
- kwargs = {"status": "COMPLETADO"}
246
+ kwargs = {"status": "COMPLETED"}
194
247
  if result:
195
248
  existing = row["verification"] or ''
196
249
  kwargs["verification"] = f"{existing}\n{result}".strip() if existing else result
@@ -206,6 +259,18 @@ def complete_followup(id: str, result: str = '') -> dict:
206
259
  archived_id = f"{id}-{today}"
207
260
  conn.execute("UPDATE followups SET id = ? WHERE id = ?", (archived_id, id))
208
261
  conn.commit()
262
+
263
+ # Fix FTS: remove old entry for original ID, add entry for archived ID
264
+ conn.execute("DELETE FROM unified_search WHERE source = 'followup' AND source_id = ?", (id,))
265
+ archived_row = conn.execute("SELECT * FROM followups WHERE id = ?", (archived_id,)).fetchone()
266
+ if archived_row:
267
+ fts_upsert(
268
+ "followup", archived_id, archived_id,
269
+ f"{archived_row['description']} {archived_row['verification'] or ''} {archived_row['reasoning'] or ''}",
270
+ "followup", commit=False,
271
+ )
272
+
273
+ # create_followup handles its own FTS entry for the new recurring ID
209
274
  create_followup(
210
275
  id=id,
211
276
  description=row["description"],
@@ -215,6 +280,15 @@ def complete_followup(id: str, result: str = '') -> dict:
215
280
  recurrence=recurrence,
216
281
  )
217
282
 
283
+ # Return accurate result: the completed one is now archived_id, not id
284
+ return {
285
+ "id": archived_id,
286
+ "status": "COMPLETED",
287
+ "recurrence": recurrence,
288
+ "next_id": id,
289
+ "next_date": next_date,
290
+ }
291
+
218
292
  return update_result
219
293
 
220
294
 
@@ -234,19 +308,19 @@ def get_followups(filter_type: str = 'all') -> list[dict]:
234
308
  today = datetime.date.today().isoformat()
235
309
  if filter_type == 'completed':
236
310
  rows = conn.execute(
237
- "SELECT * FROM followups WHERE status LIKE 'COMPLETADO%' ORDER BY updated_at DESC"
311
+ "SELECT * FROM followups WHERE status LIKE 'COMPLETED%' ORDER BY updated_at DESC"
238
312
  ).fetchall()
239
313
  elif filter_type == 'due':
240
314
  rows = conn.execute(
241
- "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETADO%' "
242
- "AND status != 'ELIMINADO' AND date IS NOT NULL AND date <= ? "
315
+ "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
316
+ "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
243
317
  "ORDER BY date ASC",
244
318
  (today,)
245
319
  ).fetchall()
246
320
  else: # 'all' — active only
247
321
  rows = conn.execute(
248
- "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETADO%' "
249
- "AND status != 'ELIMINADO' ORDER BY date ASC NULLS LAST"
322
+ "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
323
+ "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
250
324
  ).fetchall()
251
325
  return [dict(r) for r in rows]
252
326
 
package/src/db/_schema.py CHANGED
@@ -256,6 +256,45 @@ def _m13_claude_session_id(conn):
256
256
  conn.commit()
257
257
 
258
258
 
259
+ def _m14_learnings_priority_weight(conn):
260
+ """Add priority, weight, and guard usage tracking to learnings + followup priority."""
261
+ _migrate_add_column(conn, "learnings", "priority", "TEXT DEFAULT 'medium'")
262
+ _migrate_add_column(conn, "learnings", "weight", "REAL DEFAULT 0.5")
263
+ _migrate_add_column(conn, "learnings", "guard_hits", "INTEGER DEFAULT 0")
264
+ _migrate_add_column(conn, "learnings", "last_guard_hit_at", "REAL")
265
+ _migrate_add_column(conn, "followups", "priority", "TEXT DEFAULT 'medium'")
266
+
267
+
268
+ def _m15_core_rules_tables(conn):
269
+ """Core rules and version tracking tables for the core_rules plugin."""
270
+ conn.execute("""
271
+ CREATE TABLE IF NOT EXISTS core_rules (
272
+ id TEXT PRIMARY KEY,
273
+ category TEXT NOT NULL,
274
+ rule TEXT NOT NULL,
275
+ why TEXT NOT NULL,
276
+ importance INTEGER NOT NULL DEFAULT 3,
277
+ type TEXT NOT NULL DEFAULT 'advisory',
278
+ added_in TEXT DEFAULT '',
279
+ removed_in TEXT DEFAULT NULL,
280
+ is_active INTEGER NOT NULL DEFAULT 1
281
+ )
282
+ """)
283
+ conn.execute("""
284
+ CREATE TABLE IF NOT EXISTS core_rules_version (
285
+ id INTEGER PRIMARY KEY,
286
+ version TEXT NOT NULL DEFAULT '0.0.0',
287
+ updated_at TEXT DEFAULT (datetime('now'))
288
+ )
289
+ """)
290
+ # Seed the version row so UPDATE statements in the plugin always find it
291
+ conn.execute(
292
+ "INSERT OR IGNORE INTO core_rules_version (id, version) VALUES (1, '0.0.0')"
293
+ )
294
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_category ON core_rules(category)")
295
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_active ON core_rules(is_active)")
296
+
297
+
259
298
  # Migration registry — APPEND ONLY, never reorder or delete
260
299
  MIGRATIONS = [
261
300
  (1, "learnings_columns", _m1_learnings_columns),
@@ -271,6 +310,8 @@ MIGRATIONS = [
271
310
  (11, "artifact_registry", _m11_artifact_registry),
272
311
  (12, "session_checkpoints", _m12_session_checkpoints),
273
312
  (13, "claude_session_id", _m13_claude_session_id),
313
+ (14, "learnings_priority_weight", _m14_learnings_priority_weight),
314
+ (15, "core_rules_tables", _m15_core_rules_tables),
274
315
  ]
275
316
 
276
317
 
@@ -14,14 +14,22 @@ import time
14
14
  from datetime import datetime, date, timedelta
15
15
  from pathlib import Path
16
16
 
17
- NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
18
- CORTEX_DIR = Path(__file__).parent
19
- CLAUDE_DIR = Path.home() / ".nexo"
20
- SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
21
- SNAPSHOTS_DIR = CLAUDE_DIR / "snapshots"
22
- OBJECTIVE_FILE = CORTEX_DIR / "evolution-objective.json"
23
- PROMPT_FILE = CORTEX_DIR / "evolution-prompt.md"
24
- RESTORE_LOG = CLAUDE_DIR / "logs" / "snapshot-restores.log"
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
19
+ NEXO_DB = NEXO_HOME / "data" / "nexo.db"
20
+ SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
21
+ SNAPSHOTS_DIR = NEXO_HOME / "snapshots"
22
+ RESTORE_LOG = NEXO_HOME / "logs" / "snapshot-restores.log"
23
+
24
+ # Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
25
+ def _resolve_evolution_file(name: str) -> Path:
26
+ for candidate in [NEXO_HOME / "brain" / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
27
+ if candidate.exists():
28
+ return candidate
29
+ return NEXO_HOME / "brain" / name # default canonical path
30
+
31
+ OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
32
+ PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
25
33
 
26
34
  MAX_SNAPSHOTS = 8
27
35
 
@@ -105,6 +113,8 @@ def create_snapshot(files_to_backup: list) -> str:
105
113
  rel = str(fp).replace(str(Path.home()) + "/", "")
106
114
  dest = files_dir / rel
107
115
  dest.parent.mkdir(parents=True, exist_ok=True)
116
+ if os.path.abspath(str(fp)) == os.path.abspath(str(dest)):
117
+ continue # Skip: source and destination are the same file
108
118
  shutil.copy2(fp, dest)
109
119
  manifest["files"].append(rel)
110
120
 
@@ -144,9 +154,21 @@ def dry_run_restore_test() -> bool:
144
154
 
145
155
  test_file.write_text("modified_content")
146
156
 
157
+ # Find restore script: NEXO_CODE/scripts/ first, then NEXO_HOME/scripts/
158
+ _nexo_code = Path(os.environ.get("NEXO_CODE", ""))
159
+ restore_script = None
160
+ for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
161
+ NEXO_HOME / "scripts" / "nexo-snapshot-restore.sh"]:
162
+ if candidate.exists():
163
+ restore_script = candidate
164
+ break
165
+ if not restore_script:
166
+ test_file.unlink(missing_ok=True)
167
+ return False # No restore script available
168
+
147
169
  try:
148
170
  subprocess.run(
149
- [str(CLAUDE_DIR / "scripts" / "nexo-snapshot-restore.sh"), snap_dir],
171
+ [str(restore_script), snap_dir],
150
172
  capture_output=True, timeout=10, check=True
151
173
  )
152
174
  content = test_file.read_text()
@@ -200,7 +222,7 @@ INVESTIGATE using these tools:
200
222
  4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
201
223
  5. Read ~/.nexo/logs/self-audit-summary.json — system health
202
224
  6. Glob ~/.nexo/scripts/*.py — existing scripts
203
- 7. Glob ~/.nexo/nexo-mcp/plugins/*.py — existing plugins
225
+ 7. Glob ~/.nexo/plugins/*.py — existing plugins
204
226
 
205
227
  LOOK FOR:
206
228
  - Repeated errors that guard isn't preventing
@@ -210,7 +232,7 @@ LOOK FOR:
210
232
  - Patterns in self-critique that suggest systemic issues
211
233
 
212
234
  SAFETY:
213
- - Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/nexo-mcp/plugins/, ~/.nexo/cortex/
235
+ - Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/
214
236
  - IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
215
237
  - Every change needs: what file, what to change, why, risk, how to verify
216
238
 
@@ -17,7 +17,7 @@ import re
17
17
  import sys
18
18
  from pathlib import Path
19
19
 
20
- # Add nexo-mcp to path for cognitive imports
20
+ # Add source dir to path for cognitive imports
21
21
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
22
22
  import cognitive
23
23
 
@@ -40,6 +40,82 @@ record = {
40
40
  print(json.dumps(record))
41
41
  " >> "$LOG_FILE" 2>/dev/null
42
42
 
43
+ # ── Layer 1: Auto-diary every 10 tool calls ─────────────────────────
44
+ COUNTER_FILE="$NEXO_HOME/operations/.tool-call-count"
45
+ NEXO_DB="$NEXO_HOME/data/nexo.db"
46
+
47
+ # Increment counter (atomic: read+write in one step)
48
+ COUNT=1
49
+ if [ -f "$COUNTER_FILE" ]; then
50
+ COUNT=$(( $(cat "$COUNTER_FILE" 2>/dev/null || echo 0) + 1 ))
51
+ fi
52
+ echo "$COUNT" > "$COUNTER_FILE"
53
+
54
+ # Every 10 tool calls, write a mechanical diary draft to SQLite
55
+ if [ $(( COUNT % 10 )) -eq 0 ] && [ -f "$NEXO_DB" ]; then
56
+ python3 -c "
57
+ import json, sqlite3, os, sys
58
+ from datetime import datetime
59
+
60
+ db_path = '$NEXO_DB'
61
+ log_file = '$LOG_FILE'
62
+ count = $COUNT
63
+
64
+ # Read last 10 tool calls from today's log
65
+ entries = []
66
+ if os.path.isfile(log_file):
67
+ with open(log_file, 'r') as f:
68
+ lines = f.readlines()
69
+ for line in lines[-10:]:
70
+ try:
71
+ e = json.loads(line.strip())
72
+ name = e.get('tool_name', '?')
73
+ inp = e.get('tool_input', {})
74
+ # Brief args: first key's value, truncated
75
+ brief = ''
76
+ if isinstance(inp, dict):
77
+ for k, v in list(inp.items())[:1]:
78
+ brief = str(v)[:60]
79
+ entries.append(f'{name}({brief})')
80
+ except Exception:
81
+ pass
82
+
83
+ if not entries:
84
+ sys.exit(0)
85
+
86
+ tools_summary = ', '.join(entries[-10:])
87
+
88
+ # Get current session and task from sessions table
89
+ conn = sqlite3.connect(db_path, timeout=2)
90
+ conn.row_factory = sqlite3.Row
91
+ row = conn.execute(
92
+ 'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
93
+ ).fetchone()
94
+ if not row:
95
+ conn.close()
96
+ sys.exit(0)
97
+
98
+ sid = row['sid']
99
+ task = row['task'] or 'unknown'
100
+
101
+ summary = f'[AUTO-{count}] {len(entries)} tool calls: {tools_summary[:250]}. Task: {task[:100]}'
102
+
103
+ # Write to session_diary_draft (UPSERT)
104
+ conn.execute('''
105
+ INSERT INTO session_diary_draft (sid, summary_draft, tasks_seen, change_ids, decision_ids, last_context_hint, heartbeat_count, updated_at)
106
+ VALUES (?, ?, '[]', '[]', '[]', ?, 0, datetime('now'))
107
+ ON CONFLICT(sid) DO UPDATE SET
108
+ summary_draft = excluded.summary_draft,
109
+ last_context_hint = excluded.last_context_hint,
110
+ updated_at = datetime('now')
111
+ ''', (sid, summary, f'auto-diary at {count} tool calls'))
112
+ conn.commit()
113
+ conn.close()
114
+ " 2>/dev/null &
115
+ # Reset counter after writing
116
+ echo "0" > "$COUNTER_FILE"
117
+ fi
118
+
43
119
  # Cleanup: delete logs >= 30 days old (once daily, uses marker file)
44
120
  CLEANUP_MARKER="$LOG_DIR/.last-cleanup"
45
121
  if [ ! -f "$CLEANUP_MARKER" ] || [ "$(cat "$CLEANUP_MARKER" 2>/dev/null)" != "$TODAY" ]; then
@@ -27,7 +27,8 @@ echo "$NOW" > "$DEBOUNCE_FILE"
27
27
 
28
28
  # 4. Find NEXO SID mapped to this Claude session_id
29
29
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
30
- DB="$NEXO_HOME/nexo.db"
30
+ DB="$NEXO_HOME/data/nexo.db"
31
+ mkdir -p "$NEXO_HOME/data"
31
32
  [ -f "$DB" ] || exit 0
32
33
 
33
34
  NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
@@ -5,7 +5,8 @@
5
5
  set -euo pipefail
6
6
 
7
7
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
8
- NEXO_DB="$NEXO_HOME/nexo.db"
8
+ NEXO_DB="$NEXO_HOME/data/nexo.db"
9
+ mkdir -p "$NEXO_HOME/data"
9
10
  TODAY=$(date +%Y-%m-%d)
10
11
  LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
11
12
  LOG_LINES=0
@@ -6,7 +6,8 @@
6
6
  set -euo pipefail
7
7
 
8
8
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
9
- NEXO_DB="$NEXO_HOME/nexo.db"
9
+ NEXO_DB="$NEXO_HOME/data/nexo.db"
10
+ mkdir -p "$NEXO_HOME/data"
10
11
  TODAY=$(date +%Y-%m-%d)
11
12
  LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
12
13
  LOG_LINES=0
@@ -42,8 +43,109 @@ if [ -f "$NEXO_DB" ]; then
42
43
  fi
43
44
  fi
44
45
 
46
+ # ── Layer 2: Emergency auto-diary before compaction ──────────────────
47
+ # Write an actual session_diary entry (not draft) with mechanical summary
48
+ # This is the parachute — if the LLM never wrote a diary, at least this exists
49
+ if [ -f "$NEXO_DB" ]; then
50
+ python3 -c "
51
+ import json, sqlite3, os, sys
52
+ from datetime import datetime
53
+
54
+ db_path = '$NEXO_DB'
55
+ log_file = '$LOG_FILE'
56
+
57
+ conn = sqlite3.connect(db_path, timeout=3)
58
+ conn.row_factory = sqlite3.Row
59
+
60
+ # Get latest active session
61
+ row = conn.execute(
62
+ 'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
63
+ ).fetchone()
64
+ if not row:
65
+ conn.close()
66
+ sys.exit(0)
67
+
68
+ sid = row['sid']
69
+ task = row['task'] or 'unknown'
70
+
71
+ # Check if a real diary already exists for this session
72
+ has_diary = conn.execute(
73
+ 'SELECT id FROM session_diary WHERE session_id = ? LIMIT 1', (sid,)
74
+ ).fetchone()
75
+ if has_diary:
76
+ conn.close()
77
+ sys.exit(0) # LLM already wrote one, no need for emergency diary
78
+
79
+ # Find last diary timestamp to know where to start reading logs
80
+ last_diary = conn.execute(
81
+ 'SELECT created_at FROM session_diary ORDER BY created_at DESC LIMIT 1'
82
+ ).fetchone()
83
+ last_diary_ts = last_diary['created_at'] if last_diary else '1970-01-01T00:00:00Z'
84
+
85
+ # Read tool log entries since last diary
86
+ entries = []
87
+ modified_files = []
88
+ git_actions = []
89
+ if os.path.isfile(log_file):
90
+ with open(log_file, 'r') as f:
91
+ for line in f:
92
+ try:
93
+ e = json.loads(line.strip())
94
+ ts = e.get('timestamp', '')
95
+ if ts < last_diary_ts:
96
+ continue
97
+ name = e.get('tool_name', '?')
98
+ inp = e.get('tool_input', {}) or {}
99
+ brief = ''
100
+ if isinstance(inp, dict):
101
+ for k, v in list(inp.items())[:1]:
102
+ brief = str(v)[:80]
103
+ entries.append(f'{name}({brief})')
104
+ # Extract decisions from tool calls
105
+ if name in ('Edit', 'Write'):
106
+ fp = inp.get('file_path', inp.get('path', ''))
107
+ if fp:
108
+ modified_files.append(fp.split('/')[-1])
109
+ if name == 'Bash':
110
+ cmd = str(inp.get('command', ''))
111
+ if 'git commit' in cmd or 'git push' in cmd:
112
+ git_actions.append(cmd[:80])
113
+ except Exception:
114
+ pass
115
+
116
+ if not entries:
117
+ conn.close()
118
+ sys.exit(0)
119
+
120
+ # Build mechanical diary
121
+ tools_summary = ', '.join(entries[-30:])[:500]
122
+ summary = f'[EMERGENCY PRE-COMPACT] {len(entries)} tool calls since last diary. Tools: {tools_summary}'
123
+
124
+ decisions = ''
125
+ if modified_files:
126
+ decisions = 'Modified: ' + ', '.join(set(modified_files))[:300]
127
+ if git_actions:
128
+ decisions += (' | Git: ' + ', '.join(git_actions))[:200]
129
+ if not decisions:
130
+ decisions = 'No file modifications detected in tool logs'
131
+
132
+ pending = f'Current task: {task[:200]}'
133
+ context_next = 'COMPACTION HAPPENED. Read this diary to continue. Check session_checkpoints and tool-logs for full context.'
134
+
135
+ # Write actual session_diary entry
136
+ conn.execute('''
137
+ INSERT INTO session_diary
138
+ (session_id, decisions, discarded, pending, context_next,
139
+ mental_state, domain, user_signals, summary, source)
140
+ VALUES (?, ?, '', ?, ?, 'auto-generated', 'auto', '', ?, 'pre-compact-hook')
141
+ ''', (sid, decisions, pending, context_next, summary))
142
+ conn.commit()
143
+ conn.close()
144
+ " 2>/dev/null || true
145
+ fi
146
+
45
147
  cat << HOOKEOF
46
148
  {
47
- "systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context."
149
+ "systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
48
150
  }
49
151
  HOOKEOF
@@ -48,7 +48,7 @@ from datetime import date
48
48
  today_str = '$TODAY'
49
49
  weekday = '$WEEKDAY'
50
50
  nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
51
- db_path = os.path.join(nexo_home, 'nexo.db')
51
+ db_path = os.path.join(nexo_home, 'data', 'nexo.db')
52
52
 
53
53
  lines = []
54
54
  lines.append(f'## Date: {today_str} ({weekday})')
@@ -212,7 +212,11 @@ except Exception as e:
212
212
  fi
213
213
 
214
214
  # ─── Cortex Report: what happened while user was away ───
215
- CORTEX_BRIEFING="$NEXO_HOME/cortex/last-briefing.json"
215
+ # Check brain/ (canonical) first, fall back to cortex/ (legacy)
216
+ CORTEX_BRIEFING="$NEXO_HOME/brain/last-briefing.json"
217
+ if [ ! -f "$CORTEX_BRIEFING" ] && [ -f "$NEXO_HOME/cortex/last-briefing.json" ]; then
218
+ CORTEX_BRIEFING="$NEXO_HOME/cortex/last-briefing.json"
219
+ fi
216
220
  if [ -f "$CORTEX_BRIEFING" ]; then
217
221
  CORTEX_SECTION=$(python3 -c "
218
222
  import json
@@ -25,7 +25,8 @@ TOOL_LOG="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
25
25
  python3 -c "
26
26
  import sys, json, os
27
27
  nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
28
- sys.path.insert(0, os.path.join(nexo_home, 'nexo-mcp'))
28
+ nexo_code = os.environ.get('NEXO_CODE', nexo_home)
29
+ sys.path.insert(0, nexo_code)
29
30
  os.environ['NEXO_SKIP_FS_INDEX'] = '1'
30
31
  from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
31
32
  init_db()
@@ -13,7 +13,10 @@ from db import get_db
13
13
 
14
14
  def _cognitive_db():
15
15
  """Direct cognitive.db connection (for somatic_markers)."""
16
- path = os.path.join(os.path.dirname(__file__), "cognitive.db")
16
+ nexo_home = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
17
+ data_dir = os.path.join(nexo_home, "data")
18
+ os.makedirs(data_dir, exist_ok=True)
19
+ path = os.path.join(data_dir, "cognitive.db")
17
20
  conn = sqlite3.connect(path)
18
21
  conn.row_factory = sqlite3.Row
19
22
  return conn
@@ -15,7 +15,10 @@ import sys
15
15
  import time
16
16
  import numpy as np
17
17
 
18
- DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cognitive.db")
18
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
19
+ _data_dir = os.path.join(NEXO_HOME, "data")
20
+ os.makedirs(_data_dir, exist_ok=True)
21
+ DB_PATH = os.path.join(_data_dir, "cognitive.db")
19
22
  BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
20
23
 
21
24
  MODELS = {