nexo-brain 5.3.26 → 5.3.28

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 (212) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/hook_guardrails.py +44 -0
  4. package/src/server.py +3 -0
  5. package/src/tools_sessions.py +6 -1
  6. package/src/dashboard/static/favicon 2.svg +0 -32
  7. package/src/dashboard/static/nexo-logo 2.png +0 -0
  8. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  9. package/src/dashboard/static/style 2.css +0 -2458
  10. package/src/dashboard/templates/adaptive 2.html +0 -118
  11. package/src/dashboard/templates/artifacts 2.html +0 -133
  12. package/src/dashboard/templates/backups 2.html +0 -136
  13. package/src/dashboard/templates/base 2.html +0 -417
  14. package/src/dashboard/templates/calendar 2.html +0 -591
  15. package/src/dashboard/templates/chat 2.html +0 -356
  16. package/src/dashboard/templates/claims 2.html +0 -259
  17. package/src/dashboard/templates/cortex 2.html +0 -321
  18. package/src/dashboard/templates/credentials 2.html +0 -128
  19. package/src/dashboard/templates/crons 2.html +0 -370
  20. package/src/dashboard/templates/dashboard 2.html +0 -494
  21. package/src/dashboard/templates/dreams 2.html +0 -252
  22. package/src/dashboard/templates/email 2.html +0 -160
  23. package/src/dashboard/templates/evolution 2.html +0 -189
  24. package/src/dashboard/templates/feed 2.html +0 -249
  25. package/src/dashboard/templates/followup_health 2.html +0 -170
  26. package/src/dashboard/templates/graph 2.html +0 -201
  27. package/src/dashboard/templates/guard 2.html +0 -259
  28. package/src/dashboard/templates/inbox 2.html +0 -251
  29. package/src/dashboard/templates/memory 2.html +0 -420
  30. package/src/dashboard/templates/operations 2.html +0 -608
  31. package/src/dashboard/templates/plugins 2.html +0 -185
  32. package/src/dashboard/templates/protocol 2.html +0 -199
  33. package/src/dashboard/templates/rules 2.html +0 -246
  34. package/src/dashboard/templates/sentiment 2.html +0 -247
  35. package/src/dashboard/templates/sessions 2.html +0 -218
  36. package/src/dashboard/templates/skills 2.html +0 -329
  37. package/src/dashboard/templates/somatic 2.html +0 -73
  38. package/src/dashboard/templates/triggers 2.html +0 -133
  39. package/src/dashboard/templates/trust 2.html +0 -360
  40. package/src/db/__init__ 2.py +0 -259
  41. package/src/db/_core 2.py +0 -437
  42. package/src/db/_credentials 2.py +0 -124
  43. package/src/db/_episodic 2.py +0 -762
  44. package/src/db/_evolution 2.py +0 -54
  45. package/src/db/_fts 2.py +0 -406
  46. package/src/db/_goal_profiles 2.py +0 -376
  47. package/src/db/_hot_context 2.py +0 -660
  48. package/src/db/_outcomes 2.py +0 -800
  49. package/src/db/_personal_scripts 2.py +0 -582
  50. package/src/db/_sessions 2.py +0 -330
  51. package/src/db/_tasks 2.py +0 -91
  52. package/src/db/_watchers 2.py +0 -173
  53. package/src/doctor/formatters 2.py +0 -52
  54. package/src/doctor/models 2.py +0 -69
  55. package/src/doctor/planes 2.py +0 -87
  56. package/src/doctor/providers/__init__ 2.py +0 -1
  57. package/src/doctor/providers/deep 2.py +0 -367
  58. package/src/evolution_cycle 2.py +0 -519
  59. package/src/hooks/auto_capture 2.py +0 -208
  60. package/src/hooks/caffeinate-guard 2.sh +0 -8
  61. package/src/hooks/capture-session 2.sh +0 -21
  62. package/src/hooks/capture-tool-logs 2.sh +0 -158
  63. package/src/hooks/daily-briefing-check 2.sh +0 -33
  64. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  65. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  66. package/src/hooks/inbox-hook 2.sh +0 -76
  67. package/src/hooks/post-compact 2.sh +0 -152
  68. package/src/hooks/pre-compact 2.sh +0 -169
  69. package/src/hooks/protocol-guardrail 2.sh +0 -10
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  71. package/src/hooks/session-stop 2.sh +0 -52
  72. package/src/kg_populate 2.py +0 -292
  73. package/src/maintenance 2.py +0 -53
  74. package/src/memory_backends 2.py +0 -71
  75. package/src/migrate_embeddings 2.py +0 -124
  76. package/src/nexo_sdk 2.py +0 -103
  77. package/src/observability 2.py +0 -199
  78. package/src/plugin_loader 2.py +0 -217
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/artifact_registry 2.py +0 -450
  81. package/src/plugins/backup 2.py +0 -127
  82. package/src/plugins/claims_tools 2.py +0 -119
  83. package/src/plugins/cognitive_memory 2.py +0 -609
  84. package/src/plugins/core_rules 2.py +0 -252
  85. package/src/plugins/cortex 2.py +0 -1155
  86. package/src/plugins/entities 2.py +0 -67
  87. package/src/plugins/episodic_memory 2.py +0 -560
  88. package/src/plugins/evolution 2.py +0 -167
  89. package/src/plugins/goal_engine 2.py +0 -142
  90. package/src/plugins/guard 2.py +0 -862
  91. package/src/plugins/impact 2.py +0 -29
  92. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  93. package/src/plugins/media_memory_tools 2.py +0 -98
  94. package/src/plugins/memory_export 2.py +0 -196
  95. package/src/plugins/outcomes 2.py +0 -130
  96. package/src/plugins/personal_scripts 2.py +0 -117
  97. package/src/plugins/preferences 2.py +0 -47
  98. package/src/plugins/protocol 2.py +0 -1449
  99. package/src/plugins/simple_api 2.py +0 -106
  100. package/src/plugins/skills 2.py +0 -341
  101. package/src/plugins/state_watchers 2.py +0 -79
  102. package/src/plugins/update 2.py +0 -986
  103. package/src/plugins/user_state_tools 2.py +0 -43
  104. package/src/plugins/workflow 2.py +0 -588
  105. package/src/protocol_settings 2.py +0 -59
  106. package/src/public_contribution 2.py +0 -466
  107. package/src/public_evolution_queue 2.py +0 -241
  108. package/src/requirements 2.txt +0 -14
  109. package/src/retroactive_learnings 2.py +0 -373
  110. package/src/rules/__init__ 2.py +0 -0
  111. package/src/rules/core-rules 2.json +0 -331
  112. package/src/rules/migrate 2.py +0 -207
  113. package/src/runtime_power 2.py +0 -874
  114. package/src/script_registry 2.py +0 -1559
  115. package/src/scripts/check-context 2.py +0 -272
  116. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  117. package/src/scripts/deep-sleep/collect 2.py +0 -928
  118. package/src/scripts/deep-sleep/extract 2.py +0 -330
  119. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  120. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  121. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  122. package/src/scripts/nexo-agent-run 2.py +0 -75
  123. package/src/scripts/nexo-auto-update 2.py +0 -6
  124. package/src/scripts/nexo-backup 2.sh +0 -25
  125. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  126. package/src/scripts/nexo-catchup 2.py +0 -300
  127. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  128. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  129. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  130. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  131. package/src/scripts/nexo-dashboard 2.sh +0 -29
  132. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  133. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  134. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  135. package/src/scripts/nexo-hook-record 2.py +0 -42
  136. package/src/scripts/nexo-immune 2.py +0 -936
  137. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  138. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  139. package/src/scripts/nexo-install 2.py +0 -6
  140. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  141. package/src/scripts/nexo-learning-validator 2.py +0 -266
  142. package/src/scripts/nexo-migrate 2.py +0 -260
  143. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  144. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  145. package/src/scripts/nexo-pre-commit 2.py +0 -120
  146. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  147. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  148. package/src/scripts/nexo-reflection 2.py +0 -256
  149. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  150. package/src/scripts/nexo-sleep 2.py +0 -631
  151. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  152. package/src/scripts/nexo-sync-clients 2.py +0 -16
  153. package/src/scripts/nexo-synthesis 2.py +0 -475
  154. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  155. package/src/scripts/nexo-update 2.sh +0 -306
  156. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  157. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  158. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  159. package/src/server 2.py +0 -1296
  160. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  161. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  162. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  163. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  164. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  165. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  166. package/src/skills/run-release-final-audit/script 2.py +0 -259
  167. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  168. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  169. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  170. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  171. package/src/skills_runtime 2.py +0 -932
  172. package/src/state_watchers_runtime 2.py +0 -475
  173. package/src/storage_router 2.py +0 -32
  174. package/src/system_catalog 2.py +0 -786
  175. package/src/tools_coordination 2.py +0 -103
  176. package/src/tools_credentials 2.py +0 -68
  177. package/src/tools_drive 2.py +0 -487
  178. package/src/tools_hot_context 2.py +0 -163
  179. package/src/tools_learnings 2.py +0 -612
  180. package/src/tools_menu 2.py +0 -229
  181. package/src/tools_reminders 2.py +0 -88
  182. package/src/tools_reminders_crud 2.py +0 -363
  183. package/src/tools_sessions 2.py +0 -1054
  184. package/src/tools_system_catalog 2.py +0 -19
  185. package/src/tools_task_history 2.py +0 -57
  186. package/src/tools_transcripts 2.py +0 -98
  187. package/src/transcript_utils 2.py +0 -412
  188. package/src/user_context 2.py +0 -46
  189. package/src/user_data_portability 2.py +0 -328
  190. package/src/user_state_model 2.py +0 -170
  191. package/templates/CLAUDE.md 2.template +0 -108
  192. package/templates/CODEX.AGENTS.md 2.template +0 -66
  193. package/templates/launchagents/README 2.md +0 -132
  194. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  196. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  197. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  199. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  200. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  201. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  202. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  203. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  204. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  205. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  206. package/templates/nexo_helper 2.py +0 -301
  207. package/templates/openclaw 2.json +0 -13
  208. package/templates/plugin-template 2.py +0 -40
  209. package/templates/script-template 2.py +0 -59
  210. package/templates/script-template 2.sh +0 -13
  211. package/templates/skill-script-template 2.py +0 -48
  212. package/templates/skill-template 2.md +0 -33
@@ -1,928 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
- """
4
- Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
5
-
6
- Gathers transcripts, DB data, logs, and discovered files into a single
7
- plain-text context file that subsequent phases read via the configured
8
- automation backend.
9
-
10
- Environment variables:
11
- NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
12
- NEXO_CODE -- path to the NEXO source repo (optional, for self-analysis)
13
- """
14
- import json
15
- import os
16
- import re
17
- import sqlite3
18
- import sys
19
- from collections import Counter
20
- from datetime import datetime, timedelta
21
- from pathlib import Path
22
-
23
- _DEFAULT_RUNTIME_ROOT = Path(__file__).resolve().parents[2]
24
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_DEFAULT_RUNTIME_ROOT)))
25
- if str(NEXO_CODE) not in sys.path:
26
- sys.path.insert(0, str(NEXO_CODE))
27
-
28
- import transcript_utils as _transcripts
29
-
30
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
31
- DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
32
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
33
- COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
34
- _TABLE_COLUMNS_CACHE: dict[tuple[str, str], set[str]] = {}
35
-
36
- MIN_USER_MESSAGES = 3 # Skip trivial sessions
37
-
38
- # Patterns that indicate sensitive data (passwords, tokens, API keys, etc.)
39
- _SENSITIVE_PATTERNS = re.compile(
40
- r'(?:'
41
- r'sk-ant-[A-Za-z0-9_-]+' # Anthropic API keys
42
- r'|shpat_[A-Fa-f0-9]+' # Shopify admin tokens
43
- r'|shpss_[A-Fa-f0-9]+' # Shopify shared secret
44
- r'|sk-[A-Za-z0-9]{20,}' # OpenAI-style keys
45
- r'|ghp_[A-Za-z0-9]{36,}' # GitHub PATs
46
- r'|gho_[A-Za-z0-9]{36,}' # GitHub OAuth tokens
47
- r'|AIza[A-Za-z0-9_-]{35}' # Google API keys
48
- r'|ya29\.[A-Za-z0-9_-]+' # Google OAuth tokens
49
- r'|xox[bpsa]-[A-Za-z0-9-]+' # Slack tokens
50
- r'|EAAG[A-Za-z0-9]+' # Meta/Facebook tokens
51
- r'|[Pp]assword\s*[:=]\s*\S+' # password: value or password=value
52
- r'|[Ss]ecret\s*[:=]\s*\S+' # secret: value
53
- r'|[Tt]oken\s*[:=]\s*\S+' # token: value
54
- r'|[Aa]pi[_-]?[Kk]ey\s*[:=]\s*\S+' # api_key: value
55
- r')'
56
- )
57
-
58
-
59
- def _redact_sensitive(text: str) -> str:
60
- """Replace sensitive patterns in text with [REDACTED]."""
61
- return _SENSITIVE_PATTERNS.sub('[REDACTED]', text)
62
-
63
-
64
- # ── Transcript collection (Claude Code + Codex) ────────────────────────────
65
-
66
-
67
- def _session_identifier(client: str, session_file: str) -> str:
68
- return f"{client}:{session_file}"
69
-
70
-
71
- def find_claude_session_files() -> list[Path]:
72
- """Find Claude Code session JSONL files under ~/.claude/projects."""
73
- return _transcripts.find_claude_session_files()
74
-
75
-
76
- def find_codex_session_files() -> list[Path]:
77
- """Find Codex session JSONL files under ~/.codex/sessions and archived_sessions."""
78
- return _transcripts.find_codex_session_files()
79
-
80
-
81
- def extract_claude_session(jsonl_path: Path) -> dict | None:
82
- """Extract clean transcript from a Claude Code JSONL session."""
83
- return _transcripts.extract_claude_session(jsonl_path)
84
-
85
-
86
- def extract_codex_session(jsonl_path: Path) -> dict | None:
87
- """Extract clean transcript from a Codex JSONL session."""
88
- return _transcripts.extract_codex_session(jsonl_path)
89
-
90
-
91
- def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]:
92
- """Collect all sessions modified after `since_iso` (exclusive) up to `until_iso` (inclusive).
93
-
94
- Uses a watermark approach: deep sleep tracks the last processed timestamp
95
- so nothing is missed regardless of when sessions happen (day, night, etc.).
96
- """
97
- return _transcripts.collect_transcripts_since(since_iso, until_iso)
98
-
99
-
100
- # ── Database queries ──────────────────────────────────────────────────────
101
-
102
-
103
- def safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
104
- """Run a query and return rows as dicts. Returns [] on any error."""
105
- if not db_path.exists():
106
- return []
107
- try:
108
- conn = sqlite3.connect(str(db_path))
109
- conn.row_factory = sqlite3.Row
110
- rows = conn.execute(query, params).fetchall()
111
- result = [dict(r) for r in rows]
112
- conn.close()
113
- return result
114
- except Exception as e:
115
- print(f" [collect] DB query error ({db_path.name}): {e}", file=sys.stderr)
116
- return []
117
-
118
-
119
- def _table_columns(db_path: Path, table_name: str) -> set[str]:
120
- cache_key = (str(db_path), table_name)
121
- cached = _TABLE_COLUMNS_CACHE.get(cache_key)
122
- if cached is not None:
123
- return cached
124
- if not db_path.exists():
125
- _TABLE_COLUMNS_CACHE[cache_key] = set()
126
- return set()
127
- try:
128
- conn = sqlite3.connect(str(db_path))
129
- conn.row_factory = sqlite3.Row
130
- rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
131
- conn.close()
132
- except Exception:
133
- _TABLE_COLUMNS_CACHE[cache_key] = set()
134
- return set()
135
- columns = {str(row["name"]) for row in rows}
136
- _TABLE_COLUMNS_CACHE[cache_key] = columns
137
- return columns
138
-
139
-
140
- def _optional_column_sql(db_path: Path, table_name: str, column_name: str, default_sql: str = "''") -> str:
141
- if column_name in _table_columns(db_path, table_name):
142
- return column_name
143
- return f"{default_sql} AS {column_name}"
144
-
145
-
146
- def collect_followups() -> list[dict]:
147
- """Active followups from nexo.db."""
148
- return safe_query(
149
- NEXO_DB,
150
- "SELECT * FROM followups WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC"
151
- )
152
-
153
-
154
- def collect_learnings() -> list[dict]:
155
- """Active learnings from nexo.db."""
156
- return safe_query(NEXO_DB, "SELECT * FROM learnings ORDER BY updated_at DESC LIMIT 200")
157
-
158
-
159
- def collect_diaries(target_date: str) -> list[dict]:
160
- """Today's session diaries."""
161
- # Diaries store created_at as unix timestamp or ISO string -- handle both
162
- start_ts = datetime.strptime(target_date, "%Y-%m-%d").timestamp()
163
- end_ts = start_ts + 86400
164
- rows = safe_query(
165
- NEXO_DB,
166
- "SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
167
- (start_ts, end_ts)
168
- )
169
- if not rows:
170
- # Try ISO format
171
- rows = safe_query(
172
- NEXO_DB,
173
- "SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
174
- (target_date + "T00:00:00", target_date + "T23:59:59")
175
- )
176
- return rows
177
-
178
-
179
- def collect_trust_score() -> list[dict]:
180
- """Current trust score and 7-day history from cognitive.db."""
181
- return safe_query(
182
- COGNITIVE_DB,
183
- "SELECT * FROM trust_score ORDER BY rowid DESC LIMIT 1"
184
- )
185
-
186
-
187
- def _parse_diary_created_at(value) -> datetime | None:
188
- if value in (None, ""):
189
- return None
190
- try:
191
- if isinstance(value, (int, float)) or (isinstance(value, str) and str(value).strip().isdigit()):
192
- return datetime.fromtimestamp(float(value))
193
- except Exception:
194
- return None
195
- try:
196
- return datetime.fromisoformat(str(value).replace("Z", "+00:00").replace("+00:00", ""))
197
- except Exception:
198
- return None
199
-
200
-
201
- def _sample_evenly(rows: list[dict], limit: int) -> list[dict]:
202
- if limit <= 0 or not rows:
203
- return []
204
- if len(rows) <= limit:
205
- return list(rows)
206
- if limit == 1:
207
- return [rows[-1]]
208
- step = (len(rows) - 1) / float(limit - 1)
209
- indices = sorted({round(i * step) for i in range(limit)})
210
- sampled = [rows[idx] for idx in indices]
211
- i = 0
212
- while len(sampled) < limit and i < len(rows):
213
- if rows[i] not in sampled:
214
- sampled.append(rows[i])
215
- i += 1
216
- return sampled[:limit]
217
-
218
-
219
- def _compact_diary_row(row: dict) -> dict:
220
- created = _parse_diary_created_at(row.get("created_at"))
221
- return {
222
- "session_id": row.get("session_id", ""),
223
- "created_at": created.isoformat() if created else str(row.get("created_at", "")),
224
- "domain": row.get("domain", "") or "",
225
- "mental_state": row.get("mental_state", "") or "",
226
- "summary": str(row.get("summary", "") or "")[:240],
227
- "self_critique": str(row.get("self_critique", "") or "")[:240],
228
- "source": row.get("source", "") or "",
229
- }
230
-
231
-
232
- def _load_project_aliases() -> dict[str, set[str]]:
233
- atlas_path = NEXO_HOME / "brain" / "project-atlas.json"
234
- aliases: dict[str, set[str]] = {}
235
- if not atlas_path.is_file():
236
- return aliases
237
- try:
238
- payload = json.loads(atlas_path.read_text())
239
- except Exception:
240
- return aliases
241
- if not isinstance(payload, dict):
242
- return aliases
243
- for key, value in payload.items():
244
- if str(key).startswith("_"):
245
- continue
246
- canonical = str(key).strip().lower()
247
- alias_set = {canonical, canonical.replace("-", " "), canonical.replace("_", " ")}
248
- if isinstance(value, dict):
249
- for alias in value.get("aliases", []) or []:
250
- alias_value = str(alias or "").strip().lower()
251
- if alias_value:
252
- alias_set.add(alias_value)
253
- alias_set.add(alias_value.replace("-", " "))
254
- aliases[canonical] = {item for item in alias_set if item}
255
- return aliases
256
-
257
-
258
- def _match_projects(text: str, alias_map: dict[str, set[str]]) -> set[str]:
259
- haystack = str(text or "").strip().lower()
260
- if not haystack:
261
- return set()
262
- matches: set[str] = set()
263
- for canonical, aliases in alias_map.items():
264
- for alias in sorted(aliases, key=len, reverse=True):
265
- if alias and alias in haystack:
266
- matches.add(canonical)
267
- break
268
- return matches
269
-
270
-
271
- def _priority_weight(value) -> float:
272
- lowered = str(value or "").strip().lower()
273
- if lowered in {"critical", "urgent"}:
274
- return 4.0
275
- if lowered == "high":
276
- return 3.0
277
- if lowered == "medium":
278
- return 2.0
279
- if lowered == "low":
280
- return 1.0
281
- return 1.5
282
-
283
-
284
- def _compact_periodic_summary(data: dict) -> dict:
285
- return {
286
- "label": data.get("label", ""),
287
- "window_start": data.get("window_start", ""),
288
- "window_end": data.get("window_end", ""),
289
- "summary": str(data.get("summary", "") or "")[:320],
290
- "top_projects": data.get("top_projects", [])[:4],
291
- "top_patterns": data.get("top_patterns", [])[:4],
292
- "avg_mood_score": data.get("avg_mood_score"),
293
- "avg_trust_score": data.get("avg_trust_score"),
294
- }
295
-
296
-
297
- def _load_periodic_summaries(target_date: str, *, kind: str, limit: int = 2) -> list[dict]:
298
- target_day = datetime.strptime(target_date, "%Y-%m-%d")
299
- summaries: list[tuple[str, dict]] = []
300
- pattern = "*-weekly-summary.json" if kind == "weekly" else "*-monthly-summary.json"
301
- for path in sorted(DEEP_SLEEP_DIR.glob(pattern)):
302
- try:
303
- payload = json.loads(path.read_text())
304
- except Exception:
305
- continue
306
- window_end_raw = str(payload.get("window_end", "") or "")
307
- parsed = _parse_diary_created_at(window_end_raw)
308
- if parsed and parsed >= target_day:
309
- continue
310
- summaries.append((window_end_raw, _compact_periodic_summary(payload)))
311
- summaries.sort(key=lambda item: item[0], reverse=True)
312
- return [item for _, item in summaries[:limit]]
313
-
314
-
315
- def _project_priority_signals(target_day: datetime, compact_diaries: list[dict]) -> list[dict]:
316
- alias_map = _load_project_aliases()
317
- scoreboard: dict[str, dict] = {}
318
-
319
- def bump(project: str, score: float, signal_key: str, reason: str) -> None:
320
- if not project:
321
- return
322
- slot = scoreboard.setdefault(
323
- project,
324
- {
325
- "project": project,
326
- "score": 0.0,
327
- "signals": {
328
- "diary_sessions": 0,
329
- "learnings": 0,
330
- "followups": 0,
331
- "decisions": 0,
332
- },
333
- "reasons": [],
334
- },
335
- )
336
- slot["score"] += score
337
- slot["signals"][signal_key] += 1
338
- if reason and reason not in slot["reasons"]:
339
- slot["reasons"].append(reason)
340
-
341
- for row in compact_diaries:
342
- created = _parse_diary_created_at(row.get("created_at"))
343
- recency_bonus = 1.0
344
- if created:
345
- age_days = max(0.0, (target_day - created).total_seconds() / 86400)
346
- recency_bonus = 1.4 if age_days <= 7 else 1.0
347
- candidates = set()
348
- domain = str(row.get("domain", "") or "").strip().lower()
349
- if domain:
350
- candidates.add(domain)
351
- candidates |= _match_projects(" ".join([row.get("summary", ""), row.get("self_critique", "")]), alias_map)
352
- for project in candidates:
353
- bump(project, 3.0 * recency_bonus, "diary_sessions", "recent session diary activity")
354
-
355
- learning_priority_sql = _optional_column_sql(NEXO_DB, "learnings", "priority", "'medium'")
356
- learning_weight_sql = _optional_column_sql(NEXO_DB, "learnings", "weight", "0")
357
- learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
358
- learning_rows = safe_query(
359
- NEXO_DB,
360
- f"SELECT category, title, content, created_at, updated_at, {learning_priority_sql}, "
361
- f"{learning_weight_sql}, {learning_applies_sql} FROM learnings "
362
- "ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 160",
363
- )
364
- for row in learning_rows:
365
- text = " ".join(
366
- [
367
- str(row.get("applies_to", "") or ""),
368
- str(row.get("title", "") or ""),
369
- str(row.get("content", "") or ""),
370
- str(row.get("category", "") or ""),
371
- ]
372
- )
373
- matched = _match_projects(text, alias_map)
374
- if not matched:
375
- continue
376
- weight = float(row.get("weight", 0) or 0)
377
- score = 1.0 + _priority_weight(row.get("priority")) + min(2.0, max(0.0, weight))
378
- for project in matched:
379
- bump(project, score, "learnings", "recent leverage-bearing learning")
380
-
381
- followup_priority_sql = _optional_column_sql(NEXO_DB, "followups", "priority", "'medium'")
382
- followup_reasoning_sql = _optional_column_sql(NEXO_DB, "followups", "reasoning", "''")
383
- followup_rows = safe_query(
384
- NEXO_DB,
385
- f"SELECT id, description, date, status, {followup_priority_sql}, created_at, updated_at, "
386
- f"{followup_reasoning_sql} FROM followups "
387
- "WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 120",
388
- )
389
- for row in followup_rows:
390
- matched = _match_projects(
391
- " ".join(
392
- [
393
- str(row.get("description", "") or ""),
394
- str(row.get("reasoning", "") or ""),
395
- ]
396
- ),
397
- alias_map,
398
- )
399
- if not matched:
400
- continue
401
- overdue_bonus = 0.0
402
- due_value = str(row.get("date", "") or "")
403
- try:
404
- if due_value:
405
- due_dt = datetime.strptime(due_value[:10], "%Y-%m-%d")
406
- if due_dt <= target_day:
407
- overdue_bonus = 1.5
408
- except Exception:
409
- overdue_bonus = 0.0
410
- score = 1.5 + _priority_weight(row.get("priority")) + overdue_bonus
411
- for project in matched:
412
- bump(project, score, "followups", "open followup pressure")
413
-
414
- decision_status_sql = _optional_column_sql(NEXO_DB, "decisions", "status", "''")
415
- decision_reasoning_sql = _optional_column_sql(NEXO_DB, "decisions", "reasoning", "''")
416
- decision_review_due_sql = _optional_column_sql(NEXO_DB, "decisions", "review_due_at", "NULL")
417
- decision_rows = safe_query(
418
- NEXO_DB,
419
- f"SELECT domain, outcome, {decision_status_sql}, {decision_reasoning_sql}, decision, based_on, created_at, "
420
- f"{decision_review_due_sql} FROM decisions "
421
- "ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 120",
422
- )
423
- for row in decision_rows:
424
- matched = set()
425
- domain = str(row.get("domain", "") or "").strip().lower()
426
- if domain:
427
- matched.add(domain)
428
- matched |= _match_projects(
429
- " ".join(
430
- [
431
- str(row.get("reasoning", "") or ""),
432
- str(row.get("decision", "") or ""),
433
- str(row.get("based_on", "") or ""),
434
- str(row.get("outcome", "") or ""),
435
- str(row.get("status", "") or ""),
436
- ]
437
- ),
438
- alias_map,
439
- )
440
- if not matched:
441
- continue
442
- outcome = str(row.get("outcome", "") or "").lower()
443
- status = str(row.get("status", "") or "").lower()
444
- score = 2.5
445
- if any(token in outcome for token in ("fail", "error", "blocked", "regression")):
446
- score += 2.0
447
- if status in {"pending", "blocked", "open"}:
448
- score += 1.5
449
- for project in matched:
450
- bump(project, score, "decisions", "recent decision pressure")
451
-
452
- ranked = sorted(scoreboard.values(), key=lambda item: item["score"], reverse=True)
453
- for item in ranked:
454
- item["score"] = round(item["score"], 2)
455
- item["reasons"] = item["reasons"][:4]
456
- return ranked[:8]
457
-
458
-
459
- def collect_long_horizon_context(
460
- target_date: str,
461
- *,
462
- horizon_days: int = 60,
463
- recent_days: int = 14,
464
- max_diaries: int = 20,
465
- max_sessions: int = 12,
466
- ) -> dict:
467
- """Build long-horizon context blending recent and older evidence.
468
-
469
- Strategy:
470
- - recent 70% from the last `recent_days`
471
- - older 30% sampled evenly from the rest of the `horizon_days` window
472
- """
473
- target_day = datetime.strptime(target_date, "%Y-%m-%d")
474
- horizon_start = target_day - timedelta(days=horizon_days)
475
- recent_start = target_day - timedelta(days=recent_days)
476
-
477
- diary_rows = safe_query(
478
- NEXO_DB,
479
- "SELECT session_id, created_at, summary, mental_state, domain, self_critique, source "
480
- "FROM session_diary ORDER BY created_at ASC"
481
- )
482
- compact_diaries = []
483
- for row in diary_rows:
484
- created = _parse_diary_created_at(row.get("created_at"))
485
- if not created:
486
- continue
487
- if not (horizon_start <= created < target_day):
488
- continue
489
- compact_diaries.append(_compact_diary_row(row))
490
-
491
- recent_diaries = [row for row in compact_diaries if _parse_diary_created_at(row.get("created_at")) and _parse_diary_created_at(row.get("created_at")) >= recent_start]
492
- older_diaries = [row for row in compact_diaries if row not in recent_diaries]
493
- recent_quota = max(1, round(max_diaries * 0.7))
494
- older_quota = max(0, max_diaries - recent_quota)
495
- sampled_diaries = recent_diaries[-recent_quota:] + _sample_evenly(older_diaries, older_quota)
496
- sampled_diaries.sort(key=lambda row: row.get("created_at", ""))
497
-
498
- recurring_domains = Counter(row["domain"] for row in compact_diaries if row.get("domain"))
499
- recurring_states = Counter(row["mental_state"] for row in compact_diaries if row.get("mental_state"))
500
- recurring_critiques = Counter(row["self_critique"] for row in compact_diaries if row.get("self_critique"))
501
-
502
- learning_reasoning_sql = _optional_column_sql(NEXO_DB, "learnings", "reasoning", "''")
503
- learning_prevention_sql = _optional_column_sql(NEXO_DB, "learnings", "prevention", "''")
504
- learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
505
- learning_rows = safe_query(
506
- NEXO_DB,
507
- f"SELECT category, title, content, created_at, updated_at, {learning_reasoning_sql}, "
508
- f"{learning_prevention_sql}, {learning_applies_sql} "
509
- "FROM learnings ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 120"
510
- )
511
- long_horizon_learnings = []
512
- for row in learning_rows:
513
- long_horizon_learnings.append({
514
- "category": row.get("category", ""),
515
- "title": str(row.get("title", "") or "")[:140],
516
- "content": str(row.get("content", "") or "")[:260],
517
- "reasoning": str(row.get("reasoning", "") or "")[:180],
518
- "prevention": str(row.get("prevention", "") or "")[:180],
519
- "applies_to": str(row.get("applies_to", "") or "")[:180],
520
- "updated_at": str(row.get("updated_at", "") or row.get("created_at", "")),
521
- })
522
- long_horizon_learnings = long_horizon_learnings[:24]
523
-
524
- transcript_candidates: list[dict] = []
525
- transcript_files: list[tuple[str, Path]] = [
526
- ("claude_code", path) for path in find_claude_session_files()
527
- ] + [
528
- ("codex", path) for path in find_codex_session_files()
529
- ]
530
- horizon_end = target_day
531
- for client, path in transcript_files:
532
- try:
533
- modified = datetime.fromtimestamp(path.stat().st_mtime)
534
- except OSError:
535
- continue
536
- if not (horizon_start <= modified < horizon_end):
537
- continue
538
- transcript_candidates.append({
539
- "client": client,
540
- "session_file": _session_identifier(client, path.name),
541
- "modified": modified.isoformat(),
542
- "session_path": str(path),
543
- })
544
- transcript_candidates.sort(key=lambda row: row["modified"])
545
- recent_sessions = [row for row in transcript_candidates if datetime.fromisoformat(row["modified"]) >= recent_start]
546
- older_sessions = [row for row in transcript_candidates if row not in recent_sessions]
547
- recent_session_quota = max(1, round(max_sessions * 0.7))
548
- older_session_quota = max(0, max_sessions - recent_session_quota)
549
- sampled_sessions = recent_sessions[-recent_session_quota:] + _sample_evenly(older_sessions, older_session_quota)
550
- sampled_sessions.sort(key=lambda row: row["modified"])
551
-
552
- stale_followups = safe_query(
553
- NEXO_DB,
554
- "SELECT id, description, date, status, created_at, updated_at FROM followups "
555
- "WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 50"
556
- )
557
- older_than_week = []
558
- week_ago = target_day - timedelta(days=7)
559
- for row in stale_followups:
560
- created = _parse_diary_created_at(row.get("created_at"))
561
- if created and created < week_ago:
562
- older_than_week.append({
563
- "id": row.get("id", ""),
564
- "description": str(row.get("description", "") or "")[:180],
565
- "date": row.get("date", ""),
566
- "status": row.get("status", ""),
567
- "created_at": created.isoformat(),
568
- })
569
-
570
- weekly_summaries = _load_periodic_summaries(target_date, kind="weekly", limit=2)
571
- monthly_summaries = _load_periodic_summaries(target_date, kind="monthly", limit=2)
572
- project_priority_signals = _project_priority_signals(target_day, compact_diaries)
573
-
574
- return {
575
- "horizon_days": horizon_days,
576
- "recent_window_days": recent_days,
577
- "sample_strategy": "70% recent + 30% older evenly sampled",
578
- "historical_diaries": sampled_diaries,
579
- "historical_sessions": sampled_sessions,
580
- "historical_learnings": long_horizon_learnings,
581
- "recurring_domains": recurring_domains.most_common(8),
582
- "recurring_mental_states": recurring_states.most_common(8),
583
- "recurring_self_critiques": recurring_critiques.most_common(6),
584
- "stale_followups": older_than_week[:12],
585
- "project_priority_signals": project_priority_signals,
586
- "weekly_summaries": weekly_summaries,
587
- "monthly_summaries": monthly_summaries,
588
- }
589
-
590
-
591
- # ── Discovery: scan NEXO_HOME for non-core content ───────────────────────
592
-
593
- CORE_DIRS = {"data", "operations", "logs", "coordination", "brain"}
594
- CORE_FILES = {"config.json", "nexo.db", "cognitive.db"}
595
-
596
-
597
- def discover_extras() -> list[dict]:
598
- """Scan NEXO_HOME for non-core directories and files."""
599
- extras = []
600
- if not NEXO_HOME.exists():
601
- return extras
602
-
603
- for item in sorted(NEXO_HOME.iterdir()):
604
- name = item.name
605
- if name.startswith("."):
606
- continue
607
- if name in CORE_DIRS or name in CORE_FILES:
608
- continue
609
-
610
- entry = {"name": name, "path": str(item), "type": "dir" if item.is_dir() else "file"}
611
-
612
- if item.is_dir():
613
- # Count contents and list interesting files
614
- files = list(item.rglob("*"))
615
- entry["file_count"] = len([f for f in files if f.is_file()])
616
- entry["notable_files"] = [
617
- str(f.relative_to(item))
618
- for f in files
619
- if f.is_file() and f.suffix in (".py", ".sh", ".json", ".db", ".log", ".sqlite")
620
- ][:20]
621
- elif item.is_file():
622
- entry["size"] = item.stat().st_size
623
-
624
- extras.append(entry)
625
-
626
- return extras
627
-
628
-
629
- # ── LaunchAgent logs ──────────────────────────────────────────────────────
630
-
631
-
632
- def collect_error_logs(target_date: str) -> list[dict]:
633
- """Scan NEXO_HOME/logs/ for lines containing errors from today."""
634
- log_dir = NEXO_HOME / "logs"
635
- if not log_dir.exists():
636
- return []
637
-
638
- errors = []
639
- for log_file in sorted(log_dir.glob("*.log")):
640
- try:
641
- lines = log_file.read_text(errors="replace").splitlines()
642
- except Exception:
643
- continue
644
-
645
- file_errors = []
646
- for i, line in enumerate(lines):
647
- # Match lines from today that contain error indicators
648
- if target_date in line and any(
649
- kw in line.lower() for kw in ("error", "exception", "traceback", "failed", "fatal", "critical")
650
- ):
651
- # Include surrounding context (1 line before, 2 after)
652
- start = max(0, i - 1)
653
- end = min(len(lines), i + 3)
654
- file_errors.append({
655
- "line": i + 1,
656
- "context": "\n".join(lines[start:end])
657
- })
658
-
659
- if file_errors:
660
- errors.append({
661
- "file": log_file.name,
662
- "path": str(log_file),
663
- "errors": file_errors[:50] # Cap per file
664
- })
665
-
666
- return errors
667
-
668
-
669
- # ── Format output as plain text ───────────────────────────────────────────
670
-
671
-
672
- def format_section(title: str, data, indent: int = 0) -> str:
673
- """Format a data section as readable plain text."""
674
- prefix = " " * indent
675
- lines = [f"\n{'=' * 70}", f"{title}", f"{'=' * 70}"]
676
-
677
- if isinstance(data, list):
678
- if not data:
679
- lines.append(f"{prefix}(none)")
680
- else:
681
- for i, item in enumerate(data):
682
- lines.append(f"\n{prefix}--- [{i + 1}] ---")
683
- if isinstance(item, dict):
684
- for k, v in item.items():
685
- val_str = str(v)
686
- if len(val_str) > 500:
687
- val_str = val_str[:500] + "..."
688
- lines.append(f"{prefix} {k}: {val_str}")
689
- else:
690
- lines.append(f"{prefix} {item}")
691
- elif isinstance(data, dict):
692
- for k, v in data.items():
693
- val_str = str(v)
694
- if len(val_str) > 500:
695
- val_str = val_str[:500] + "..."
696
- lines.append(f"{prefix}{k}: {val_str}")
697
- elif isinstance(data, str):
698
- lines.append(data)
699
- else:
700
- lines.append(str(data))
701
-
702
- return "\n".join(lines)
703
-
704
-
705
- def format_transcripts(sessions: list[dict]) -> str:
706
- """Format transcripts in a readable way for Claude to analyze."""
707
- lines = [f"\n{'=' * 70}", "SESSION TRANSCRIPTS", f"{'=' * 70}"]
708
- lines.append(f"Total sessions: {len(sessions)}")
709
-
710
- for i, session in enumerate(sessions):
711
- lines.append(f"\n{'─' * 60}")
712
- lines.append(f"SESSION {i + 1}: {session['session_file']}")
713
- lines.append(f"Client: {session.get('client', 'unknown')}")
714
- if session.get("source"):
715
- lines.append(f"Source: {session['source']}")
716
- lines.append(f"Modified: {session['modified']}")
717
- lines.append(f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}")
718
- lines.append(f"{'─' * 60}")
719
-
720
- for msg in session["messages"]:
721
- role = "USER" if msg["role"] == "user" else "AGENT"
722
- idx = msg.get("index", "?")
723
- lines.append(f"\n[{role} @{idx}]")
724
- lines.append(_redact_sensitive(msg["text"]))
725
-
726
- if session["tool_uses"]:
727
- lines.append(f"\n -- Tool usage log --")
728
- for tu in session["tool_uses"]:
729
- file_info = f" [{_redact_sensitive(tu['file'][:80])}]" if tu.get("file") else ""
730
- lines.append(f" - {tu['tool']}{file_info}")
731
-
732
- return "\n".join(lines)
733
-
734
-
735
- # ── Main ──────────────────────────────────────────────────────────────────
736
-
737
-
738
- def main():
739
- # Watermark-based collection: since_iso and until_iso passed by the wrapper script
740
- # argv[1] = run_id (date label for output files)
741
- # argv[2] = since_iso (exclusive lower bound, e.g. "2026-04-01T04:30:00")
742
- # argv[3] = until_iso (inclusive upper bound, e.g. "2026-04-02T04:30:00") — optional, defaults to now
743
- run_id = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
744
- since_iso = sys.argv[2] if len(sys.argv) > 2 else ""
745
- until_iso = sys.argv[3] if len(sys.argv) > 3 else ""
746
-
747
- DEEP_SLEEP_DIR.mkdir(parents=True, exist_ok=True)
748
-
749
- print(f"[collect] Phase 1: Collecting context (run_id={run_id})")
750
-
751
- # 1. Transcripts — watermark-based
752
- if since_iso:
753
- print(f"[collect] Gathering transcripts since {since_iso}" + (f" until {until_iso}" if until_iso else ""))
754
- sessions = collect_transcripts_since(since_iso, until_iso)
755
- else:
756
- # Fallback: collect everything from last 48h (safe catch-all)
757
- fallback_since = (datetime.now() - timedelta(hours=48)).isoformat()
758
- print(f"[collect] No watermark — collecting last 48h since {fallback_since}")
759
- sessions = collect_transcripts_since(fallback_since)
760
- print(f" Found {len(sessions)} sessions")
761
-
762
- if not sessions:
763
- print(f"[collect] No new sessions found. Writing minimal context file.")
764
- output_file = DEEP_SLEEP_DIR / f"{run_id}-context.txt"
765
- output_file.write_text(
766
- f"Deep Sleep Context for {run_id}\n\nNo sessions found.\n"
767
- )
768
- print(f"[collect] Output: {output_file}")
769
- return
770
-
771
- target_date = run_id # Keep variable name for downstream compat
772
-
773
- # 2. Core DB data
774
- print("[collect] Querying databases...")
775
- followups = collect_followups()
776
- print(f" Active followups: {len(followups)}")
777
-
778
- learnings = collect_learnings()
779
- print(f" Learnings: {len(learnings)}")
780
-
781
- diaries = collect_diaries(target_date)
782
- print(f" Diaries today: {len(diaries)}")
783
-
784
- trust_history = collect_trust_score()
785
- print(f" Trust events (7d): {len(trust_history)}")
786
-
787
- # 3. Discovery
788
- print("[collect] Scanning for non-core content...")
789
- extras = discover_extras()
790
- print(f" Discovered {len(extras)} extra items")
791
-
792
- # 4. Error logs
793
- print("[collect] Checking error logs...")
794
- error_logs = collect_error_logs(target_date)
795
- print(f" Log files with errors: {len(error_logs)}")
796
-
797
- print("[collect] Building long-horizon context...")
798
- long_horizon = collect_long_horizon_context(target_date)
799
- print(
800
- " Long horizon: "
801
- f"{len(long_horizon.get('historical_diaries', []))} diary samples, "
802
- f"{len(long_horizon.get('historical_sessions', []))} session samples"
803
- )
804
-
805
- # 5. Build per-session files + shared context
806
- date_dir = DEEP_SLEEP_DIR / target_date
807
- date_dir.mkdir(parents=True, exist_ok=True)
808
- print(f"[collect] Writing session files to {date_dir}/")
809
-
810
- # Shared context (followups, learnings, diaries, etc.) — one file
811
- shared_parts = [
812
- f"Deep Sleep Shared Context -- {target_date}",
813
- f"Generated at: {datetime.now().isoformat()}",
814
- f"NEXO_HOME: {NEXO_HOME}",
815
- f"Sessions: {len(sessions)}",
816
- ]
817
- shared_parts.append(format_section("ACTIVE FOLLOWUPS", followups))
818
- shared_parts.append(format_section("LEARNINGS (recent 200)", learnings))
819
- shared_parts.append(format_section("SESSION DIARIES TODAY", diaries))
820
- shared_parts.append(format_section("TRUST SCORE HISTORY (7d)", trust_history))
821
- shared_parts.append(format_section("DISCOVERED NON-CORE CONTENT", extras))
822
- shared_parts.append(format_section("ERROR LOGS", error_logs))
823
- shared_parts.append(format_section("LONG-HORIZON CONTEXT (60d blend)", long_horizon))
824
-
825
- shared_text = "\n".join(shared_parts)
826
- shared_file = date_dir / "shared-context.txt"
827
- shared_file.write_text(shared_text, encoding="utf-8")
828
- print(f" Shared context: {len(shared_text) / 1024:.0f} KB")
829
-
830
- long_horizon_file = date_dir / "long-horizon-context.json"
831
- long_horizon_file.write_text(json.dumps(long_horizon, indent=2, ensure_ascii=False), encoding="utf-8")
832
- print(f" Long horizon JSON: {long_horizon_file.name}")
833
-
834
- # Individual session files
835
- session_files_written = []
836
- session_txt_map = {}
837
- total_size = len(shared_text.encode("utf-8"))
838
- for i, session in enumerate(sessions):
839
- raw_id = session["session_file"].replace(".jsonl", "").replace(":", "-")
840
- sid_short = raw_id[:30]
841
- filename = f"session-{i+1:02d}-{sid_short}.txt"
842
- session_path = date_dir / filename
843
-
844
- lines = [
845
- f"Session: {session['session_file']}",
846
- f"Display name: {session.get('display_name', session['session_file'])}",
847
- f"Client: {session.get('client', 'unknown')}",
848
- f"Source: {session.get('source', 'unknown')}",
849
- f"Modified: {session['modified']}",
850
- f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}",
851
- f"{'─' * 60}",
852
- ]
853
- if session.get("cwd"):
854
- lines.insert(4, f"CWD: {session['cwd']}")
855
- if session.get("originator"):
856
- lines.insert(4, f"Originator: {session['originator']}")
857
- for msg in session["messages"]:
858
- role = "USER" if msg["role"] == "user" else "AGENT"
859
- idx = msg.get("index", "?")
860
- lines.append(f"\n[{role} @{idx}]")
861
- lines.append(_redact_sensitive(msg["text"]))
862
-
863
- if session["tool_uses"]:
864
- lines.append(f"\n -- Tool usage log --")
865
- for tu in session["tool_uses"]:
866
- file_info = f" [{_redact_sensitive(tu['file'][:80])}]" if tu.get("file") else ""
867
- lines.append(f" - {tu['tool']}{file_info}")
868
-
869
- session_text = "\n".join(lines)
870
- session_path.write_text(session_text, encoding="utf-8")
871
- session_files_written.append(filename)
872
- session_txt_map[session["session_file"]] = filename
873
- total_size += len(session_text.encode("utf-8"))
874
- print(f" {filename}: {len(session_text) / 1024:.0f} KB")
875
-
876
- # Also keep legacy single context file for backwards compat
877
- legacy_parts = [
878
- f"Deep Sleep Context -- {target_date}",
879
- f"Generated at: {datetime.now().isoformat()}",
880
- f"NEXO_HOME: {NEXO_HOME}",
881
- f"Sessions: {len(sessions)}",
882
- ]
883
- legacy_parts.append(format_transcripts(sessions))
884
- legacy_parts.append(shared_text)
885
- legacy_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
886
- legacy_file.write_text("\n".join(legacy_parts), encoding="utf-8")
887
-
888
- # Metadata JSON
889
- meta = {
890
- "date": target_date,
891
- "sessions_found": len(sessions),
892
- "session_files": [s["session_file"] for s in sessions],
893
- "session_txt_files": session_files_written,
894
- "session_txt_map": session_txt_map,
895
- "session_manifest": [
896
- {
897
- "session_id": s["session_file"],
898
- "display_name": s.get("display_name", s["session_file"]),
899
- "client": s.get("client", "unknown"),
900
- "source": s.get("source", ""),
901
- "session_path": s.get("session_path", ""),
902
- "session_txt_file": session_txt_map.get(s["session_file"], ""),
903
- }
904
- for s in sessions
905
- ],
906
- "total_messages": sum(s["message_count"] for s in sessions),
907
- "total_tool_uses": sum(s["tool_use_count"] for s in sessions),
908
- "followups_active": len(followups),
909
- "learnings_count": len(learnings),
910
- "diaries_today": len(diaries),
911
- "error_log_files": len(error_logs),
912
- "date_dir": str(date_dir),
913
- "shared_context_file": str(shared_file),
914
- "long_horizon_file": str(long_horizon_file),
915
- "context_file": str(legacy_file),
916
- "total_size_bytes": total_size,
917
- }
918
- meta_file = DEEP_SLEEP_DIR / f"{target_date}-meta.json"
919
- with open(meta_file, "w") as f:
920
- json.dump(meta, f, indent=2, ensure_ascii=False)
921
-
922
- print(f"\n[collect] Done. {len(session_files_written)} session files + shared context ({total_size / 1024:.0f} KB total)")
923
- print(f"[collect] Dir: {date_dir}")
924
- print(f"[collect] Meta: {meta_file}")
925
-
926
-
927
- if __name__ == "__main__":
928
- main()