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,631 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Sleep System v2 — The brain dreams.
4
-
5
- Before: 834 lines with word-overlap "intelligence" for learning consolidation.
6
- Now: Stage A (mechanical cleanup) stays pure Python. Stage B (dreaming) uses
7
- the configured automation backend to understand, deduplicate, and prune with real intelligence.
8
-
9
- Triggered hourly via LaunchAgent. Runs ONCE per day, first time Mac is awake.
10
- If interrupted (power loss, crash), resumes on next trigger.
11
-
12
- Stage A — Housekeeping (Python pure):
13
- Delete old logs, rotate files, trim JSON. No intelligence needed.
14
-
15
- Stage B — Dreaming (automation backend):
16
- Review learnings for duplicates and contradictions with UNDERSTANDING.
17
- Prune MEMORY.md if over limit. Clean preferences. Compress old observations.
18
- One CLI call that does what 500 lines of word-overlap couldn't.
19
- """
20
-
21
- import fcntl
22
- import json
23
- import os
24
- import re
25
- import shutil
26
- import sqlite3
27
- import subprocess
28
- import sys
29
- from datetime import datetime, date, timedelta
30
- from pathlib import Path
31
-
32
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
33
- _script_dir = Path(__file__).resolve().parent
34
- _repo_src = _script_dir.parent
35
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
36
- if str(NEXO_CODE) not in sys.path:
37
- sys.path.insert(0, str(NEXO_CODE))
38
-
39
- from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
40
- try:
41
- from client_preferences import resolve_user_model as _resolve_user_model
42
- _USER_MODEL = _resolve_user_model()
43
- except Exception:
44
- _USER_MODEL = ""
45
-
46
-
47
- # ─── Paths ────────────────────────────────────────────────────────────────────
48
- CLAUDE_DIR = NEXO_HOME
49
- BRAIN_DIR = CLAUDE_DIR / "brain"
50
- COORD_DIR = CLAUDE_DIR / "coordination"
51
- MEMORY_DIR = CLAUDE_DIR / "memory"
52
- DAEMON_LOGS_DIR = CLAUDE_DIR / "daemon" / "logs"
53
-
54
- DAILY_SUMMARIES_DIR = BRAIN_DIR / "daily_summaries"
55
- SESSION_ARCHIVE_DIR = BRAIN_DIR / "session_archive"
56
- COMPRESSED_MEMORIES_DIR = BRAIN_DIR / "compressed_memories"
57
-
58
- HEARTBEAT_LOG = COORD_DIR / "heartbeat-log.json"
59
- REFLECTION_LOG = COORD_DIR / "reflection-log.json"
60
- SLEEP_LOG = COORD_DIR / "sleep-log.json"
61
-
62
- MEMORY_MD = NEXO_HOME / "memory" / "MEMORY.md"
63
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
64
- CLAUDE_MEM_DB = Path.home() / ".claude-mem" / "claude-mem.db"
65
- def _resolve_claude_cli() -> Path:
66
- """Find claude CLI: saved path > PATH > common locations."""
67
- saved = NEXO_HOME / "config" / "claude-cli-path"
68
- if saved.exists():
69
- p = Path(saved.read_text().strip())
70
- if p.exists():
71
- return p
72
- found = shutil.which("claude")
73
- if found:
74
- return Path(found)
75
- for candidate in [
76
- Path.home() / ".local" / "bin" / "claude",
77
- Path.home() / ".npm-global" / "bin" / "claude",
78
- Path("/usr/local/bin/claude"),
79
- ]:
80
- if candidate.exists():
81
- return candidate
82
- return Path.home() / ".local" / "bin" / "claude"
83
-
84
- CLAUDE_CLI = _resolve_claude_cli()
85
-
86
- LAST_RUN_FILE = COORD_DIR / "sleep-last-run"
87
- LOCK_FILE = COORD_DIR / "sleep.lock"
88
- PROCESS_LOCK = COORD_DIR / "sleep-process.lock"
89
-
90
- TODAY = date.today()
91
- NOW = datetime.now()
92
- TIMESTAMP = NOW.strftime("%Y-%m-%d %H:%M")
93
-
94
-
95
- # ─── Run-once & resume logic (unchanged from v1) ──────────────────────────────
96
-
97
- def already_ran_today() -> bool:
98
- if not LAST_RUN_FILE.exists():
99
- return False
100
- try:
101
- return LAST_RUN_FILE.read_text().strip() == str(TODAY)
102
- except Exception:
103
- return False
104
-
105
-
106
- def was_interrupted() -> bool:
107
- if not LOCK_FILE.exists():
108
- return False
109
- try:
110
- lock_data = json.loads(LOCK_FILE.read_text())
111
- if lock_data.get("date") != str(TODAY):
112
- LOCK_FILE.unlink()
113
- return False
114
- lock_pid = lock_data.get("pid")
115
- if lock_pid:
116
- try:
117
- os.kill(lock_pid, 0)
118
- log(f"Another instance running (PID {lock_pid}). Exiting.")
119
- return False
120
- except ProcessLookupError:
121
- log(f"Interrupted run (phase: {lock_data.get('phase', '?')}). Resuming.")
122
- return True
123
- except PermissionError:
124
- return False
125
- LOCK_FILE.unlink()
126
- return False
127
- except Exception:
128
- LOCK_FILE.unlink(missing_ok=True)
129
- return False
130
-
131
-
132
- def get_interrupted_phase() -> str:
133
- try:
134
- return json.loads(LOCK_FILE.read_text()).get("phase", "stage_a")
135
- except Exception:
136
- return "stage_a"
137
-
138
-
139
- def set_lock(phase: str):
140
- save_json(LOCK_FILE, {"date": str(TODAY), "phase": phase, "started": TIMESTAMP, "pid": os.getpid()})
141
-
142
-
143
- def mark_complete():
144
- LAST_RUN_FILE.write_text(str(TODAY))
145
- LOCK_FILE.unlink(missing_ok=True)
146
-
147
-
148
- # ─── Helpers ──────────────────────────────────────────────────────────────────
149
-
150
- def log(msg: str):
151
- ts = datetime.now().strftime("%Y-%m-%d %H:%M")
152
- print(f"[{ts}] {msg}")
153
-
154
-
155
- def load_json(path: Path, default=None):
156
- if not path.exists():
157
- return default if default is not None else {}
158
- try:
159
- return json.loads(path.read_text())
160
- except Exception:
161
- return default if default is not None else {}
162
-
163
-
164
- def save_json(path: Path, data):
165
- path.parent.mkdir(parents=True, exist_ok=True)
166
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
167
-
168
-
169
- def parse_date_from_stem(stem: str):
170
- m = re.search(r'(\d{4}-\d{2}-\d{2})', stem)
171
- if m:
172
- try:
173
- return date.fromisoformat(m.group(1))
174
- except ValueError:
175
- return None
176
- return None
177
-
178
-
179
- def append_sleep_log(entry: dict):
180
- entries = load_json(SLEEP_LOG, [])
181
- if not isinstance(entries, list):
182
- entries = []
183
- entries.append(entry)
184
- if len(entries) > 90:
185
- entries = entries[-90:]
186
- save_json(SLEEP_LOG, entries)
187
-
188
-
189
- # ─── Stage A: Mechanical cleanup (UNCHANGED from v1) ─────────────────────────
190
-
191
- def stage_a_cleanup() -> dict:
192
- """Pure Python cleanup. No LLM calls."""
193
- stats = {
194
- "a1_daily_summaries_deleted": 0,
195
- "a2_session_archives_deleted": 0,
196
- "a3_logs_rotated": 0,
197
- "a4_compressed_memories_deleted": 0,
198
- "a5_heartbeat_trimmed": False,
199
- "a6_reflection_trimmed": False,
200
- "a7_daemon_logs_deleted": 0,
201
- }
202
-
203
- # A1: Delete daily_summaries/*.md >90 days
204
- cutoff_90 = TODAY - timedelta(days=90)
205
- if DAILY_SUMMARIES_DIR.exists():
206
- for f in DAILY_SUMMARIES_DIR.glob("*.md"):
207
- d = parse_date_from_stem(f.stem)
208
- if d and d < cutoff_90:
209
- try:
210
- f.unlink()
211
- stats["a1_daily_summaries_deleted"] += 1
212
- except Exception:
213
- pass
214
-
215
- # A2: Delete session_archive/*.jsonl >30 days
216
- cutoff_30 = TODAY - timedelta(days=30)
217
- if SESSION_ARCHIVE_DIR.exists():
218
- for f in SESSION_ARCHIVE_DIR.glob("*.jsonl"):
219
- d = parse_date_from_stem(f.stem)
220
- if d and d < cutoff_30:
221
- try:
222
- f.unlink()
223
- stats["a2_session_archives_deleted"] += 1
224
- except Exception:
225
- pass
226
-
227
- # A3: Rotate coordination/*-stdout.log if >5MB
228
- if COORD_DIR.exists():
229
- for f in COORD_DIR.glob("*-stdout.log"):
230
- try:
231
- if f.stat().st_size > 5 * 1024 * 1024:
232
- lines = f.read_text().splitlines()
233
- keep = lines[-500:]
234
- f.write_text("\n".join(keep) + "\n")
235
- stats["a3_logs_rotated"] += 1
236
- except Exception:
237
- pass
238
-
239
- # A4: Delete compressed_memories/week_*.md >180 days
240
- cutoff_180 = TODAY - timedelta(days=180)
241
- if COMPRESSED_MEMORIES_DIR.exists():
242
- for f in COMPRESSED_MEMORIES_DIR.glob("week_*.md"):
243
- d = parse_date_from_stem(f.stem)
244
- if d and d < cutoff_180:
245
- try:
246
- f.unlink()
247
- stats["a4_compressed_memories_deleted"] += 1
248
- except Exception:
249
- pass
250
-
251
- # A5: Trim heartbeat-log.json to 200 entries
252
- if HEARTBEAT_LOG.exists():
253
- try:
254
- data = load_json(HEARTBEAT_LOG, [])
255
- if isinstance(data, list) and len(data) > 200:
256
- save_json(HEARTBEAT_LOG, data[-200:])
257
- stats["a5_heartbeat_trimmed"] = True
258
- except Exception:
259
- pass
260
-
261
- # A6: Trim reflection-log.json to 60 entries
262
- if REFLECTION_LOG.exists():
263
- try:
264
- data = load_json(REFLECTION_LOG, [])
265
- if isinstance(data, list) and len(data) > 60:
266
- save_json(REFLECTION_LOG, data[-60:])
267
- stats["a6_reflection_trimmed"] = True
268
- except Exception:
269
- pass
270
-
271
- # A7: Delete daemon/logs/ dirs >14 days
272
- cutoff_14 = TODAY - timedelta(days=14)
273
- if DAEMON_LOGS_DIR.exists():
274
- for d_path in sorted(DAEMON_LOGS_DIR.iterdir()):
275
- if not d_path.is_dir():
276
- continue
277
- d = parse_date_from_stem(d_path.name)
278
- if d and d < cutoff_14:
279
- try:
280
- shutil.rmtree(d_path)
281
- stats["a7_daemon_logs_deleted"] += 1
282
- except Exception:
283
- pass
284
-
285
- # A8: Delete cortex/logs/*.log >7 days, truncate launchd >5MB
286
- cutoff_7 = TODAY - timedelta(days=7)
287
- cortex_logs = NEXO_HOME / "cortex" / "logs"
288
- if cortex_logs.exists():
289
- for f in cortex_logs.glob("*.log"):
290
- if f.name.startswith("launchd-"):
291
- try:
292
- if f.stat().st_size > 5 * 1024 * 1024:
293
- lines = f.read_text().splitlines()
294
- f.write_text("\n".join(lines[-500:]) + "\n")
295
- stats["a3_logs_rotated"] += 1
296
- except Exception:
297
- pass
298
- continue
299
- d = parse_date_from_stem(f.stem)
300
- if d and d < cutoff_7:
301
- try:
302
- f.unlink()
303
- except Exception:
304
- pass
305
-
306
- return stats
307
-
308
-
309
- # ─── Stage B: Dreaming (automation backend) ─────────────────────────────────
310
-
311
- def collect_brain_state() -> dict:
312
- """Collect all data the CLI needs to dream."""
313
- state = {"learnings": [], "preferences": [], "memory_md_lines": 0,
314
- "claude_mem_old": 0, "feedback_count": 0}
315
-
316
- if NEXO_DB.exists():
317
- try:
318
- conn = sqlite3.connect(str(NEXO_DB))
319
- conn.row_factory = sqlite3.Row
320
-
321
- # Learnings
322
- rows = conn.execute(
323
- "SELECT id, title, content, category, created_at FROM learnings "
324
- "WHERE status='active' ORDER BY id"
325
- ).fetchall()
326
- state["learnings"] = [dict(r) for r in rows]
327
-
328
- # Preferences
329
- rows = conn.execute("SELECT key, value, category, updated_at FROM preferences").fetchall()
330
- state["preferences"] = [dict(r) for r in rows]
331
-
332
- conn.close()
333
- except Exception as e:
334
- log(f"DB error: {e}")
335
-
336
- # MEMORY.md
337
- if MEMORY_MD.exists():
338
- state["memory_md_lines"] = len(MEMORY_MD.read_text().splitlines())
339
-
340
- # claude-mem.db old observations
341
- if CLAUDE_MEM_DB.exists():
342
- try:
343
- cutoff = int((datetime.now() - timedelta(days=60)).timestamp() * 1000)
344
- conn = sqlite3.connect(str(CLAUDE_MEM_DB))
345
- state["claude_mem_old"] = conn.execute(
346
- "SELECT COUNT(*) FROM observations WHERE created_at_epoch < ?", (cutoff,)
347
- ).fetchone()[0]
348
- conn.close()
349
- except Exception:
350
- pass
351
-
352
- # Feedback count
353
- state["feedback_count"] = len(list(MEMORY_MD.parent.glob("feedback_*.md")))
354
-
355
- return state
356
-
357
-
358
- def should_dream(state: dict) -> bool:
359
- """Check if there's enough to justify a CLI call."""
360
- return (
361
- len(state["learnings"]) > 10
362
- or state["memory_md_lines"] > 170
363
- or len(state["preferences"]) > 5
364
- or state["claude_mem_old"] > 500
365
- )
366
-
367
-
368
- def dream(state: dict) -> dict:
369
- """The brain dreams — CLI does the intelligent work."""
370
-
371
- # Truncate learnings JSON if too large
372
- learnings_json = json.dumps(state["learnings"], ensure_ascii=False, indent=1)
373
- if len(learnings_json) > 15000:
374
- learnings_json = learnings_json[:15000] + "\n... (truncated)"
375
-
376
- tasks = []
377
-
378
- tasks.append(f"""TASK 1: LEARNING CONSOLIDATION ({len(state['learnings'])} active)
379
- Review these learnings and identify:
380
- a) DUPLICATES: learnings that say the same thing differently.
381
- b) CONTRADICTIONS: learnings that contradict each other.
382
- c) STALE: learnings about bugs/issues fixed >60 days ago that are never referenced.
383
-
384
- Write your findings to {COORD_DIR}/sleep-report.md with sections:
385
- - "## Duplicates to archive" — list learning IDs to archive and why
386
- - "## Contradictions" — pairs of conflicting learnings
387
- - "## Stale candidates" — IDs of learnings that may be obsolete
388
-
389
- Also write a machine-readable file {COORD_DIR}/sleep-actions.json:
390
- {{"archive_ids": [1, 2, 3], "contradiction_pairs": [[4, 5]], "stale_ids": [6, 7]}}
391
-
392
- The wrapper will execute the actual DB operations based on this JSON.
393
-
394
- LEARNINGS:
395
- {learnings_json}""")
396
-
397
- if state["memory_md_lines"] > 170:
398
- tasks.append(f"""TASK 2: MEMORY.MD COMPRESSION ({state['memory_md_lines']} lines, limit 200)
399
- File: {MEMORY_MD}
400
- Read it, compress resolved incidents >21 days, merge duplicates.
401
- NEVER delete: credentials, legal entity info, CRITICAL rules, infrastructure.
402
- Target: <180 lines.""")
403
-
404
- if len(state["preferences"]) > 5:
405
- tasks.append(f"""TASK 3: PREFERENCES CLEANUP ({len(state['preferences'])} entries)
406
- Review the preferences and identify duplicate keys.
407
- Add to sleep-actions.json: "duplicate_preference_keys": ["key1", "key2", ...]
408
- The wrapper will handle the actual DB cleanup safely.""")
409
-
410
- if state["claude_mem_old"] > 500:
411
- tasks.append(f"""TASK 4: OLD OBSERVATIONS ({state['claude_mem_old']} entries >60d)
412
- Note in sleep-report.md that old observations should be cleaned.
413
- Add to sleep-actions.json: "clean_old_observations": true
414
- The wrapper will handle the actual DB cleanup safely.""")
415
-
416
- tasks_str = "\n\n".join(tasks)
417
-
418
- prompt = f"""FIRST: Call nexo_startup(task='deep-sleep nightly maintenance') to register this session.
419
-
420
- You are NEXO Sleep — the nightly brain maintenance process.
421
- Like a human brain during sleep: consolidate important memories, discard noise,
422
- detect conflicts, prepare state for tomorrow.
423
- Use nexo_learning_add, nexo_followup_create, nexo_session_diary_write and other MCP tools directly.
424
-
425
- BRAIN STATE:
426
- - {len(state['learnings'])} active learnings
427
- - {state['memory_md_lines']} lines in MEMORY.md (limit: 200)
428
- - {len(state['preferences'])} preferences
429
- - {state['feedback_count']} feedback files
430
- - {state['claude_mem_old']} old observations (>60d)
431
-
432
- {tasks_str}
433
-
434
- ABSOLUTE RULES:
435
- - NEVER delete legal entity info (LLC, SLU, EIN, NIF, project)
436
- - NEVER delete credentials, tokens, API keys, secrets
437
- - NEVER delete rules marked CRITICAL or MAX PRIORITY
438
- - NEVER delete infrastructure info (servers, repos, deploys)
439
- - When in doubt, DON'T delete
440
-
441
- Write a summary to {COORD_DIR}/sleep-report.md when done.
442
- Execute without asking."""
443
-
444
- log("Stage B: Invoking automation backend — dreaming...")
445
- try:
446
- result = run_automation_prompt(
447
- prompt,
448
- model=_USER_MODEL or "opus",
449
- timeout=21600,
450
- output_format="text",
451
- allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
452
- )
453
-
454
- if result.returncode != 0:
455
- log(f"Stage B: CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
456
- return {"error": result.returncode}
457
-
458
- log(f"Stage B: Dreaming complete. Output: {len(result.stdout or '')} chars")
459
- return {"ok": True, "output_len": len(result.stdout or "")}
460
-
461
- except AutomationBackendUnavailableError as e:
462
- log(f"Stage B: automation backend unavailable: {e}")
463
- return {"error": "backend-unavailable"}
464
- except subprocess.TimeoutExpired:
465
- log("Stage B: CLI timed out (600s)")
466
- return {"error": "timeout"}
467
- except Exception as e:
468
- log(f"Stage B: Exception: {e}")
469
- return {"error": str(e)}
470
-
471
-
472
- def execute_dream_actions(actions: dict, state: dict):
473
- """Execute the DB actions decided by CLI, safely in Python."""
474
- log("Stage B2: Executing dream actions...")
475
-
476
- # Archive duplicate/stale learnings
477
- archive_ids = actions.get("archive_ids", []) + actions.get("stale_ids", [])
478
- if archive_ids and NEXO_DB.exists():
479
- try:
480
- conn = sqlite3.connect(str(NEXO_DB))
481
- for lid in archive_ids:
482
- if isinstance(lid, int):
483
- conn.execute(
484
- "UPDATE learnings SET status='archived' WHERE id=? AND status='active'",
485
- (lid,)
486
- )
487
- conn.commit()
488
- conn.close()
489
- log(f" Archived {len(archive_ids)} learnings: {archive_ids}")
490
- except Exception as e:
491
- log(f" Error archiving learnings: {e}")
492
-
493
- # Clean duplicate preferences
494
- dup_keys = actions.get("duplicate_preference_keys", [])
495
- if dup_keys and NEXO_DB.exists():
496
- try:
497
- conn = sqlite3.connect(str(NEXO_DB))
498
- for key in dup_keys:
499
- if isinstance(key, str):
500
- # Keep newest, delete older duplicates
501
- conn.execute(
502
- "DELETE FROM preferences WHERE key = ? AND rowid NOT IN "
503
- "(SELECT rowid FROM preferences WHERE key = ? ORDER BY updated_at DESC LIMIT 1)",
504
- (key, key)
505
- )
506
- conn.commit()
507
- conn.close()
508
- log(f" Cleaned {len(dup_keys)} duplicate preference keys")
509
- except Exception as e:
510
- log(f" Error cleaning preferences: {e}")
511
-
512
- # Clean old observations
513
- if actions.get("clean_old_observations") and CLAUDE_MEM_DB.exists():
514
- try:
515
- cutoff_ms = int((datetime.now() - timedelta(days=60)).timestamp() * 1000)
516
- conn = sqlite3.connect(str(CLAUDE_MEM_DB))
517
- deleted = conn.execute(
518
- "DELETE FROM observations WHERE created_at_epoch < ? "
519
- "AND discovery_tokens < 300 "
520
- "AND id NOT IN (SELECT id FROM observations WHERE "
521
- "title LIKE '%CRITICO%' OR title LIKE '%credential%' "
522
- "OR title LIKE '%token%' OR title LIKE '%API%' "
523
- "OR title LIKE '%LLC%' OR title LIKE '%SLU%') "
524
- "LIMIT 200",
525
- (cutoff_ms,)
526
- ).rowcount
527
- conn.execute(
528
- "DELETE FROM observations_fts WHERE rowid NOT IN "
529
- "(SELECT id FROM observations)"
530
- )
531
- conn.execute("VACUUM")
532
- conn.commit()
533
- conn.close()
534
- log(f" Cleaned {deleted} old observations")
535
- except Exception as e:
536
- log(f" Error cleaning observations: {e}")
537
-
538
- log("Stage B2: Actions complete.")
539
-
540
-
541
- # ─── Main ────────────────────────────────────────────────────────────────────
542
-
543
- def main():
544
- log("=" * 60)
545
- log("NEXO Sleep System v2 starting")
546
-
547
- # Process lock
548
- try:
549
- lock_fd = open(PROCESS_LOCK, "w")
550
- fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
551
- lock_fd.write(str(os.getpid()))
552
- lock_fd.flush()
553
- except (IOError, OSError):
554
- log("Another sleep instance running. Exiting.")
555
- sys.exit(0)
556
-
557
- try:
558
- if already_ran_today():
559
- log("Already ran today. Exiting.")
560
- sys.exit(0)
561
-
562
- start_phase = "stage_a"
563
- if was_interrupted():
564
- start_phase = get_interrupted_phase()
565
-
566
- run_log = {"date": str(TODAY), "started": TIMESTAMP,
567
- "stage_a": None, "stage_b": None, "completed": None}
568
- sleep_had_errors = False
569
-
570
- # Stage A: Housekeeping (mechanical)
571
- if start_phase == "stage_a":
572
- set_lock("stage_a")
573
- log("─── Stage A: Housekeeping ───")
574
- run_log["stage_a"] = stage_a_cleanup()
575
-
576
- # Stage B: Dreaming (intelligent)
577
- set_lock("stage_b")
578
- log("─── Stage B: Dreaming ───")
579
- state = collect_brain_state()
580
-
581
- if should_dream(state):
582
- log(f"Brain state: {len(state['learnings'])} learnings, "
583
- f"{state['memory_md_lines']} MEMORY lines, "
584
- f"{state['claude_mem_old']} old observations")
585
- dream_result = dream(state)
586
- run_log["stage_b"] = dream_result
587
-
588
- if "error" in dream_result:
589
- log(f"Stage B: Dreaming failed ({dream_result['error']}). "
590
- "Stage A cleanup completed successfully. Not marking catchup to allow retry.")
591
- sleep_had_errors = True
592
- else:
593
- # Stage B2: Execute actions from CLI output
594
- actions_file = COORD_DIR / "sleep-actions.json"
595
- if actions_file.exists():
596
- try:
597
- actions = json.loads(actions_file.read_text())
598
- execute_dream_actions(actions, state)
599
- except Exception as e:
600
- log(f"Stage B2: Error executing actions: {e}")
601
- else:
602
- log("Brain is clean -- no dreaming needed.")
603
- run_log["stage_b"] = {"skipped": True}
604
-
605
- # Done
606
- run_log["completed"] = datetime.now().strftime("%Y-%m-%d %H:%M")
607
- mark_complete()
608
- append_sleep_log(run_log)
609
- log(f"NEXO Sleep v2 complete at {run_log['completed']}")
610
-
611
- # Register for catch-up only if all stages succeeded
612
- if not sleep_had_errors:
613
- try:
614
- state_file = NEXO_HOME / "operations" / ".catchup-state.json"
615
- st = json.loads(state_file.read_text()) if state_file.exists() else {}
616
- st["sleep"] = datetime.now().isoformat()
617
- state_file.write_text(json.dumps(st, indent=2))
618
- except Exception:
619
- pass
620
-
621
- finally:
622
- try:
623
- fcntl.flock(lock_fd, fcntl.LOCK_UN)
624
- lock_fd.close()
625
- PROCESS_LOCK.unlink(missing_ok=True)
626
- except Exception:
627
- pass
628
-
629
-
630
- if __name__ == "__main__":
631
- main()
@@ -1,35 +0,0 @@
1
- #!/bin/bash
2
- # NEXO Snapshot Restore — restores files from a snapshot directory.
3
- # Usage: nexo-snapshot-restore.sh <snapshot-dir>
4
- set -euo pipefail
5
-
6
- SNAP_DIR="${1:?Usage: nexo-snapshot-restore.sh <snapshot-dir>}"
7
- MANIFEST="$SNAP_DIR/manifest.json"
8
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
9
- RESTORE_LOG="$NEXO_HOME/logs/snapshot-restores.log"
10
-
11
- if [ ! -f "$MANIFEST" ]; then
12
- echo "ERROR: No manifest.json in $SNAP_DIR" >&2
13
- exit 1
14
- fi
15
-
16
- TS=$(date "+%Y-%m-%d %H:%M:%S")
17
- mkdir -p "$(dirname "$RESTORE_LOG")"
18
- echo "[$TS] Restoring from $SNAP_DIR" >> "$RESTORE_LOG"
19
-
20
- python3 -c "
21
- import json, shutil, os
22
- manifest = json.load(open('$MANIFEST'))
23
- for rel_path in manifest.get('files', []):
24
- src = os.path.join('$SNAP_DIR', 'files', rel_path)
25
- dst = os.path.expanduser('~/' + rel_path)
26
- if os.path.exists(src):
27
- os.makedirs(os.path.dirname(dst), exist_ok=True)
28
- shutil.copy2(src, dst)
29
- print(f' Restored: {rel_path}')
30
- else:
31
- print(f' SKIP (not in snapshot): {rel_path}')
32
- print('Restore complete.')
33
- "
34
-
35
- echo "[$TS] Restore complete from $SNAP_DIR" >> "$RESTORE_LOG"
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import sys
5
- from pathlib import Path
6
-
7
-
8
- ROOT = Path(__file__).resolve().parents[1]
9
- if str(ROOT) not in sys.path:
10
- sys.path.insert(0, str(ROOT))
11
-
12
- from client_sync import main
13
-
14
-
15
- if __name__ == "__main__":
16
- raise SystemExit(main())