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,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
4
5
 
@@ -116,8 +117,15 @@ def extract_session(jsonl_path: Path) -> dict | None:
116
117
  }
117
118
 
118
119
 
119
- def collect_transcripts(target_date: str) -> list[dict]:
120
- """Collect all sessions modified on the target date."""
120
+ def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]:
121
+ """Collect all sessions modified after `since_iso` (exclusive) up to `until_iso` (inclusive).
122
+
123
+ Uses a watermark approach: deep sleep tracks the last processed timestamp
124
+ so nothing is missed regardless of when sessions happen (day, night, etc.).
125
+ """
126
+ since_dt = datetime.fromisoformat(since_iso)
127
+ until_dt = datetime.fromisoformat(until_iso) if until_iso else datetime.now()
128
+
121
129
  sessions = []
122
130
  for sdir in find_session_dirs():
123
131
  for f in sdir.glob("*.jsonl"):
@@ -125,7 +133,7 @@ def collect_transcripts(target_date: str) -> list[dict]:
125
133
  mtime = datetime.fromtimestamp(f.stat().st_mtime)
126
134
  except OSError:
127
135
  continue
128
- if mtime.strftime("%Y-%m-%d") == target_date:
136
+ if since_dt < mtime <= until_dt:
129
137
  session = extract_session(f)
130
138
  if session:
131
139
  session["modified"] = mtime.isoformat()
@@ -339,25 +347,40 @@ def format_transcripts(sessions: list[dict]) -> str:
339
347
 
340
348
 
341
349
  def main():
342
- target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
350
+ # Watermark-based collection: since_iso and until_iso passed by the wrapper script
351
+ # argv[1] = run_id (date label for output files)
352
+ # argv[2] = since_iso (exclusive lower bound, e.g. "2026-04-01T04:30:00")
353
+ # argv[3] = until_iso (inclusive upper bound, e.g. "2026-04-02T04:30:00") — optional, defaults to now
354
+ run_id = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
355
+ since_iso = sys.argv[2] if len(sys.argv) > 2 else ""
356
+ until_iso = sys.argv[3] if len(sys.argv) > 3 else ""
357
+
343
358
  DEEP_SLEEP_DIR.mkdir(parents=True, exist_ok=True)
344
359
 
345
- print(f"[collect] Phase 1: Collecting context for {target_date}")
360
+ print(f"[collect] Phase 1: Collecting context (run_id={run_id})")
346
361
 
347
- # 1. Transcripts
348
- print("[collect] Gathering transcripts...")
349
- sessions = collect_transcripts(target_date)
362
+ # 1. Transcripts — watermark-based
363
+ if since_iso:
364
+ print(f"[collect] Gathering transcripts since {since_iso}" + (f" until {until_iso}" if until_iso else ""))
365
+ sessions = collect_transcripts_since(since_iso, until_iso)
366
+ else:
367
+ # Fallback: collect everything from last 48h (safe catch-all)
368
+ fallback_since = (datetime.now() - timedelta(hours=48)).isoformat()
369
+ print(f"[collect] No watermark — collecting last 48h since {fallback_since}")
370
+ sessions = collect_transcripts_since(fallback_since)
350
371
  print(f" Found {len(sessions)} sessions")
351
372
 
352
373
  if not sessions:
353
- print(f"[collect] No sessions found for {target_date}. Writing minimal context file.")
354
- output_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
374
+ print(f"[collect] No new sessions found. Writing minimal context file.")
375
+ output_file = DEEP_SLEEP_DIR / f"{run_id}-context.txt"
355
376
  output_file.write_text(
356
- f"Deep Sleep Context for {target_date}\n\nNo sessions found for this date.\n"
377
+ f"Deep Sleep Context for {run_id}\n\nNo sessions found.\n"
357
378
  )
358
379
  print(f"[collect] Output: {output_file}")
359
380
  return
360
381
 
382
+ target_date = run_id # Keep variable name for downstream compat
383
+
361
384
  # 2. Core DB data
362
385
  print("[collect] Querying databases...")
363
386
  followups = collect_followups()
@@ -58,6 +58,22 @@ Detect work that was started but not finished in this session:
58
58
  - Investigations started but conclusions never reached
59
59
  Only flag if the work was NOT captured in a followup or reminder.
60
60
 
61
+ ### 9. Skill Candidates (Reusable Procedures)
62
+ Detect multi-step tasks that were completed successfully and could be reused:
63
+ - Tasks that required 3+ distinct steps to complete
64
+ - Tasks where the agent followed a clear sequence of actions
65
+ - Procedures that are likely to be repeated in the future
66
+ - Examples: deploying code, configuring a service, running an audit, setting up infrastructure
67
+
68
+ For each candidate, extract:
69
+ - The full step-by-step procedure (what was actually done, in order)
70
+ - Tags describing the domain (e.g., "shopify", "chrome", "deploy")
71
+ - Trigger phrases that would indicate this procedure is needed (e.g., "deploy extension", "push theme")
72
+ - Any gotchas or warnings discovered during execution
73
+
74
+ Only flag if the procedure was SUCCESSFUL (the task was completed without major failures).
75
+ Do NOT flag trivial tasks (single-step actions, simple file edits, quick lookups).
76
+
61
77
  ### 8. Productivity Patterns
62
78
  Analyze how the session went in terms of efficiency:
63
79
  - How many times did the agent need correction before getting it right?
@@ -195,6 +211,28 @@ Return ONLY valid JSON. No markdown code fences. No explanation text before or a
195
211
  }
196
212
  ],
197
213
 
214
+ "skill_candidates": [
215
+ {
216
+ "name": "Short name for the procedure (e.g., Deploy Chrome Extension)",
217
+ "description": "What this procedure accomplishes (1-2 sentences)",
218
+ "steps": [
219
+ "Step 1: What was done first",
220
+ "Step 2: What was done next",
221
+ "Step 3: etc."
222
+ ],
223
+ "tags": ["domain1", "domain2"],
224
+ "trigger_phrases": ["phrase that would trigger this", "another trigger"],
225
+ "gotchas": ["Warning or caveat discovered during execution"],
226
+ "evidence": {
227
+ "type": "transcript",
228
+ "session_id": "filename.jsonl",
229
+ "message_index": 10,
230
+ "quote": "Start of the multi-step task"
231
+ },
232
+ "confidence": 0.85
233
+ }
234
+ ],
235
+
198
236
  "productivity_score": {
199
237
  "corrections_needed": 0,
200
238
  "proactivity": "reactive|mixed|proactive",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 2: Extract findings from each session using Claude CLI.
4
5
 
@@ -113,6 +114,14 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
113
114
  try:
114
115
  env = os.environ.copy()
115
116
  env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
117
+ env.pop("CLAUDECODE", None)
118
+ env.pop("CLAUDE_CODE", None)
119
+
120
+ JSON_SYSTEM_PROMPT = (
121
+ "You are a JSON-only analyst. Your ENTIRE response must be a single valid JSON object. "
122
+ "No text before it. No text after it. No markdown fences. No explanations. "
123
+ "If you want to summarize, put it inside the JSON fields. Start with { and end with }."
124
+ )
116
125
 
117
126
  result = subprocess.run(
118
127
  [
@@ -120,8 +129,9 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
120
129
  "-p", prompt,
121
130
  "--model", "opus",
122
131
  "--output-format", "text",
132
+ "--append-system-prompt", JSON_SYSTEM_PROMPT,
123
133
  "--allowedTools",
124
- "Read,Grep,Bash,mcp__nexo__nexo_startup,mcp__nexo__nexo_learning_search,mcp__nexo__nexo_recall"
134
+ "Read,Grep,Bash"
125
135
  ],
126
136
  capture_output=True,
127
137
  text=True,
@@ -139,6 +149,28 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
139
149
  if not line.strip().startswith("Post-mortem") and line.strip()
140
150
  )
141
151
  parsed = extract_json_from_response(output)
152
+
153
+ # Fallback: if Claude returned text instead of JSON, ask a short conversion call
154
+ if not parsed and len(output.strip()) > 50:
155
+ print(f" Got text instead of JSON ({len(output)} chars). Converting...")
156
+ convert_prompt = (
157
+ f"Convert the following analysis into the exact JSON schema required. "
158
+ f"Return ONLY the JSON object, nothing else.\n\n"
159
+ f"Analysis:\n{output[:8000]}\n\n"
160
+ f"Required schema: session_id, findings[], emotional_timeline[], "
161
+ f"abandoned_projects[], skill_candidates[], productivity_score, protocol_summary"
162
+ )
163
+ convert_result = subprocess.run(
164
+ [claude_bin, "-p", convert_prompt, "--model", "sonnet",
165
+ "--output-format", "text",
166
+ "--append-system-prompt", JSON_SYSTEM_PROMPT],
167
+ capture_output=True, text=True, timeout=120, env=env
168
+ )
169
+ if convert_result.returncode == 0:
170
+ parsed = extract_json_from_response(convert_result.stdout)
171
+ if parsed:
172
+ print(f" Conversion succeeded")
173
+
142
174
  if not parsed:
143
175
  # Save raw output for debugging
144
176
  debug_file = DEEP_SLEEP_DIR / f"debug-extract-{session_id[:20]}.txt"
@@ -207,32 +239,70 @@ def main():
207
239
  print(f"[extract] Phase 2: Analyzing {len(session_files)} sessions for {target_date}")
208
240
  print(f"[extract] Claude CLI: {claude_bin}")
209
241
 
242
+ # Checkpoint directory: one JSON per session, survives crashes
243
+ checkpoint_dir = date_dir / "checkpoints"
244
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
245
+
210
246
  all_extractions = []
211
247
  total_findings = 0
248
+ skipped = 0
249
+ MAX_RETRIES = 3
212
250
 
213
251
  for i, session_id in enumerate(session_files):
252
+ sid_safe = session_id.replace(".jsonl", "")[:30]
253
+ checkpoint_file = checkpoint_dir / f"{sid_safe}.json"
254
+
255
+ # Resume: skip already-processed sessions
256
+ if checkpoint_file.exists():
257
+ try:
258
+ with open(checkpoint_file) as f:
259
+ cached = json.load(f)
260
+ findings_count = len(cached.get("findings", []))
261
+ total_findings += findings_count
262
+ all_extractions.append(cached)
263
+ skipped += 1
264
+ print(f"[extract] Session {i + 1}/{len(session_files)}: {session_id} (cached, {findings_count} findings)")
265
+ continue
266
+ except (json.JSONDecodeError, KeyError):
267
+ pass # Corrupted checkpoint, re-process
268
+
214
269
  print(f"[extract] Session {i + 1}/{len(session_files)}: {session_id}")
215
270
 
216
- result = analyze_session(session_id, date_dir, shared_context_file, claude_bin)
271
+ # Retry loop
272
+ result = None
273
+ for attempt in range(1, MAX_RETRIES + 1):
274
+ result = analyze_session(session_id, date_dir, shared_context_file, claude_bin)
275
+ if result:
276
+ break
277
+ if attempt < MAX_RETRIES:
278
+ print(f" -> Attempt {attempt}/{MAX_RETRIES} failed, retrying...")
217
279
 
218
280
  if result:
219
281
  findings_count = len(result.get("findings", []))
220
282
  total_findings += findings_count
221
283
  all_extractions.append(result)
222
- print(f" -> {findings_count} findings extracted")
284
+ # Save checkpoint
285
+ with open(checkpoint_file, "w") as f:
286
+ json.dump(result, f, indent=2, ensure_ascii=False)
287
+ print(f" -> {findings_count} findings extracted (checkpointed)")
223
288
  else:
224
- print(f" -> Extraction failed, continuing with next session")
225
- all_extractions.append({
289
+ print(f" -> Failed after {MAX_RETRIES} attempts, marking as failed")
290
+ failed_entry = {
226
291
  "session_id": session_id,
227
292
  "findings": [],
228
- "error": "Extraction failed"
229
- })
293
+ "error": f"Extraction failed after {MAX_RETRIES} attempts"
294
+ }
295
+ all_extractions.append(failed_entry)
296
+ # Save failed checkpoint too (so we don't retry forever)
297
+ with open(checkpoint_file, "w") as f:
298
+ json.dump(failed_entry, f, indent=2, ensure_ascii=False)
230
299
 
231
300
  # Merge into output
232
301
  output = {
233
302
  "date": target_date,
234
303
  "sessions_analyzed": len(session_files),
235
304
  "sessions_succeeded": len([e for e in all_extractions if "error" not in e]),
305
+ "sessions_cached": skipped,
236
306
  "total_findings": total_findings,
237
307
  "extractions": all_extractions
238
308
  }
@@ -241,7 +311,10 @@ def main():
241
311
  with open(output_file, "w") as f:
242
312
  json.dump(output, f, indent=2, ensure_ascii=False)
243
313
 
244
- print(f"\n[extract] Done. {total_findings} total findings from {len(session_files)} sessions.")
314
+ if skipped:
315
+ print(f"\n[extract] Done. {total_findings} findings from {len(session_files)} sessions ({skipped} cached, {len(session_files) - skipped} new).")
316
+ else:
317
+ print(f"\n[extract] Done. {total_findings} findings from {len(session_files)} sessions.")
245
318
  print(f"[extract] Output: {output_file}")
246
319
 
247
320
 
@@ -73,6 +73,19 @@ Consider ALL of these:
73
73
 
74
74
  The score should feel fair. A day with 2 minor corrections and 10 tasks completed is still a good day (75+). A day with 1 catastrophic error might be a 40 even if everything else was fine.
75
75
 
76
+ ### 9. Skill Extraction
77
+ Consolidate `skill_candidates` from all session extractions into publishable skills:
78
+ - Merge similar procedures from different sessions into a single skill
79
+ - Generalize: replace session-specific IDs, paths, or names with placeholders or descriptions
80
+ - Only include skills with confidence >= 0.7
81
+ - Check if a similar skill already exists (use `nexo_skill_match` if available) — if so, note it for merging instead of creating new
82
+
83
+ For each skill, generate:
84
+ - A unique ID starting with `SK-` (e.g., `SK-DEPLOY-CHROME-EXT`)
85
+ - Name, description, tags, trigger_patterns
86
+ - The full step-by-step procedure as the skill content
87
+ - Source session IDs for traceability
88
+
76
89
  ### 8. Consolidated Actions
77
90
  Merge and deduplicate all findings into a final action list. Each action should have:
78
91
  - `action_type`: `learning_add`, `followup_create`, `morning_briefing_item`
@@ -122,9 +135,24 @@ Return ONLY valid JSON. No markdown code fences. No explanation text.
122
135
  }
123
136
  ],
124
137
 
138
+ "skills": [
139
+ {
140
+ "id": "SK-SHORT-ID",
141
+ "name": "Human readable name",
142
+ "description": "What this procedure does (1-2 sentences)",
143
+ "steps": ["Step 1", "Step 2", "Step 3"],
144
+ "tags": ["tag1", "tag2"],
145
+ "trigger_patterns": ["trigger phrase 1", "trigger phrase 2"],
146
+ "gotchas": ["Warning or caveat"],
147
+ "source_sessions": ["session1.jsonl"],
148
+ "confidence": 0.85,
149
+ "merge_with": null
150
+ }
151
+ ],
152
+
125
153
  "actions": [
126
154
  {
127
- "action_type": "learning_add|followup_create|morning_briefing_item",
155
+ "action_type": "learning_add|followup_create|skill_create|morning_briefing_item",
128
156
  "action_class": "auto_apply|draft_for_morning",
129
157
  "confidence": 0.9,
130
158
  "impact": "low|medium|high",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
4
5
 
@@ -115,6 +116,8 @@ def main():
115
116
  try:
116
117
  env = os.environ.copy()
117
118
  env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
119
+ env.pop("CLAUDECODE", None)
120
+ env.pop("CLAUDE_CODE", None)
118
121
 
119
122
  result = subprocess.run(
120
123
  [
@@ -123,7 +126,7 @@ def main():
123
126
  "--model", "opus",
124
127
  "--output-format", "text",
125
128
  "--allowedTools",
126
- "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__nexo_startup,mcp__nexo__nexo_learning_search,mcp__nexo__nexo_recall,mcp__nexo__nexo_reminders"
129
+ "Read,Grep,Bash"
127
130
  ],
128
131
  capture_output=True,
129
132
  text=True,
@@ -1,20 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- NEXO Catch-Up — Runs at Mac boot to execute any missed scheduled tasks.
3
+ NEXO Catch-Up — Runs at boot/wake to recover any missed scheduled tasks.
4
4
 
5
- When the Mac was asleep/off during scheduled times, launchd does NOT retry
6
- missed StartCalendarInterval jobs. This script detects what was missed and
7
- runs them in the correct order.
5
+ Tasks are loaded dynamically from crons/manifest.json (single source of truth).
6
+ Only scheduled crons (with hour/minute) are recovered interval-based crons
7
+ (immune, watchdog, auto-close) restart automatically via launchd/systemd.
8
8
 
9
- Scheduled tasks (ordered by intended run time):
10
- 03:00 cognitive-decay (Ebbinghaus decay + STM→LTM promotion)
11
- 03:00 evolution (weekly, Sundays only)
12
- 04:00 — sleep (session cleanup)
13
- 07:00 — self-audit (health checks + weekly cognitive GC on Sundays)
14
- 23:30 — postmortem (consolidation + sensory register)
15
-
16
- Logic: For each task, check if its last successful run was before the
17
- most recent scheduled time. If so, run it now.
9
+ Logic: For each scheduled task, check if its last successful run was before
10
+ the most recent scheduled time. If so, run it now. Only marks success on exit 0.
11
+ Uses cron/launchd weekday convention (0=Sunday) converted to Python (0=Monday).
18
12
  """
19
13
 
20
14
  import json
@@ -52,6 +46,49 @@ def _resolve_python() -> str:
52
46
  return sys.executable
53
47
 
54
48
  NEXO_PYTHON = _resolve_python()
49
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
50
+ MANIFEST = NEXO_CODE / "crons" / "manifest.json"
51
+
52
+
53
+ def _load_tasks_from_manifest() -> list[tuple]:
54
+ """Read scheduled tasks from manifest.json — single source of truth.
55
+
56
+ Only includes crons with a schedule (hour/minute). Excludes interval-based
57
+ crons (immune, watchdog, auto-close) and run_at_load (catchup itself).
58
+ Returns: list of (name, hour, minute, python_or_bash, script, weekday)
59
+ """
60
+ if not MANIFEST.exists():
61
+ log(f"WARNING: manifest not found at {MANIFEST}, using empty task list")
62
+ return []
63
+
64
+ with open(MANIFEST) as f:
65
+ data = json.load(f)
66
+
67
+ tasks = []
68
+ for cron in data.get("crons", []):
69
+ schedule = cron.get("schedule")
70
+ if not schedule or "hour" not in schedule:
71
+ continue # Skip interval-based and run_at_load crons
72
+ if cron["id"] == "catchup":
73
+ continue # Don't catch up ourselves
74
+
75
+ script = cron["script"]
76
+ script_type = cron.get("type", "python")
77
+ interpreter = NEXO_PYTHON if script_type == "python" else "/bin/bash"
78
+ weekday = schedule.get("weekday")
79
+
80
+ tasks.append((
81
+ cron["id"],
82
+ schedule["hour"],
83
+ schedule["minute"],
84
+ interpreter,
85
+ Path(script).name,
86
+ weekday,
87
+ ))
88
+
89
+ # Sort by hour, minute for correct execution order
90
+ tasks.sort(key=lambda t: (t[1], t[2]))
91
+ return tasks
55
92
 
56
93
 
57
94
  def log(msg: str):
@@ -83,7 +120,11 @@ def last_scheduled_time(hour: int, minute: int, weekday: int = None) -> datetime
83
120
 
84
121
  if weekday is not None:
85
122
  # Weekly task — find the most recent matching weekday
86
- days_since = (now.weekday() - weekday) % 7
123
+ # Manifest uses cron/launchd convention: 0=Sunday, 6=Saturday
124
+ # Python datetime.weekday() uses: 0=Monday, 6=Sunday
125
+ # Convert: manifest 0 (Sun) -> python 6, manifest 1 (Mon) -> python 0, etc.
126
+ py_weekday = (weekday - 1) % 7
127
+ days_since = (now.weekday() - py_weekday) % 7
87
128
  target = now - timedelta(days=days_since)
88
129
  target = target.replace(hour=hour, minute=minute, second=0, microsecond=0)
89
130
  if target > now:
@@ -130,13 +171,14 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
130
171
  )
131
172
  if result.returncode == 0:
132
173
  log(f" OK {name} (exit 0)")
174
+ state[name] = datetime.now().isoformat()
175
+ save_state(state)
176
+ return True
133
177
  else:
134
- log(f" WARN {name} (exit {result.returncode})")
178
+ log(f" FAIL {name} (exit {result.returncode})")
135
179
  if result.stderr:
136
180
  log(f" stderr: {result.stderr[:300]}")
137
- state[name] = datetime.now().isoformat()
138
- save_state(state)
139
- return True
181
+ return False
140
182
  except subprocess.TimeoutExpired:
141
183
  log(f" TIMEOUT {name} (300s)")
142
184
  return False
@@ -149,17 +191,8 @@ def main():
149
191
  log("=== NEXO Catch-Up starting (boot/wake) ===")
150
192
  state = load_state()
151
193
 
152
- # Define tasks in execution order (matching their intended schedule order)
153
- # Note: auto-update is handled by the MCP server on startup, not by catchup.
154
- tasks = [
155
- # (name, hour, minute, python, script, weekday)
156
- ("cognitive-decay", 3, 0, NEXO_PYTHON, "nexo-cognitive-decay.py", None),
157
- ("evolution", 3, 0, NEXO_PYTHON, "nexo-evolution-run.py", 6), # Sunday = 6
158
- ("sleep", 4, 0, NEXO_PYTHON, "nexo-sleep.py", None),
159
- ("self-audit", 7, 0, NEXO_PYTHON, "nexo-daily-self-audit.py", None),
160
- ("github-monitor", 8, 0, NEXO_PYTHON, "nexo-github-monitor.py", None),
161
- ("postmortem", 23, 30, NEXO_PYTHON, "nexo-postmortem-consolidator.py", None),
162
- ]
194
+ # Read tasks from manifest single source of truth
195
+ tasks = _load_tasks_from_manifest()
163
196
 
164
197
  ran = 0
165
198
  skipped = 0
@@ -187,6 +220,9 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
187
220
  if not CLAUDE_CLI.exists():
188
221
  log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
189
222
  return
223
+ auth_check = subprocess.run(
224
+ [str(CLAUDE_CLI), "-p", "reply OK", "--output-format", "text"],
225
+ capture_output=True, text=True, timeout=30
190
226
  )
191
227
  if auth_check.returncode != 0:
192
228
  # CLI not authenticated, skip gracefully
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # NEXO Cron Wrapper — Records execution in cron_runs table.
3
+ # Usage: nexo-cron-wrapper.sh <cron_id> <command...>
4
+ # Example: nexo-cron-wrapper.sh deep-sleep bash nexo-deep-sleep.sh
5
+ #
6
+ # Wraps any cron command to automatically record start/end/exit_code/summary.
7
+ # Used by sync.py when generating LaunchAgents from manifest.json.
8
+
9
+ set -uo pipefail
10
+
11
+ CRON_ID="${1:?Usage: nexo-cron-wrapper.sh <cron_id> <command...>}"
12
+ shift
13
+
14
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
15
+ DB="$NEXO_HOME/data/nexo.db"
16
+
17
+ # Record start
18
+ RUN_ID=$(sqlite3 "$DB" "INSERT INTO cron_runs (cron_id) VALUES ('$CRON_ID'); SELECT last_insert_rowid();" 2>/dev/null)
19
+
20
+ if [ -z "$RUN_ID" ]; then
21
+ # DB not ready — run without tracking
22
+ exec "$@"
23
+ fi
24
+
25
+ # Run the actual command, capture output
26
+ OUTPUT_FILE=$(mktemp)
27
+ "$@" > "$OUTPUT_FILE" 2>&1
28
+ EXIT_CODE=$?
29
+
30
+ # Extract summary (last meaningful line, max 500 chars)
31
+ SUMMARY=$(tail -5 "$OUTPUT_FILE" | grep -v "^$" | tail -1 | head -c 500 | sed "s/'/''/g")
32
+
33
+ # Extract error if failed
34
+ ERROR=""
35
+ if [ $EXIT_CODE -ne 0 ]; then
36
+ ERROR=$(grep -i "error\|exception\|fail\|traceback" "$OUTPUT_FILE" | tail -1 | head -c 500 | sed "s/'/''/g")
37
+ fi
38
+
39
+ # Record end
40
+ sqlite3 "$DB" "
41
+ UPDATE cron_runs SET
42
+ ended_at = datetime('now'),
43
+ exit_code = $EXIT_CODE,
44
+ summary = '$SUMMARY',
45
+ error = '$ERROR',
46
+ duration_secs = ROUND((julianday(datetime('now')) - julianday(started_at)) * 86400, 1)
47
+ WHERE id = $RUN_ID;
48
+ " 2>/dev/null
49
+
50
+ # Clean output
51
+ rm -f "$OUTPUT_FILE"
52
+
53
+ exit $EXIT_CODE
@@ -532,8 +532,10 @@ def main():
532
532
  "counts": {"error": errors, "warn": warns, "info": infos}
533
533
  }, indent=2))
534
534
 
535
- # Stage B: CLI interpretation
536
- interpret_findings(findings)
535
+ # Stage B: CLI interpretation (graceful fallback if CLI unavailable)
536
+ cli_ok = interpret_findings(findings)
537
+ if not cli_ok:
538
+ log("Stage B: CLI unavailable or failed. Stage A results saved to self-audit-summary.json.")
537
539
 
538
540
  # Register for catch-up
539
541
  try: