nexo-brain 2.1.0 → 2.3.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 (297) hide show
  1. package/README.md +7 -7
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +1 -1
  4. package/scripts/migrate-to-unified 2.sh +813 -0
  5. package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
  6. package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
  7. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  8. package/scripts/nexo-preflight.sh +236 -0
  9. package/scripts/pre-commit-check 2.sh +55 -0
  10. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  12. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-314.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__/plugin_loader.cpython-310.pyc +0 -0
  17. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  21. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  22. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  26. package/src/auto_close_sessions 2.py +159 -0
  27. package/src/auto_update 2.py +634 -0
  28. package/src/auto_update.py +25 -0
  29. package/src/claim_graph 2.py +323 -0
  30. package/src/cognitive/__init__ 2.py +62 -0
  31. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  32. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  33. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  35. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  36. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  40. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-312.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-312.pyc +0 -0
  45. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  47. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  48. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  49. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  50. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  51. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  52. package/src/cognitive/_core 2.py +567 -0
  53. package/src/cognitive/_decay 2.py +382 -0
  54. package/src/cognitive/_ingest 2.py +892 -0
  55. package/src/cognitive/_memory 2.py +912 -0
  56. package/src/cognitive/_search 2.py +949 -0
  57. package/src/cognitive/_trust 2.py +464 -0
  58. package/src/cognitive/_trust.py +10 -36
  59. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  60. package/src/crons/manifest 2.json +106 -0
  61. package/src/crons/manifest.json +6 -13
  62. package/src/crons/sync 2.py +217 -0
  63. package/src/crons/sync.py +151 -6
  64. package/src/dashboard/__init__ 2.py +0 -0
  65. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  66. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  67. package/src/dashboard/app 2.py +789 -0
  68. package/src/db/__init__ 2.py +89 -0
  69. package/src/db/__init__.py +13 -0
  70. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  71. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  73. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  74. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  75. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  76. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  77. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  78. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  79. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  80. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  111. package/src/db/_core 2.py +417 -0
  112. package/src/db/_credentials 2.py +124 -0
  113. package/src/db/_cron_runs.py +74 -0
  114. package/src/db/_entities 2.py +178 -0
  115. package/src/db/_episodic 2.py +738 -0
  116. package/src/db/_episodic.py +40 -6
  117. package/src/db/_evolution 2.py +54 -0
  118. package/src/db/_fts 2.py +406 -0
  119. package/src/db/_learnings 2.py +168 -0
  120. package/src/db/_reminders 2.py +338 -0
  121. package/src/db/_schema 2.py +364 -0
  122. package/src/db/_schema.py +64 -0
  123. package/src/db/_sessions 2.py +300 -0
  124. package/src/db/_skills.py +514 -0
  125. package/src/db/_tasks 2.py +91 -0
  126. package/src/evolution_cycle 2.py +266 -0
  127. package/src/hnsw_index 2.py +254 -0
  128. package/src/hooks/auto_capture 2.py +208 -0
  129. package/src/hooks/caffeinate-guard 2.sh +8 -0
  130. package/src/hooks/capture-session 2.sh +21 -0
  131. package/src/hooks/capture-session.sh +2 -0
  132. package/src/hooks/capture-tool-logs 2.sh +127 -0
  133. package/src/hooks/capture-tool-logs.sh +3 -2
  134. package/src/hooks/daily-briefing-check 2.sh +33 -0
  135. package/src/hooks/inbox-hook 2.sh +76 -0
  136. package/src/hooks/inbox-hook.sh +3 -2
  137. package/src/hooks/post-compact 2.sh +148 -0
  138. package/src/hooks/post-compact.sh +1 -1
  139. package/src/hooks/pre-compact 2.sh +151 -0
  140. package/src/hooks/pre-compact.sh +1 -1
  141. package/src/hooks/session-start 2.sh +268 -0
  142. package/src/hooks/session-start.sh +6 -3
  143. package/src/hooks/session-stop 2.sh +140 -0
  144. package/src/hooks/session-stop.sh +14 -102
  145. package/src/kg_populate 2.py +290 -0
  146. package/src/knowledge_graph 2.py +257 -0
  147. package/src/maintenance 2.py +59 -0
  148. package/src/migrate_embeddings 2.py +122 -0
  149. package/src/plugin_loader 2.py +202 -0
  150. package/src/plugins/__init__ 2.py +0 -0
  151. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.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__/adaptive_mode.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  183. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  185. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  187. package/src/plugins/adaptive_mode 2.py +805 -0
  188. package/src/plugins/agents 2.py +52 -0
  189. package/src/plugins/artifact_registry 2.py +450 -0
  190. package/src/plugins/backup 2.py +104 -0
  191. package/src/plugins/cognitive_memory 2.py +564 -0
  192. package/src/plugins/core_rules 2.py +252 -0
  193. package/src/plugins/cortex 2.py +299 -0
  194. package/src/plugins/entities 2.py +67 -0
  195. package/src/plugins/episodic_memory 2.py +533 -0
  196. package/src/plugins/episodic_memory.py +5 -3
  197. package/src/plugins/evolution 2.py +115 -0
  198. package/src/plugins/guard 2.py +746 -0
  199. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  200. package/src/plugins/preferences 2.py +47 -0
  201. package/src/plugins/schedule.py +212 -0
  202. package/src/plugins/skills.py +264 -0
  203. package/src/plugins/update 2.py +256 -0
  204. package/src/requirements 2.txt +12 -0
  205. package/src/rules/__init__ 2.py +0 -0
  206. package/src/rules/core-rules 2.json +331 -0
  207. package/src/rules/migrate 2.py +207 -0
  208. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  229. package/src/scripts/check-context 2.py +264 -0
  230. package/src/scripts/deep-sleep/apply_findings.py +168 -8
  231. package/src/scripts/deep-sleep/collect.py +33 -11
  232. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  233. package/src/scripts/deep-sleep/extract.py +80 -8
  234. package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
  235. package/src/scripts/deep-sleep/synthesize.py +3 -1
  236. package/src/scripts/nexo-auto-update 2.py +6 -0
  237. package/src/scripts/nexo-backup 2.sh +25 -0
  238. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  239. package/src/scripts/nexo-catchup 2.py +242 -0
  240. package/src/scripts/nexo-catchup.py +65 -29
  241. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  242. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  243. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  244. package/src/scripts/nexo-daily-self-audit.py +4 -2
  245. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  246. package/src/scripts/nexo-deep-sleep.sh +66 -77
  247. package/src/scripts/nexo-evolution-run 2.py +597 -0
  248. package/src/scripts/nexo-evolution-run.py +13 -0
  249. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  250. package/src/scripts/nexo-immune 2.py +927 -0
  251. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  252. package/src/scripts/nexo-install 2.py +6 -0
  253. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  254. package/src/scripts/nexo-learning-housekeep.py +156 -1
  255. package/src/scripts/nexo-learning-validator 2.py +207 -0
  256. package/src/scripts/nexo-learning-validator.py +19 -0
  257. package/src/scripts/nexo-migrate 2.py +232 -0
  258. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  259. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  260. package/src/scripts/nexo-pre-commit 2.py +120 -0
  261. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  262. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  263. package/src/scripts/nexo-reflection 2.py +253 -0
  264. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  265. package/src/scripts/nexo-send-email 2.py +25 -0
  266. package/src/scripts/nexo-send-reply 2.py +178 -0
  267. package/src/scripts/nexo-sleep 2.py +592 -0
  268. package/src/scripts/nexo-sleep.py +16 -11
  269. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  270. package/src/scripts/nexo-synthesis 2.py +253 -0
  271. package/src/scripts/nexo-synthesis.py +46 -3
  272. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  273. package/src/scripts/nexo-update 2.sh +161 -0
  274. package/src/scripts/nexo-watchdog 2.sh +878 -0
  275. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  276. package/src/scripts/nexo-watchdog.sh +72 -19
  277. package/src/server 2.py +733 -0
  278. package/src/server.py +11 -2
  279. package/src/storage_router 2.py +32 -0
  280. package/src/tools_coordination 2.py +102 -0
  281. package/src/tools_credentials 2.py +68 -0
  282. package/src/tools_learnings 2.py +220 -0
  283. package/src/tools_menu 2.py +227 -0
  284. package/src/tools_reminders 2.py +86 -0
  285. package/src/tools_reminders_crud 2.py +159 -0
  286. package/src/tools_reminders_crud.py +7 -0
  287. package/src/tools_sessions 2.py +476 -0
  288. package/src/tools_task_history 2.py +57 -0
  289. package/templates/CLAUDE.md 2.template +63 -0
  290. package/templates/openclaw 2.json +13 -0
  291. package/tests/__init__ 2.py +0 -0
  292. package/tests/conftest 2.py +71 -0
  293. package/tests/test_cognitive 2.py +205 -0
  294. package/tests/test_knowledge_graph 2.py +140 -0
  295. package/tests/test_migrations 2.py +137 -0
  296. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  297. /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -0
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # nexo-inbox-hook.sh — PostToolUse: automatic inter-terminal inbox check (D+)
3
+ #
4
+ # Zero output when no messages = zero tokens consumed in Claude's context.
5
+ # Reads SQLite directly (no MCP overhead). Write-only: INSERT OR IGNORE for mark-as-read.
6
+ # Debounce: skips if last check was <2 seconds ago.
7
+
8
+ INPUT=$(cat)
9
+
10
+ # 1. Skip read-only tools (same logic as capture-tool-logs.sh)
11
+ TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
12
+ case "$TOOL_NAME" in
13
+ Read|Grep|Glob|LS|Skill|ToolSearch|Agent) exit 0 ;;
14
+ esac
15
+
16
+ # 2. Extract Claude Code session_id
17
+ CLAUDE_SID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)
18
+ [ -z "$CLAUDE_SID" ] && exit 0
19
+
20
+ # 3. Debounce: skip if last check <2s ago
21
+ DEBOUNCE_FILE="/tmp/nexo-inbox-ts-${CLAUDE_SID}"
22
+ NOW=$(date +%s)
23
+ LAST=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
24
+ DIFF=$((NOW - LAST))
25
+ [ "$DIFF" -lt 2 ] && exit 0
26
+ echo "$NOW" > "$DEBOUNCE_FILE"
27
+
28
+ # 4. Find NEXO SID mapped to this Claude session_id
29
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
30
+ DB="$NEXO_HOME/data/nexo.db"
31
+ [ -f "$DB" ] || exit 0
32
+
33
+ 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)
34
+ [ -z "$NEXO_SID" ] && exit 0
35
+
36
+ # 5. Check inbox — messages addressed to this session or broadcast
37
+ MESSAGES=$(sqlite3 -separator '|' "$DB" "
38
+ SELECT m.id, m.from_sid, m.text FROM messages m
39
+ WHERE (m.to_sid = 'all' OR m.to_sid = '${NEXO_SID}')
40
+ AND m.from_sid != '${NEXO_SID}'
41
+ AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = '${NEXO_SID}')
42
+ LIMIT 5;
43
+ " 2>/dev/null)
44
+
45
+ # 6. Check pending questions
46
+ QUESTIONS=$(sqlite3 -separator '|' "$DB" "
47
+ SELECT qid, from_sid, question FROM questions
48
+ WHERE to_sid = '${NEXO_SID}' AND answer IS NULL
49
+ LIMIT 3;
50
+ " 2>/dev/null)
51
+
52
+ # 7. If empty → silent exit (0 tokens consumed)
53
+ [ -z "$MESSAGES" ] && [ -z "$QUESTIONS" ] && exit 0
54
+
55
+ # 8. Format and output (injected into Claude's context)
56
+ echo ""
57
+ echo "📨 INTER-TERMINAL MESSAGE (auto-detected):"
58
+
59
+ if [ -n "$MESSAGES" ]; then
60
+ echo "$MESSAGES" | while IFS='|' read -r mid from text; do
61
+ echo " [$from]: $text"
62
+ # Mark as read (lightweight INSERT, WAL mode, no lock contention)
63
+ sqlite3 "$DB" "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES ('${mid}', '${NEXO_SID}');" 2>/dev/null
64
+ done
65
+ fi
66
+
67
+ if [ -n "$QUESTIONS" ]; then
68
+ echo " ⚠ PREGUNTAS de otra terminal — responder con nexo_answer:"
69
+ echo "$QUESTIONS" | while IFS='|' read -r qid from question; do
70
+ echo " Q[$qid] de [$from]: $question"
71
+ done
72
+ fi
73
+
74
+ exit 0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ """DEPRECATED: Use 'npx nexo-brain' instead. This installer is no longer maintained."""
3
+ import sys
4
+ print("This installer is deprecated. Please use: npx nexo-brain")
5
+ print("See: https://github.com/wazionapps/nexo#installation")
6
+ sys.exit(1)
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
3
+
4
+ Runs daily. Adjusts learning weights based on usage (guard_hits),
5
+ detects duplicates via semantic similarity, and archives stale learnings.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sqlite3
11
+ import sys
12
+ import time
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+
16
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
17
+ # Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
18
+ _script_dir = Path(__file__).resolve().parent
19
+ _repo_src = _script_dir.parent # src/scripts/ -> src/
20
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
21
+
22
+ sys.path.insert(0, str(NEXO_CODE))
23
+
24
+ DB_PATH = NEXO_HOME / "data" / "nexo.db"
25
+ STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
26
+
27
+ # Weight adjustment rates
28
+ GUARD_HIT_BOOST = 0.02 # per guard hit since last run
29
+ DECAY_RATE = 0.005 # daily decay for unused learnings
30
+ MIN_WEIGHT = 0.05
31
+ MAX_WEIGHT = 1.0
32
+ DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
33
+ ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
34
+
35
+
36
+ def get_db():
37
+ conn = sqlite3.connect(str(DB_PATH))
38
+ conn.row_factory = sqlite3.Row
39
+ return conn
40
+
41
+
42
+ def update_catchup_state():
43
+ try:
44
+ state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
45
+ except Exception:
46
+ state = {}
47
+ state["learning-housekeep"] = datetime.now().isoformat()
48
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
49
+ STATE_FILE.write_text(json.dumps(state, indent=2))
50
+
51
+
52
+ def adjust_weights(conn):
53
+ """Boost weight for frequently-used learnings, decay unused ones."""
54
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
55
+ now = time.time()
56
+ one_day_ago = now - 86400
57
+
58
+ learnings = conn.execute(
59
+ "SELECT id, weight, guard_hits, last_guard_hit_at, priority, created_at "
60
+ "FROM learnings WHERE status = 'active'"
61
+ ).fetchall()
62
+
63
+ adjusted = 0
64
+ for l in learnings:
65
+ old_weight = l["weight"] or 0.5
66
+ hits = l["guard_hits"] or 0
67
+ last_hit = l["last_guard_hit_at"] or 0
68
+ priority = l["priority"] or "medium"
69
+
70
+ # Priority floor — critical learnings never drop below 0.5
71
+ priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
72
+
73
+ new_weight = old_weight
74
+
75
+ if last_hit > one_day_ago:
76
+ # Recent guard hit — boost
77
+ recent_hits = 1 # Simplified: at least 1 hit today
78
+ new_weight = min(MAX_WEIGHT, old_weight + (GUARD_HIT_BOOST * recent_hits))
79
+ else:
80
+ # No recent hits — decay
81
+ new_weight = max(priority_floor, old_weight - DECAY_RATE)
82
+
83
+ new_weight = max(MIN_WEIGHT, min(MAX_WEIGHT, new_weight))
84
+
85
+ if abs(new_weight - old_weight) > 0.001:
86
+ conn.execute("UPDATE learnings SET weight = ? WHERE id = ?", (round(new_weight, 4), l["id"]))
87
+ adjusted += 1
88
+
89
+ conn.commit()
90
+ print(f"[{ts}] Weight adjustment: {adjusted}/{len(learnings)} learnings adjusted")
91
+ return adjusted
92
+
93
+
94
+ def auto_prioritize(conn):
95
+ """Auto-upgrade priority based on guard hits and repetitions."""
96
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
97
+
98
+ # Learnings with 10+ guard hits that are still medium → upgrade to high
99
+ upgraded = conn.execute(
100
+ "UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7) "
101
+ "WHERE status = 'active' AND priority = 'medium' AND guard_hits >= 10"
102
+ ).rowcount
103
+
104
+ # Learnings with repetitions (same error happened again) → upgrade to high
105
+ repeated = conn.execute(
106
+ """UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7)
107
+ WHERE status = 'active' AND priority IN ('medium', 'low')
108
+ AND id IN (SELECT original_learning_id FROM error_repetitions)"""
109
+ ).rowcount
110
+
111
+ conn.commit()
112
+ total = upgraded + repeated
113
+ if total > 0:
114
+ print(f"[{ts}] Auto-prioritize: {upgraded} by guard_hits, {repeated} by repetitions")
115
+ return total
116
+
117
+
118
+ def detect_duplicates(conn):
119
+ """Find semantically similar learnings using fastembed."""
120
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
121
+ try:
122
+ from fastembed import TextEmbedding
123
+ import numpy as np
124
+ except ImportError:
125
+ print(f"[{ts}] Dedup skipped: fastembed not available")
126
+ return []
127
+
128
+ learnings = conn.execute(
129
+ "SELECT id, title, content, weight, guard_hits FROM learnings WHERE status = 'active'"
130
+ ).fetchall()
131
+
132
+ if len(learnings) < 2:
133
+ return []
134
+
135
+ model = TextEmbedding("BAAI/bge-base-en-v1.5")
136
+ texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
137
+ embeddings = list(model.embed(texts))
138
+ embeddings = np.array(embeddings)
139
+
140
+ # Normalize
141
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
142
+ norms[norms == 0] = 1
143
+ embeddings = embeddings / norms
144
+
145
+ duplicates = []
146
+ for i in range(len(learnings)):
147
+ for j in range(i + 1, len(learnings)):
148
+ sim = float(np.dot(embeddings[i], embeddings[j]))
149
+ if sim >= DEDUP_THRESHOLD:
150
+ # Keep the one with higher weight/hits
151
+ a, b = learnings[i], learnings[j]
152
+ score_a = (a["weight"] or 0.5) + (a["guard_hits"] or 0) * 0.01
153
+ score_b = (b["weight"] or 0.5) + (b["guard_hits"] or 0) * 0.01
154
+ keep, drop = (a, b) if score_a >= score_b else (b, a)
155
+ duplicates.append({
156
+ "keep_id": keep["id"], "keep_title": keep["title"],
157
+ "drop_id": drop["id"], "drop_title": drop["title"],
158
+ "similarity": round(sim, 3)
159
+ })
160
+
161
+ if duplicates:
162
+ print(f"[{ts}] Duplicates found: {len(duplicates)} pairs (>= {DEDUP_THRESHOLD})")
163
+ for d in duplicates[:10]:
164
+ print(f"[{ts}] [{d['similarity']}] keep #{d['keep_id']} '{d['keep_title'][:40]}', archive #{d['drop_id']} '{d['drop_title'][:40]}'")
165
+ # Archive the duplicate (don't delete — just mark inactive)
166
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (d["drop_id"],))
167
+ conn.commit()
168
+ else:
169
+ print(f"[{ts}] No duplicates found ({len(learnings)} learnings scanned)")
170
+
171
+ return duplicates
172
+
173
+
174
+ def archive_stale(conn):
175
+ """Archive learnings with very low weight and no recent guard hits."""
176
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
177
+ cutoff = time.time() - (ARCHIVE_AFTER_DAYS * 86400)
178
+
179
+ stale = conn.execute(
180
+ "SELECT id, title, weight, last_guard_hit_at FROM learnings "
181
+ "WHERE status = 'active' AND weight < 0.1 AND priority NOT IN ('critical', 'high') "
182
+ "AND (last_guard_hit_at IS NULL OR last_guard_hit_at < ?)",
183
+ (cutoff,)
184
+ ).fetchall()
185
+
186
+ if stale:
187
+ for s in stale:
188
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (s["id"],))
189
+ print(f"[{ts}] Archived #{s['id']} '{s['title'][:50]}' (weight={s['weight']:.2f})")
190
+ conn.commit()
191
+ print(f"[{ts}] Archived {len(stale)} stale learnings")
192
+ else:
193
+ print(f"[{ts}] No stale learnings to archive")
194
+
195
+ return len(stale)
196
+
197
+
198
+ def print_summary(conn):
199
+ """Print summary stats."""
200
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
201
+ stats = conn.execute(
202
+ """SELECT
203
+ COUNT(*) as total,
204
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
205
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
206
+ SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END) as critical,
207
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
208
+ SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
209
+ SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
210
+ printf('%.2f', AVG(CASE WHEN status = 'active' THEN weight END)) as avg_weight
211
+ FROM learnings"""
212
+ ).fetchone()
213
+ print(f"[{ts}] Summary: {stats['active']} active, {stats['archived']} archived | "
214
+ f"Priority: {stats['critical']}C {stats['high']}H {stats['medium']}M {stats['low']}L | "
215
+ f"Avg weight: {stats['avg_weight']}")
216
+
217
+
218
+ def main():
219
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
220
+ print(f"[{ts}] Learning housekeeping starting...")
221
+
222
+ conn = get_db()
223
+
224
+ # 1. Adjust weights based on usage
225
+ adjust_weights(conn)
226
+
227
+ # 2. Auto-prioritize based on guard hits and repetitions
228
+ auto_prioritize(conn)
229
+
230
+ # 3. Detect and archive duplicates
231
+ detect_duplicates(conn)
232
+
233
+ # 4. Archive stale learnings
234
+ archive_stale(conn)
235
+
236
+ # 5. Summary
237
+ print_summary(conn)
238
+
239
+ conn.close()
240
+ update_catchup_state()
241
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Done.")
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
@@ -31,6 +31,7 @@ MIN_WEIGHT = 0.05
31
31
  MAX_WEIGHT = 1.0
32
32
  DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
33
33
  ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
34
+ REVIEW_EXTEND_DAYS = 30 # extend review_due by this many days when confirming
34
35
 
35
36
 
36
37
  def get_db():
@@ -195,6 +196,157 @@ def archive_stale(conn):
195
196
  return len(stale)
196
197
 
197
198
 
199
+ def _reconcile_decision_outcome(conn, decision_id: int, decision_text: str) -> str | None:
200
+ """Try to find evidence of a decision's outcome in diaries, followups, and change_log.
201
+
202
+ Returns outcome text if found, None otherwise.
203
+ """
204
+ # Extract keywords from the decision for matching
205
+ keywords = [w for w in decision_text.lower().split() if len(w) > 4][:5]
206
+ if not keywords:
207
+ return None
208
+
209
+ like_clauses = " OR ".join(f"summary LIKE ?" for _ in keywords)
210
+ like_params = [f"%{kw}%" for kw in keywords]
211
+
212
+ # Check session diaries for evidence
213
+ diary_match = conn.execute(
214
+ f"SELECT summary FROM session_diary WHERE ({like_clauses}) "
215
+ "AND created_at > (SELECT created_at FROM decisions WHERE id = ?) "
216
+ "ORDER BY created_at DESC LIMIT 1",
217
+ like_params + [decision_id]
218
+ ).fetchone()
219
+ if diary_match:
220
+ return f"[auto-reconciled from diary] {diary_match['summary'][:200]}"
221
+
222
+ # Check completed followups
223
+ like_clauses_f = " OR ".join(f"description LIKE ?" for _ in keywords)
224
+ followup_match = conn.execute(
225
+ f"SELECT description, verification FROM followups WHERE status = 'COMPLETED' "
226
+ f"AND ({like_clauses_f}) ORDER BY date DESC LIMIT 1",
227
+ like_params
228
+ ).fetchone()
229
+ if followup_match:
230
+ result = followup_match['verification'] or followup_match['description']
231
+ return f"[auto-reconciled from followup] {result[:200]}"
232
+
233
+ # Check change_log (schema: what_changed, why, commit_ref, affects)
234
+ like_clauses_c = " OR ".join(f"what_changed LIKE ?" for _ in keywords)
235
+ change_match = conn.execute(
236
+ f"SELECT what_changed, why, commit_ref FROM change_log WHERE ({like_clauses_c}) "
237
+ "ORDER BY created_at DESC LIMIT 1",
238
+ like_params
239
+ ).fetchone()
240
+ if change_match:
241
+ ref = change_match['commit_ref'] or ''
242
+ desc = change_match['what_changed'] or change_match['why'] or ''
243
+ return f"[auto-reconciled from change_log] {desc[:150]} {ref}"
244
+
245
+ return None
246
+
247
+
248
+ def process_overdue_reviews(conn):
249
+ """Process learnings and decisions whose review_due_at has passed.
250
+
251
+ Learnings:
252
+ - guard_hits > 5 since last review -> confirm (extend review_due by 30 days)
253
+ - guard_hits = 0 and weight < 0.3 -> archive
254
+ - otherwise -> extend review_due by 30 days (still useful, just not urgent)
255
+
256
+ Decisions:
257
+ - status = 'pending_review' and review_due_at < now -> archive if >30 days old
258
+ """
259
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
260
+ now = time.time()
261
+ now_iso = datetime.now().isoformat(timespec="seconds")
262
+
263
+ # --- Overdue learnings ---
264
+ try:
265
+ overdue_learnings = conn.execute(
266
+ "SELECT id, title, weight, guard_hits, review_due_at, last_reviewed_at "
267
+ "FROM learnings "
268
+ "WHERE review_due_at IS NOT NULL AND review_due_at <= ? AND status = 'active'",
269
+ (now,)
270
+ ).fetchall()
271
+ except Exception as e:
272
+ print(f"[{ts}] Overdue reviews: error querying learnings: {e}")
273
+ return 0
274
+
275
+ confirmed = 0
276
+ archived = 0
277
+ for l in overdue_learnings:
278
+ lid = l["id"]
279
+ hits = l["guard_hits"] or 0
280
+ weight = l["weight"] or 0.5
281
+ last_reviewed = l["last_reviewed_at"] or 0
282
+
283
+ if hits > 5:
284
+ # Active and useful -- confirm: extend review date
285
+ new_due = now + (REVIEW_EXTEND_DAYS * 86400)
286
+ conn.execute(
287
+ "UPDATE learnings SET review_due_at = ?, last_reviewed_at = ? WHERE id = ?",
288
+ (new_due, now, lid)
289
+ )
290
+ confirmed += 1
291
+ elif hits == 0 and weight < 0.3:
292
+ # Unused and low weight -- archive
293
+ conn.execute(
294
+ "UPDATE learnings SET status = 'archived' WHERE id = ?",
295
+ (lid,)
296
+ )
297
+ archived += 1
298
+ print(f"[{ts}] Archived overdue learning #{lid} '{l['title'][:50]}' (hits=0, weight={weight:.2f})")
299
+ else:
300
+ # Middle ground -- extend review date, keep active
301
+ new_due = now + (REVIEW_EXTEND_DAYS * 86400)
302
+ conn.execute(
303
+ "UPDATE learnings SET review_due_at = ?, last_reviewed_at = ? WHERE id = ?",
304
+ (new_due, now, lid)
305
+ )
306
+ confirmed += 1
307
+
308
+ # --- Overdue decisions ---
309
+ decision_archived = 0
310
+ try:
311
+ cutoff_30d = (datetime.now() - timedelta(days=30)).isoformat(timespec="seconds")
312
+ overdue_decisions = conn.execute(
313
+ "SELECT id, decision, created_at FROM decisions "
314
+ "WHERE status = 'pending_review' AND review_due_at IS NOT NULL AND review_due_at <= ?",
315
+ (now_iso,)
316
+ ).fetchall()
317
+
318
+ for d in overdue_decisions:
319
+ did = d["id"]
320
+ created = d["created_at"] or ""
321
+ decision_text = d["decision"] or ""
322
+
323
+ # Try to reconcile outcome from diaries, followups, change_log
324
+ outcome = _reconcile_decision_outcome(conn, did, decision_text)
325
+ if outcome:
326
+ conn.execute(
327
+ "UPDATE decisions SET status = 'resolved', outcome = ? WHERE id = ?",
328
+ (outcome, did)
329
+ )
330
+ decision_archived += 1
331
+ print(f"[{ts}] Resolved decision #{did} '{decision_text[:50]}' — outcome found in logs")
332
+ elif created < cutoff_30d:
333
+ conn.execute(
334
+ "UPDATE decisions SET status = 'archived' WHERE id = ?",
335
+ (did,)
336
+ )
337
+ decision_archived += 1
338
+ print(f"[{ts}] Archived decision #{did} '{decision_text[:50]}' (>30d, no outcome found)")
339
+ except Exception as e:
340
+ print(f"[{ts}] Overdue reviews: error processing decisions: {e}")
341
+
342
+ conn.commit()
343
+ total_learnings = len(overdue_learnings) if 'overdue_learnings' in dir() else 0
344
+ total_decisions = len(overdue_decisions) if 'overdue_decisions' in dir() else 0
345
+ print(f"[{ts}] Overdue reviews: {total_learnings} learnings ({confirmed} confirmed, {archived} archived), "
346
+ f"{total_decisions} decisions ({decision_archived} archived)")
347
+ return confirmed + archived + decision_archived
348
+
349
+
198
350
  def print_summary(conn):
199
351
  """Print summary stats."""
200
352
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -233,7 +385,10 @@ def main():
233
385
  # 4. Archive stale learnings
234
386
  archive_stale(conn)
235
387
 
236
- # 5. Summary
388
+ # 5. Process overdue reviews (review_due_at < now)
389
+ process_overdue_reviews(conn)
390
+
391
+ # 6. Summary
237
392
  print_summary(conn)
238
393
 
239
394
  conn.close()