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.
Files changed (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
@@ -1,13 +1,11 @@
1
1
  #!/bin/bash
2
- # NEXO Deep Sleep — Complete overnight session analysis
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
- # Features:
8
- # - Catch-up: if yesterday was missed (Mac off/asleep), runs it first
9
- # - Logs to $NEXO_HOME/logs/deep-sleep.log
10
- # - Marks completion in .last-run for watchdog monitoring
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
- LAST_RUN_FILE="$DEEP_SLEEP_DIR/.last-run"
19
- TODAY=$(date +%Y-%m-%d)
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
- run_analysis() {
26
- local DATE="$1"
27
- log "=== Deep Sleep v2 starting for $DATE ==="
28
-
29
- # Phase 1: Collect all context (Python, no LLM)
30
- log "Phase 1: Collecting context for $DATE..."
31
- python3 "$SCRIPT_DIR/deep-sleep/collect.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
32
-
33
- if [ ! -f "$DEEP_SLEEP_DIR/$DATE-context.txt" ]; then
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
- if [ -n "$DAY_BEFORE" ] && [ "$LAST_RUN" != "$DAY_BEFORE" ] && [ "$LAST_RUN" != "$YESTERDAY" ]; then
86
- # Day before yesterday wasn't analyzed — catch up
87
- if [ ! -f "$DEEP_SLEEP_DIR/$DAY_BEFORE-analysis.json" ]; then
88
- log "*** CATCH-UP: $DAY_BEFORE was missed. Running now. ***"
89
- run_analysis "$DAY_BEFORE" || log "Catch-up for $DAY_BEFORE failed."
90
- fi
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
- # --- Run yesterday's analysis (main task — at 4:30 AM, today has no sessions yet) ---
94
- run_analysis "$YESTERDAY"
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
- # Mark completion with yesterday's date (what we actually analyzed)
97
- echo "$YESTERDAY" > "$LAST_RUN_FILE"
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. Summary
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 falling back to skip (no v1 fallback)")
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
- run_log["stage_b"] = dream(state)
554
-
555
- # Stage B2: Execute actions from CLI output
556
- actions_file = COORD_DIR / "sleep-actions.json"
557
- if actions_file.exists():
558
- try:
559
- actions = json.loads(actions_file.read_text())
560
- execute_dream_actions(actions, state)
561
- except Exception as e:
562
- log(f"Stage B2: Error executing actions: {e}")
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 no dreaming needed.")
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 every 2 hours via LaunchAgent. Executes ONCE per day (internal gate).
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 {TODAY_STR} ===")
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 will retry next trigger.")
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: