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,74 +0,0 @@
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
@@ -1,6 +0,0 @@
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)
@@ -1,245 +0,0 @@
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()
@@ -1,207 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Learning Validator — Cross-checks findings against existing learnings.
4
-
5
- Wrapper collects the finding + all learnings from SQLite, then passes
6
- to Claude CLI (opus) to make an intelligent determination of whether
7
- the finding is known, related, or genuinely new.
8
-
9
- Usage as CLI:
10
- python3 nexo-learning-validator.py "finding text to validate"
11
- python3 nexo-learning-validator.py --category project "finding text"
12
-
13
- Usage as library:
14
- from nexo_learning_validator import validate_finding
15
- result = validate_finding("CRITICAL: message_id column is NULL")
16
- if result["known"]:
17
- print(f"Already known: {result['matching_learnings']}")
18
-
19
- Exit codes:
20
- 0 = Finding is NEW (not known)
21
- 1 = Finding is KNOWN (matches existing learning)
22
- """
23
-
24
- import json
25
- import os
26
- import sqlite3
27
- import subprocess
28
- import sys
29
- from pathlib import Path
30
-
31
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
32
-
33
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
34
- CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
35
-
36
-
37
- def get_all_learnings(category: str = None) -> list[dict]:
38
- """Fetch all learnings from nexo.db."""
39
- conn = sqlite3.connect(str(NEXO_DB), timeout=10)
40
- conn.row_factory = sqlite3.Row
41
- if category:
42
- rows = conn.execute(
43
- "SELECT id, category, title, content FROM learnings WHERE category = ?",
44
- (category,)
45
- ).fetchall()
46
- else:
47
- rows = conn.execute(
48
- "SELECT id, category, title, content FROM learnings"
49
- ).fetchall()
50
- conn.close()
51
- return [dict(r) for r in rows]
52
-
53
-
54
- def validate_finding(finding: str, category: str = None) -> dict:
55
- """
56
- Validate a finding against existing learnings using Claude CLI.
57
-
58
- Returns:
59
- {
60
- "known": bool,
61
- "confidence": float (0-1),
62
- "matching_learnings": [{"id": int, "title": str, "similarity": float}],
63
- "recommendation": str
64
- }
65
- """
66
- learnings = get_all_learnings(category)
67
-
68
- if not learnings:
69
- return {
70
- "known": False,
71
- "confidence": 0,
72
- "matching_learnings": [],
73
- "recommendation": "No learnings in DB — finding is new by default"
74
- }
75
-
76
- # Build compact learnings reference for CLI
77
- learnings_ref = []
78
- for l in learnings:
79
- learnings_ref.append({
80
- "id": l["id"],
81
- "cat": l["category"],
82
- "title": l["title"],
83
- "content": (l["content"] or "")[:300],
84
- })
85
-
86
- prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
87
-
88
- NEW FINDING:
89
- {finding}
90
-
91
- EXISTING LEARNINGS ({len(learnings_ref)} total):
92
- {json.dumps(learnings_ref, indent=1)}
93
-
94
- Respond with ONLY valid JSON (no markdown, no code fences):
95
- {{
96
- "known": true/false,
97
- "confidence": 0.0-1.0,
98
- "matching_learnings": [
99
- {{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
100
- ],
101
- "recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
102
- }}
103
-
104
- Rules:
105
- - confidence >= 0.7 and same root cause = known: true
106
- - confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
107
- - confidence < 0.55 = known: false
108
- - Max 5 matching_learnings, sorted by similarity descending
109
- - If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
110
- - Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
111
-
112
- # Try CLI first, fall back to mechanical similarity
113
- if CLAUDE_CLI.exists():
114
- # Fallback: mechanical SequenceMatcher (original logic)
115
- return _mechanical_validate(finding, learnings)
116
-
117
-
118
- def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
119
- """Fallback validation using SequenceMatcher when CLI is unavailable."""
120
- from difflib import SequenceMatcher
121
-
122
- threshold = 0.45
123
- finding_kw = _extract_keywords(finding)
124
- matches = []
125
-
126
- for learning in learnings:
127
- title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
128
- content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
129
-
130
- learning_text = f"{learning['title']} {learning['content'] or ''}"
131
- learning_kw = _extract_keywords(learning_text)
132
- kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
133
-
134
- combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
135
-
136
- if combined >= threshold:
137
- matches.append({
138
- "id": learning["id"],
139
- "category": learning["category"],
140
- "title": learning["title"],
141
- "similarity": round(combined, 3),
142
- })
143
-
144
- matches.sort(key=lambda x: x["similarity"], reverse=True)
145
- top = matches[:5]
146
-
147
- if not top:
148
- return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
149
-
150
- best = top[0]["similarity"]
151
- if best >= 0.7:
152
- return {"known": True, "confidence": best, "matching_learnings": top,
153
- "recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
154
- elif best >= 0.55:
155
- return {"known": True, "confidence": best, "matching_learnings": top,
156
- "recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
157
- else:
158
- return {"known": False, "confidence": best, "matching_learnings": top,
159
- "recommendation": "POSSIBLY RELATED but different enough to report"}
160
-
161
-
162
- def _extract_keywords(text: str) -> set:
163
- """Extract meaningful keywords from text."""
164
- stop_words = {
165
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
166
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
167
- 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
168
- 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
169
- 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
170
- 'error', 'critical', 'warning', 'bug', 'issue', 'problem', 'fix',
171
- 'el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'que', 'por',
172
- }
173
- words = set()
174
- for word in text.lower().split():
175
- clean = ''.join(c for c in word if c.isalnum() or c == '_')
176
- if clean and len(clean) > 2 and clean not in stop_words:
177
- words.add(clean)
178
- return words
179
-
180
-
181
- def main():
182
- import argparse
183
- parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
184
- parser.add_argument("finding", help="The finding text to validate")
185
- parser.add_argument("--category", "-c", help="Filter learnings by category")
186
- parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
187
- args = parser.parse_args()
188
-
189
- result = validate_finding(args.finding, args.category)
190
-
191
- if args.json:
192
- print(json.dumps(result, indent=2))
193
- else:
194
- status = "KNOWN" if result["known"] else "NEW"
195
- print(f"Status: {status} (confidence: {result['confidence']:.0%})")
196
- print(f"Recommendation: {result['recommendation']}")
197
- if result["matching_learnings"]:
198
- print(f"Related learnings:")
199
- for m in result["matching_learnings"]:
200
- cat = m.get('category', '?')
201
- print(f" #{m['id']} [{cat}] {m['title']} ({m['similarity']:.0%})")
202
-
203
- sys.exit(1 if result["known"] else 0)
204
-
205
-
206
- if __name__ == "__main__":
207
- main()