nexo-brain 5.3.26 → 5.3.27

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