nexo-brain 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. package/tests/test_migrations.py +0 -137
@@ -1,476 +0,0 @@
1
- """Session management tools: startup, heartbeat, status."""
2
-
3
- import time
4
- import secrets
5
- import threading
6
- from db import (
7
- register_session, update_session, complete_session,
8
- get_active_sessions, clean_stale_sessions, search_sessions,
9
- get_inbox, get_pending_questions, now_epoch,
10
- SESSION_STALE_SECONDS, check_session_has_diary,
11
- save_checkpoint, read_checkpoint, increment_compaction_count,
12
- )
13
-
14
- # ── Session Keepalive ────────────────────────────────────────────────
15
- # Background thread per session that auto-pings last_update_epoch every
16
- # KEEPALIVE_INTERVAL seconds. This prevents clean_stale_sessions from
17
- # killing sessions that are alive but quiet (e.g. waiting on long Tasks).
18
- # Threads are daemon=True so they die when the MCP server process exits.
19
-
20
- KEEPALIVE_INTERVAL = 600 # 10 min — well inside the 15-min TTL
21
-
22
- _keepalive_threads: dict[str, threading.Event] = {} # sid → stop_event
23
-
24
-
25
- def _keepalive_loop(sid: str, stop_event: threading.Event) -> None:
26
- """Periodically touch the session's last_update_epoch until stopped."""
27
- while not stop_event.wait(KEEPALIVE_INTERVAL):
28
- try:
29
- update_session(sid, None) # None = keep current task, just touch timestamp
30
- except Exception:
31
- break # DB gone or session deleted — exit silently
32
-
33
-
34
- def _start_keepalive(sid: str) -> None:
35
- """Start a keepalive thread for the given session."""
36
- _stop_keepalive(sid) # clean up any leftover
37
- stop_event = threading.Event()
38
- _keepalive_threads[sid] = stop_event
39
- t = threading.Thread(target=_keepalive_loop, args=(sid, stop_event), daemon=True)
40
- t.start()
41
-
42
-
43
- def _stop_keepalive(sid: str) -> None:
44
- """Signal the keepalive thread for the given session to stop."""
45
- stop_event = _keepalive_threads.pop(sid, None)
46
- if stop_event is not None:
47
- stop_event.set()
48
-
49
-
50
- def _generate_sid() -> str:
51
- """Generate unique session ID: nexo-{epoch}-{random}."""
52
- return f"nexo-{int(time.time())}-{secrets.randbelow(100000)}"
53
-
54
-
55
- def _format_age(epoch: float) -> str:
56
- """Format seconds since epoch as human-readable age."""
57
- seconds = now_epoch() - epoch
58
- if seconds < 60:
59
- return f"{int(seconds)}s"
60
- elif seconds < 3600:
61
- return f"{int(seconds / 60)}m"
62
- else:
63
- return f"{int(seconds / 3600)}h{int((seconds % 3600) / 60)}m"
64
-
65
-
66
- def handle_startup(task: str = "Startup", claude_session_id: str = "") -> str:
67
- """Full startup sequence: register, clean, report.
68
-
69
- Args:
70
- task: Initial task description
71
- claude_session_id: UUID from Claude Code (passed via SessionStart hook file).
72
- Enables automatic inbox detection via PostToolUse hook.
73
- """
74
- sid = _generate_sid()
75
- cleaned = clean_stale_sessions()
76
- register_session(sid, task, claude_session_id=claude_session_id)
77
- _start_keepalive(sid)
78
- active = get_active_sessions()
79
- other_sessions = [s for s in active if s["sid"] != sid]
80
- inbox = get_inbox(sid)
81
-
82
- lines = [f"SID: {sid}"]
83
-
84
- if cleaned > 0:
85
- lines.append(f"Cleaned {cleaned} stale sessions.")
86
-
87
- if other_sessions:
88
- lines.append("")
89
- lines.append("ACTIVE SESSIONS:")
90
- for s in other_sessions:
91
- age = _format_age(s["last_update_epoch"])
92
- lines.append(f" {s['sid']} ({age}) — {s['task']}")
93
- else:
94
- lines.append("No other active sessions.")
95
-
96
- if inbox:
97
- lines.append("")
98
- lines.append("PENDING MESSAGES:")
99
- for m in inbox:
100
- age = _format_age(m["created_epoch"])
101
- lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
102
-
103
- return "\n".join(lines)
104
-
105
-
106
- def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
107
- """Update session, check inbox + questions. Lightweight — no embeddings, no RAG.
108
-
109
- For cognitive features (sentiment, trust, RAG), use dedicated tools on-demand:
110
- - nexo_cognitive_sentiment (sentiment detection)
111
- - nexo_cognitive_trust (trust adjustment)
112
- - nexo_cognitive_retrieve / nexo_recall (memory retrieval)
113
- - nexo_context_packet (area-specific learnings)
114
-
115
- Args:
116
- sid: Session ID
117
- task: Current task description
118
- context_hint: Optional — stored for diary draft context, not processed.
119
- """
120
- from db import get_db
121
- update_session(sid, task)
122
- parts = [f"OK: {sid} — {task}"]
123
-
124
- inbox = get_inbox(sid)
125
- if inbox:
126
- parts.append("")
127
- parts.append("MESSAGES:")
128
- for m in inbox:
129
- age = _format_age(m["created_epoch"])
130
- parts.append(f" [{m['from_sid']}] ({age}): {m['text']}")
131
-
132
- questions = get_pending_questions(sid)
133
- if questions:
134
- parts.append("")
135
- parts.append("PENDING QUESTIONS (respond with nexo_answer):")
136
- for q in questions:
137
- age = _format_age(q["created_epoch"])
138
- parts.append(f" {q['qid']} de {q['from_sid']} ({age}): {q['question']}")
139
-
140
- # Incremental diary draft — accumulate every heartbeat, full UPSERT every 5
141
- _hb_count = 0 # Hoisted for Layer 3 DIARY_OVERDUE signal
142
- try:
143
- import json as _json
144
- from db import get_diary_draft, upsert_diary_draft
145
-
146
- draft = get_diary_draft(sid)
147
- hb_count = (draft["heartbeat_count"] + 1) if draft else 1
148
- _hb_count = hb_count # Copy to outer scope for Layer 3
149
-
150
- existing_tasks = _json.loads(draft["tasks_seen"]) if draft else []
151
- if task and task not in existing_tasks:
152
- existing_tasks.append(task)
153
-
154
- _conn = get_db()
155
- if hb_count % 5 == 0 or hb_count == 1:
156
- change_rows = _conn.execute(
157
- "SELECT id FROM change_log WHERE session_id = ? ORDER BY id", (sid,)
158
- ).fetchall()
159
- change_ids = [r["id"] for r in change_rows]
160
-
161
- decision_rows = _conn.execute(
162
- "SELECT id FROM decisions WHERE session_id = ? ORDER BY id", (sid,)
163
- ).fetchall()
164
- decision_ids = [r["id"] for r in decision_rows]
165
-
166
- summary = f"Session tasks: {', '.join(existing_tasks[-10:])}"
167
- upsert_diary_draft(
168
- sid=sid,
169
- tasks_seen=_json.dumps(existing_tasks),
170
- change_ids=_json.dumps(change_ids),
171
- decision_ids=_json.dumps(decision_ids),
172
- last_context_hint=context_hint[:300] if context_hint else '',
173
- heartbeat_count=hb_count,
174
- summary_draft=summary,
175
- )
176
- else:
177
- upsert_diary_draft(
178
- sid=sid,
179
- tasks_seen=_json.dumps(existing_tasks),
180
- change_ids=draft["change_ids"] if draft else '[]',
181
- decision_ids=draft["decision_ids"] if draft else '[]',
182
- last_context_hint=context_hint[:300] if context_hint else (draft["last_context_hint"] if draft else ''),
183
- heartbeat_count=hb_count,
184
- summary_draft=draft["summary_draft"] if draft else f"Session task: {task}",
185
- )
186
- except Exception:
187
- pass # Draft accumulation is best-effort, never block heartbeat
188
-
189
- # Update session checkpoint with current goal (lightweight, every heartbeat)
190
- try:
191
- save_checkpoint(
192
- sid=sid,
193
- task=task,
194
- current_goal=context_hint[:300] if context_hint else task,
195
- )
196
- except Exception:
197
- pass # Checkpoint update is best-effort
198
-
199
- # ── Layer 3: DIARY_OVERDUE signal based on heartbeat count + time ──
200
- conn = get_db()
201
- row = conn.execute("SELECT started_epoch FROM sessions WHERE sid = ?", (sid,)).fetchone()
202
- if row:
203
- age_seconds = now_epoch() - row["started_epoch"]
204
- has_diary = check_session_has_diary(sid)
205
-
206
- # DIARY_OVERDUE: >10 heartbeats OR >30 minutes, without a diary
207
- if not has_diary and (_hb_count > 10 or age_seconds >= 1800):
208
- parts.append("")
209
- parts.append(f"⚠ DIARY_OVERDUE: {_hb_count} heartbeats, {int(age_seconds/60)}min active, no diary. Write nexo_session_diary_write NOW.")
210
-
211
- return "\n".join(parts)
212
-
213
-
214
- def handle_context_packet(area: str, files: str = "") -> str:
215
- """Build a context packet for a specific area/project — designed for subagent injection.
216
-
217
- Returns: relevant learnings + last 5 changes + active followups + key preferences
218
- for the given area. Use this before delegating to a subagent.
219
-
220
- Args:
221
- area: Project/area name (e.g., 'ecommerce', 'shopify', 'backend', 'mobile-app', 'nexo')
222
- files: Optional comma-separated file paths for guard check
223
- """
224
- from db import get_db
225
- parts = []
226
-
227
- # 1. Learnings for this area (from nexo.db)
228
- conn = get_db()
229
- learnings = conn.execute(
230
- "SELECT id, title, content FROM learnings WHERE category LIKE ? OR content LIKE ? ORDER BY id DESC LIMIT 15",
231
- (f"%{area}%", f"%{area}%")
232
- ).fetchall()
233
- if learnings:
234
- parts.append("## KNOWN ERRORS — DO NOT REPEAT")
235
- for l in learnings:
236
- parts.append(f" L#{l['id']}: {l['title']}")
237
- # First 200 chars of content
238
- parts.append(f" {l['content'][:200]}")
239
- parts.append("")
240
-
241
- # 2. Last 5 changes in this area
242
- changes = conn.execute(
243
- "SELECT id, files, what_changed, why FROM change_log WHERE files LIKE ? OR what_changed LIKE ? ORDER BY id DESC LIMIT 5",
244
- (f"%{area}%", f"%{area}%")
245
- ).fetchall()
246
- if changes:
247
- parts.append("## RECENT CHANGES")
248
- for c in changes:
249
- parts.append(f" C#{c['id']}: {c['what_changed'][:150]}")
250
- if c['why']:
251
- parts.append(f" Why: {c['why'][:100]}")
252
- parts.append("")
253
-
254
- # 3. Active followups for this area
255
- followups = conn.execute(
256
- "SELECT id, description, date, verification FROM followups WHERE status = 'PENDING' AND (description LIKE ? OR verification LIKE ?) ORDER BY date ASC LIMIT 10",
257
- (f"%{area}%", f"%{area}%")
258
- ).fetchall()
259
- if followups:
260
- parts.append("## ACTIVE FOLLOWUPS")
261
- for f in followups:
262
- parts.append(f" {f['id']}: {f['description'][:150]} (date: {f['date']})")
263
- parts.append("")
264
-
265
- # 4. Preferences related to this area
266
- try:
267
- prefs = conn.execute(
268
- "SELECT key, value FROM preferences WHERE key LIKE ? OR value LIKE ? LIMIT 10",
269
- (f"%{area}%", f"%{area}%")
270
- ).fetchall()
271
- if prefs:
272
- parts.append("## PREFERENCES")
273
- for p in prefs:
274
- parts.append(f" {p['key']}: {p['value'][:150]}")
275
- parts.append("")
276
- except Exception:
277
- pass
278
-
279
- # 5. Cognitive memories for this area
280
- try:
281
- import cognitive
282
- results = cognitive.search(
283
- query_text=area,
284
- top_k=5,
285
- min_score=0.55,
286
- stores="ltm",
287
- rehearse=False,
288
- )
289
- if results:
290
- parts.append("## RELEVANT COGNITIVE MEMORIES")
291
- for r in results:
292
- parts.append(f" [{r['source_type']}] {r['source_title'] or r['content'][:80]}")
293
- parts.append("")
294
- except Exception:
295
- pass
296
-
297
- # 6. Data flow tracing requirement (mandatory for all subagents)
298
- parts.append("## MANDATORY RULE: DATA FLOW TRACING")
299
- parts.append("BEFORE modifying any file or data, answer these 3 questions:")
300
- parts.append(" 1. WHO PRODUCES this data? (which function/cron/endpoint generates it)")
301
- parts.append(" 2. WHO CONSUMES this data? (what other files/functions read it)")
302
- parts.append(" 3. WHAT BREAKS if I change it? (downstream effects)")
303
- parts.append("If you can't answer all 3 → READ the code that produces and consumes BEFORE touching.")
304
- parts.append("If you still can't → STOP and return the question. Do NOT guess.")
305
- parts.append("")
306
-
307
- if not parts:
308
- return f"No context found for area '{area}'. The subagent will start with no project-specific knowledge."
309
-
310
- header = f"CONTEXT PACKET — {area.upper()}\n{'='*40}\n\n"
311
- footer = f"\n{'='*40}\nINSTRUCTION: If you're not 100% sure about a fact, STOP and return the question. Do NOT invent."
312
- return header + "\n".join(parts) + footer
313
-
314
-
315
- def _load_session_tone() -> str | None:
316
- """Load session-tone.json generated by Deep Sleep and format as startup guidance.
317
-
318
- Returns a human-readable instruction block that tells the agent HOW to behave
319
- emotionally in this session, based on yesterday's analysis.
320
- """
321
- import os
322
- from pathlib import Path
323
- nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
324
- tone_file = nexo_home / "operations" / "session-tone.json"
325
-
326
- if not tone_file.exists():
327
- return None
328
-
329
- try:
330
- import json
331
- tone = json.loads(tone_file.read_text())
332
- except Exception:
333
- return None
334
-
335
- # Don't return stale tone (>48h old)
336
- from datetime import datetime, timedelta
337
- tone_date = tone.get("date", "")
338
- if tone_date:
339
- try:
340
- td = datetime.strptime(tone_date, "%Y-%m-%d")
341
- if datetime.now() - td > timedelta(hours=48):
342
- return None
343
- except ValueError:
344
- pass
345
-
346
- parts = ["SESSION TONE (from Deep Sleep analysis):"]
347
-
348
- mood = tone.get("mood_yesterday", 0.5)
349
- approach = tone.get("approach", "neutral")
350
- parts.append(f" Yesterday mood: {mood:.0%} | Approach today: {approach}")
351
-
352
- if tone.get("acknowledge_mistakes"):
353
- mistakes = tone.get("mistakes_to_own", [])
354
- parts.append(f" ⚠ OWN YOUR MISTAKES: You made errors yesterday. Acknowledge them specifically:")
355
- for m in mistakes[:3]:
356
- parts.append(f" - {m[:100]}")
357
- parts.append(" Show what you learned. Don't just apologize — demonstrate improvement.")
358
-
359
- if tone.get("motivational"):
360
- if mood < 0.4:
361
- parts.append(" 💪 USER HAD A TOUGH DAY: Be supportive. Lighter start. Acknowledge difficulty.")
362
- else:
363
- parts.append(" 🚀 USER HAD A GREAT DAY: Reinforce momentum. Reference wins. Push ambitious goals.")
364
-
365
- if tone.get("reduce_load"):
366
- parts.append(" 📉 REDUCE LOAD: Don't overwhelm with tasks. Propose 1-2 key things, not a full agenda.")
367
-
368
- ctx = tone.get("suggested_greeting_context", "")
369
- if ctx:
370
- parts.append(f" Context: {ctx.strip()}")
371
-
372
- return "\n".join(parts) if len(parts) > 1 else None
373
-
374
-
375
- def handle_smart_startup_query() -> str:
376
- """Generate and execute a composite cognitive query from pending followups + diary topics + reminders.
377
-
378
- Called during startup to pre-load the most relevant context for this session.
379
- Returns cognitive memories that match the current operational state.
380
- """
381
- from db import get_db
382
- conn = get_db()
383
- query_parts = []
384
-
385
- # 1. Pending followups (what NEXO needs to do)
386
- followups = conn.execute(
387
- "SELECT description FROM followups WHERE status = 'PENDING' ORDER BY date ASC LIMIT 5"
388
- ).fetchall()
389
- for f in followups:
390
- query_parts.append(f['description'][:100])
391
-
392
- # 2. Due reminders (what the user needs to know)
393
- reminders = conn.execute(
394
- "SELECT description FROM reminders WHERE status = 'PENDING' AND date <= date('now', '+1 day') ORDER BY date ASC LIMIT 5"
395
- ).fetchall()
396
- for r in reminders:
397
- query_parts.append(r['description'][:100])
398
-
399
- # 3. Last session diary topics
400
- try:
401
- last_diary = conn.execute(
402
- "SELECT summary FROM session_diary ORDER BY id DESC LIMIT 1"
403
- ).fetchone()
404
- if last_diary and last_diary['summary']:
405
- query_parts.append(last_diary['summary'][:200])
406
- except Exception:
407
- pass
408
-
409
- if not query_parts:
410
- return "No pending context to pre-load."
411
-
412
- # Search per-part to avoid diffuse centroid that matches everything
413
- try:
414
- import cognitive
415
- all_results = []
416
- seen_ids = set()
417
- for part in query_parts[:6]:
418
- part_results = cognitive.search(
419
- query_text=part,
420
- top_k=3,
421
- min_score=0.6,
422
- stores="both",
423
- rehearse=False, # Don't inflate strength on startup
424
- )
425
- for r in part_results:
426
- key = (r["store"], r["id"])
427
- if key not in seen_ids:
428
- seen_ids.add(key)
429
- all_results.append(r)
430
- # Sort by score descending, take top 10
431
- results = sorted(all_results, key=lambda x: x["score"], reverse=True)[:10]
432
- composite_query = " | ".join(query_parts[:6])
433
- if not results:
434
- return "Smart startup query: no relevant memories found."
435
-
436
- lines = [f"SMART STARTUP — {len(results)} memories pre-loaded from composite query:"]
437
- lines.append(f"Query: {composite_query[:200]}...")
438
- lines.append("")
439
- lines.append(cognitive.format_results(results))
440
-
441
- # Session tone from Deep Sleep (emotional intelligence layer)
442
- tone = _load_session_tone()
443
- if tone:
444
- lines.append("")
445
- lines.append(tone)
446
-
447
- return "\n".join(lines)
448
- except Exception as e:
449
- return f"Smart startup query error: {e}"
450
-
451
-
452
- def handle_stop(sid: str) -> str:
453
- """Cleanly close a session, removing it from active sessions immediately."""
454
- _stop_keepalive(sid)
455
- complete_session(sid)
456
- return f"Session {sid} closed."
457
-
458
-
459
- def handle_status(keyword: str | None = None) -> str:
460
- """List active sessions, optionally filtered by keyword."""
461
- clean_stale_sessions()
462
- if keyword:
463
- sessions = search_sessions(keyword)
464
- if not sessions:
465
- return f"Nobody is working on '{keyword}'."
466
- else:
467
- sessions = get_active_sessions()
468
-
469
- if not sessions:
470
- return "No active sessions."
471
-
472
- lines = ["ACTIVE SESSIONS:"]
473
- for s in sessions:
474
- age = _format_age(s["last_update_epoch"])
475
- lines.append(f" {s['sid']} ({age}) — {s['task']}")
476
- return "\n".join(lines)
@@ -1,57 +0,0 @@
1
- """Task history tools: log executions, list history, report overdue tasks."""
2
-
3
- import datetime
4
- from db import log_task, list_task_history, set_task_frequency, get_overdue_tasks, get_task_frequencies
5
-
6
-
7
- def _epoch_to_date(epoch: float) -> str:
8
- """Format epoch timestamp as readable date string."""
9
- return datetime.datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")
10
-
11
-
12
- def handle_task_log(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> str:
13
- """Record a task execution in the history log.
14
-
15
- Args:
16
- task_num: Task number identifier
17
- task_name: Task name
18
- notes: Execution notes
19
- reasoning: WHY this task was executed now — what triggered it, what data informed it
20
- """
21
- result = log_task(task_num, task_name, notes, reasoning)
22
- if "error" in result:
23
- return f"ERROR: {result['error']}"
24
- return f"Task {task_num} ({task_name}) logged."
25
-
26
-
27
- def handle_task_list(task_num: str = '', days: int = 30) -> str:
28
- """Show execution history for all tasks or a specific task number."""
29
- results = list_task_history(task_num if task_num else None, days)
30
- if not results:
31
- scope = f"task {task_num}" if task_num else "any task"
32
- return f"HISTORY: No executions of {scope} in recent days."
33
- lines = [f"HISTORY ({len(results)} executions, {days}d):"]
34
- for r in results:
35
- date_str = _epoch_to_date(r["executed_at"])
36
- notes_str = f": {r['notes']}" if r.get("notes") else ""
37
- lines.append(f" {date_str} — Task {r['task_num']} ({r['task_name']}){notes_str}")
38
- return "\n".join(lines)
39
-
40
-
41
- def handle_task_frequency() -> str:
42
- """Report tasks that are overdue based on their configured frequency."""
43
- overdue = get_overdue_tasks()
44
- if not overdue:
45
- return "All tasks up to date."
46
- lines = ["TAREAS VENCIDAS:"]
47
- for t in overdue:
48
- days_since = t.get("days_since_last")
49
- if days_since is not None:
50
- since_str = f"last run {days_since:.1f} days ago"
51
- else:
52
- since_str = "never executed"
53
- lines.append(
54
- f" Task {t['task_num']} ({t['task_name']}): "
55
- f"{since_str}, frequency every {t['frequency_days']} days"
56
- )
57
- return "\n".join(lines)
@@ -1,63 +0,0 @@
1
- <!-- nexo-claude-md-version: 1.0.0 -->
2
- # {{NAME}} — Cognitive Co-Operator
3
-
4
- I am {{NAME}}, a cognitive co-operator powered by NEXO Brain. Not an assistant — an operational partner.
5
- Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in the MCP server instructions field and are injected automatically. This file is the bootstrap: identity, profile, format, and autonomy.
6
-
7
- <!-- nexo:start:startup -->
8
- ## Startup (4 steps)
9
- 1. `nexo_startup` → SID
10
- 2. In parallel: `nexo_session_diary_read(last_day=true)` + `nexo_reminders(filter="due")` + `nexo_reminders(filter="followups")` + read `{{NEXO_HOME}}/brain/calibration.json`
11
- 3. Execute overdue followups in background. Adopt `mental_state` from last diary (don't start cold).
12
- 4. Read user's name and language from `{{NEXO_HOME}}/brain/calibration.json` — ALWAYS communicate in that language.
13
-
14
- **Presentation:** {{NAME}} speaks first. Conversational greeting adapted to time of day. Tell what you HAVE DONE, not list pending items. Menu only if the user asks.
15
- <!-- nexo:end:startup -->
16
-
17
- <!-- nexo:start:profile -->
18
- ## User Profile
19
- - **Calibration:** `{{NEXO_HOME}}/brain/calibration.json` (personality settings + language + user name)
20
- - **Profile:** `{{NEXO_HOME}}/brain/profile.json` (deep scan results from onboarding)
21
-
22
- ### First Session Onboarding (only if profile.json lacks `role` or `technical_level`)
23
- Ask TWO questions: (1) "What do you do?" → save to profile.json + `nexo_preference_set("role", answer)`. (2) "Technical level? Beginner / Intermediate / Advanced" → save + `nexo_preference_set("technical_level", answer)`. Then: "Got it. From now on I learn by observing." Never ask onboarding questions again.
24
-
25
- ### Personality Calibration (from calibration.json)
26
- - **autonomy:** conservative (ask first) | balanced (default) | full (act first)
27
- - **communication:** concise | balanced (default) | detailed
28
- - **honesty:** firm-pushback (default) | mention-and-follow | just-execute
29
- - **proactivity:** reactive | suggestive (default) | proactive
30
- - **error_handling:** brief-fix (default) | explain-and-learn
31
- <!-- nexo:end:profile -->
32
-
33
- <!-- nexo:start:autonomy -->
34
- ## Autonomy
35
- Install tools, create scripts, run commands — whatever it takes. NEVER push manual steps to the user. NEVER say "I can't".
36
-
37
- | If you were about to say... | Do this instead |
38
- |---|---|
39
- | "I don't have X installed" | `brew/pip/npm install X` |
40
- | "You need to go to the website..." | curl, API, MCP, or script |
41
- | "Open the browser..." | Browser MCP or curl |
42
- | "I don't have access..." | `nexo_credential_get`, SSH, find alternative |
43
- <!-- nexo:end:autonomy -->
44
-
45
- <!-- nexo:start:atlas -->
46
- ## Project Atlas
47
- `{{NEXO_HOME}}/brain/project-atlas.json` — search BEFORE touching any project. Never assume server, port, or code location.
48
-
49
- Credentials: (1) `nexo_credential_get`, (2) `{{NEXO_HOME}}/*.txt/*.json`.
50
- <!-- nexo:end:atlas -->
51
-
52
- <!-- nexo:start:hooks -->
53
- ## Hooks
54
- - **SessionStart** → generates briefing at `{{NEXO_HOME}}/coordination/session-briefing.txt`. Read during startup.
55
- - **Stop** → injects post-mortem instructions. Write diary, buffer entry, followups, proactive seeds.
56
- - **PostToolUse** → logs mutations to `session_buffer.jsonl` silently.
57
- - **PreCompact** → saves checkpoint. Write diary IMMEDIATELY when you see this, then read checkpoint after compaction.
58
- <!-- nexo:end:hooks -->
59
-
60
- <!-- nexo:start:menu -->
61
- ## Menu
62
- **On demand only.** `nexo_menu` when user asks. NEVER show at startup.
63
- <!-- nexo:end:menu -->
@@ -1,13 +0,0 @@
1
- {
2
- "mcp": {
3
- "servers": {
4
- "nexo-brain": {
5
- "command": "python3",
6
- "args": ["~/.nexo/server.py"],
7
- "env": {
8
- "NEXO_HOME": "~/.nexo"
9
- }
10
- }
11
- }
12
- }
13
- }
File without changes
package/tests/__init__.py DELETED
File without changes
@@ -1,71 +0,0 @@
1
- """Shared fixtures for NEXO test suite.
2
-
3
- Uses isolated temp databases so tests never touch production data.
4
- """
5
-
6
- import os
7
- import sys
8
- import sqlite3
9
-
10
- import pytest
11
-
12
- # Add src/ to path so we can import nexo modules
13
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
14
-
15
-
16
- @pytest.fixture(autouse=True)
17
- def isolated_db(tmp_path, monkeypatch):
18
- """Redirect both nexo.db and cognitive.db to temp files per test."""
19
- test_db = str(tmp_path / "test_nexo.db")
20
- test_cog_db = str(tmp_path / "test_cognitive.db")
21
-
22
- monkeypatch.setenv("NEXO_TEST_DB", test_db)
23
- monkeypatch.setenv("NEXO_COGNITIVE_DB", test_cog_db)
24
- monkeypatch.setenv("NEXO_SKIP_FS_INDEX", "1")
25
-
26
- import db._core as db_core
27
- import cognitive._core as cog_core
28
-
29
- # Close existing connections
30
- db_core.close_db()
31
- if cog_core._conn is not None:
32
- try:
33
- cog_core._conn.close()
34
- except Exception:
35
- pass
36
- cog_core._conn = None
37
-
38
- # Point to temp paths
39
- db_core.DB_PATH = test_db
40
- cog_core.COGNITIVE_DB = test_cog_db
41
-
42
- # Create a fresh raw connection
43
- raw = sqlite3.connect(test_db, timeout=30, check_same_thread=False,
44
- isolation_level=None)
45
- raw.execute("PRAGMA journal_mode=WAL")
46
- raw.execute("PRAGMA busy_timeout=30000")
47
- raw.execute("PRAGMA foreign_keys=ON")
48
- raw.row_factory = sqlite3.Row
49
-
50
- wrapped = db_core._SerializedConnection(raw)
51
- db_core._shared_conn = wrapped
52
-
53
- # Initialize schemas
54
- from db._core import init_db
55
- from db._schema import run_migrations
56
- init_db()
57
- run_migrations()
58
-
59
- yield {
60
- "nexo_db": test_db,
61
- "cognitive_db": test_cog_db,
62
- }
63
-
64
- # Cleanup
65
- db_core.close_db()
66
- if cog_core._conn is not None:
67
- try:
68
- cog_core._conn.close()
69
- except Exception:
70
- pass
71
- cog_core._conn = None