nexo-brain 5.3.13 → 5.3.15

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 (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -1
  3. package/package.json +1 -1
  4. package/src/crons/sync.py +18 -4
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_entities.py +1 -1
  43. package/src/db/_episodic 2.py +762 -0
  44. package/src/db/_evolution 2.py +54 -0
  45. package/src/db/_fts 2.py +406 -0
  46. package/src/db/_goal_profiles 2.py +376 -0
  47. package/src/db/_hot_context 2.py +660 -0
  48. package/src/db/_outcomes 2.py +800 -0
  49. package/src/db/_personal_scripts 2.py +582 -0
  50. package/src/db/_sessions 2.py +330 -0
  51. package/src/db/_tasks 2.py +91 -0
  52. package/src/db/_watchers 2.py +173 -0
  53. package/src/doctor/formatters 2.py +52 -0
  54. package/src/doctor/models 2.py +69 -0
  55. package/src/doctor/planes 2.py +87 -0
  56. package/src/doctor/providers/__init__ 2.py +1 -0
  57. package/src/doctor/providers/deep 2.py +367 -0
  58. package/src/evolution_cycle 2.py +519 -0
  59. package/src/hooks/auto_capture 2.py +208 -0
  60. package/src/hooks/caffeinate-guard 2.sh +8 -0
  61. package/src/hooks/capture-session 2.sh +21 -0
  62. package/src/hooks/capture-tool-logs 2.sh +158 -0
  63. package/src/hooks/daily-briefing-check 2.sh +33 -0
  64. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  65. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  66. package/src/hooks/inbox-hook 2.sh +76 -0
  67. package/src/hooks/post-compact 2.sh +152 -0
  68. package/src/hooks/pre-compact 2.sh +169 -0
  69. package/src/hooks/protocol-guardrail 2.sh +10 -0
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  71. package/src/hooks/session-stop 2.sh +52 -0
  72. package/src/kg_populate 2.py +292 -0
  73. package/src/maintenance 2.py +53 -0
  74. package/src/memory_backends 2.py +71 -0
  75. package/src/migrate_embeddings 2.py +124 -0
  76. package/src/nexo_sdk 2.py +103 -0
  77. package/src/observability 2.py +199 -0
  78. package/src/plugin_loader 2.py +217 -0
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/agents.py +10 -3
  81. package/src/plugins/artifact_registry 2.py +450 -0
  82. package/src/plugins/backup 2.py +127 -0
  83. package/src/plugins/claims_tools 2.py +119 -0
  84. package/src/plugins/cognitive_memory 2.py +609 -0
  85. package/src/plugins/core_rules 2.py +252 -0
  86. package/src/plugins/cortex 2.py +1155 -0
  87. package/src/plugins/entities 2.py +67 -0
  88. package/src/plugins/episodic_memory 2.py +560 -0
  89. package/src/plugins/evolution 2.py +167 -0
  90. package/src/plugins/goal_engine 2.py +142 -0
  91. package/src/plugins/guard 2.py +862 -0
  92. package/src/plugins/impact 2.py +29 -0
  93. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  94. package/src/plugins/media_memory_tools 2.py +98 -0
  95. package/src/plugins/memory_export 2.py +196 -0
  96. package/src/plugins/outcomes 2.py +130 -0
  97. package/src/plugins/personal_scripts 2.py +117 -0
  98. package/src/plugins/preferences 2.py +47 -0
  99. package/src/plugins/protocol 2.py +1449 -0
  100. package/src/plugins/schedule.py +2 -1
  101. package/src/plugins/simple_api 2.py +106 -0
  102. package/src/plugins/skills 2.py +341 -0
  103. package/src/plugins/state_watchers 2.py +79 -0
  104. package/src/plugins/update 2.py +986 -0
  105. package/src/plugins/user_state_tools 2.py +43 -0
  106. package/src/plugins/workflow 2.py +588 -0
  107. package/src/protocol_settings 2.py +59 -0
  108. package/src/public_contribution 2.py +466 -0
  109. package/src/public_evolution_queue 2.py +241 -0
  110. package/src/requirements 2.txt +14 -0
  111. package/src/requirements.txt +1 -1
  112. package/src/retroactive_learnings 2.py +373 -0
  113. package/src/rules/__init__ 2.py +0 -0
  114. package/src/rules/core-rules 2.json +331 -0
  115. package/src/rules/migrate 2.py +207 -0
  116. package/src/runtime_power 2.py +874 -0
  117. package/src/runtime_power.py +18 -1
  118. package/src/script_registry 2.py +1559 -0
  119. package/src/scripts/check-context 2.py +272 -0
  120. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  121. package/src/scripts/deep-sleep/collect 2.py +928 -0
  122. package/src/scripts/deep-sleep/extract 2.py +330 -0
  123. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  124. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  125. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  126. package/src/scripts/nexo-agent-run 2.py +75 -0
  127. package/src/scripts/nexo-auto-update 2.py +6 -0
  128. package/src/scripts/nexo-backup 2.sh +25 -0
  129. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  130. package/src/scripts/nexo-catchup 2.py +300 -0
  131. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  132. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  133. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  134. package/src/scripts/nexo-cron-wrapper.sh +7 -0
  135. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  136. package/src/scripts/nexo-dashboard 2.sh +29 -0
  137. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  138. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  139. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  140. package/src/scripts/nexo-hook-record 2.py +42 -0
  141. package/src/scripts/nexo-immune 2.py +936 -0
  142. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  143. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  144. package/src/scripts/nexo-install 2.py +6 -0
  145. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  146. package/src/scripts/nexo-learning-validator 2.py +266 -0
  147. package/src/scripts/nexo-migrate 2.py +260 -0
  148. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  149. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  150. package/src/scripts/nexo-pre-commit 2.py +120 -0
  151. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  152. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  153. package/src/scripts/nexo-reflection 2.py +256 -0
  154. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  155. package/src/scripts/nexo-sleep 2.py +631 -0
  156. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  157. package/src/scripts/nexo-sync-clients 2.py +16 -0
  158. package/src/scripts/nexo-synthesis 2.py +475 -0
  159. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  160. package/src/scripts/nexo-update 2.sh +306 -0
  161. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  162. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  163. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  164. package/src/server 2.py +1296 -0
  165. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  166. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  167. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  168. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  169. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  170. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  171. package/src/skills/run-release-final-audit/script 2.py +259 -0
  172. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  173. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  174. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  175. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  176. package/src/skills_runtime 2.py +932 -0
  177. package/src/state_watchers_runtime 2.py +475 -0
  178. package/src/storage_router 2.py +32 -0
  179. package/src/system_catalog 2.py +786 -0
  180. package/src/tools_coordination 2.py +103 -0
  181. package/src/tools_credentials 2.py +68 -0
  182. package/src/tools_drive 2.py +487 -0
  183. package/src/tools_hot_context 2.py +163 -0
  184. package/src/tools_learnings 2.py +612 -0
  185. package/src/tools_menu 2.py +229 -0
  186. package/src/tools_reminders 2.py +88 -0
  187. package/src/tools_reminders_crud 2.py +363 -0
  188. package/src/tools_sessions 2.py +1054 -0
  189. package/src/tools_system_catalog 2.py +19 -0
  190. package/src/tools_task_history 2.py +57 -0
  191. package/src/tools_transcripts 2.py +98 -0
  192. package/src/transcript_utils 2.py +412 -0
  193. package/src/user_context 2.py +46 -0
  194. package/src/user_data_portability 2.py +328 -0
  195. package/src/user_state_model 2.py +170 -0
  196. package/templates/CLAUDE.md 2.template +108 -0
  197. package/templates/CODEX.AGENTS.md 2.template +66 -0
  198. package/templates/launchagents/README 2.md +132 -0
  199. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  200. package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
  201. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  202. package/templates/launchagents/com.nexo.catchup.plist +1 -1
  203. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  204. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  205. package/templates/launchagents/com.nexo.dashboard.plist +1 -1
  206. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  207. package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
  208. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  209. package/templates/launchagents/com.nexo.evolution.plist +1 -1
  210. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  211. package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
  212. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  213. package/templates/launchagents/com.nexo.immune.plist +1 -1
  214. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  215. package/templates/launchagents/com.nexo.postmortem.plist +1 -1
  216. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  217. package/templates/launchagents/com.nexo.self-audit.plist +1 -1
  218. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  219. package/templates/launchagents/com.nexo.synthesis.plist +1 -1
  220. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  221. package/templates/launchagents/com.nexo.watchdog.plist +1 -1
  222. package/templates/nexo_helper 2.py +301 -0
  223. package/templates/openclaw 2.json +13 -0
  224. package/templates/plugin-template 2.py +40 -0
  225. package/templates/script-template 2.py +59 -0
  226. package/templates/script-template 2.sh +13 -0
  227. package/templates/script-template.py +5 -4
  228. package/templates/skill-script-template 2.py +48 -0
  229. package/templates/skill-script-template.py +2 -1
  230. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ """NEXO Impact Scorer — recalculate followup impact scores for real queues."""
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
12
+ _script_dir = Path(__file__).resolve().parent
13
+ _repo_src = _script_dir.parent
14
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
15
+ if str(NEXO_CODE) not in sys.path:
16
+ sys.path.insert(0, str(NEXO_CODE))
17
+
18
+ import db as nexo_db
19
+
20
+ LOG_FILE = NEXO_HOME / "logs" / "impact-scorer.log"
21
+ SUMMARY_FILE = NEXO_HOME / "coordination" / "impact-scorer-summary.json"
22
+
23
+
24
+ def log(message: str):
25
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
26
+ line = f"[{ts}] {message}"
27
+ print(line, flush=True)
28
+ LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
29
+ with open(LOG_FILE, "a") as handle:
30
+ handle.write(line + "\n")
31
+
32
+
33
+ def _active_followup_scores() -> dict[str, float]:
34
+ conn = nexo_db.get_db()
35
+ rows = conn.execute(
36
+ """SELECT id, impact_score FROM followups
37
+ WHERE status IN ('PENDING', 'ACTIVE', 'WAITING', 'BLOCKED')"""
38
+ ).fetchall()
39
+ return {str(row["id"]): float(row["impact_score"] or 0.0) for row in rows}
40
+
41
+
42
+ def _reasoning_for(row: dict) -> str:
43
+ factors = row.get("impact_factors") or {}
44
+ if isinstance(factors, str):
45
+ try:
46
+ factors = json.loads(factors)
47
+ except json.JSONDecodeError:
48
+ factors = {}
49
+ if isinstance(factors, dict):
50
+ return str(factors.get("reasoning") or "").strip()
51
+ return ""
52
+
53
+
54
+ def main() -> int:
55
+ log("=== Impact Scorer starting ===")
56
+ nexo_db.init_db()
57
+ previous_scores = _active_followup_scores()
58
+ scored = nexo_db.score_active_followups(limit=500)
59
+ top = [
60
+ {
61
+ "id": row.get("id"),
62
+ "date": row.get("date"),
63
+ "priority": row.get("priority"),
64
+ "impact_score": row.get("impact_score", 0),
65
+ "impact_factors": row.get("impact_factors") or {},
66
+ "impact_reasoning": _reasoning_for(row),
67
+ }
68
+ for row in scored[:5]
69
+ ]
70
+ top_changes = sorted(
71
+ [
72
+ {
73
+ "id": row.get("id"),
74
+ "priority": row.get("priority"),
75
+ "date": row.get("date"),
76
+ "impact_score": row.get("impact_score", 0),
77
+ "previous_score": previous_scores.get(str(row.get("id")), 0.0),
78
+ "delta": round(float(row.get("impact_score") or 0.0) - previous_scores.get(str(row.get("id")), 0.0), 2),
79
+ "impact_reasoning": _reasoning_for(row),
80
+ }
81
+ for row in scored
82
+ ],
83
+ key=lambda item: (abs(float(item["delta"])), float(item["impact_score"] or 0.0)),
84
+ reverse=True,
85
+ )[:5]
86
+ summary = {
87
+ "scored_at": datetime.now().isoformat(timespec="seconds"),
88
+ "scored_count": len(scored),
89
+ "top_followups": top,
90
+ "top_changes": top_changes,
91
+ }
92
+ SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
93
+ SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
94
+ log(f"Scored {len(scored)} active followups.")
95
+ for item in top:
96
+ log(
97
+ f"Top -> {item['id']} score={item['impact_score']} "
98
+ f"priority={item['priority']} date={item['date'] or '—'} "
99
+ f"because={item['impact_reasoning'] or 'no reasoning'}"
100
+ )
101
+ changed = [item for item in top_changes if abs(float(item["delta"])) >= 1.0]
102
+ if changed:
103
+ log("Strong top-5 changes:")
104
+ for item in changed:
105
+ direction = "+" if float(item["delta"]) >= 0 else ""
106
+ log(
107
+ f" Δ {item['id']} {direction}{item['delta']:.2f} -> {item['impact_score']:.2f} "
108
+ f"priority={item['priority']} because={item['impact_reasoning'] or 'no reasoning'}"
109
+ )
110
+ else:
111
+ log("Strong top-5 changes: none")
112
+ log("=== Impact Scorer complete ===")
113
+ return 0
114
+
115
+
116
+ if __name__ == "__main__":
117
+ raise SystemExit(main())
@@ -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 (external_session_id = '${CLAUDE_SID}' OR 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,401 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ """NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
4
+
5
+ Runs daily. Adjusts learning weights based on usage (guard_hits),
6
+ detects duplicates via semantic similarity, and archives stale learnings.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sqlite3
12
+ import sys
13
+ import time
14
+ from datetime import datetime, timedelta
15
+ from pathlib import Path
16
+
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
+ # Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
19
+ _script_dir = Path(__file__).resolve().parent
20
+ _repo_src = _script_dir.parent # src/scripts/ -> src/
21
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
22
+
23
+ sys.path.insert(0, str(NEXO_CODE))
24
+
25
+ DB_PATH = NEXO_HOME / "data" / "nexo.db"
26
+ STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
27
+
28
+ # Weight adjustment rates
29
+ GUARD_HIT_BOOST = 0.02 # per guard hit since last run
30
+ DECAY_RATE = 0.005 # daily decay for unused learnings
31
+ MIN_WEIGHT = 0.05
32
+ MAX_WEIGHT = 1.0
33
+ DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
34
+ ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
35
+ REVIEW_EXTEND_DAYS = 30 # extend review_due by this many days when confirming
36
+
37
+
38
+ def get_db():
39
+ conn = sqlite3.connect(str(DB_PATH))
40
+ conn.row_factory = sqlite3.Row
41
+ return conn
42
+
43
+
44
+ def update_catchup_state():
45
+ try:
46
+ state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
47
+ except Exception:
48
+ state = {}
49
+ state["learning-housekeep"] = datetime.now().isoformat()
50
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
51
+ STATE_FILE.write_text(json.dumps(state, indent=2))
52
+
53
+
54
+ def adjust_weights(conn):
55
+ """Boost weight for frequently-used learnings, decay unused ones."""
56
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
57
+ now = time.time()
58
+ one_day_ago = now - 86400
59
+
60
+ learnings = conn.execute(
61
+ "SELECT id, weight, guard_hits, last_guard_hit_at, priority, created_at "
62
+ "FROM learnings WHERE status = 'active'"
63
+ ).fetchall()
64
+
65
+ adjusted = 0
66
+ for l in learnings:
67
+ old_weight = l["weight"] or 0.5
68
+ hits = l["guard_hits"] or 0
69
+ last_hit = l["last_guard_hit_at"] or 0
70
+ priority = l["priority"] or "medium"
71
+
72
+ # Priority floor — critical learnings never drop below 0.5
73
+ priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}.get(priority, 0.1)
74
+
75
+ new_weight = old_weight
76
+
77
+ if last_hit > one_day_ago:
78
+ # Recent guard hit — boost
79
+ recent_hits = 1 # Simplified: at least 1 hit today
80
+ new_weight = min(MAX_WEIGHT, old_weight + (GUARD_HIT_BOOST * recent_hits))
81
+ else:
82
+ # No recent hits — decay
83
+ new_weight = max(priority_floor, old_weight - DECAY_RATE)
84
+
85
+ new_weight = max(MIN_WEIGHT, min(MAX_WEIGHT, new_weight))
86
+
87
+ if abs(new_weight - old_weight) > 0.001:
88
+ conn.execute("UPDATE learnings SET weight = ? WHERE id = ?", (round(new_weight, 4), l["id"]))
89
+ adjusted += 1
90
+
91
+ conn.commit()
92
+ print(f"[{ts}] Weight adjustment: {adjusted}/{len(learnings)} learnings adjusted")
93
+ return adjusted
94
+
95
+
96
+ def auto_prioritize(conn):
97
+ """Auto-upgrade priority based on guard hits and repetitions."""
98
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
99
+
100
+ # Learnings with 10+ guard hits that are still medium → upgrade to high
101
+ upgraded = conn.execute(
102
+ "UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7) "
103
+ "WHERE status = 'active' AND priority = 'medium' AND guard_hits >= 10"
104
+ ).rowcount
105
+
106
+ # Learnings with repetitions (same error happened again) → upgrade to high
107
+ repeated = conn.execute(
108
+ """UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7)
109
+ WHERE status = 'active' AND priority IN ('medium', 'low')
110
+ AND id IN (SELECT original_learning_id FROM error_repetitions)"""
111
+ ).rowcount
112
+
113
+ conn.commit()
114
+ total = upgraded + repeated
115
+ if total > 0:
116
+ print(f"[{ts}] Auto-prioritize: {upgraded} by guard_hits, {repeated} by repetitions")
117
+ return total
118
+
119
+
120
+ def detect_duplicates(conn):
121
+ """Find semantically similar learnings using fastembed."""
122
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
123
+ try:
124
+ from fastembed import TextEmbedding
125
+ import numpy as np
126
+ except ImportError:
127
+ print(f"[{ts}] Dedup skipped: fastembed not available")
128
+ return []
129
+
130
+ learnings = conn.execute(
131
+ "SELECT id, title, content, weight, guard_hits FROM learnings WHERE status = 'active'"
132
+ ).fetchall()
133
+
134
+ if len(learnings) < 2:
135
+ return []
136
+
137
+ model = TextEmbedding("BAAI/bge-base-en-v1.5")
138
+ texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
139
+ embeddings = list(model.embed(texts))
140
+ embeddings = np.array(embeddings)
141
+
142
+ # Normalize
143
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
144
+ norms[norms == 0] = 1
145
+ embeddings = embeddings / norms
146
+
147
+ duplicates = []
148
+ for i in range(len(learnings)):
149
+ for j in range(i + 1, len(learnings)):
150
+ sim = float(np.dot(embeddings[i], embeddings[j]))
151
+ if sim >= DEDUP_THRESHOLD:
152
+ # Keep the one with higher weight/hits
153
+ a, b = learnings[i], learnings[j]
154
+ score_a = (a["weight"] or 0.5) + (a["guard_hits"] or 0) * 0.01
155
+ score_b = (b["weight"] or 0.5) + (b["guard_hits"] or 0) * 0.01
156
+ keep, drop = (a, b) if score_a >= score_b else (b, a)
157
+ duplicates.append({
158
+ "keep_id": keep["id"], "keep_title": keep["title"],
159
+ "drop_id": drop["id"], "drop_title": drop["title"],
160
+ "similarity": round(sim, 3)
161
+ })
162
+
163
+ if duplicates:
164
+ print(f"[{ts}] Duplicates found: {len(duplicates)} pairs (>= {DEDUP_THRESHOLD})")
165
+ for d in duplicates[:10]:
166
+ print(f"[{ts}] [{d['similarity']}] keep #{d['keep_id']} '{d['keep_title'][:40]}', archive #{d['drop_id']} '{d['drop_title'][:40]}'")
167
+ # Archive the duplicate (don't delete — just mark inactive)
168
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (d["drop_id"],))
169
+ conn.commit()
170
+ else:
171
+ print(f"[{ts}] No duplicates found ({len(learnings)} learnings scanned)")
172
+
173
+ return duplicates
174
+
175
+
176
+ def archive_stale(conn):
177
+ """Archive learnings with very low weight and no recent guard hits."""
178
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
179
+ cutoff = time.time() - (ARCHIVE_AFTER_DAYS * 86400)
180
+
181
+ stale = conn.execute(
182
+ "SELECT id, title, weight, last_guard_hit_at FROM learnings "
183
+ "WHERE status = 'active' AND weight < 0.1 AND priority NOT IN ('critical', 'high') "
184
+ "AND (last_guard_hit_at IS NULL OR last_guard_hit_at < ?)",
185
+ (cutoff,)
186
+ ).fetchall()
187
+
188
+ if stale:
189
+ for s in stale:
190
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (s["id"],))
191
+ print(f"[{ts}] Archived #{s['id']} '{s['title'][:50]}' (weight={s['weight']:.2f})")
192
+ conn.commit()
193
+ print(f"[{ts}] Archived {len(stale)} stale learnings")
194
+ else:
195
+ print(f"[{ts}] No stale learnings to archive")
196
+
197
+ return len(stale)
198
+
199
+
200
+ def _reconcile_decision_outcome(conn, decision_id: int, decision_text: str) -> str | None:
201
+ """Try to find evidence of a decision's outcome in diaries, followups, and change_log.
202
+
203
+ Returns outcome text if found, None otherwise.
204
+ """
205
+ # Extract keywords from the decision for matching
206
+ keywords = [w for w in decision_text.lower().split() if len(w) > 4][:5]
207
+ if not keywords:
208
+ return None
209
+
210
+ like_clauses = " OR ".join(f"summary LIKE ?" for _ in keywords)
211
+ like_params = [f"%{kw}%" for kw in keywords]
212
+
213
+ # Check session diaries for evidence
214
+ diary_match = conn.execute(
215
+ f"SELECT summary FROM session_diary WHERE ({like_clauses}) "
216
+ "AND created_at > (SELECT created_at FROM decisions WHERE id = ?) "
217
+ "ORDER BY created_at DESC LIMIT 1",
218
+ like_params + [decision_id]
219
+ ).fetchone()
220
+ if diary_match:
221
+ return f"[auto-reconciled from diary] {diary_match['summary'][:200]}"
222
+
223
+ # Check completed followups
224
+ like_clauses_f = " OR ".join(f"description LIKE ?" for _ in keywords)
225
+ followup_match = conn.execute(
226
+ f"SELECT description, verification FROM followups WHERE status = 'COMPLETED' "
227
+ f"AND ({like_clauses_f}) ORDER BY date DESC LIMIT 1",
228
+ like_params
229
+ ).fetchone()
230
+ if followup_match:
231
+ result = followup_match['verification'] or followup_match['description']
232
+ return f"[auto-reconciled from followup] {result[:200]}"
233
+
234
+ # Check change_log (schema: what_changed, why, commit_ref, affects)
235
+ like_clauses_c = " OR ".join(f"what_changed LIKE ?" for _ in keywords)
236
+ change_match = conn.execute(
237
+ f"SELECT what_changed, why, commit_ref FROM change_log WHERE ({like_clauses_c}) "
238
+ "ORDER BY created_at DESC LIMIT 1",
239
+ like_params
240
+ ).fetchone()
241
+ if change_match:
242
+ ref = change_match['commit_ref'] or ''
243
+ desc = change_match['what_changed'] or change_match['why'] or ''
244
+ return f"[auto-reconciled from change_log] {desc[:150]} {ref}"
245
+
246
+ return None
247
+
248
+
249
+ def process_overdue_reviews(conn):
250
+ """Process learnings and decisions whose review_due_at has passed.
251
+
252
+ Learnings:
253
+ - guard_hits > 5 since last review -> confirm (extend review_due by 30 days)
254
+ - guard_hits = 0 and weight < 0.3 -> archive
255
+ - otherwise -> extend review_due by 30 days (still useful, just not urgent)
256
+
257
+ Decisions:
258
+ - status = 'pending_review' and review_due_at < now -> archive if >30 days old
259
+ """
260
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
261
+ now = time.time()
262
+ now_iso = datetime.now().isoformat(timespec="seconds")
263
+
264
+ # --- Overdue learnings ---
265
+ try:
266
+ overdue_learnings = conn.execute(
267
+ "SELECT id, title, weight, guard_hits, review_due_at, last_reviewed_at "
268
+ "FROM learnings "
269
+ "WHERE review_due_at IS NOT NULL AND review_due_at <= ? AND status = 'active'",
270
+ (now,)
271
+ ).fetchall()
272
+ except Exception as e:
273
+ print(f"[{ts}] Overdue reviews: error querying learnings: {e}")
274
+ return 0
275
+
276
+ confirmed = 0
277
+ archived = 0
278
+ for l in overdue_learnings:
279
+ lid = l["id"]
280
+ hits = l["guard_hits"] or 0
281
+ weight = l["weight"] or 0.5
282
+ last_reviewed = l["last_reviewed_at"] or 0
283
+
284
+ if hits > 5:
285
+ # Active and useful -- confirm: extend review date
286
+ new_due = now + (REVIEW_EXTEND_DAYS * 86400)
287
+ conn.execute(
288
+ "UPDATE learnings SET review_due_at = ?, last_reviewed_at = ? WHERE id = ?",
289
+ (new_due, now, lid)
290
+ )
291
+ confirmed += 1
292
+ elif hits == 0 and weight < 0.3:
293
+ # Unused and low weight -- archive
294
+ conn.execute(
295
+ "UPDATE learnings SET status = 'archived' WHERE id = ?",
296
+ (lid,)
297
+ )
298
+ archived += 1
299
+ print(f"[{ts}] Archived overdue learning #{lid} '{l['title'][:50]}' (hits=0, weight={weight:.2f})")
300
+ else:
301
+ # Middle ground -- extend review date, keep active
302
+ new_due = now + (REVIEW_EXTEND_DAYS * 86400)
303
+ conn.execute(
304
+ "UPDATE learnings SET review_due_at = ?, last_reviewed_at = ? WHERE id = ?",
305
+ (new_due, now, lid)
306
+ )
307
+ confirmed += 1
308
+
309
+ # --- Overdue decisions ---
310
+ decision_archived = 0
311
+ try:
312
+ cutoff_30d = (datetime.now() - timedelta(days=30)).isoformat(timespec="seconds")
313
+ overdue_decisions = conn.execute(
314
+ "SELECT id, decision, created_at FROM decisions "
315
+ "WHERE status = 'pending_review' AND review_due_at IS NOT NULL AND review_due_at <= ?",
316
+ (now_iso,)
317
+ ).fetchall()
318
+
319
+ for d in overdue_decisions:
320
+ did = d["id"]
321
+ created = d["created_at"] or ""
322
+ decision_text = d["decision"] or ""
323
+
324
+ # Try to reconcile outcome from diaries, followups, change_log
325
+ outcome = _reconcile_decision_outcome(conn, did, decision_text)
326
+ if outcome:
327
+ conn.execute(
328
+ "UPDATE decisions SET status = 'resolved', outcome = ? WHERE id = ?",
329
+ (outcome, did)
330
+ )
331
+ decision_archived += 1
332
+ print(f"[{ts}] Resolved decision #{did} '{decision_text[:50]}' — outcome found in logs")
333
+ elif created < cutoff_30d:
334
+ conn.execute(
335
+ "UPDATE decisions SET status = 'archived' WHERE id = ?",
336
+ (did,)
337
+ )
338
+ decision_archived += 1
339
+ print(f"[{ts}] Archived decision #{did} '{decision_text[:50]}' (>30d, no outcome found)")
340
+ except Exception as e:
341
+ print(f"[{ts}] Overdue reviews: error processing decisions: {e}")
342
+
343
+ conn.commit()
344
+ total_learnings = len(overdue_learnings) if 'overdue_learnings' in dir() else 0
345
+ total_decisions = len(overdue_decisions) if 'overdue_decisions' in dir() else 0
346
+ print(f"[{ts}] Overdue reviews: {total_learnings} learnings ({confirmed} confirmed, {archived} archived), "
347
+ f"{total_decisions} decisions ({decision_archived} archived)")
348
+ return confirmed + archived + decision_archived
349
+
350
+
351
+ def print_summary(conn):
352
+ """Print summary stats."""
353
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
354
+ stats = conn.execute(
355
+ """SELECT
356
+ COUNT(*) as total,
357
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
358
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
359
+ SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END) as critical,
360
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
361
+ SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
362
+ SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
363
+ printf('%.2f', AVG(CASE WHEN status = 'active' THEN weight END)) as avg_weight
364
+ FROM learnings"""
365
+ ).fetchone()
366
+ print(f"[{ts}] Summary: {stats['active']} active, {stats['archived']} archived | "
367
+ f"Priority: {stats['critical']}C {stats['high']}H {stats['medium']}M {stats['low']}L | "
368
+ f"Avg weight: {stats['avg_weight']}")
369
+
370
+
371
+ def main():
372
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
373
+ print(f"[{ts}] Learning housekeeping starting...")
374
+
375
+ conn = get_db()
376
+
377
+ # 1. Adjust weights based on usage
378
+ adjust_weights(conn)
379
+
380
+ # 2. Auto-prioritize based on guard hits and repetitions
381
+ auto_prioritize(conn)
382
+
383
+ # 3. Detect and archive duplicates
384
+ detect_duplicates(conn)
385
+
386
+ # 4. Archive stale learnings
387
+ archive_stale(conn)
388
+
389
+ # 5. Process overdue reviews (review_due_at < now)
390
+ process_overdue_reviews(conn)
391
+
392
+ # 6. Summary
393
+ print_summary(conn)
394
+
395
+ conn.close()
396
+ update_catchup_state()
397
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Done.")
398
+
399
+
400
+ if __name__ == "__main__":
401
+ main()