nexo-brain 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/package.json +6 -3
- package/src/auto_update.py +26 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +150 -6
- package/src/db/__init__.py +13 -0
- package/src/db/_core.py +1 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +41 -6
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +515 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugin_loader.py +1 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update.py +1 -0
- package/src/scripts/deep-sleep/apply_findings.py +111 -8
- package/src/scripts/deep-sleep/collect.py +34 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +81 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
- package/src/scripts/deep-sleep/synthesize.py +4 -1
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-learning-housekeep.py +157 -1
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-watchdog.sh +91 -30
- package/src/server.py +6 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-github-monitor.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# NEXO Deep Sleep —
|
|
2
|
+
# NEXO Deep Sleep — Overnight session analysis with watermark tracking
|
|
3
3
|
# Runs at 4:30 AM via LaunchAgent
|
|
4
|
-
# Reads ALL session transcripts from the day, analyzes with Claude CLI,
|
|
5
|
-
# and applies findings (learnings, feedbacks, followups, trust adjustments)
|
|
6
4
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# Watermark approach: tracks the last processed timestamp so nothing is missed.
|
|
6
|
+
# Sessions from late-night/early-morning work are included in the next run.
|
|
7
|
+
#
|
|
8
|
+
# Logs to $NEXO_HOME/logs/deep-sleep.log
|
|
11
9
|
|
|
12
10
|
set -euo pipefail
|
|
13
11
|
|
|
@@ -15,83 +13,74 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
15
13
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
16
14
|
LOG_DIR="$NEXO_HOME/logs"
|
|
17
15
|
DEEP_SLEEP_DIR="$NEXO_HOME/operations/deep-sleep"
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
WATERMARK_FILE="$DEEP_SLEEP_DIR/.watermark"
|
|
17
|
+
RUN_ID=$(date +%Y-%m-%d)
|
|
20
18
|
|
|
21
19
|
mkdir -p "$LOG_DIR" "$DEEP_SLEEP_DIR"
|
|
22
20
|
|
|
23
21
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/deep-sleep.log"; }
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
log "No context file generated for $DATE. Skipping."
|
|
35
|
-
return 0
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# Check meta for session count
|
|
39
|
-
SESSIONS=0
|
|
40
|
-
if [ -f "$DEEP_SLEEP_DIR/$DATE-meta.json" ]; then
|
|
41
|
-
SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-meta.json'))['sessions_found'])")
|
|
42
|
-
elif [ -f "$DEEP_SLEEP_DIR/$DATE-index.json" ]; then
|
|
43
|
-
SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-index.json'))['sessions_found'])")
|
|
44
|
-
fi
|
|
45
|
-
if [ "$SESSIONS" -eq 0 ]; then
|
|
46
|
-
log "No sessions found for $DATE. Skipping."
|
|
47
|
-
return 0
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Phase 2: Extract findings per session (Claude Opus)
|
|
51
|
-
log "Phase 2: Extracting findings from $SESSIONS sessions..."
|
|
52
|
-
python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
53
|
-
|
|
54
|
-
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-extractions.json" ]; then
|
|
55
|
-
log "Extraction failed for $DATE. No output."
|
|
56
|
-
return 1
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# Phase 3: Cross-session synthesis (Claude Opus, one call)
|
|
60
|
-
log "Phase 3: Synthesizing cross-session findings..."
|
|
61
|
-
python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
62
|
-
|
|
63
|
-
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-synthesis.json" ]; then
|
|
64
|
-
log "Synthesis failed for $DATE. Falling back to extractions only."
|
|
65
|
-
# Fall back: apply extractions directly
|
|
66
|
-
cp "$DEEP_SLEEP_DIR/$DATE-extractions.json" "$DEEP_SLEEP_DIR/$DATE-synthesis.json"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# Phase 4: Apply findings
|
|
70
|
-
log "Phase 4: Applying findings..."
|
|
71
|
-
python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
72
|
-
|
|
73
|
-
log "=== Deep Sleep v2 complete for $DATE ==="
|
|
74
|
-
return 0
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
# --- Catch-up: check if the day before yesterday was missed ---
|
|
78
|
-
YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d 2>/dev/null)
|
|
79
|
-
DAY_BEFORE=$(date -v-2d +%Y-%m-%d 2>/dev/null || date -d "2 days ago" +%Y-%m-%d 2>/dev/null)
|
|
80
|
-
LAST_RUN=""
|
|
81
|
-
if [ -f "$LAST_RUN_FILE" ]; then
|
|
82
|
-
LAST_RUN=$(cat "$LAST_RUN_FILE")
|
|
23
|
+
# Read watermark (last processed timestamp)
|
|
24
|
+
SINCE=""
|
|
25
|
+
if [ -f "$WATERMARK_FILE" ]; then
|
|
26
|
+
SINCE=$(cat "$WATERMARK_FILE")
|
|
27
|
+
log "Watermark: processing sessions since $SINCE"
|
|
28
|
+
else
|
|
29
|
+
# First run ever: process last 48h
|
|
30
|
+
SINCE=$(date -v-2d '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -d "2 days ago" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null)
|
|
31
|
+
log "No watermark found. First run, collecting since $SINCE"
|
|
83
32
|
fi
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
34
|
+
UNTIL=$(date '+%Y-%m-%dT%H:%M:%S')
|
|
35
|
+
|
|
36
|
+
log "=== Deep Sleep v2 starting (run_id=$RUN_ID) ==="
|
|
37
|
+
|
|
38
|
+
# Phase 1: Collect all context (Python, no LLM)
|
|
39
|
+
log "Phase 1: Collecting context since $SINCE until $UNTIL..."
|
|
40
|
+
python3 "$SCRIPT_DIR/deep-sleep/collect.py" "$RUN_ID" "$SINCE" "$UNTIL" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
41
|
+
|
|
42
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-context.txt" ]; then
|
|
43
|
+
log "No context file generated. Skipping."
|
|
44
|
+
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
45
|
+
log "Watermark updated to $UNTIL (no sessions to process)"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check meta for session count
|
|
50
|
+
SESSIONS=0
|
|
51
|
+
if [ -f "$DEEP_SLEEP_DIR/$RUN_ID-meta.json" ]; then
|
|
52
|
+
SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$RUN_ID-meta.json'))['sessions_found'])")
|
|
53
|
+
fi
|
|
54
|
+
if [ "$SESSIONS" -eq 0 ]; then
|
|
55
|
+
log "No sessions found. Skipping."
|
|
56
|
+
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
57
|
+
log "Watermark updated to $UNTIL (no sessions)"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Phase 2: Extract findings per session (Claude Opus)
|
|
62
|
+
log "Phase 2: Extracting findings from $SESSIONS sessions..."
|
|
63
|
+
python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
64
|
+
|
|
65
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-extractions.json" ]; then
|
|
66
|
+
log "Extraction failed. Watermark NOT updated (will retry next run)."
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Phase 3: Cross-session synthesis (Claude Opus, one call)
|
|
71
|
+
log "Phase 3: Synthesizing cross-session findings..."
|
|
72
|
+
python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
73
|
+
|
|
74
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-synthesis.json" ]; then
|
|
75
|
+
log "Synthesis failed. Falling back to extractions only."
|
|
76
|
+
cp "$DEEP_SLEEP_DIR/$RUN_ID-extractions.json" "$DEEP_SLEEP_DIR/$RUN_ID-synthesis.json"
|
|
91
77
|
fi
|
|
92
78
|
|
|
93
|
-
#
|
|
94
|
-
|
|
79
|
+
# Phase 4: Apply findings
|
|
80
|
+
log "Phase 4: Applying findings..."
|
|
81
|
+
python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
95
82
|
|
|
96
|
-
#
|
|
97
|
-
echo "$
|
|
83
|
+
# Update watermark on success
|
|
84
|
+
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
85
|
+
log "Watermark updated to $UNTIL"
|
|
86
|
+
log "=== Deep Sleep v2 complete (run_id=$RUN_ID) ==="
|
|
@@ -104,6 +104,19 @@ CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def verify_claude_cli() -> bool:
|
|
107
|
+
"""Check Claude CLI is available and authenticated."""
|
|
108
|
+
if not CLAUDE_CLI.exists():
|
|
109
|
+
return False
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
[str(CLAUDE_CLI), "-p", "reply OK", "--output-format", "text"],
|
|
113
|
+
capture_output=True, text=True, timeout=30
|
|
114
|
+
)
|
|
115
|
+
return result.returncode == 0
|
|
116
|
+
except Exception:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
107
120
|
def call_claude_cli(prompt: str) -> str:
|
|
108
121
|
"""Call claude -p prompt --model opus via subprocess. Returns stdout text."""
|
|
109
122
|
env = os.environ.copy()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
"""NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
|
|
3
4
|
|
|
4
5
|
Runs daily. Adjusts learning weights based on usage (guard_hits),
|
|
@@ -31,6 +32,7 @@ MIN_WEIGHT = 0.05
|
|
|
31
32
|
MAX_WEIGHT = 1.0
|
|
32
33
|
DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
|
|
33
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
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def get_db():
|
|
@@ -195,6 +197,157 @@ def archive_stale(conn):
|
|
|
195
197
|
return len(stale)
|
|
196
198
|
|
|
197
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
|
+
|
|
198
351
|
def print_summary(conn):
|
|
199
352
|
"""Print summary stats."""
|
|
200
353
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -233,7 +386,10 @@ def main():
|
|
|
233
386
|
# 4. Archive stale learnings
|
|
234
387
|
archive_stale(conn)
|
|
235
388
|
|
|
236
|
-
# 5.
|
|
389
|
+
# 5. Process overdue reviews (review_due_at < now)
|
|
390
|
+
process_overdue_reviews(conn)
|
|
391
|
+
|
|
392
|
+
# 6. Summary
|
|
237
393
|
print_summary(conn)
|
|
238
394
|
|
|
239
395
|
conn.close()
|
|
@@ -111,6 +111,25 @@ Rules:
|
|
|
111
111
|
|
|
112
112
|
# Try CLI first, fall back to mechanical similarity
|
|
113
113
|
if CLAUDE_CLI.exists():
|
|
114
|
+
try:
|
|
115
|
+
env = os.environ.copy()
|
|
116
|
+
env["NEXO_HEADLESS"] = "1"
|
|
117
|
+
env.pop("CLAUDECODE", None)
|
|
118
|
+
env.pop("CLAUDE_CODE", None)
|
|
119
|
+
learnings_text = "\n".join(
|
|
120
|
+
f"[#{l.get('id','')}] {l.get('title','')}: {l.get('content','')[:200]}"
|
|
121
|
+
for l in learnings[:20]
|
|
122
|
+
)
|
|
123
|
+
prompt = f"{VALIDATE_PROMPT}\n\nFinding:\n{finding}\n\nExisting learnings:\n{learnings_text}"
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "sonnet", "--output-format", "text"],
|
|
126
|
+
capture_output=True, text=True, timeout=60, env=env
|
|
127
|
+
)
|
|
128
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
129
|
+
parsed = json.loads(result.stdout.strip())
|
|
130
|
+
return parsed
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
114
133
|
# Fallback: mechanical SequenceMatcher (original logic)
|
|
115
134
|
return _mechanical_validate(finding, learnings)
|
|
116
135
|
|
|
@@ -387,10 +387,11 @@ def main():
|
|
|
387
387
|
if not data["diaries"]:
|
|
388
388
|
log("No session diaries today. Nothing to consolidate.")
|
|
389
389
|
else:
|
|
390
|
-
# Stage 2: CLI intelligence
|
|
390
|
+
# Stage 2: CLI intelligence (graceful fallback: Stage 3 still runs)
|
|
391
391
|
success = consolidate_with_cli(data)
|
|
392
392
|
if not success:
|
|
393
|
-
log("Stage 2 failed
|
|
393
|
+
log("Stage 2 failed (CLI unavailable or error). "
|
|
394
|
+
"Skipping intelligent consolidation. Stage 3 (sensory + force) will still run.")
|
|
394
395
|
|
|
395
396
|
# Stage 3: Sensory Register (mechanical, kept from v1)
|
|
396
397
|
try:
|
|
@@ -550,18 +550,23 @@ def main():
|
|
|
550
550
|
log(f"Brain state: {len(state['learnings'])} learnings, "
|
|
551
551
|
f"{state['memory_md_lines']} MEMORY lines, "
|
|
552
552
|
f"{state['claude_mem_old']} old observations")
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
553
|
+
dream_result = dream(state)
|
|
554
|
+
run_log["stage_b"] = dream_result
|
|
555
|
+
|
|
556
|
+
if "error" in dream_result:
|
|
557
|
+
log(f"Stage B: Dreaming failed ({dream_result['error']}). "
|
|
558
|
+
"Stage A cleanup completed successfully. Marking done to avoid retry loop.")
|
|
559
|
+
else:
|
|
560
|
+
# Stage B2: Execute actions from CLI output
|
|
561
|
+
actions_file = COORD_DIR / "sleep-actions.json"
|
|
562
|
+
if actions_file.exists():
|
|
563
|
+
try:
|
|
564
|
+
actions = json.loads(actions_file.read_text())
|
|
565
|
+
execute_dream_actions(actions, state)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
log(f"Stage B2: Error executing actions: {e}")
|
|
563
568
|
else:
|
|
564
|
-
log("Brain is clean
|
|
569
|
+
log("Brain is clean -- no dreaming needed.")
|
|
565
570
|
run_log["stage_b"] = {"skipped": True}
|
|
566
571
|
|
|
567
572
|
# Done
|
|
@@ -6,7 +6,7 @@ Before: ~400 lines of Python concatenating SQL results into markdown sections.
|
|
|
6
6
|
Now: Collects raw data, passes to Claude CLI (sonnet) which synthesizes
|
|
7
7
|
with real understanding of what matters for tomorrow.
|
|
8
8
|
|
|
9
|
-
Runs
|
|
9
|
+
Runs daily at 06:00 via LaunchAgent.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import fcntl
|
|
@@ -213,6 +213,47 @@ Execute without asking."""
|
|
|
213
213
|
return False
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def fallback_synthesis(data: dict):
|
|
217
|
+
"""Write a basic synthesis from raw data when CLI is unavailable."""
|
|
218
|
+
log("Fallback: writing basic synthesis from raw data...")
|
|
219
|
+
lines = [f"# NEXO Daily Synthesis -- {TODAY_STR}", "",
|
|
220
|
+
"*(Generated by fallback -- CLI was unavailable)*", ""]
|
|
221
|
+
|
|
222
|
+
if data.get("learnings"):
|
|
223
|
+
lines.append("## Errors & Learnings")
|
|
224
|
+
for l in data["learnings"][:10]:
|
|
225
|
+
lines.append(f"- [{l.get('category', 'general')}] {l.get('title', 'untitled')}")
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
if data.get("decisions"):
|
|
229
|
+
lines.append("## Decisions Made")
|
|
230
|
+
for d in data["decisions"][:10]:
|
|
231
|
+
lines.append(f"- [{d.get('domain', 'general')}] {d.get('decision', '')[:120]}")
|
|
232
|
+
lines.append("")
|
|
233
|
+
|
|
234
|
+
if data.get("changes"):
|
|
235
|
+
lines.append("## Changes Deployed")
|
|
236
|
+
for c in data["changes"][:10]:
|
|
237
|
+
lines.append(f"- {c.get('what_changed', '')[:120]}")
|
|
238
|
+
lines.append("")
|
|
239
|
+
|
|
240
|
+
if data.get("overdue_reminders"):
|
|
241
|
+
lines.append("## Overdue Reminders")
|
|
242
|
+
for r in data["overdue_reminders"][:10]:
|
|
243
|
+
lines.append(f"- #{r.get('id', '?')} {r.get('title', '')} (due {r.get('due_date', '?')})")
|
|
244
|
+
lines.append("")
|
|
245
|
+
|
|
246
|
+
if data.get("pending_followups"):
|
|
247
|
+
lines.append("## Pending Followups")
|
|
248
|
+
for f in data["pending_followups"][:10]:
|
|
249
|
+
lines.append(f"- #{f.get('id', '?')} {f.get('title', '')} (due {f.get('due_date', '?')})")
|
|
250
|
+
lines.append("")
|
|
251
|
+
|
|
252
|
+
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
OUTPUT_FILE.write_text("\n".join(lines))
|
|
254
|
+
log(f"Fallback synthesis written to {OUTPUT_FILE}")
|
|
255
|
+
|
|
256
|
+
|
|
216
257
|
def main():
|
|
217
258
|
if not should_run():
|
|
218
259
|
log(f"Already ran today ({TODAY_STR}). Skipping.")
|
|
@@ -220,7 +261,7 @@ def main():
|
|
|
220
261
|
|
|
221
262
|
lock_fd = acquire_lock()
|
|
222
263
|
try:
|
|
223
|
-
log(f"=== NEXO Synthesis v2
|
|
264
|
+
log(f"=== NEXO Synthesis v2 -- {TODAY_STR} ===")
|
|
224
265
|
|
|
225
266
|
data = collect_data()
|
|
226
267
|
log(f"Collected: {len(data.get('learnings', []))} learnings, "
|
|
@@ -234,7 +275,9 @@ def main():
|
|
|
234
275
|
mark_done()
|
|
235
276
|
log("Synthesis v2 complete.")
|
|
236
277
|
else:
|
|
237
|
-
log("Synthesis failed
|
|
278
|
+
log("Synthesis CLI failed -- writing fallback synthesis.")
|
|
279
|
+
fallback_synthesis(data)
|
|
280
|
+
mark_done()
|
|
238
281
|
|
|
239
282
|
# Register for catch-up
|
|
240
283
|
try:
|