nexo-brain 2.0.0 → 2.1.0

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 (238) hide show
  1. package/README.md +140 -41
  2. package/package.json +15 -3
  3. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  4. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  5. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  6. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  7. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  8. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  9. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  10. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  11. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  12. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  17. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  18. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  19. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  21. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  22. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  23. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  38. package/src/crons/manifest.json +106 -0
  39. package/src/crons/sync.py +217 -0
  40. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  41. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  42. package/src/dashboard/app.py +16 -2
  43. package/src/dashboard/templates/dashboard.html +3 -2
  44. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  45. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  48. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  49. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  50. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  51. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  52. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  53. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  54. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  55. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  56. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  80. package/src/db/_episodic.py +1 -1
  81. package/src/db/_reminders.py +9 -5
  82. package/src/hooks/session-stop.sh +2 -1
  83. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  85. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  86. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  87. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  88. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  89. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  90. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  91. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  92. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  93. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  94. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  95. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  96. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  97. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  98. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  99. package/src/plugins/core_rules.py +34 -17
  100. package/src/plugins/update.py +18 -0
  101. package/src/scripts/check-context.py +4 -7
  102. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  103. package/src/scripts/deep-sleep/apply_findings.py +512 -167
  104. package/src/scripts/deep-sleep/collect.py +480 -0
  105. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  106. package/src/scripts/deep-sleep/extract.py +249 -0
  107. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  108. package/src/scripts/deep-sleep/synthesize.py +191 -0
  109. package/src/scripts/nexo-catchup.py +5 -8
  110. package/src/scripts/nexo-daily-self-audit.py +28 -19
  111. package/src/scripts/nexo-deep-sleep.sh +31 -16
  112. package/src/scripts/nexo-evolution-run.py +5 -20
  113. package/src/scripts/nexo-followup-hygiene.py +4 -2
  114. package/src/scripts/nexo-github-monitor.py +6 -9
  115. package/src/scripts/nexo-immune.py +4 -17
  116. package/src/scripts/nexo-learning-validator.py +0 -29
  117. package/src/scripts/nexo-postmortem-consolidator.py +9 -20
  118. package/src/scripts/nexo-proactive-dashboard.py +1 -0
  119. package/src/scripts/nexo-sleep.py +8 -18
  120. package/src/scripts/nexo-synthesis.py +8 -19
  121. package/src/tools_menu.py +1 -1
  122. package/src/tools_sessions.py +67 -0
  123. package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
  124. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  125. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  126. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  127. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  128. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  129. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  130. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  131. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  132. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  133. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  134. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  135. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  136. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  137. package/src/__pycache__/server.cpython-310.pyc +0 -0
  138. package/src/__pycache__/server.cpython-314.pyc +0 -0
  139. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  140. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  141. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  142. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  143. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  144. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  145. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  146. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  147. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  148. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  149. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  151. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  152. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  155. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  156. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  158. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  159. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  160. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  161. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  162. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  163. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  164. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  165. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  166. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  167. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  169. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  170. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  171. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  172. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  173. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  174. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  175. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  176. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  177. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  178. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  179. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  180. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  181. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  182. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  183. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  184. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  185. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  186. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  187. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  188. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  189. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  190. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  191. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  192. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  193. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  194. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  195. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  196. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  197. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  198. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  199. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  200. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  201. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  202. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  203. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  204. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  205. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  206. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  207. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  208. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  216. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  217. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  218. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  219. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  220. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  221. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  222. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  223. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  224. package/src/scripts/deep-sleep/prompt.md +0 -109
  225. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  226. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  228. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  229. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  230. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  231. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  232. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  233. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  234. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  235. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  236. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  237. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  238. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
4
+
5
+ Gathers transcripts, DB data, logs, and discovered files into a single
6
+ plain-text context file that subsequent phases read via Claude's Read tool.
7
+
8
+ Environment variables:
9
+ NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
10
+ NEXO_CODE -- path to the NEXO source repo (optional, for self-analysis)
11
+ """
12
+ import json
13
+ import os
14
+ import sqlite3
15
+ import sys
16
+ from datetime import datetime, timedelta
17
+ from pathlib import Path
18
+
19
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
20
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", ""))
21
+ DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
22
+ NEXO_DB = NEXO_HOME / "data" / "nexo.db"
23
+ COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
24
+
25
+ MIN_USER_MESSAGES = 3 # Skip trivial sessions
26
+
27
+ # ── Transcript collection (kept from collect_transcripts.py) ──────────────
28
+
29
+
30
+ def find_session_dirs() -> list[Path]:
31
+ """Find all Claude Code project directories that contain .jsonl files."""
32
+ claude_dir = Path.home() / ".claude" / "projects"
33
+ if not claude_dir.exists():
34
+ return []
35
+ dirs = set()
36
+ for jsonl in claude_dir.rglob("*.jsonl"):
37
+ dirs.add(jsonl.parent)
38
+ return list(dirs)
39
+
40
+
41
+ def extract_session(jsonl_path: Path) -> dict | None:
42
+ """Extract clean transcript from a session JSONL file."""
43
+ messages = []
44
+ tool_uses = []
45
+ user_msg_count = 0
46
+
47
+ try:
48
+ with open(jsonl_path, "r") as f:
49
+ for line_no, line in enumerate(f, 1):
50
+ line = line.strip()
51
+ if not line:
52
+ continue
53
+ try:
54
+ d = json.loads(line)
55
+ except json.JSONDecodeError:
56
+ continue
57
+
58
+ msg_type = d.get("type")
59
+
60
+ # User messages
61
+ if msg_type == "user":
62
+ content = d.get("message", {}).get("content", "")
63
+ if isinstance(content, str) and content.strip():
64
+ if content.startswith("<system-reminder>"):
65
+ continue
66
+ messages.append({
67
+ "role": "user",
68
+ "index": line_no,
69
+ "text": content[:5000],
70
+ "uuid": d.get("uuid", "")
71
+ })
72
+ user_msg_count += 1
73
+
74
+ # Assistant messages
75
+ elif msg_type in ("message", "assistant"):
76
+ msg = d.get("message", {})
77
+ content_blocks = msg.get("content", [])
78
+ text_parts = []
79
+ for block in content_blocks:
80
+ if isinstance(block, dict):
81
+ if block.get("type") == "text":
82
+ text_parts.append(block.get("text", ""))
83
+ elif block.get("type") == "tool_use":
84
+ tool_input = block.get("input", {})
85
+ tool_uses.append({
86
+ "tool": block.get("name", ""),
87
+ "input_keys": list(tool_input.keys()) if isinstance(tool_input, dict) else [],
88
+ "file": (
89
+ tool_input.get("file_path", "")
90
+ or str(tool_input.get("command", ""))[:100]
91
+ ) if isinstance(tool_input, dict) else ""
92
+ })
93
+ if text_parts:
94
+ combined = "\n".join(text_parts)[:5000]
95
+ messages.append({
96
+ "role": "assistant",
97
+ "index": line_no,
98
+ "text": combined
99
+ })
100
+
101
+ except Exception as e:
102
+ print(f" [collect] Error reading {jsonl_path}: {e}", file=sys.stderr)
103
+ return None
104
+
105
+ if user_msg_count < MIN_USER_MESSAGES:
106
+ return None
107
+
108
+ return {
109
+ "session_file": jsonl_path.name,
110
+ "session_path": str(jsonl_path),
111
+ "message_count": len(messages),
112
+ "user_message_count": user_msg_count,
113
+ "tool_use_count": len(tool_uses),
114
+ "messages": messages,
115
+ "tool_uses": tool_uses
116
+ }
117
+
118
+
119
+ def collect_transcripts(target_date: str) -> list[dict]:
120
+ """Collect all sessions modified on the target date."""
121
+ sessions = []
122
+ for sdir in find_session_dirs():
123
+ for f in sdir.glob("*.jsonl"):
124
+ try:
125
+ mtime = datetime.fromtimestamp(f.stat().st_mtime)
126
+ except OSError:
127
+ continue
128
+ if mtime.strftime("%Y-%m-%d") == target_date:
129
+ session = extract_session(f)
130
+ if session:
131
+ session["modified"] = mtime.isoformat()
132
+ sessions.append(session)
133
+ sessions.sort(key=lambda s: s["modified"])
134
+ return sessions
135
+
136
+
137
+ # ── Database queries ──────────────────────────────────────────────────────
138
+
139
+
140
+ def safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
141
+ """Run a query and return rows as dicts. Returns [] on any error."""
142
+ if not db_path.exists():
143
+ return []
144
+ try:
145
+ conn = sqlite3.connect(str(db_path))
146
+ conn.row_factory = sqlite3.Row
147
+ rows = conn.execute(query, params).fetchall()
148
+ result = [dict(r) for r in rows]
149
+ conn.close()
150
+ return result
151
+ except Exception as e:
152
+ print(f" [collect] DB query error ({db_path.name}): {e}", file=sys.stderr)
153
+ return []
154
+
155
+
156
+ def collect_followups() -> list[dict]:
157
+ """Active followups from nexo.db."""
158
+ return safe_query(
159
+ NEXO_DB,
160
+ "SELECT * FROM followups WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC"
161
+ )
162
+
163
+
164
+ def collect_learnings() -> list[dict]:
165
+ """Active learnings from nexo.db."""
166
+ return safe_query(NEXO_DB, "SELECT * FROM learnings ORDER BY updated_at DESC LIMIT 200")
167
+
168
+
169
+ def collect_diaries(target_date: str) -> list[dict]:
170
+ """Today's session diaries."""
171
+ # Diaries store created_at as unix timestamp or ISO string -- handle both
172
+ start_ts = datetime.strptime(target_date, "%Y-%m-%d").timestamp()
173
+ end_ts = start_ts + 86400
174
+ rows = safe_query(
175
+ NEXO_DB,
176
+ "SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
177
+ (start_ts, end_ts)
178
+ )
179
+ if not rows:
180
+ # Try ISO format
181
+ rows = safe_query(
182
+ NEXO_DB,
183
+ "SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
184
+ (target_date + "T00:00:00", target_date + "T23:59:59")
185
+ )
186
+ return rows
187
+
188
+
189
+ def collect_trust_score() -> list[dict]:
190
+ """Current trust score and 7-day history from cognitive.db."""
191
+ return safe_query(
192
+ COGNITIVE_DB,
193
+ "SELECT * FROM trust_score ORDER BY rowid DESC LIMIT 1"
194
+ )
195
+
196
+
197
+ # ── Discovery: scan NEXO_HOME for non-core content ───────────────────────
198
+
199
+ CORE_DIRS = {"data", "operations", "logs", "coordination", "brain"}
200
+ CORE_FILES = {"config.json", "nexo.db", "cognitive.db"}
201
+
202
+
203
+ def discover_extras() -> list[dict]:
204
+ """Scan NEXO_HOME for non-core directories and files."""
205
+ extras = []
206
+ if not NEXO_HOME.exists():
207
+ return extras
208
+
209
+ for item in sorted(NEXO_HOME.iterdir()):
210
+ name = item.name
211
+ if name.startswith("."):
212
+ continue
213
+ if name in CORE_DIRS or name in CORE_FILES:
214
+ continue
215
+
216
+ entry = {"name": name, "path": str(item), "type": "dir" if item.is_dir() else "file"}
217
+
218
+ if item.is_dir():
219
+ # Count contents and list interesting files
220
+ files = list(item.rglob("*"))
221
+ entry["file_count"] = len([f for f in files if f.is_file()])
222
+ entry["notable_files"] = [
223
+ str(f.relative_to(item))
224
+ for f in files
225
+ if f.is_file() and f.suffix in (".py", ".sh", ".json", ".db", ".log", ".sqlite")
226
+ ][:20]
227
+ elif item.is_file():
228
+ entry["size"] = item.stat().st_size
229
+
230
+ extras.append(entry)
231
+
232
+ return extras
233
+
234
+
235
+ # ── LaunchAgent logs ──────────────────────────────────────────────────────
236
+
237
+
238
+ def collect_error_logs(target_date: str) -> list[dict]:
239
+ """Scan NEXO_HOME/logs/ for lines containing errors from today."""
240
+ log_dir = NEXO_HOME / "logs"
241
+ if not log_dir.exists():
242
+ return []
243
+
244
+ errors = []
245
+ for log_file in sorted(log_dir.glob("*.log")):
246
+ try:
247
+ lines = log_file.read_text(errors="replace").splitlines()
248
+ except Exception:
249
+ continue
250
+
251
+ file_errors = []
252
+ for i, line in enumerate(lines):
253
+ # Match lines from today that contain error indicators
254
+ if target_date in line and any(
255
+ kw in line.lower() for kw in ("error", "exception", "traceback", "failed", "fatal", "critical")
256
+ ):
257
+ # Include surrounding context (1 line before, 2 after)
258
+ start = max(0, i - 1)
259
+ end = min(len(lines), i + 3)
260
+ file_errors.append({
261
+ "line": i + 1,
262
+ "context": "\n".join(lines[start:end])
263
+ })
264
+
265
+ if file_errors:
266
+ errors.append({
267
+ "file": log_file.name,
268
+ "path": str(log_file),
269
+ "errors": file_errors[:50] # Cap per file
270
+ })
271
+
272
+ return errors
273
+
274
+
275
+ # ── Format output as plain text ───────────────────────────────────────────
276
+
277
+
278
+ def format_section(title: str, data, indent: int = 0) -> str:
279
+ """Format a data section as readable plain text."""
280
+ prefix = " " * indent
281
+ lines = [f"\n{'=' * 70}", f"{title}", f"{'=' * 70}"]
282
+
283
+ if isinstance(data, list):
284
+ if not data:
285
+ lines.append(f"{prefix}(none)")
286
+ else:
287
+ for i, item in enumerate(data):
288
+ lines.append(f"\n{prefix}--- [{i + 1}] ---")
289
+ if isinstance(item, dict):
290
+ for k, v in item.items():
291
+ val_str = str(v)
292
+ if len(val_str) > 500:
293
+ val_str = val_str[:500] + "..."
294
+ lines.append(f"{prefix} {k}: {val_str}")
295
+ else:
296
+ lines.append(f"{prefix} {item}")
297
+ elif isinstance(data, dict):
298
+ for k, v in data.items():
299
+ val_str = str(v)
300
+ if len(val_str) > 500:
301
+ val_str = val_str[:500] + "..."
302
+ lines.append(f"{prefix}{k}: {val_str}")
303
+ elif isinstance(data, str):
304
+ lines.append(data)
305
+ else:
306
+ lines.append(str(data))
307
+
308
+ return "\n".join(lines)
309
+
310
+
311
+ def format_transcripts(sessions: list[dict]) -> str:
312
+ """Format transcripts in a readable way for Claude to analyze."""
313
+ lines = [f"\n{'=' * 70}", "SESSION TRANSCRIPTS", f"{'=' * 70}"]
314
+ lines.append(f"Total sessions: {len(sessions)}")
315
+
316
+ for i, session in enumerate(sessions):
317
+ lines.append(f"\n{'─' * 60}")
318
+ lines.append(f"SESSION {i + 1}: {session['session_file']}")
319
+ lines.append(f"Modified: {session['modified']}")
320
+ lines.append(f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}")
321
+ lines.append(f"{'─' * 60}")
322
+
323
+ for msg in session["messages"]:
324
+ role = "USER" if msg["role"] == "user" else "AGENT"
325
+ idx = msg.get("index", "?")
326
+ lines.append(f"\n[{role} @{idx}]")
327
+ lines.append(msg["text"])
328
+
329
+ if session["tool_uses"]:
330
+ lines.append(f"\n -- Tool usage log --")
331
+ for tu in session["tool_uses"]:
332
+ file_info = f" [{tu['file'][:80]}]" if tu.get("file") else ""
333
+ lines.append(f" - {tu['tool']}{file_info}")
334
+
335
+ return "\n".join(lines)
336
+
337
+
338
+ # ── Main ──────────────────────────────────────────────────────────────────
339
+
340
+
341
+ def main():
342
+ target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
343
+ DEEP_SLEEP_DIR.mkdir(parents=True, exist_ok=True)
344
+
345
+ print(f"[collect] Phase 1: Collecting context for {target_date}")
346
+
347
+ # 1. Transcripts
348
+ print("[collect] Gathering transcripts...")
349
+ sessions = collect_transcripts(target_date)
350
+ print(f" Found {len(sessions)} sessions")
351
+
352
+ 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"
355
+ output_file.write_text(
356
+ f"Deep Sleep Context for {target_date}\n\nNo sessions found for this date.\n"
357
+ )
358
+ print(f"[collect] Output: {output_file}")
359
+ return
360
+
361
+ # 2. Core DB data
362
+ print("[collect] Querying databases...")
363
+ followups = collect_followups()
364
+ print(f" Active followups: {len(followups)}")
365
+
366
+ learnings = collect_learnings()
367
+ print(f" Learnings: {len(learnings)}")
368
+
369
+ diaries = collect_diaries(target_date)
370
+ print(f" Diaries today: {len(diaries)}")
371
+
372
+ trust_history = collect_trust_score()
373
+ print(f" Trust events (7d): {len(trust_history)}")
374
+
375
+ # 3. Discovery
376
+ print("[collect] Scanning for non-core content...")
377
+ extras = discover_extras()
378
+ print(f" Discovered {len(extras)} extra items")
379
+
380
+ # 4. Error logs
381
+ print("[collect] Checking error logs...")
382
+ error_logs = collect_error_logs(target_date)
383
+ print(f" Log files with errors: {len(error_logs)}")
384
+
385
+ # 5. Build per-session files + shared context
386
+ date_dir = DEEP_SLEEP_DIR / target_date
387
+ date_dir.mkdir(parents=True, exist_ok=True)
388
+ print(f"[collect] Writing session files to {date_dir}/")
389
+
390
+ # Shared context (followups, learnings, diaries, etc.) — one file
391
+ shared_parts = [
392
+ f"Deep Sleep Shared Context -- {target_date}",
393
+ f"Generated at: {datetime.now().isoformat()}",
394
+ f"NEXO_HOME: {NEXO_HOME}",
395
+ f"Sessions: {len(sessions)}",
396
+ ]
397
+ shared_parts.append(format_section("ACTIVE FOLLOWUPS", followups))
398
+ shared_parts.append(format_section("LEARNINGS (recent 200)", learnings))
399
+ shared_parts.append(format_section("SESSION DIARIES TODAY", diaries))
400
+ shared_parts.append(format_section("TRUST SCORE HISTORY (7d)", trust_history))
401
+ shared_parts.append(format_section("DISCOVERED NON-CORE CONTENT", extras))
402
+ shared_parts.append(format_section("ERROR LOGS", error_logs))
403
+
404
+ shared_text = "\n".join(shared_parts)
405
+ shared_file = date_dir / "shared-context.txt"
406
+ shared_file.write_text(shared_text, encoding="utf-8")
407
+ print(f" Shared context: {len(shared_text) / 1024:.0f} KB")
408
+
409
+ # Individual session files
410
+ session_files_written = []
411
+ total_size = len(shared_text.encode("utf-8"))
412
+ for i, session in enumerate(sessions):
413
+ sid_short = session["session_file"].replace(".jsonl", "")[:20]
414
+ filename = f"session-{i+1:02d}-{sid_short}.txt"
415
+ session_path = date_dir / filename
416
+
417
+ lines = [
418
+ f"Session: {session['session_file']}",
419
+ f"Modified: {session['modified']}",
420
+ f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}",
421
+ f"{'─' * 60}",
422
+ ]
423
+ for msg in session["messages"]:
424
+ role = "USER" if msg["role"] == "user" else "AGENT"
425
+ idx = msg.get("index", "?")
426
+ lines.append(f"\n[{role} @{idx}]")
427
+ lines.append(msg["text"])
428
+
429
+ if session["tool_uses"]:
430
+ lines.append(f"\n -- Tool usage log --")
431
+ for tu in session["tool_uses"]:
432
+ file_info = f" [{tu['file'][:80]}]" if tu.get("file") else ""
433
+ lines.append(f" - {tu['tool']}{file_info}")
434
+
435
+ session_text = "\n".join(lines)
436
+ session_path.write_text(session_text, encoding="utf-8")
437
+ session_files_written.append(filename)
438
+ total_size += len(session_text.encode("utf-8"))
439
+ print(f" {filename}: {len(session_text) / 1024:.0f} KB")
440
+
441
+ # Also keep legacy single context file for backwards compat
442
+ legacy_parts = [
443
+ f"Deep Sleep Context -- {target_date}",
444
+ f"Generated at: {datetime.now().isoformat()}",
445
+ f"NEXO_HOME: {NEXO_HOME}",
446
+ f"Sessions: {len(sessions)}",
447
+ ]
448
+ legacy_parts.append(format_transcripts(sessions))
449
+ legacy_parts.append(shared_text)
450
+ legacy_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
451
+ legacy_file.write_text("\n".join(legacy_parts), encoding="utf-8")
452
+
453
+ # Metadata JSON
454
+ meta = {
455
+ "date": target_date,
456
+ "sessions_found": len(sessions),
457
+ "session_files": [s["session_file"] for s in sessions],
458
+ "session_txt_files": session_files_written,
459
+ "total_messages": sum(s["message_count"] for s in sessions),
460
+ "total_tool_uses": sum(s["tool_use_count"] for s in sessions),
461
+ "followups_active": len(followups),
462
+ "learnings_count": len(learnings),
463
+ "diaries_today": len(diaries),
464
+ "error_log_files": len(error_logs),
465
+ "date_dir": str(date_dir),
466
+ "shared_context_file": str(shared_file),
467
+ "context_file": str(legacy_file),
468
+ "total_size_bytes": total_size,
469
+ }
470
+ meta_file = DEEP_SLEEP_DIR / f"{target_date}-meta.json"
471
+ with open(meta_file, "w") as f:
472
+ json.dump(meta, f, indent=2, ensure_ascii=False)
473
+
474
+ print(f"\n[collect] Done. {len(session_files_written)} session files + shared context ({total_size / 1024:.0f} KB total)")
475
+ print(f"[collect] Dir: {date_dir}")
476
+ print(f"[collect] Meta: {meta_file}")
477
+
478
+
479
+ if __name__ == "__main__":
480
+ main()