nexo-brain 5.3.20 → 5.3.21
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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- 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()
|