nexo-brain 1.7.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 (247) hide show
  1. package/README.md +25 -24
  2. package/bin/nexo-brain.js +680 -381
  3. package/package.json +4 -1
  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_close_sessions.cpython-310.pyc +0 -0
  8. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  9. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  10. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  11. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  12. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  17. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  18. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  19. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  20. package/src/__pycache__/server.cpython-310.pyc +0 -0
  21. package/src/__pycache__/server.cpython-314.pyc +0 -0
  22. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  26. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  27. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  28. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  29. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  30. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  31. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  32. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  33. package/src/auto_close_sessions.py +1 -1
  34. package/src/auto_update.py +634 -0
  35. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  40. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  45. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  47. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  48. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  49. package/src/cognitive/_core.py +7 -3
  50. package/src/cognitive/_decay.py +1 -1
  51. package/src/cognitive/_search.py +1 -0
  52. package/src/cognitive/_trust.py +3 -3
  53. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  54. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  55. package/src/dashboard/app.py +8 -2
  56. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  80. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  81. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  82. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  83. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  84. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  85. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  86. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  87. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  88. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  89. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  90. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  91. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  92. package/src/db/_core.py +5 -1
  93. package/src/db/_episodic.py +1 -3
  94. package/src/db/_reminders.py +36 -1
  95. package/src/db/_schema.py +31 -0
  96. package/src/evolution_cycle.py +33 -11
  97. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  98. package/src/hooks/auto_capture.py +1 -1
  99. package/src/hooks/capture-tool-logs.sh +76 -0
  100. package/src/hooks/inbox-hook.sh +2 -1
  101. package/src/hooks/post-compact.sh +2 -1
  102. package/src/hooks/pre-compact.sh +104 -2
  103. package/src/hooks/session-start.sh +6 -2
  104. package/src/hooks/session-stop.sh +2 -1
  105. package/src/kg_populate.py +4 -1
  106. package/src/migrate_embeddings.py +4 -1
  107. package/src/plugin_loader.py +100 -34
  108. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  109. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  110. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  111. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  112. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  113. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  114. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  115. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  116. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  117. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  118. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  119. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  120. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  121. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  122. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  123. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  124. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  125. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  126. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  127. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  128. package/src/plugins/agents.py +2 -2
  129. package/src/plugins/backup.py +5 -4
  130. package/src/plugins/core_rules.py +5 -1
  131. package/src/plugins/episodic_memory.py +14 -5
  132. package/src/plugins/evolution.py +6 -2
  133. package/src/plugins/guard.py +20 -11
  134. package/src/plugins/update.py +238 -0
  135. package/src/requirements.txt +12 -0
  136. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  137. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  138. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  139. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  140. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  141. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  142. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  143. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  144. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  145. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  146. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  147. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  148. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  149. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  150. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  151. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  152. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  153. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  154. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  155. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  156. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  157. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  158. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  159. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  160. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  161. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  162. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  163. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  164. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  165. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  166. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  167. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  168. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  169. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  170. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  171. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  172. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  173. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  174. package/src/scripts/check-context.py +9 -1
  175. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  176. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  177. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  178. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  179. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  180. package/src/scripts/deep-sleep/apply_findings.py +3 -3
  181. package/src/scripts/nexo-auto-update.py +4 -211
  182. package/src/scripts/nexo-backup.sh +5 -13
  183. package/src/scripts/nexo-brain-activation.sh +26 -26
  184. package/src/scripts/nexo-catchup.py +36 -22
  185. package/src/scripts/nexo-cognitive-decay.py +7 -3
  186. package/src/scripts/nexo-daily-self-audit.py +30 -10
  187. package/src/scripts/nexo-evolution-run.py +35 -10
  188. package/src/scripts/nexo-followup-hygiene.py +2 -2
  189. package/src/scripts/nexo-github-monitor.py +11 -4
  190. package/src/scripts/nexo-immune.py +17 -2
  191. package/src/scripts/nexo-inbox-hook.sh +2 -1
  192. package/src/scripts/nexo-install.py +4 -225
  193. package/src/scripts/nexo-learning-housekeep.py +7 -3
  194. package/src/scripts/nexo-learning-validator.py +10 -2
  195. package/src/scripts/nexo-migrate.py +9 -3
  196. package/src/scripts/nexo-postmortem-consolidator.py +22 -4
  197. package/src/scripts/nexo-pre-commit.py +3 -1
  198. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  199. package/src/scripts/nexo-proactive-dashboard.py +4 -4
  200. package/src/scripts/nexo-runtime-preflight.py +59 -55
  201. package/src/scripts/nexo-send-email.py +1 -1
  202. package/src/scripts/nexo-send-reply.py +3 -1
  203. package/src/scripts/nexo-sleep.py +19 -5
  204. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  205. package/src/scripts/nexo-synthesis.py +17 -2
  206. package/src/scripts/nexo-tcc-approve.sh +79 -0
  207. package/src/scripts/nexo-update.sh +161 -0
  208. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  209. package/src/scripts/nexo-watchdog.sh +22 -13
  210. package/src/server.py +77 -28
  211. package/src/storage_router.py +6 -2
  212. package/src/tools_learnings.py +6 -6
  213. package/src/tools_reminders_crud.py +10 -8
  214. package/src/tools_sessions.py +9 -4
  215. package/templates/CLAUDE.md.template +14 -80
  216. package/templates/launchagents/README.md +7 -7
  217. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  218. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  219. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  220. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  221. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  222. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  223. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  224. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  225. package/templates/launchagents/com.nexo.immune.plist +4 -0
  226. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  227. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  228. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  229. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  230. package/templates/openclaw.json +1 -1
  231. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  232. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  233. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  234. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  235. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  236. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  237. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  238. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  239. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  240. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  241. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  242. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  243. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  244. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
  245. package/tests/conftest.py +2 -2
  246. package/tests/test_cognitive.py +7 -6
  247. package/tests/test_migrations.py +26 -0
@@ -254,7 +254,7 @@ def dream_cycle(max_insights: int = 50) -> dict:
254
254
  })
255
255
 
256
256
  if len(recent_memories) < 2:
257
- return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories)}
257
+ return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories), "candidates_found": 0}
258
258
 
259
259
  # 2. Get already-dreamed pairs to skip
260
260
  dreamed = set()
@@ -1,5 +1,6 @@
1
1
  """NEXO Cognitive — Search, retrieval, ranking."""
2
2
  import math
3
+ import sqlite3
3
4
  import numpy as np
4
5
  from datetime import datetime
5
6
  from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob, _get_model, _get_reranker, rerank_results, EMBEDDING_DIM
@@ -1,7 +1,7 @@
1
1
  """NEXO Cognitive — Trust scoring, sentiment, dissonance."""
2
2
  import re
3
3
  import numpy as np
4
- from datetime import datetime
4
+ from datetime import datetime, timedelta
5
5
  from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array
6
6
  from cognitive._core import POSITIVE_SIGNALS, NEGATIVE_SIGNALS, URGENCY_SIGNALS
7
7
 
@@ -314,13 +314,13 @@ def detect_sentiment(text: str) -> dict:
314
314
  sentiment = "negative"
315
315
  intensity = min(1.0, 0.3 + neg_score * 0.15)
316
316
  if intensity > 0.7:
317
- guidance = "MODE: Ultra-conciso. Cero explicaciones. Resolver y mostrar resultado."
317
+ guidance = "MODE: Ultra-concise. Zero explanations. Solve and show result."
318
318
  else:
319
319
  guidance = "MODE: Concise. Less context, more direct action."
320
320
  elif pos_score > neg_score and pos_score >= 1:
321
321
  sentiment = "positive"
322
322
  intensity = min(1.0, 0.3 + pos_score * 0.15)
323
- guidance = "MODE: Normal. Buen momento para proponer ideas de backlog o mejoras."
323
+ guidance = "MODE: Normal. Good time to suggest backlog ideas or improvements."
324
324
  elif urgency_hits:
325
325
  sentiment = "urgent"
326
326
  intensity = 0.8
@@ -10,6 +10,7 @@ Usage:
10
10
  import argparse
11
11
  import json
12
12
  import os
13
+ import platform
13
14
  import subprocess
14
15
  import sys
15
16
  import time
@@ -22,7 +23,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
22
23
  from fastapi.staticfiles import StaticFiles
23
24
  from pydantic import BaseModel
24
25
 
25
- # Add parent dir to path so we can import nexo-mcp modules
26
+ # Add parent dir to path so we can import NEXO modules
26
27
  _PARENT = str(Path(__file__).resolve().parent.parent)
27
28
  if _PARENT not in sys.path:
28
29
  sys.path.insert(0, _PARENT)
@@ -63,7 +64,7 @@ async def create_tables():
63
64
 
64
65
 
65
66
  # ---------------------------------------------------------------------------
66
- # Lazy imports — modules live in the parent nexo-mcp directory
67
+ # Lazy imports — modules live in the parent source directory
67
68
  # ---------------------------------------------------------------------------
68
69
 
69
70
  def _cognitive():
@@ -602,6 +603,11 @@ async def api_ops_execute(fid: str):
602
603
  return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
603
604
  item = dict(row)
604
605
  description = item["description"].replace('"', '\\"').replace("'", "\\'")
606
+ if platform.system() != "Darwin":
607
+ return JSONResponse(
608
+ {"error": "This operation requires macOS (uses osascript to open Terminal)"},
609
+ status_code=501,
610
+ )
605
611
  script = f'tell application "Terminal" to do script "claude \\"NEXO: execute followup #{fid} — {description}\\""'
606
612
  subprocess.Popen(["osascript", "-e", script])
607
613
  return {"success": True, "followup_id": fid}
package/src/db/_core.py CHANGED
@@ -9,11 +9,15 @@ import datetime
9
9
  import pathlib
10
10
  import threading
11
11
 
12
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
13
+ _data_dir = os.path.join(NEXO_HOME, "data")
14
+ os.makedirs(_data_dir, exist_ok=True)
15
+
12
16
  DB_PATH = os.environ.get(
13
17
  "NEXO_TEST_DB",
14
18
  os.environ.get(
15
19
  "NEXO_DB",
16
- os.path.join(os.path.dirname(os.path.abspath(__file__)), "nexo.db"),
20
+ os.path.join(_data_dir, "nexo.db"),
17
21
  ),
18
22
  )
19
23
 
@@ -148,9 +148,7 @@ def update_change_commit(id: int, commit_ref: str) -> dict:
148
148
  fts_upsert("change", str(id), r.get("files",""), body, "change_log", commit=False)
149
149
 
150
150
  # Auto-resolve followups that match this change
151
- resolved = auto_resolve_followups(r)
152
- if resolved:
153
- r["auto_resolved_followups"] = resolved
151
+ r["_auto_resolved"] = auto_resolve_followups(r)
154
152
  return r
155
153
 
156
154
 
@@ -134,11 +134,22 @@ def create_followup(id: str, description: str, date: str = None,
134
134
  reasoning: str = '', recurrence: str = None) -> dict:
135
135
  """Create a new followup with optional reasoning and recurrence.
136
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
+
137
140
  recurrence format: 'weekly:monday', 'monthly:1', 'monthly:10', 'quarterly', etc.
138
141
  When a recurring followup is completed, a new one is auto-created with the next date.
139
142
  """
140
143
  conn = get_db()
141
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
+
142
153
  try:
143
154
  conn.execute(
144
155
  "INSERT INTO followups (id, date, description, verification, status, reasoning, recurrence, created_at, updated_at) "
@@ -150,7 +161,10 @@ def create_followup(id: str, description: str, date: str = None,
150
161
  except sqlite3.IntegrityError:
151
162
  return {"error": f"Followup {id} already exists. Use update instead."}
152
163
  row = conn.execute("SELECT * FROM followups WHERE id = ?", (id,)).fetchone()
153
- return dict(row)
164
+ result = dict(row)
165
+ if warning:
166
+ result["warning"] = warning
167
+ return result
154
168
 
155
169
 
156
170
  def update_followup(id: str, **kwargs) -> dict:
@@ -245,6 +259,18 @@ def complete_followup(id: str, result: str = '') -> dict:
245
259
  archived_id = f"{id}-{today}"
246
260
  conn.execute("UPDATE followups SET id = ? WHERE id = ?", (archived_id, id))
247
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
248
274
  create_followup(
249
275
  id=id,
250
276
  description=row["description"],
@@ -254,6 +280,15 @@ def complete_followup(id: str, result: str = '') -> dict:
254
280
  recurrence=recurrence,
255
281
  )
256
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
+
257
292
  return update_result
258
293
 
259
294
 
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