nexo-brain 5.3.26 → 5.3.28

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 (212) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/hook_guardrails.py +44 -0
  4. package/src/server.py +3 -0
  5. package/src/tools_sessions.py +6 -1
  6. package/src/dashboard/static/favicon 2.svg +0 -32
  7. package/src/dashboard/static/nexo-logo 2.png +0 -0
  8. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  9. package/src/dashboard/static/style 2.css +0 -2458
  10. package/src/dashboard/templates/adaptive 2.html +0 -118
  11. package/src/dashboard/templates/artifacts 2.html +0 -133
  12. package/src/dashboard/templates/backups 2.html +0 -136
  13. package/src/dashboard/templates/base 2.html +0 -417
  14. package/src/dashboard/templates/calendar 2.html +0 -591
  15. package/src/dashboard/templates/chat 2.html +0 -356
  16. package/src/dashboard/templates/claims 2.html +0 -259
  17. package/src/dashboard/templates/cortex 2.html +0 -321
  18. package/src/dashboard/templates/credentials 2.html +0 -128
  19. package/src/dashboard/templates/crons 2.html +0 -370
  20. package/src/dashboard/templates/dashboard 2.html +0 -494
  21. package/src/dashboard/templates/dreams 2.html +0 -252
  22. package/src/dashboard/templates/email 2.html +0 -160
  23. package/src/dashboard/templates/evolution 2.html +0 -189
  24. package/src/dashboard/templates/feed 2.html +0 -249
  25. package/src/dashboard/templates/followup_health 2.html +0 -170
  26. package/src/dashboard/templates/graph 2.html +0 -201
  27. package/src/dashboard/templates/guard 2.html +0 -259
  28. package/src/dashboard/templates/inbox 2.html +0 -251
  29. package/src/dashboard/templates/memory 2.html +0 -420
  30. package/src/dashboard/templates/operations 2.html +0 -608
  31. package/src/dashboard/templates/plugins 2.html +0 -185
  32. package/src/dashboard/templates/protocol 2.html +0 -199
  33. package/src/dashboard/templates/rules 2.html +0 -246
  34. package/src/dashboard/templates/sentiment 2.html +0 -247
  35. package/src/dashboard/templates/sessions 2.html +0 -218
  36. package/src/dashboard/templates/skills 2.html +0 -329
  37. package/src/dashboard/templates/somatic 2.html +0 -73
  38. package/src/dashboard/templates/triggers 2.html +0 -133
  39. package/src/dashboard/templates/trust 2.html +0 -360
  40. package/src/db/__init__ 2.py +0 -259
  41. package/src/db/_core 2.py +0 -437
  42. package/src/db/_credentials 2.py +0 -124
  43. package/src/db/_episodic 2.py +0 -762
  44. package/src/db/_evolution 2.py +0 -54
  45. package/src/db/_fts 2.py +0 -406
  46. package/src/db/_goal_profiles 2.py +0 -376
  47. package/src/db/_hot_context 2.py +0 -660
  48. package/src/db/_outcomes 2.py +0 -800
  49. package/src/db/_personal_scripts 2.py +0 -582
  50. package/src/db/_sessions 2.py +0 -330
  51. package/src/db/_tasks 2.py +0 -91
  52. package/src/db/_watchers 2.py +0 -173
  53. package/src/doctor/formatters 2.py +0 -52
  54. package/src/doctor/models 2.py +0 -69
  55. package/src/doctor/planes 2.py +0 -87
  56. package/src/doctor/providers/__init__ 2.py +0 -1
  57. package/src/doctor/providers/deep 2.py +0 -367
  58. package/src/evolution_cycle 2.py +0 -519
  59. package/src/hooks/auto_capture 2.py +0 -208
  60. package/src/hooks/caffeinate-guard 2.sh +0 -8
  61. package/src/hooks/capture-session 2.sh +0 -21
  62. package/src/hooks/capture-tool-logs 2.sh +0 -158
  63. package/src/hooks/daily-briefing-check 2.sh +0 -33
  64. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  65. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  66. package/src/hooks/inbox-hook 2.sh +0 -76
  67. package/src/hooks/post-compact 2.sh +0 -152
  68. package/src/hooks/pre-compact 2.sh +0 -169
  69. package/src/hooks/protocol-guardrail 2.sh +0 -10
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  71. package/src/hooks/session-stop 2.sh +0 -52
  72. package/src/kg_populate 2.py +0 -292
  73. package/src/maintenance 2.py +0 -53
  74. package/src/memory_backends 2.py +0 -71
  75. package/src/migrate_embeddings 2.py +0 -124
  76. package/src/nexo_sdk 2.py +0 -103
  77. package/src/observability 2.py +0 -199
  78. package/src/plugin_loader 2.py +0 -217
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/artifact_registry 2.py +0 -450
  81. package/src/plugins/backup 2.py +0 -127
  82. package/src/plugins/claims_tools 2.py +0 -119
  83. package/src/plugins/cognitive_memory 2.py +0 -609
  84. package/src/plugins/core_rules 2.py +0 -252
  85. package/src/plugins/cortex 2.py +0 -1155
  86. package/src/plugins/entities 2.py +0 -67
  87. package/src/plugins/episodic_memory 2.py +0 -560
  88. package/src/plugins/evolution 2.py +0 -167
  89. package/src/plugins/goal_engine 2.py +0 -142
  90. package/src/plugins/guard 2.py +0 -862
  91. package/src/plugins/impact 2.py +0 -29
  92. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  93. package/src/plugins/media_memory_tools 2.py +0 -98
  94. package/src/plugins/memory_export 2.py +0 -196
  95. package/src/plugins/outcomes 2.py +0 -130
  96. package/src/plugins/personal_scripts 2.py +0 -117
  97. package/src/plugins/preferences 2.py +0 -47
  98. package/src/plugins/protocol 2.py +0 -1449
  99. package/src/plugins/simple_api 2.py +0 -106
  100. package/src/plugins/skills 2.py +0 -341
  101. package/src/plugins/state_watchers 2.py +0 -79
  102. package/src/plugins/update 2.py +0 -986
  103. package/src/plugins/user_state_tools 2.py +0 -43
  104. package/src/plugins/workflow 2.py +0 -588
  105. package/src/protocol_settings 2.py +0 -59
  106. package/src/public_contribution 2.py +0 -466
  107. package/src/public_evolution_queue 2.py +0 -241
  108. package/src/requirements 2.txt +0 -14
  109. package/src/retroactive_learnings 2.py +0 -373
  110. package/src/rules/__init__ 2.py +0 -0
  111. package/src/rules/core-rules 2.json +0 -331
  112. package/src/rules/migrate 2.py +0 -207
  113. package/src/runtime_power 2.py +0 -874
  114. package/src/script_registry 2.py +0 -1559
  115. package/src/scripts/check-context 2.py +0 -272
  116. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  117. package/src/scripts/deep-sleep/collect 2.py +0 -928
  118. package/src/scripts/deep-sleep/extract 2.py +0 -330
  119. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  120. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  121. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  122. package/src/scripts/nexo-agent-run 2.py +0 -75
  123. package/src/scripts/nexo-auto-update 2.py +0 -6
  124. package/src/scripts/nexo-backup 2.sh +0 -25
  125. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  126. package/src/scripts/nexo-catchup 2.py +0 -300
  127. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  128. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  129. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  130. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  131. package/src/scripts/nexo-dashboard 2.sh +0 -29
  132. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  133. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  134. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  135. package/src/scripts/nexo-hook-record 2.py +0 -42
  136. package/src/scripts/nexo-immune 2.py +0 -936
  137. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  138. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  139. package/src/scripts/nexo-install 2.py +0 -6
  140. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  141. package/src/scripts/nexo-learning-validator 2.py +0 -266
  142. package/src/scripts/nexo-migrate 2.py +0 -260
  143. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  144. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  145. package/src/scripts/nexo-pre-commit 2.py +0 -120
  146. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  147. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  148. package/src/scripts/nexo-reflection 2.py +0 -256
  149. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  150. package/src/scripts/nexo-sleep 2.py +0 -631
  151. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  152. package/src/scripts/nexo-sync-clients 2.py +0 -16
  153. package/src/scripts/nexo-synthesis 2.py +0 -475
  154. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  155. package/src/scripts/nexo-update 2.sh +0 -306
  156. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  157. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  158. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  159. package/src/server 2.py +0 -1296
  160. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  161. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  162. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  163. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  164. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  165. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  166. package/src/skills/run-release-final-audit/script 2.py +0 -259
  167. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  168. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  169. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  170. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  171. package/src/skills_runtime 2.py +0 -932
  172. package/src/state_watchers_runtime 2.py +0 -475
  173. package/src/storage_router 2.py +0 -32
  174. package/src/system_catalog 2.py +0 -786
  175. package/src/tools_coordination 2.py +0 -103
  176. package/src/tools_credentials 2.py +0 -68
  177. package/src/tools_drive 2.py +0 -487
  178. package/src/tools_hot_context 2.py +0 -163
  179. package/src/tools_learnings 2.py +0 -612
  180. package/src/tools_menu 2.py +0 -229
  181. package/src/tools_reminders 2.py +0 -88
  182. package/src/tools_reminders_crud 2.py +0 -363
  183. package/src/tools_sessions 2.py +0 -1054
  184. package/src/tools_system_catalog 2.py +0 -19
  185. package/src/tools_task_history 2.py +0 -57
  186. package/src/tools_transcripts 2.py +0 -98
  187. package/src/transcript_utils 2.py +0 -412
  188. package/src/user_context 2.py +0 -46
  189. package/src/user_data_portability 2.py +0 -328
  190. package/src/user_state_model 2.py +0 -170
  191. package/templates/CLAUDE.md 2.template +0 -108
  192. package/templates/CODEX.AGENTS.md 2.template +0 -66
  193. package/templates/launchagents/README 2.md +0 -132
  194. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  196. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  197. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  199. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  200. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  201. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  202. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  203. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  204. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  205. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  206. package/templates/nexo_helper 2.py +0 -301
  207. package/templates/openclaw 2.json +0 -13
  208. package/templates/plugin-template 2.py +0 -40
  209. package/templates/script-template 2.py +0 -59
  210. package/templates/script-template 2.sh +0 -13
  211. package/templates/skill-script-template 2.py +0 -48
  212. package/templates/skill-template 2.md +0 -33
@@ -1,117 +0,0 @@
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())
@@ -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 (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
@@ -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,401 +0,0 @@
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()