nexo-brain 2.2.0 → 2.3.1

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 (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
@@ -1,91 +0,0 @@
1
- """NEXO DB — Tasks module."""
2
- from db._core import get_db, now_epoch
3
-
4
- # ── Task History & Frequencies ─────────────────────────────────────
5
-
6
- def log_task(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> dict:
7
- """Log a task execution with optional reasoning."""
8
- conn = get_db()
9
- now = now_epoch()
10
- cursor = conn.execute(
11
- "INSERT INTO task_history (task_num, task_name, executed_at, notes, reasoning) "
12
- "VALUES (?, ?, ?, ?, ?)",
13
- (task_num, task_name, now, notes, reasoning)
14
- )
15
- conn.commit()
16
- row = conn.execute(
17
- "SELECT * FROM task_history WHERE id = ?", (cursor.lastrowid,)
18
- ).fetchone()
19
- return dict(row)
20
-
21
-
22
- def list_task_history(task_num: str = None, days: int = 30) -> list[dict]:
23
- """List task execution history, optionally filtered by task_num."""
24
- conn = get_db()
25
- cutoff = now_epoch() - (days * 86400)
26
- if task_num:
27
- rows = conn.execute(
28
- "SELECT * FROM task_history WHERE task_num = ? AND executed_at >= ? "
29
- "ORDER BY executed_at DESC",
30
- (task_num, cutoff)
31
- ).fetchall()
32
- else:
33
- rows = conn.execute(
34
- "SELECT * FROM task_history WHERE executed_at >= ? "
35
- "ORDER BY executed_at DESC",
36
- (cutoff,)
37
- ).fetchall()
38
- return [dict(r) for r in rows]
39
-
40
-
41
- def set_task_frequency(task_num: str, task_name: str,
42
- frequency_days: int, description: str = '') -> dict:
43
- """Set or update the expected frequency for a task."""
44
- conn = get_db()
45
- conn.execute(
46
- "INSERT OR REPLACE INTO task_frequencies (task_num, task_name, frequency_days, description) "
47
- "VALUES (?, ?, ?, ?)",
48
- (task_num, task_name, frequency_days, description)
49
- )
50
- conn.commit()
51
- row = conn.execute(
52
- "SELECT * FROM task_frequencies WHERE task_num = ?", (task_num,)
53
- ).fetchone()
54
- return dict(row)
55
-
56
-
57
- def get_overdue_tasks() -> list[dict]:
58
- """Get tasks where last execution exceeds the configured frequency."""
59
- conn = get_db()
60
- freqs = conn.execute("SELECT * FROM task_frequencies").fetchall()
61
- now = now_epoch()
62
- overdue = []
63
- for f in freqs:
64
- last = conn.execute(
65
- "SELECT MAX(executed_at) as last_exec FROM task_history WHERE task_num = ?",
66
- (f["task_num"],)
67
- ).fetchone()
68
- last_exec = last["last_exec"] if last and last["last_exec"] else None
69
- threshold = f["frequency_days"] * 86400
70
- if last_exec is None or (now - last_exec) > threshold:
71
- days_ago = round((now - last_exec) / 86400, 1) if last_exec else None
72
- overdue.append({
73
- "task_num": f["task_num"],
74
- "task_name": f["task_name"],
75
- "frequency_days": f["frequency_days"],
76
- "last_executed": last_exec,
77
- "days_since_last": days_ago,
78
- "description": f["description"]
79
- })
80
- return overdue
81
-
82
-
83
- def get_task_frequencies() -> list[dict]:
84
- """Get all configured task frequencies."""
85
- conn = get_db()
86
- rows = conn.execute(
87
- "SELECT * FROM task_frequencies ORDER BY task_num ASC"
88
- ).fetchall()
89
- return [dict(r) for r in rows]
90
-
91
-
@@ -1,266 +0,0 @@
1
- """NEXO Evolution Cycle — Self-improvement via Opus API.
2
-
3
- Runs weekly after DMN. Analyzes patterns, proposes improvements.
4
- v1: observe-only (all proposals logged as 'proposed' for the user to review).
5
- v1.1 (future): sandbox execution of auto-approved changes.
6
- """
7
-
8
- import json
9
- import os
10
- import shutil
11
- import subprocess
12
- import sqlite3
13
- import time
14
- from datetime import datetime, date, timedelta
15
- from pathlib import Path
16
-
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")
33
-
34
- MAX_SNAPSHOTS = 8
35
-
36
-
37
- def load_objective() -> dict:
38
- if OBJECTIVE_FILE.exists():
39
- return json.loads(OBJECTIVE_FILE.read_text())
40
- return {}
41
-
42
-
43
- def save_objective(obj: dict):
44
- OBJECTIVE_FILE.write_text(json.dumps(obj, indent=2, ensure_ascii=False))
45
-
46
-
47
- def get_week_data(db_path: str) -> dict:
48
- """Gather last 7 days of learnings, decisions, changes, diaries."""
49
- conn = sqlite3.connect(db_path, timeout=10)
50
- conn.row_factory = sqlite3.Row
51
- cutoff_epoch = time.time() - 7 * 86400
52
- cutoff_date = (date.today() - timedelta(days=7)).isoformat()
53
-
54
- data = {}
55
-
56
- rows = conn.execute(
57
- "SELECT category, title, content FROM learnings WHERE created_at > ? ORDER BY created_at DESC LIMIT 50",
58
- (cutoff_epoch,)
59
- ).fetchall()
60
- data["learnings"] = [dict(r) for r in rows]
61
-
62
- rows = conn.execute(
63
- "SELECT domain, decision, alternatives, based_on, confidence, outcome FROM decisions "
64
- "WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
65
- (cutoff_date,)
66
- ).fetchall()
67
- data["decisions"] = [dict(r) for r in rows]
68
-
69
- rows = conn.execute(
70
- "SELECT files, what_changed, why, affects, risks FROM change_log "
71
- "WHERE created_at > ? ORDER BY created_at DESC LIMIT 30",
72
- (cutoff_date,)
73
- ).fetchall()
74
- data["changes"] = [dict(r) for r in rows]
75
-
76
- rows = conn.execute(
77
- "SELECT summary, decisions as diary_decisions, pending, mental_state, domain, user_signals "
78
- "FROM session_diary WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
79
- (cutoff_date,)
80
- ).fetchall()
81
- data["diaries"] = [dict(r) for r in rows]
82
-
83
- rows = conn.execute(
84
- "SELECT * FROM evolution_log ORDER BY id DESC LIMIT 20"
85
- ).fetchall()
86
- data["evolution_history"] = [dict(r) for r in rows]
87
-
88
- rows = conn.execute(
89
- "SELECT dimension, score, delta, measured_at FROM evolution_metrics "
90
- "WHERE id IN (SELECT MAX(id) FROM evolution_metrics GROUP BY dimension)"
91
- ).fetchall()
92
- data["current_metrics"] = {r["dimension"]: dict(r) for r in rows}
93
-
94
- conn.close()
95
- return data
96
-
97
-
98
- def create_snapshot(files_to_backup: list) -> str:
99
- """Create a snapshot of specific files before modification."""
100
- ts = datetime.now().strftime("%Y-%m-%dT%H:%M")
101
- snap_dir = SNAPSHOTS_DIR / ts
102
- files_dir = snap_dir / "files"
103
-
104
- manifest = {
105
- "created_at": datetime.now().isoformat(),
106
- "files": [],
107
- "reason": "evolution_cycle"
108
- }
109
-
110
- for filepath in files_to_backup:
111
- fp = Path(filepath).expanduser()
112
- if fp.exists():
113
- rel = str(fp).replace(str(Path.home()) + "/", "")
114
- dest = files_dir / rel
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
118
- shutil.copy2(fp, dest)
119
- manifest["files"].append(rel)
120
-
121
- snap_dir.mkdir(parents=True, exist_ok=True)
122
- (snap_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
123
-
124
- latest = SNAPSHOTS_DIR / "latest"
125
- if latest.is_symlink():
126
- latest.unlink()
127
- latest.symlink_to(snap_dir)
128
-
129
- _cleanup_snapshots()
130
- return str(snap_dir)
131
-
132
-
133
- def _cleanup_snapshots():
134
- """Remove old snapshots, keeping MAX_SNAPSHOTS most recent + golden."""
135
- if not SNAPSHOTS_DIR.exists():
136
- return
137
- snaps = sorted(
138
- [d for d in SNAPSHOTS_DIR.iterdir()
139
- if d.is_dir() and d.name not in ("latest", "golden")],
140
- key=lambda d: d.stat().st_mtime,
141
- reverse=True
142
- )
143
- for old in snaps[MAX_SNAPSHOTS:]:
144
- shutil.rmtree(old)
145
-
146
-
147
- def dry_run_restore_test() -> bool:
148
- """Test that snapshot+restore works before making real changes."""
149
- test_file = SANDBOX_DIR / "restore-test.txt"
150
- test_file.parent.mkdir(parents=True, exist_ok=True)
151
- test_file.write_text("original_content")
152
-
153
- snap_dir = create_snapshot([str(test_file)])
154
-
155
- test_file.write_text("modified_content")
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
-
169
- try:
170
- subprocess.run(
171
- [str(restore_script), snap_dir],
172
- capture_output=True, timeout=10, check=True
173
- )
174
- content = test_file.read_text()
175
- test_file.unlink(missing_ok=True)
176
- # Clean up test snapshot
177
- snap_path = Path(snap_dir)
178
- if snap_path.exists():
179
- shutil.rmtree(snap_path)
180
- return content == "original_content"
181
- except Exception:
182
- test_file.unlink(missing_ok=True)
183
- return False
184
-
185
-
186
- def build_evolution_prompt(week_data: dict, objective: dict) -> str:
187
- """Build a SHORT prompt — CLI investigates on its own using tools."""
188
-
189
- # Summary stats only — CLI will dig deeper with tools
190
- stats = {
191
- "learnings_this_week": len(week_data.get("learnings", [])),
192
- "decisions_this_week": len(week_data.get("decisions", [])),
193
- "changes_this_week": len(week_data.get("changes", [])),
194
- "diaries_this_week": len(week_data.get("diaries", [])),
195
- "evolution_history": len(week_data.get("evolution_history", [])),
196
- "current_scores": {dim: m["score"] for dim, m in week_data.get("current_metrics", {}).items()},
197
- }
198
-
199
- mode = objective.get("evolution_mode", "auto")
200
- total = objective.get("total_evolutions", 0)
201
- max_auto = max_auto_changes(total)
202
-
203
- prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
204
-
205
- YOUR JOB: Analyze the past week and propose concrete improvements to NEXO's codebase.
206
-
207
- WEEK SUMMARY:
208
- - {stats['learnings_this_week']} new learnings
209
- - {stats['decisions_this_week']} decisions made
210
- - {stats['changes_this_week']} code changes deployed
211
- - {stats['diaries_this_week']} session diaries
212
- - {stats['evolution_history']} past evolution proposals
213
- - Current scores: {json.dumps(stats['current_scores'])}
214
-
215
- MODE: {mode} ({"proposals only, owner reviews" if mode == "review" else f"max {max_auto} auto-applied changes"})
216
- CYCLE: #{total + 1}
217
-
218
- INVESTIGATE using these tools:
219
- 1. Bash: sqlite3 {NEXO_DB} "SELECT category, title FROM learnings WHERE created_at > {time.time() - 7*86400} ORDER BY created_at DESC LIMIT 30"
220
- 2. Bash: sqlite3 {NEXO_DB} "SELECT area, COUNT(*) as cnt FROM error_repetitions GROUP BY area ORDER BY cnt DESC LIMIT 10"
221
- 3. Read ~/.nexo/coordination/daily-synthesis.md — today's context
222
- 4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
223
- 5. Read ~/.nexo/logs/self-audit-summary.json — system health
224
- 6. Glob ~/.nexo/scripts/*.py — existing scripts
225
- 7. Glob ~/.nexo/plugins/*.py — existing plugins
226
-
227
- LOOK FOR:
228
- - Repeated errors that guard isn't preventing
229
- - Scripts or processes that are failing or underperforming
230
- - Missing functionality that session diaries keep asking for
231
- - Redundant code or config that could be simplified
232
- - Patterns in self-critique that suggest systemic issues
233
-
234
- SAFETY:
235
- - Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/
236
- - IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
237
- - Every change needs: what file, what to change, why, risk, how to verify
238
-
239
- OUTPUT FORMAT (JSON):
240
- {{
241
- "analysis": "one paragraph summary of what you found",
242
- "patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
243
- "proposals": [
244
- {{
245
- "classification": "auto" or "propose",
246
- "dimension": "reliability|proactivity|efficiency|safety|learning",
247
- "action": "what to do",
248
- "reasoning": "why",
249
- "scope": "local",
250
- "changes": [{{"file": "path", "operation": "create|replace|append", "search": "text to find", "content": "new text"}}]
251
- }}
252
- ]
253
- }}
254
-
255
- Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
256
-
257
- return prompt
258
-
259
-
260
- def max_auto_changes(total_evolutions: int) -> int:
261
- """Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
262
- if total_evolutions < 4:
263
- return 1
264
- elif total_evolutions < 8:
265
- return 2
266
- return 3
@@ -1,254 +0,0 @@
1
- """NEXO HNSW Vector Index — Optional acceleration for cognitive search.
2
-
3
- When memory count exceeds THRESHOLD (default 10_000), this module builds and
4
- maintains an HNSW index for approximate nearest neighbor search. Falls back
5
- gracefully to brute-force when hnswlib is not available or index is cold.
6
-
7
- Usage in cognitive.search():
8
- from hnsw_index import hnsw_search
9
- candidates = hnsw_search(query_vec, store="stm", top_k=50)
10
- # candidates is a list of (memory_id, distance) or None if not available
11
- """
12
-
13
- import os
14
- import sqlite3
15
- import threading
16
- import numpy as np
17
- from pathlib import Path
18
- from typing import Optional
19
-
20
- try:
21
- import hnswlib
22
- HNSWLIB_AVAILABLE = True
23
- except ImportError:
24
- HNSWLIB_AVAILABLE = False
25
-
26
- # When to activate HNSW (below this, brute force is fine)
27
- ACTIVATION_THRESHOLD = int(os.environ.get("NEXO_HNSW_THRESHOLD", "10000"))
28
-
29
- # Index params
30
- EMBEDDING_DIM = 768
31
- EF_CONSTRUCTION = 200 # Higher = better recall during build, slower
32
- M = 16 # Connections per node (16 is good for 768-dim)
33
- EF_SEARCH = 50 # Higher = better recall during search
34
-
35
- # Index file paths
36
- _INDEX_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hnsw_indices")
37
-
38
- # In-memory indices (one per store)
39
- _indices: dict = {} # {"stm": hnswlib.Index, "ltm": hnswlib.Index}
40
- _index_lock = threading.Lock()
41
- _id_maps: dict = {} # {"stm": {internal_id: db_id}, "ltm": {internal_id: db_id}}
42
-
43
-
44
- def is_available() -> bool:
45
- """Check if HNSW is available and should be used."""
46
- return HNSWLIB_AVAILABLE
47
-
48
-
49
- def _index_path(store: str) -> str:
50
- return os.path.join(_INDEX_DIR, f"{store}.bin")
51
-
52
-
53
- def _id_map_path(store: str) -> str:
54
- return os.path.join(_INDEX_DIR, f"{store}_ids.npy")
55
-
56
-
57
- def should_activate(store: str = "both") -> bool:
58
- """Check if memory count exceeds threshold, making HNSW worthwhile."""
59
- if not HNSWLIB_AVAILABLE:
60
- return False
61
- try:
62
- import cognitive
63
- db = cognitive._get_db()
64
- total = 0
65
- if store in ("both", "stm"):
66
- total += db.execute("SELECT COUNT(*) FROM stm_memories WHERE promoted_to_ltm = 0").fetchone()[0]
67
- if store in ("both", "ltm"):
68
- total += db.execute("SELECT COUNT(*) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0]
69
- return total >= ACTIVATION_THRESHOLD
70
- except Exception:
71
- return False
72
-
73
-
74
- def build_index(store: str) -> dict:
75
- """Build HNSW index from all active memories in the given store.
76
-
77
- Args:
78
- store: "stm" or "ltm"
79
-
80
- Returns:
81
- {"count": N, "store": store, "status": "built"} or error dict
82
- """
83
- if not HNSWLIB_AVAILABLE:
84
- return {"error": "hnswlib not installed"}
85
-
86
- try:
87
- import cognitive
88
- db = cognitive._get_db()
89
- except Exception as e:
90
- return {"error": str(e)}
91
-
92
- table = "stm_memories" if store == "stm" else "ltm_memories"
93
- where = "promoted_to_ltm = 0" if store == "stm" else "is_dormant = 0"
94
-
95
- rows = db.execute(f"SELECT id, embedding FROM {table} WHERE {where}").fetchall()
96
- if not rows:
97
- return {"count": 0, "store": store, "status": "empty"}
98
-
99
- count = len(rows)
100
- index = hnswlib.Index(space='cosine', dim=EMBEDDING_DIM)
101
- index.init_index(max_elements=max(count * 2, 1000), ef_construction=EF_CONSTRUCTION, M=M)
102
- index.set_ef(EF_SEARCH)
103
-
104
- id_map = {}
105
- vectors = []
106
- internal_ids = []
107
-
108
- for i, row in enumerate(rows):
109
- vec = np.frombuffer(row["embedding"], dtype=np.float32)
110
- if len(vec) != EMBEDDING_DIM:
111
- continue
112
- vectors.append(vec)
113
- internal_ids.append(i)
114
- id_map[i] = row["id"]
115
-
116
- if not vectors:
117
- return {"count": 0, "store": store, "status": "no_valid_vectors"}
118
-
119
- data = np.array(vectors, dtype=np.float32)
120
- ids = np.array(internal_ids, dtype=np.int64)
121
- index.add_items(data, ids)
122
-
123
- # Save to disk
124
- Path(_INDEX_DIR).mkdir(exist_ok=True)
125
- index.save_index(_index_path(store))
126
- np.save(_id_map_path(store), id_map)
127
-
128
- with _index_lock:
129
- _indices[store] = index
130
- _id_maps[store] = id_map
131
-
132
- return {"count": count, "store": store, "status": "built"}
133
-
134
-
135
- def load_index(store: str) -> bool:
136
- """Load a previously built index from disk."""
137
- if not HNSWLIB_AVAILABLE:
138
- return False
139
-
140
- idx_path = _index_path(store)
141
- map_path = _id_map_path(store) + ".npy" if not _id_map_path(store).endswith(".npy") else _id_map_path(store)
142
-
143
- if not os.path.exists(idx_path):
144
- return False
145
-
146
- try:
147
- index = hnswlib.Index(space='cosine', dim=EMBEDDING_DIM)
148
- index.load_index(idx_path)
149
- index.set_ef(EF_SEARCH)
150
-
151
- id_map = np.load(map_path, allow_pickle=True).item()
152
-
153
- with _index_lock:
154
- _indices[store] = index
155
- _id_maps[store] = id_map
156
- return True
157
- except Exception:
158
- return False
159
-
160
-
161
- def search(query_vec: np.ndarray, store: str = "stm", top_k: int = 50) -> Optional[list[tuple[int, float]]]:
162
- """Search the HNSW index for approximate nearest neighbors.
163
-
164
- Args:
165
- query_vec: Query embedding (768-dim float32)
166
- store: "stm" or "ltm"
167
- top_k: Number of results
168
-
169
- Returns:
170
- List of (db_memory_id, cosine_distance) or None if index not available.
171
- Note: hnswlib with cosine space returns 1 - cosine_similarity as distance.
172
- """
173
- with _index_lock:
174
- index = _indices.get(store)
175
- id_map = _id_maps.get(store)
176
-
177
- if index is None or id_map is None:
178
- # Try loading from disk
179
- if load_index(store):
180
- with _index_lock:
181
- index = _indices.get(store)
182
- id_map = _id_maps.get(store)
183
- if index is None:
184
- return None
185
-
186
- try:
187
- query = query_vec.reshape(1, -1).astype(np.float32)
188
- labels, distances = index.knn_query(query, k=min(top_k, index.get_current_count()))
189
- results = []
190
- for label, dist in zip(labels[0], distances[0]):
191
- db_id = id_map.get(int(label))
192
- if db_id is not None:
193
- # Convert cosine distance to similarity: sim = 1 - dist
194
- results.append((db_id, float(1.0 - dist)))
195
- return results
196
- except Exception:
197
- return None
198
-
199
-
200
- def add_item(store: str, db_id: int, embedding: np.ndarray) -> bool:
201
- """Incrementally add a single item to the index (for new ingestions)."""
202
- with _index_lock:
203
- index = _indices.get(store)
204
- id_map = _id_maps.get(store)
205
-
206
- if index is None or id_map is None:
207
- return False
208
-
209
- try:
210
- internal_id = max(id_map.keys()) + 1 if id_map else 0
211
- # Resize if needed
212
- if index.get_current_count() >= index.get_max_elements() - 1:
213
- index.resize_index(index.get_max_elements() * 2)
214
-
215
- vec = embedding.reshape(1, -1).astype(np.float32)
216
- index.add_items(vec, np.array([internal_id], dtype=np.int64))
217
-
218
- with _index_lock:
219
- id_map[internal_id] = db_id
220
- return True
221
- except Exception:
222
- return False
223
-
224
-
225
- def invalidate(store: str = "both"):
226
- """Remove indices from memory (forces rebuild on next use)."""
227
- with _index_lock:
228
- if store in ("both", "stm"):
229
- _indices.pop("stm", None)
230
- _id_maps.pop("stm", None)
231
- if store in ("both", "ltm"):
232
- _indices.pop("ltm", None)
233
- _id_maps.pop("ltm", None)
234
-
235
-
236
- def stats() -> dict:
237
- """Return HNSW index statistics."""
238
- result = {
239
- "hnswlib_available": HNSWLIB_AVAILABLE,
240
- "activation_threshold": ACTIVATION_THRESHOLD,
241
- "indices": {},
242
- }
243
- with _index_lock:
244
- for store in ("stm", "ltm"):
245
- idx = _indices.get(store)
246
- if idx:
247
- result["indices"][store] = {
248
- "count": idx.get_current_count(),
249
- "max_elements": idx.get_max_elements(),
250
- "ef_search": EF_SEARCH,
251
- }
252
- else:
253
- result["indices"][store] = {"status": "not_loaded"}
254
- return result