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
@@ -1,219 +1,564 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Deep Sleep Step 3: Apply findings.
4
- Takes the analysis output and writes feedback memories + trust adjustments.
3
+ Deep Sleep v2 -- Phase 4: Apply synthesized findings.
4
+
5
+ Reads $DATE-synthesis.json and executes actions:
6
+ - learning_add: inserts learnings into nexo.db
7
+ - followup_create: inserts followups into nexo.db
8
+ - morning_briefing_item: writes to morning briefing file
9
+
10
+ All actions are idempotent (dedupe_key checked against last 7 days),
11
+ backed up before mutation, and logged to $DATE-applied.json.
12
+
13
+ Environment variables:
14
+ NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
5
15
  """
16
+ import hashlib
6
17
  import json
7
18
  import os
8
19
  import sqlite3
9
20
  import sys
10
- from datetime import datetime
21
+ from datetime import datetime, timedelta
11
22
  from pathlib import Path
12
23
 
13
24
  NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
14
-
15
25
  DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
16
26
  NEXO_DB = NEXO_HOME / "data" / "nexo.db"
17
-
18
-
19
- def find_memory_dir() -> Path:
20
- """Find the Claude Code auto-memory directory."""
21
- claude_dir = Path.home() / ".claude" / "projects"
22
- for d in claude_dir.iterdir():
23
- if d.is_dir():
24
- mem_dir = d / "memory"
25
- if mem_dir.exists():
26
- return mem_dir
27
- # Fallback: create under first project dir
28
- for d in claude_dir.iterdir():
29
- if d.is_dir():
30
- mem_dir = d / "memory"
31
- mem_dir.mkdir(exist_ok=True)
32
- return mem_dir
33
- return claude_dir / "memory"
34
-
35
-
36
- def write_feedback_memory(memory_dir: Path, filename: str, name: str, description: str, content: str):
37
- """Write a feedback memory file."""
38
- filepath = memory_dir / filename
39
- feedback = f"""---
40
- name: {name}
41
- description: {description}
42
- type: feedback
43
- ---
44
-
45
- {content}
46
- """
47
- filepath.write_text(feedback)
48
-
49
-
50
- def update_memory_index(memory_dir: Path, new_entries: list[dict]):
51
- """Append new entries to MEMORY.md index."""
52
- index_file = memory_dir / "MEMORY.md"
53
- if not index_file.exists() or not new_entries:
54
- return
55
-
56
- current = index_file.read_text()
57
- lines_to_add = []
58
- for entry in new_entries:
59
- line = f"- **{entry['title']}:** `{entry['filename']}` --- {entry['summary']}"
60
- if line not in current:
61
- lines_to_add.append(line)
62
-
63
- if lines_to_add:
64
- current += "\n" + "\n".join(lines_to_add) + "\n"
65
- index_file.write_text(current)
66
-
67
-
68
- def adjust_trust(points: int, context: str):
69
- """Record trust adjustment in cognitive.db if available."""
70
- cog_db = NEXO_HOME / "data" / "cognitive.db"
71
- if not cog_db.exists():
72
- return
27
+ COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
28
+ OPERATIONS_DIR = NEXO_HOME / "operations"
29
+ BACKUP_DIR = DEEP_SLEEP_DIR # backups stored alongside outputs
30
+
31
+
32
+ def generate_run_id(target_date: str) -> str:
33
+ """Generate a unique run ID for this execution."""
34
+ ts = datetime.now().strftime("%H%M%S")
35
+ return f"{target_date}-{ts}"
36
+
37
+
38
+ def load_recent_dedupe_keys(target_date: str, days: int = 7) -> set[str]:
39
+ """Load dedupe_keys from applied files in the last N days."""
40
+ keys = set()
41
+ base_date = datetime.strptime(target_date, "%Y-%m-%d")
42
+ for i in range(days):
43
+ d = (base_date - timedelta(days=i)).strftime("%Y-%m-%d")
44
+ applied_file = DEEP_SLEEP_DIR / f"{d}-applied.json"
45
+ if applied_file.exists():
46
+ try:
47
+ with open(applied_file) as f:
48
+ data = json.load(f)
49
+ for action in data.get("applied_actions", []):
50
+ dk = action.get("dedupe_key", "")
51
+ if dk:
52
+ keys.add(dk)
53
+ except (json.JSONDecodeError, KeyError):
54
+ continue
55
+ return keys
56
+
57
+
58
+ def backup_db(db_path: Path, run_id: str) -> Path | None:
59
+ """Create a backup of a database before mutations."""
60
+ if not db_path.exists():
61
+ return None
62
+ backup_path = BACKUP_DIR / f"{run_id}-backup-{db_path.name}"
73
63
  try:
74
- conn = sqlite3.connect(str(cog_db))
75
- conn.execute(
76
- "INSERT INTO trust_events (event, context, points, created_at) VALUES (?, ?, ?, ?)",
77
- ("deep_sleep_violations", context, points, datetime.now().isoformat())
78
- )
79
- conn.commit()
80
- conn.close()
81
- except Exception:
82
- pass
64
+ import shutil
65
+ shutil.copy2(str(db_path), str(backup_path))
66
+ return backup_path
67
+ except Exception as e:
68
+ print(f" [apply] Warning: backup failed for {db_path.name}: {e}", file=sys.stderr)
69
+ return None
83
70
 
84
71
 
85
- def add_learning(category: str, title: str, content: str) -> bool:
86
- """Add a learning to nexo.db using real schema."""
72
+ def add_learning(category: str, title: str, content: str) -> dict:
73
+ """Add a learning to nexo.db. Returns result dict."""
87
74
  if not NEXO_DB.exists():
88
- return False
75
+ return {"success": False, "error": "nexo.db not found"}
89
76
  try:
90
77
  now = datetime.now().timestamp()
91
78
  conn = sqlite3.connect(str(NEXO_DB))
92
- conn.execute(
79
+ cursor = conn.execute(
93
80
  "INSERT INTO learnings (category, title, content, created_at, updated_at, reasoning) VALUES (?, ?, ?, ?, ?, ?)",
94
- (category, title, content, now, now, "Deep Sleep overnight analysis")
81
+ (category, title, content, now, now, "Deep Sleep v2 overnight analysis")
95
82
  )
83
+ learning_id = cursor.lastrowid
96
84
  conn.commit()
97
85
  conn.close()
98
- return True
86
+ return {"success": True, "id": learning_id}
99
87
  except Exception as e:
100
- print(f" Error adding learning: {e}", file=sys.stderr)
101
- return False
88
+ return {"success": False, "error": str(e)}
102
89
 
103
90
 
104
- def add_followup(followup_id: str, description: str, date: str = None) -> bool:
105
- """Add a followup to nexo.db using real schema."""
91
+ def create_followup(description: str, date: str = "") -> dict:
92
+ """Create a followup in nexo.db. Returns result dict."""
106
93
  if not NEXO_DB.exists():
107
- return False
94
+ return {"success": False, "error": "nexo.db not found"}
108
95
  try:
109
96
  now = datetime.now().timestamp()
97
+ # Generate a deterministic ID
98
+ fid = "NF-DS-" + hashlib.md5(description.encode()).hexdigest()[:8].upper()
110
99
  conn = sqlite3.connect(str(NEXO_DB))
111
100
  conn.execute(
112
101
  "INSERT OR IGNORE INTO followups (id, description, date, status, created_at, updated_at, reasoning) VALUES (?, ?, ?, 'PENDING', ?, ?, ?)",
113
- (followup_id, description, date or "", now, now, "Deep Sleep overnight analysis")
102
+ (fid, description, date, now, now, "Deep Sleep v2 overnight analysis")
114
103
  )
115
104
  conn.commit()
116
105
  conn.close()
117
- return True
106
+ return {"success": True, "id": fid}
118
107
  except Exception as e:
119
- print(f" Error adding followup: {e}", file=sys.stderr)
120
- return False
121
-
122
-
123
- def apply(analysis: dict):
124
- """Apply all findings from deep sleep analysis."""
125
- memory_dir = find_memory_dir()
126
- actions_taken = []
127
- memory_entries = []
128
- date = analysis["date"]
129
-
130
- print(f"\nApplying findings for {date}...")
131
-
132
- # 1. Uncaptured corrections → learnings + feedback memories
133
- for i, correction in enumerate(analysis.get("uncaptured_corrections", [])):
134
- severity = correction.get("severity", "medium")
135
- category = correction.get("category", "process")
136
- content = correction.get("what_nexo_should_have_saved", "")
137
- quote = correction.get("quote", "")
138
-
139
- # All corrections → learnings
140
- learning_title = f"[Deep Sleep] {content[:80]}"
141
- learning_content = f"User said: \"{quote}\"\nContext: {correction.get('context', '')}\nRepeated: {correction.get('times_repeated', 1)} times"
142
- if add_learning(category, learning_title, learning_content):
143
- actions_taken.append(f"learning_add: {learning_title[:50]}")
144
-
145
- # High/critical → also feedback memories
146
- if severity in ("high", "critical"):
147
- safe_name = category.replace(" ", "_").lower()
148
- filename = f"ds_{date}_{safe_name}_{i}.md"
149
- write_feedback_memory(
150
- memory_dir, filename,
151
- name=content[:60],
152
- description=f"Deep sleep detected uncaptured correction ({severity})",
153
- content=f"{content}\n\n**Why:** User said: \"{quote}\"\nContext: {correction.get('context', '')}\n\n**How to apply:** {content}"
154
- )
155
- memory_entries.append({
156
- "title": content[:40],
157
- "filename": filename,
158
- "summary": f"Deep sleep {date}, severity {severity}"
108
+ return {"success": False, "error": str(e)}
109
+
110
+
111
+ def update_calibration_mood(synthesis: dict) -> dict:
112
+ """Update mood in calibration.json based on emotional analysis."""
113
+ calibration_file = NEXO_HOME / "brain" / "calibration.json"
114
+ if not calibration_file.exists():
115
+ return {"success": False, "error": "calibration.json not found"}
116
+
117
+ emotional_day = synthesis.get("emotional_day", {})
118
+ if not emotional_day:
119
+ return {"success": False, "error": "no emotional_day data"}
120
+
121
+ try:
122
+ cal = json.loads(calibration_file.read_text())
123
+
124
+ # Add/update mood history
125
+ if "mood_history" not in cal:
126
+ cal["mood_history"] = []
127
+
128
+ cal["mood_history"].append({
129
+ "date": synthesis.get("date", ""),
130
+ "score": emotional_day.get("mood_score", 0.5),
131
+ "arc": emotional_day.get("mood_arc", ""),
132
+ "triggers": emotional_day.get("recurring_triggers", {}),
133
+ })
134
+
135
+ # Keep last 30 days
136
+ cal["mood_history"] = cal["mood_history"][-30:]
137
+
138
+ # Apply calibration recommendation if any
139
+ rec = emotional_day.get("calibration_recommendation")
140
+ if rec and rec != "null":
141
+ if "calibration_notes" not in cal:
142
+ cal["calibration_notes"] = []
143
+ cal["calibration_notes"].append({
144
+ "date": synthesis.get("date", ""),
145
+ "recommendation": rec,
146
+ "applied": False,
159
147
  })
160
- actions_taken.append(f"feedback_write: {filename}")
161
-
162
- # 2. Missed commitments → followups
163
- for i, commitment in enumerate(analysis.get("missed_commitments", [])):
164
- fid = f"NF-DS-{date}-{i}"
165
- desc = f"[Deep Sleep] {commitment.get('commitment', '')[:100]}"
166
- if add_followup(fid, desc, commitment.get("due_date")):
167
- actions_taken.append(f"followup: {desc[:50]}")
168
-
169
- # 3. Trust adjustments for critical violations
170
- critical_violations = [v for v in analysis.get("protocol_violations", []) if v.get("severity") == "critical"]
171
- if critical_violations:
172
- points = -3 * len(critical_violations)
173
- adjust_trust(points, f"{len(critical_violations)} critical violations on {date}")
174
- actions_taken.append(f"trust: {points} points ({len(critical_violations)} critical violations)")
175
-
176
- # 3. Update MEMORY.md index
177
- update_memory_index(memory_dir, memory_entries)
178
- if memory_entries:
179
- actions_taken.append(f"memory_index: {len(memory_entries)} entries added")
180
-
181
- # 4. Save applied actions log
182
- applied_log = {
183
- "date": date,
184
- "applied_at": datetime.now().isoformat(),
185
- "actions_taken": actions_taken,
186
- "corrections_processed": len(analysis.get("uncaptured_corrections", [])),
187
- "compliance": analysis.get("protocol_compliance", {}).get("overall_compliance", 0)
148
+ # Keep last 10
149
+ cal["calibration_notes"] = cal["calibration_notes"][-10:]
150
+
151
+ calibration_file.write_text(json.dumps(cal, indent=2, ensure_ascii=False))
152
+ return {"success": True, "mood_score": emotional_day.get("mood_score")}
153
+ except Exception as e:
154
+ return {"success": False, "error": str(e)}
155
+
156
+
157
+ def create_abandoned_followups(synthesis: dict) -> list[dict]:
158
+ """Create followups for truly abandoned projects."""
159
+ results = []
160
+ abandoned = synthesis.get("abandoned_projects", [])
161
+ for proj in abandoned:
162
+ if proj.get("has_followup"):
163
+ continue
164
+ rec = proj.get("recommendation", "")
165
+ if "ignore" in rec.lower():
166
+ continue
167
+ result = create_followup(
168
+ description=f"[Abandoned] {proj.get('description', '')}",
169
+ date="" # No date it's a discovered gap
170
+ )
171
+ results.append(result)
172
+ return results
173
+
174
+
175
+ def generate_session_tone(synthesis: dict, target_date: str) -> dict:
176
+ """Generate emotional tone guidance for next session startup.
177
+
178
+ This is the 'psychology' layer — tells NEXO how to behave emotionally
179
+ based on yesterday's analysis. Read by startup hook to adapt greeting.
180
+ """
181
+ emotional = synthesis.get("emotional_day", {})
182
+ productivity = synthesis.get("productivity_day", {})
183
+ patterns = synthesis.get("cross_session_patterns", [])
184
+ abandoned = synthesis.get("abandoned_projects", [])
185
+ mood_score = emotional.get("mood_score", 0.5)
186
+ corrections = productivity.get("total_corrections", 0)
187
+ proactivity = productivity.get("overall_proactivity", "mixed")
188
+
189
+ tone = {
190
+ "date": target_date,
191
+ "mood_yesterday": mood_score,
192
+ "approach": "neutral",
193
+ "opening_style": "normal",
194
+ "acknowledge_mistakes": False,
195
+ "mistakes_to_own": [],
196
+ "motivational": False,
197
+ "reduce_load": False,
198
+ "suggested_greeting_context": "",
188
199
  }
189
200
 
190
- applied_file = DEEP_SLEEP_DIR / f"{date}-applied.json"
191
- with open(applied_file, "w") as f:
192
- json.dump(applied_log, f, indent=2, ensure_ascii=False)
201
+ # Agent made many mistakes yesterday → own it, apologize, show learning
202
+ if corrections > 5:
203
+ tone["acknowledge_mistakes"] = True
204
+ tone["opening_style"] = "humble"
205
+ # Collect what went wrong
206
+ high_patterns = [p["pattern"] for p in patterns if p.get("severity") == "high"]
207
+ tone["mistakes_to_own"] = high_patterns[:3]
208
+ tone["suggested_greeting_context"] = (
209
+ f"Yesterday the agent needed {corrections} corrections. "
210
+ f"Acknowledge specific mistakes, show what was learned, "
211
+ f"and demonstrate improvement from the first interaction."
212
+ )
213
+
214
+ # User had a bad day → supportive, less pressure
215
+ if mood_score < 0.4:
216
+ tone["approach"] = "supportive"
217
+ tone["motivational"] = True
218
+ tone["reduce_load"] = True
219
+ frustration_triggers = emotional.get("recurring_triggers", {}).get("frustration", [])
220
+ tone["suggested_greeting_context"] += (
221
+ f" User had a tough day (mood {mood_score:.0%}). "
222
+ f"Be supportive, acknowledge the difficulty, and propose a lighter start. "
223
+ f"Avoid these frustration triggers: {', '.join(frustration_triggers[:3])}."
224
+ )
193
225
 
194
- print(f"Applied {len(actions_taken)} actions:")
195
- for a in actions_taken:
196
- print(f" {a}")
226
+ # User had a great day → reinforce, push momentum
227
+ elif mood_score > 0.7:
228
+ tone["approach"] = "energetic"
229
+ tone["motivational"] = True
230
+ flow_triggers = emotional.get("recurring_triggers", {}).get("flow", [])
231
+ tone["suggested_greeting_context"] += (
232
+ f" User had a great day (mood {mood_score:.0%}). "
233
+ f"Reinforce the momentum. Reference yesterday's wins. "
234
+ f"Propose ambitious next steps. Flow triggers: {', '.join(flow_triggers[:3])}."
235
+ )
197
236
 
198
- return applied_log
237
+ # Agent was too reactive → be proactive today
238
+ if proactivity == "reactive":
239
+ tone["approach"] = "proactive"
240
+ tone["suggested_greeting_context"] += (
241
+ " Agent was too reactive yesterday — today lead with proposals, "
242
+ "don't wait for instructions."
243
+ )
244
+
245
+ # There are abandoned projects → gently bring up
246
+ if abandoned:
247
+ truly_abandoned = [a for a in abandoned if not a.get("has_followup")]
248
+ if truly_abandoned:
249
+ tone["suggested_greeting_context"] += (
250
+ f" {len(truly_abandoned)} project(s) were started but not finished. "
251
+ f"Offer to pick them up today without pressure."
252
+ )
253
+
254
+ return tone
255
+
256
+
257
+ def write_morning_briefing(target_date: str, synthesis: dict) -> Path:
258
+ """Write the morning briefing file from synthesis data."""
259
+ briefing_dir = OPERATIONS_DIR
260
+ briefing_dir.mkdir(parents=True, exist_ok=True)
261
+ briefing_file = briefing_dir / "morning-briefing.md"
262
+
263
+ # Generate session tone for startup
264
+ tone = generate_session_tone(synthesis, target_date)
265
+ tone_file = briefing_dir / "session-tone.json"
266
+ tone_file.write_text(json.dumps(tone, indent=2, ensure_ascii=False))
267
+
268
+ lines = [
269
+ f"# Morning Briefing -- {target_date}",
270
+ f"_Generated by Deep Sleep at {datetime.now().strftime('%H:%M')}_",
271
+ ""
272
+ ]
273
+
274
+ # Summary
275
+ summary = synthesis.get("summary", "")
276
+ if summary:
277
+ lines.append(f"> {summary}")
278
+ lines.append("")
279
+
280
+ # Morning agenda
281
+ agenda = synthesis.get("morning_agenda", [])
282
+ if agenda:
283
+ lines.append("## Agenda")
284
+ lines.append("")
285
+ for item in agenda:
286
+ priority = item.get("priority", "?")
287
+ title = item.get("title", "")
288
+ desc = item.get("description", "")
289
+ item_type = item.get("type", "")
290
+ lines.append(f"### {priority}. {title}")
291
+ if item_type:
292
+ lines.append(f"_Type: {item_type}_")
293
+ lines.append(desc)
294
+ if item.get("context"):
295
+ lines.append(f"\n> {item['context']}")
296
+ lines.append("")
297
+
298
+ # Emotional day
299
+ emotional = synthesis.get("emotional_day", {})
300
+ if emotional:
301
+ mood_score = emotional.get("mood_score", 0.5)
302
+ mood_bar = "🟢" if mood_score >= 0.7 else "🟡" if mood_score >= 0.4 else "🔴"
303
+ lines.append(f"## Mood {mood_bar} {mood_score:.0%}")
304
+ lines.append("")
305
+ if emotional.get("mood_arc"):
306
+ lines.append(emotional["mood_arc"])
307
+ triggers = emotional.get("recurring_triggers", {})
308
+ if triggers.get("frustration"):
309
+ lines.append(f"**Frustration triggers:** {', '.join(triggers['frustration'])}")
310
+ if triggers.get("flow"):
311
+ lines.append(f"**Flow triggers:** {', '.join(triggers['flow'])}")
312
+ if emotional.get("calibration_recommendation"):
313
+ lines.append(f"\n💡 **Recommendation:** {emotional['calibration_recommendation']}")
314
+ lines.append("")
315
+
316
+ # Productivity
317
+ productivity = synthesis.get("productivity_day", {})
318
+ if productivity:
319
+ lines.append("## Productivity")
320
+ lines.append("")
321
+ lines.append(f"- Corrections needed: {productivity.get('total_corrections', '?')}")
322
+ lines.append(f"- Proactivity: {productivity.get('overall_proactivity', '?')}")
323
+ if productivity.get("tool_insights"):
324
+ lines.append(f"- Tools: {productivity['tool_insights']}")
325
+ inefficiencies = productivity.get("systemic_inefficiencies", [])
326
+ if inefficiencies:
327
+ lines.append(f"- Issues: {', '.join(inefficiencies)}")
328
+ lines.append("")
329
+
330
+ # Abandoned projects
331
+ abandoned = synthesis.get("abandoned_projects", [])
332
+ if abandoned:
333
+ truly_abandoned = [a for a in abandoned if not a.get("has_followup")]
334
+ if truly_abandoned:
335
+ lines.append("## Abandoned Projects")
336
+ lines.append("")
337
+ for a in truly_abandoned:
338
+ lines.append(f"- {a.get('description', '?')}")
339
+ if a.get("recommendation"):
340
+ lines.append(f" → {a['recommendation']}")
341
+ lines.append("")
342
+
343
+ # Cross-session patterns
344
+ patterns = synthesis.get("cross_session_patterns", [])
345
+ if patterns:
346
+ lines.append("## Patterns Detected")
347
+ lines.append("")
348
+ for p in patterns:
349
+ severity = p.get("severity", "")
350
+ lines.append(f"- **[{severity}]** {p.get('pattern', '')}")
351
+ sessions = p.get("sessions", [])
352
+ if sessions:
353
+ lines.append(f" Sessions: {', '.join(sessions)}")
354
+ lines.append("")
355
+
356
+ # Draft actions (things that need user decision)
357
+ draft_actions = [
358
+ a for a in synthesis.get("actions", [])
359
+ if a.get("action_class") == "draft_for_morning"
360
+ ]
361
+ if draft_actions:
362
+ lines.append("## Items for Review")
363
+ lines.append("")
364
+ for a in draft_actions:
365
+ confidence = a.get("confidence", 0)
366
+ lines.append(f"- **{a.get('action_type', '')}** (confidence: {confidence:.0%})")
367
+ content = a.get("content", {})
368
+ if isinstance(content, dict):
369
+ title = content.get("title", content.get("description", ""))
370
+ lines.append(f" {title}")
371
+ evidence = a.get("evidence", [])
372
+ if evidence and isinstance(evidence, list):
373
+ for ev in evidence[:2]:
374
+ quote = ev.get("quote", "")
375
+ if quote:
376
+ lines.append(f' > "{quote}"')
377
+ lines.append("")
378
+
379
+ # Context packets
380
+ packets = synthesis.get("context_packets", [])
381
+ if packets:
382
+ lines.append("## Context for Today's Work")
383
+ lines.append("")
384
+ for p in packets:
385
+ lines.append(f"### {p.get('topic', 'Unknown')}")
386
+ lines.append(f"**Last state:** {p.get('last_state', 'N/A')}")
387
+ files = p.get("key_files", [])
388
+ if files:
389
+ lines.append(f"**Files:** {', '.join(files)}")
390
+ questions = p.get("open_questions", [])
391
+ if questions:
392
+ lines.append("**Open questions:**")
393
+ for q in questions:
394
+ lines.append(f" - {q}")
395
+ lines.append("")
396
+
397
+ briefing_file.write_text("\n".join(lines), encoding="utf-8")
398
+ return briefing_file
399
+
400
+
401
+ def apply_action(action: dict, run_id: str) -> dict:
402
+ """Apply a single action and return the result log."""
403
+ action_type = action.get("action_type", "")
404
+ action_class = action.get("action_class", "")
405
+ content = action.get("content", {})
406
+ dedupe_key = action.get("dedupe_key", "")
407
+
408
+ applied_id = f"{run_id}-{hashlib.md5(dedupe_key.encode()).hexdigest()[:8]}"
409
+
410
+ log_entry = {
411
+ "applied_action_id": applied_id,
412
+ "action_type": action_type,
413
+ "action_class": action_class,
414
+ "dedupe_key": dedupe_key,
415
+ "timestamp": datetime.now().isoformat(),
416
+ "status": "skipped",
417
+ "details": {}
418
+ }
419
+
420
+ # Only auto_apply actions get executed
421
+ if action_class != "auto_apply":
422
+ log_entry["status"] = "deferred_to_morning"
423
+ log_entry["details"] = {"reason": "action_class is not auto_apply"}
424
+ return log_entry
425
+
426
+ if not isinstance(content, dict):
427
+ log_entry["status"] = "error"
428
+ log_entry["details"] = {"error": "content is not a dict"}
429
+ return log_entry
430
+
431
+ if action_type == "learning_add":
432
+ result = add_learning(
433
+ category=content.get("category", "process"),
434
+ title=content.get("title", "Deep Sleep finding"),
435
+ content=content.get("content", content.get("description", ""))
436
+ )
437
+ log_entry["status"] = "applied" if result.get("success") else "error"
438
+ log_entry["details"] = result
439
+
440
+ elif action_type == "followup_create":
441
+ result = create_followup(
442
+ description=content.get("description", content.get("title", "")),
443
+ date=content.get("date", "")
444
+ )
445
+ log_entry["status"] = "applied" if result.get("success") else "error"
446
+ log_entry["details"] = result
447
+
448
+ elif action_type == "morning_briefing_item":
449
+ # These are included in the briefing file, not applied separately
450
+ log_entry["status"] = "included_in_briefing"
451
+
452
+ else:
453
+ log_entry["status"] = "unknown_type"
454
+ log_entry["details"] = {"error": f"Unknown action_type: {action_type}"}
455
+
456
+ return log_entry
199
457
 
200
458
 
201
459
  def main():
202
- date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
460
+ target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
203
461
 
204
- analysis_file = DEEP_SLEEP_DIR / f"{date}-analysis.json"
205
- if not analysis_file.exists():
206
- print(f"No analysis found for {date}. Run analyze_session.py first.")
462
+ synthesis_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
463
+ if not synthesis_file.exists():
464
+ print(f"[apply] No synthesis file for {target_date}. Run synthesize.py first.")
207
465
  sys.exit(1)
208
466
 
209
- with open(analysis_file) as f:
210
- analysis = json.load(f)
467
+ with open(synthesis_file) as f:
468
+ synthesis = json.load(f)
469
+
470
+ run_id = generate_run_id(target_date)
471
+ actions = synthesis.get("actions", [])
472
+ print(f"[apply] Phase 4: Applying findings for {target_date} (run: {run_id})")
473
+ print(f"[apply] Actions to process: {len(actions)}")
474
+
475
+ # Load recent dedupe keys for idempotency
476
+ existing_keys = load_recent_dedupe_keys(target_date)
477
+ print(f"[apply] Existing dedupe keys (7d): {len(existing_keys)}")
478
+
479
+ # Backup databases before mutations
480
+ auto_apply_count = sum(1 for a in actions if a.get("action_class") == "auto_apply")
481
+ if auto_apply_count > 0:
482
+ print("[apply] Creating database backups...")
483
+ nexo_backup = backup_db(NEXO_DB, run_id)
484
+ cog_backup = backup_db(COGNITIVE_DB, run_id)
485
+ if nexo_backup:
486
+ print(f" Backup: {nexo_backup}")
487
+ if cog_backup:
488
+ print(f" Backup: {cog_backup}")
489
+
490
+ # Process actions
491
+ applied_actions = []
492
+ stats = {"applied": 0, "deferred": 0, "skipped_dedupe": 0, "errors": 0}
493
+
494
+ for action in actions:
495
+ dedupe_key = action.get("dedupe_key", "")
496
+
497
+ # Idempotency check
498
+ if dedupe_key and dedupe_key in existing_keys:
499
+ applied_actions.append({
500
+ "applied_action_id": f"{run_id}-deduped",
501
+ "action_type": action.get("action_type"),
502
+ "dedupe_key": dedupe_key,
503
+ "status": "skipped_dedupe",
504
+ "timestamp": datetime.now().isoformat()
505
+ })
506
+ stats["skipped_dedupe"] += 1
507
+ continue
508
+
509
+ result = apply_action(action, run_id)
510
+ applied_actions.append(result)
511
+
512
+ if result["status"] == "applied":
513
+ stats["applied"] += 1
514
+ print(f" Applied: {action.get('action_type')} -- {action.get('content', {}).get('title', '')[:50]}")
515
+ elif result["status"] == "deferred_to_morning":
516
+ stats["deferred"] += 1
517
+ elif result["status"] == "error":
518
+ stats["errors"] += 1
519
+ print(f" Error: {result.get('details', {}).get('error', 'unknown')}", file=sys.stderr)
520
+
521
+ # Update mood in calibration.json
522
+ print("[apply] Updating mood/calibration...")
523
+ mood_result = update_calibration_mood(synthesis)
524
+ if mood_result.get("success"):
525
+ stats["applied"] += 1
526
+ print(f" Mood score: {mood_result.get('mood_score', '?')}")
527
+ else:
528
+ print(f" Mood skip: {mood_result.get('error', '?')}")
529
+
530
+ # Create followups for abandoned projects
531
+ abandoned_results = create_abandoned_followups(synthesis)
532
+ for r in abandoned_results:
533
+ if r.get("success"):
534
+ stats["applied"] += 1
535
+ print(f" Abandoned project followup: {r.get('id')}")
536
+
537
+ # Write morning briefing
538
+ print("[apply] Writing morning briefing...")
539
+ briefing_path = write_morning_briefing(target_date, synthesis)
540
+ print(f" Briefing: {briefing_path}")
541
+
542
+ # Write applied log
543
+ applied_log = {
544
+ "date": target_date,
545
+ "run_id": run_id,
546
+ "applied_at": datetime.now().isoformat(),
547
+ "stats": stats,
548
+ "applied_actions": applied_actions,
549
+ "summary": synthesis.get("summary", ""),
550
+ }
211
551
 
212
- result = apply(analysis)
552
+ applied_file = DEEP_SLEEP_DIR / f"{target_date}-applied.json"
553
+ with open(applied_file, "w") as f:
554
+ json.dump(applied_log, f, indent=2, ensure_ascii=False)
213
555
 
214
- compliance = analysis.get("protocol_compliance", {}).get("overall_compliance", 0)
215
- print(f"\nDeep Sleep {date} — {result['corrections_processed']} corrections, "
216
- f"{compliance:.0%} compliance, {len(result['actions_taken'])} actions applied")
556
+ print(f"\n[apply] Done.")
557
+ print(f" Applied: {stats['applied']}")
558
+ print(f" Deferred to morning: {stats['deferred']}")
559
+ print(f" Skipped (dedupe): {stats['skipped_dedupe']}")
560
+ print(f" Errors: {stats['errors']}")
561
+ print(f"[apply] Log: {applied_file}")
217
562
 
218
563
 
219
564
  if __name__ == "__main__":