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
@@ -69,14 +69,16 @@ def get_reminders(filter_type: str = 'all') -> list[dict]:
69
69
  elif filter_type == 'due':
70
70
  rows = conn.execute(
71
71
  "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
72
- "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
72
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
73
+ "AND date IS NOT NULL AND date <= ? "
73
74
  "ORDER BY date ASC",
74
75
  (today,)
75
76
  ).fetchall()
76
77
  else: # 'all' — active only
77
78
  rows = conn.execute(
78
79
  "SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
79
- "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
80
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
81
+ "ORDER BY date ASC NULLS LAST"
80
82
  ).fetchall()
81
83
  return [dict(r) for r in rows]
82
84
 
@@ -100,7 +102,7 @@ def find_similar_followups(description: str, threshold: float = 0.3) -> list[dic
100
102
  conn = get_db()
101
103
  rows = conn.execute(
102
104
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
103
- "AND status != 'DELETED'"
105
+ "AND status NOT IN ('DELETED','archived','blocked','waiting')"
104
106
  ).fetchall()
105
107
 
106
108
  def tokenize(text: str) -> set:
@@ -134,11 +136,22 @@ def create_followup(id: str, description: str, date: str = None,
134
136
  reasoning: str = '', recurrence: str = None) -> dict:
135
137
  """Create a new followup with optional reasoning and recurrence.
136
138
 
139
+ Checks for similar open followups before creating. If a match is found,
140
+ returns a warning with the existing followup ID (still creates the new one).
141
+
137
142
  recurrence format: 'weekly:monday', 'monthly:1', 'monthly:10', 'quarterly', etc.
138
143
  When a recurring followup is completed, a new one is auto-created with the next date.
139
144
  """
140
145
  conn = get_db()
141
146
  now = now_epoch()
147
+
148
+ # Anti-duplicate check
149
+ similar = find_similar_followups(description)
150
+ warning = ""
151
+ if similar:
152
+ ids = ", ".join(s["id"] for s in similar[:3])
153
+ warning = f" ⚠ SIMILAR FOLLOWUPS EXIST: {ids} (scores: {', '.join(str(s['_similarity']) for s in similar[:3])}). Consider updating instead."
154
+
142
155
  try:
143
156
  conn.execute(
144
157
  "INSERT INTO followups (id, date, description, verification, status, reasoning, recurrence, created_at, updated_at) "
@@ -150,7 +163,10 @@ def create_followup(id: str, description: str, date: str = None,
150
163
  except sqlite3.IntegrityError:
151
164
  return {"error": f"Followup {id} already exists. Use update instead."}
152
165
  row = conn.execute("SELECT * FROM followups WHERE id = ?", (id,)).fetchone()
153
- return dict(row)
166
+ result = dict(row)
167
+ if warning:
168
+ result["warning"] = warning
169
+ return result
154
170
 
155
171
 
156
172
  def update_followup(id: str, **kwargs) -> dict:
@@ -245,6 +261,18 @@ def complete_followup(id: str, result: str = '') -> dict:
245
261
  archived_id = f"{id}-{today}"
246
262
  conn.execute("UPDATE followups SET id = ? WHERE id = ?", (archived_id, id))
247
263
  conn.commit()
264
+
265
+ # Fix FTS: remove old entry for original ID, add entry for archived ID
266
+ conn.execute("DELETE FROM unified_search WHERE source = 'followup' AND source_id = ?", (id,))
267
+ archived_row = conn.execute("SELECT * FROM followups WHERE id = ?", (archived_id,)).fetchone()
268
+ if archived_row:
269
+ fts_upsert(
270
+ "followup", archived_id, archived_id,
271
+ f"{archived_row['description']} {archived_row['verification'] or ''} {archived_row['reasoning'] or ''}",
272
+ "followup", commit=False,
273
+ )
274
+
275
+ # create_followup handles its own FTS entry for the new recurring ID
248
276
  create_followup(
249
277
  id=id,
250
278
  description=row["description"],
@@ -254,6 +282,15 @@ def complete_followup(id: str, result: str = '') -> dict:
254
282
  recurrence=recurrence,
255
283
  )
256
284
 
285
+ # Return accurate result: the completed one is now archived_id, not id
286
+ return {
287
+ "id": archived_id,
288
+ "status": "COMPLETED",
289
+ "recurrence": recurrence,
290
+ "next_id": id,
291
+ "next_date": next_date,
292
+ }
293
+
257
294
  return update_result
258
295
 
259
296
 
@@ -278,14 +315,16 @@ def get_followups(filter_type: str = 'all') -> list[dict]:
278
315
  elif filter_type == 'due':
279
316
  rows = conn.execute(
280
317
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
281
- "AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
318
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
319
+ "AND date IS NOT NULL AND date <= ? "
282
320
  "ORDER BY date ASC",
283
321
  (today,)
284
322
  ).fetchall()
285
323
  else: # 'all' — active only
286
324
  rows = conn.execute(
287
325
  "SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
288
- "AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
326
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
327
+ "ORDER BY date ASC NULLS LAST"
289
328
  ).fetchall()
290
329
  return [dict(r) for r in rows]
291
330
 
package/src/db/_schema.py CHANGED
@@ -265,6 +265,36 @@ def _m14_learnings_priority_weight(conn):
265
265
  _migrate_add_column(conn, "followups", "priority", "TEXT DEFAULT 'medium'")
266
266
 
267
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
+
268
298
  # Migration registry — APPEND ONLY, never reorder or delete
269
299
  MIGRATIONS = [
270
300
  (1, "learnings_columns", _m1_learnings_columns),
@@ -281,6 +311,7 @@ MIGRATIONS = [
281
311
  (12, "session_checkpoints", _m12_session_checkpoints),
282
312
  (13, "claude_session_id", _m13_claude_session_id),
283
313
  (14, "learnings_priority_weight", _m14_learnings_priority_weight),
314
+ (15, "core_rules_tables", _m15_core_rules_tables),
284
315
  ]
285
316
 
286
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()
@@ -57,7 +58,8 @@ SESSION_START_TS="$NEXO_HOME/operations/.session-start-ts"
57
58
  # 0.5. Detect non-interactive (claude -p) sessions — skip post-mortem entirely
58
59
  # SessionStart hook writes .session-start-ts. If missing or stale (>30 min),
59
60
  # this is likely a -p script session — approve immediately.
60
- if [ ! -f "$SESSION_START_TS" ] || [ "$(($(date +%s) - $(cat "$SESSION_START_TS" 2>/dev/null || echo 0)))" -gt 1800 ]; then
61
+ # Also skip if NEXO_HEADLESS=1 is set (explicit headless mode for scripts).
62
+ if [ "${NEXO_HEADLESS:-}" = "1" ] || [ ! -f "$SESSION_START_TS" ] || [ "$(($(date +%s) - $(cat "$SESSION_START_TS" 2>/dev/null || echo 0)))" -gt 1800 ]; then
61
63
  cat << 'HOOKEOF'
62
64
  {
63
65
  "decision": "approve"
@@ -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 = {