nexo-brain 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
package/README.md CHANGED
@@ -283,13 +283,13 @@ NEXO Brain doesn't just respond — it runs 15 autonomous processes in the backg
283
283
  | **prevent-sleep** | Always (daemon) | Keeps machine awake for nocturnal processes (caffeinate/systemd-inhibit) |
284
284
  | **evolution** | Weekly (Sun) | Self-improvement proposals — NEXO suggests and applies enhancements |
285
285
  | **followup-hygiene** | Weekly (Sun) | Normalizes statuses, flags stale followups, cleans orphans |
286
+ | **learning-housekeep** | 03:15 daily | Dedup learnings, adjust weights by usage, process overdue reviews, reconcile decision outcomes |
286
287
  | **immune** | Every 30 min | Quarantine processing, memory promotion/rejection, synaptic pruning |
287
- | **synthesis** | Every 2 hours | Memory synthesis — discovers cross-memory patterns |
288
- | **backup** | Every hour | SQLite database backups |
289
- | **watchdog** | Every 5 min | Monitors services, LaunchAgents, and infrastructure health |
288
+ | **synthesis** | 06:00 daily | Memory synthesis — discovers cross-memory patterns |
289
+ | **watchdog** | Every 30 min | Monitors services, LaunchAgents, and infrastructure health |
290
290
  | **auto-close-sessions** | Every 5 min | Cleans stale sessions |
291
291
 
292
- All processes are defined in `src/crons/manifest.json` and auto-synced to your system by `nexo_update`. On macOS they run via LaunchAgents; on Linux via systemd user timers (or crontab fallback). Personal crons (your own scripts) are never touched by the sync. If your Mac was asleep during a scheduled process, the catch-up script re-runs everything in order when it wakes.
292
+ Core processes are defined in `src/crons/manifest.json` and auto-synced to your system by `nexo_update`. On macOS they run via LaunchAgents; on Linux via systemd user timers. `tcc-approve`, `prevent-sleep`, and `backup` are platform/personal helpers — not in the manifest but listed above for completeness. Personal crons (your own scripts) are never touched by the sync. If your Mac was asleep during a scheduled process, the catch-up script re-runs everything in order when it wakes.
293
293
 
294
294
  ## Deep Sleep v2 — Overnight Learning (v2.1.0)
295
295
 
@@ -755,7 +755,7 @@ If NEXO Brain is useful to you, consider:
755
755
  - **Auto-resolve followups**: Change log entries automatically cross-reference and complete matching open followups.
756
756
  - **Free-form learning categories**: No more hardcoded category validation — use any category name.
757
757
  - **CLAUDE.md template rewrite**: 494 to 127 lines, compact procedural format with full heartbeat signal reactions.
758
- - **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var. Zero personal data in the repo.
758
+ - **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var. No credentials or personal data in the distributed package. Migration scripts and maintainer tooling use configurable paths.
759
759
 
760
760
  ### v1.6.0 — Nervous System + Dashboard v2 (2026-03-30)
761
761
  - **Nervous System**: 11 autonomous scripts (decay, deep sleep, self-audit, catchup, evolution, followup hygiene, immune, watchdog, github monitor, learning validator)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
6
6
  "bin": {
@@ -56,8 +56,11 @@
56
56
  "bin/nexo-brain.js",
57
57
  "bin/postinstall.js",
58
58
  "src/",
59
+ "!src/**/__pycache__",
60
+ "!src/**/*.pyc",
61
+ "!src/**/*.pyo",
59
62
  "templates/",
60
- "scripts/",
61
- "tests/"
63
+ "README.md",
64
+ "LICENSE"
62
65
  ]
63
66
  }
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO Auto-Update — lightweight startup check for git updates and file-based migrations.
2
3
 
3
4
  Called once per server startup. Respects a 1-hour cooldown to avoid redundant checks.
@@ -93,6 +94,27 @@ def _read_package_version() -> str:
93
94
  return "unknown"
94
95
 
95
96
 
97
+ # ── Hook sync ────────────────────────────────────────────────────────
98
+
99
+ def _sync_hooks():
100
+ """Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after a git pull."""
101
+ import shutil
102
+ hooks_src = SRC_DIR / "hooks"
103
+ hooks_dest = NEXO_HOME / "hooks"
104
+ if not hooks_src.is_dir():
105
+ return
106
+ hooks_dest.mkdir(parents=True, exist_ok=True)
107
+ synced = 0
108
+ for f in hooks_src.iterdir():
109
+ if f.is_file() and f.suffix == ".sh":
110
+ dest = hooks_dest / f.name
111
+ shutil.copy2(str(f), str(dest))
112
+ os.chmod(str(dest), 0o755)
113
+ synced += 1
114
+ if synced:
115
+ _log(f"Synced {synced} hook(s) to {hooks_dest}")
116
+
117
+
96
118
  # ── Git-based auto-update ────────────────────────────────────────────
97
119
 
98
120
  def _check_git_updates() -> str | None:
@@ -140,6 +162,10 @@ def _check_git_updates() -> str | None:
140
162
  # Run DB migrations after pull
141
163
  _run_db_migrations()
142
164
 
165
+ # Sync hooks to NEXO_HOME (nexo-brain.js copies them on install,
166
+ # but auto-update via git pull bypasses nexo-brain.js)
167
+ _sync_hooks()
168
+
143
169
  msg = f"Auto-updated: {old_version} -> {new_version}" if old_version != new_version else f"Auto-updated (v{new_version}, new commits)"
144
170
  _log(msg)
145
171
  return msg
@@ -70,35 +70,28 @@
70
70
  {
71
71
  "id": "followup-hygiene",
72
72
  "script": "scripts/nexo-followup-hygiene.py",
73
- "schedule": {"hour": 5, "minute": 0},
73
+ "schedule": {"hour": 5, "minute": 0, "weekday": 0},
74
74
  "description": "Clean stale followups, archive completed, validate dates",
75
75
  "core": true
76
76
  },
77
77
  {
78
78
  "id": "synthesis",
79
79
  "script": "scripts/nexo-synthesis.py",
80
- "interval_seconds": 7200,
81
- "description": "Periodic synthesis — cross-reference learnings, decisions, changes",
80
+ "schedule": {"hour": 6, "minute": 0},
81
+ "description": "Daily synthesis — cross-reference learnings, decisions, changes",
82
82
  "core": true
83
83
  },
84
84
  {
85
85
  "id": "auto-close-sessions",
86
- "script": "scripts/nexo-auto-close-sessions.py",
86
+ "script": "auto_close_sessions.py",
87
87
  "interval_seconds": 300,
88
88
  "description": "Close stale sessions that lost their parent process",
89
89
  "core": true
90
90
  },
91
- {
92
- "id": "github-monitor",
93
- "script": "scripts/nexo-github-monitor.py",
94
- "schedule": {"hour": 8, "minute": 0},
95
- "description": "Monitor GitHub repo — issues, PRs, stars, auto-respond",
96
- "core": true
97
- },
98
- {
91
+ {
99
92
  "id": "catchup",
100
93
  "script": "scripts/nexo-catchup.py",
101
- "schedule": {"hour": 8, "minute": 30},
94
+ "run_at_load": true,
102
95
  "description": "Morning catchup briefing for the user",
103
96
  "core": true
104
97
  }
package/src/crons/sync.py CHANGED
@@ -42,15 +42,55 @@ def load_manifest() -> list[dict]:
42
42
  return data.get("crons", [])
43
43
 
44
44
 
45
+ def _copy_script_to_nexo_home(src: Path) -> Path:
46
+ """Copy a script from NEXO_CODE to NEXO_HOME/scripts/ for Sandbox compatibility.
47
+
48
+ macOS Sandbox blocks LaunchAgents from executing scripts in ~/Documents/.
49
+ We copy scripts to NEXO_HOME/scripts/ which is outside the Sandbox restricted paths.
50
+ """
51
+ dest_dir = NEXO_HOME / "scripts"
52
+ dest_dir.mkdir(parents=True, exist_ok=True)
53
+
54
+ if src.is_dir():
55
+ import shutil
56
+ dest = dest_dir / src.name
57
+ if dest.exists():
58
+ shutil.rmtree(dest)
59
+ shutil.copytree(src, dest)
60
+ return dest
61
+ else:
62
+ dest = dest_dir / src.name
63
+ import shutil
64
+ shutil.copy2(src, dest)
65
+ dest.chmod(0o755)
66
+ return dest
67
+
68
+
45
69
  def build_plist(cron: dict) -> dict:
46
70
  """Build a macOS LaunchAgent plist dict from a manifest entry."""
47
71
  cron_id = cron["id"]
48
72
  label = f"{LABEL_PREFIX}{cron_id}"
49
- script_path = str(NEXO_CODE / cron["script"])
73
+ script_src = NEXO_CODE / cron["script"]
50
74
  script_type = cron.get("type", "python")
51
75
 
76
+ # Copy scripts to NEXO_HOME/scripts/ to avoid macOS Sandbox restrictions
77
+ script_dest = _copy_script_to_nexo_home(script_src)
78
+ script_path = str(script_dest)
79
+
80
+ # Also copy the wrapper and any subdirectories (e.g., deep-sleep/)
81
+ wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
82
+ wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
83
+ wrapper_path = str(wrapper_dest)
84
+
85
+ # Copy script subdirectories if they exist (e.g., deep-sleep/ for nexo-deep-sleep.sh)
86
+ script_name = script_src.stem # e.g., "nexo-deep-sleep"
87
+ subdir_name = script_name.replace("nexo-", "") # e.g., "deep-sleep"
88
+ subdir_src = NEXO_CODE / "scripts" / subdir_name
89
+ if subdir_src.is_dir():
90
+ _copy_script_to_nexo_home(subdir_src)
91
+
52
92
  if script_type == "shell":
53
- program_args = ["/bin/bash", script_path]
93
+ program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
54
94
  else:
55
95
  # Find python3
56
96
  python_candidates = [
@@ -64,7 +104,7 @@ def build_plist(cron: dict) -> dict:
64
104
  if Path(p).exists():
65
105
  python_bin = p
66
106
  break
67
- program_args = [python_bin, script_path]
107
+ program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
68
108
 
69
109
  plist = {
70
110
  "Label": label,
@@ -84,7 +124,9 @@ def build_plist(cron: dict) -> dict:
84
124
  }
85
125
 
86
126
  # Schedule
87
- if "interval_seconds" in cron:
127
+ if cron.get("run_at_load"):
128
+ plist["RunAtLoad"] = True
129
+ elif "interval_seconds" in cron:
88
130
  plist["StartInterval"] = cron["interval_seconds"]
89
131
  elif "schedule" in cron:
90
132
  cal = {}
@@ -126,6 +168,8 @@ def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
126
168
  return True
127
169
  if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
128
170
  return True
171
+ if existing.get("RunAtLoad") != new_plist.get("RunAtLoad"):
172
+ return True
129
173
  return False
130
174
 
131
175
 
@@ -157,8 +201,12 @@ def unload_plist(plist_path: Path, dry_run: bool):
157
201
 
158
202
 
159
203
  def sync(dry_run: bool = False):
160
- if platform.system() != "Darwin":
161
- log("Not macOS cron sync only supports LaunchAgents. Skipping.")
204
+ system = platform.system()
205
+ if system == "Linux":
206
+ sync_linux(dry_run)
207
+ return
208
+ if system != "Darwin":
209
+ log(f"Unsupported platform: {system}. Skipping.")
162
210
  return
163
211
 
164
212
  LOG_DIR.mkdir(parents=True, exist_ok=True)
@@ -210,6 +258,102 @@ def sync(dry_run: bool = False):
210
258
  log("Sync complete.")
211
259
 
212
260
 
261
+ def sync_linux(dry_run: bool = False):
262
+ """Sync manifest to systemd user timers (Linux)."""
263
+ unit_dir = Path.home() / ".config" / "systemd" / "user"
264
+ unit_dir.mkdir(parents=True, exist_ok=True)
265
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
266
+
267
+ manifest_crons = load_manifest()
268
+ wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
269
+ wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
270
+
271
+ log(f"Manifest: {len(manifest_crons)} core crons")
272
+
273
+ python_bin = "/usr/bin/python3"
274
+ for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
275
+ if Path(p).exists():
276
+ python_bin = p
277
+ break
278
+
279
+ for cron in manifest_crons:
280
+ cron_id = cron["id"]
281
+ script_src = NEXO_CODE / cron["script"]
282
+ script_dest = _copy_script_to_nexo_home(script_src)
283
+ script_type = cron.get("type", "python")
284
+
285
+ # Copy subdirectories
286
+ subdir_name = script_src.stem.replace("nexo-", "")
287
+ subdir_src = NEXO_CODE / "scripts" / subdir_name
288
+ if subdir_src.is_dir():
289
+ _copy_script_to_nexo_home(subdir_src)
290
+
291
+ if script_type == "shell":
292
+ exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} /bin/bash {script_dest}"
293
+ else:
294
+ exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} {python_bin} {script_dest}"
295
+
296
+ service_path = unit_dir / f"nexo-{cron_id}.service"
297
+ timer_path = unit_dir / f"nexo-{cron_id}.timer"
298
+
299
+ service_content = f"""[Unit]
300
+ Description=NEXO: {cron.get('description', cron_id)}
301
+
302
+ [Service]
303
+ Type=oneshot
304
+ ExecStart={exec_cmd}
305
+ Environment=NEXO_HOME={NEXO_HOME}
306
+ Environment=NEXO_CODE={NEXO_CODE}
307
+ Environment=HOME={Path.home()}
308
+ """
309
+
310
+ if cron.get("run_at_load"):
311
+ timer_spec = "OnBootSec=0"
312
+ elif "interval_seconds" in cron:
313
+ timer_spec = f"OnUnitActiveSec={cron['interval_seconds']}s\nOnBootSec=60s"
314
+ elif "schedule" in cron:
315
+ s = cron["schedule"]
316
+ h, m = s.get("hour", 0), s.get("minute", 0)
317
+ if "weekday" in s:
318
+ days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
319
+ timer_spec = f"OnCalendar={days[s['weekday']]} *-*-* {h:02d}:{m:02d}:00"
320
+ else:
321
+ timer_spec = f"OnCalendar=*-*-* {h:02d}:{m:02d}:00"
322
+ else:
323
+ log(f" SKIP {cron_id}: no schedule or interval")
324
+ continue
325
+
326
+ timer_content = f"""[Unit]
327
+ Description=NEXO timer: {cron.get('description', cron_id)}
328
+
329
+ [Timer]
330
+ {timer_spec}
331
+ Persistent=true
332
+
333
+ [Install]
334
+ WantedBy=timers.target
335
+ """
336
+
337
+ if dry_run:
338
+ log(f" DRY-RUN: would install {cron_id}")
339
+ continue
340
+
341
+ service_path.write_text(service_content)
342
+ timer_path.write_text(timer_content)
343
+ log(f" Installed: {cron_id}")
344
+
345
+ if not dry_run:
346
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
347
+ for cron in manifest_crons:
348
+ subprocess.run(
349
+ ["systemctl", "--user", "enable", "--now", f"nexo-{cron['id']}.timer"],
350
+ capture_output=True
351
+ )
352
+ log("systemd timers enabled.")
353
+
354
+ log("Sync complete.")
355
+
356
+
213
357
  if __name__ == "__main__":
214
358
  dry_run = "--dry-run" in sys.argv
215
359
  if dry_run:
@@ -87,3 +87,16 @@ from db._evolution import (
87
87
  insert_evolution_metric, get_latest_metrics,
88
88
  insert_evolution_log, get_evolution_history, update_evolution_log_status,
89
89
  )
90
+
91
+ # Cron execution history
92
+ from db._cron_runs import (
93
+ cron_run_start, cron_run_end, cron_runs_recent, cron_runs_summary,
94
+ )
95
+
96
+ # Skills
97
+ from db._skills import (
98
+ create_skill, get_skill, list_skills, search_skills,
99
+ update_skill, delete_skill,
100
+ record_usage as record_skill_usage,
101
+ match_skills, merge_skills, get_skill_stats, decay_unused_skills,
102
+ )
package/src/db/_core.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """SQLite database for NEXO session coordination."""
2
3
 
3
4
  import sqlite3
@@ -0,0 +1,74 @@
1
+ """NEXO DB — Cron execution history."""
2
+ from db._core import get_db
3
+
4
+
5
+ def cron_run_start(cron_id: str) -> int:
6
+ """Record a cron starting. Returns the run ID."""
7
+ conn = get_db()
8
+ cursor = conn.execute(
9
+ "INSERT INTO cron_runs (cron_id) VALUES (?)", (cron_id,)
10
+ )
11
+ conn.commit()
12
+ return cursor.lastrowid
13
+
14
+
15
+ def cron_run_end(run_id: int, exit_code: int, summary: str = '', error: str = ''):
16
+ """Record a cron finishing."""
17
+ conn = get_db()
18
+ conn.execute(
19
+ """UPDATE cron_runs
20
+ SET ended_at = datetime('now'),
21
+ exit_code = ?,
22
+ summary = ?,
23
+ error = ?,
24
+ duration_secs = ROUND((julianday(datetime('now')) - julianday(started_at)) * 86400, 1)
25
+ WHERE id = ?""",
26
+ (exit_code, summary[:500], error[:500], run_id)
27
+ )
28
+ conn.commit()
29
+
30
+
31
+ def cron_runs_recent(hours: int = 24, cron_id: str = '') -> list[dict]:
32
+ """Get recent cron executions."""
33
+ conn = get_db()
34
+ if cron_id:
35
+ rows = conn.execute(
36
+ """SELECT * FROM cron_runs
37
+ WHERE cron_id = ? AND started_at >= datetime('now', ?)
38
+ ORDER BY started_at DESC""",
39
+ (cron_id, f"-{hours} hours")
40
+ ).fetchall()
41
+ else:
42
+ rows = conn.execute(
43
+ """SELECT * FROM cron_runs
44
+ WHERE started_at >= datetime('now', ?)
45
+ ORDER BY started_at DESC""",
46
+ (f"-{hours} hours",)
47
+ ).fetchall()
48
+ return [dict(r) for r in rows]
49
+
50
+
51
+ def cron_runs_summary(hours: int = 24) -> list[dict]:
52
+ """Get summary per cron: last run, success rate, avg duration."""
53
+ conn = get_db()
54
+ rows = conn.execute(
55
+ """SELECT
56
+ cron_id,
57
+ COUNT(*) as total_runs,
58
+ SUM(CASE WHEN exit_code = 0 THEN 1 ELSE 0 END) as succeeded,
59
+ SUM(CASE WHEN exit_code != 0 OR exit_code IS NULL THEN 1 ELSE 0 END) as failed,
60
+ ROUND(AVG(duration_secs), 1) as avg_duration,
61
+ MAX(started_at) as last_run,
62
+ (SELECT exit_code FROM cron_runs cr2
63
+ WHERE cr2.cron_id = cron_runs.cron_id
64
+ ORDER BY started_at DESC LIMIT 1) as last_exit_code,
65
+ (SELECT summary FROM cron_runs cr3
66
+ WHERE cr3.cron_id = cron_runs.cron_id AND cr3.summary != ''
67
+ ORDER BY started_at DESC LIMIT 1) as last_summary
68
+ FROM cron_runs
69
+ WHERE started_at >= datetime('now', ?)
70
+ GROUP BY cron_id
71
+ ORDER BY last_run DESC""",
72
+ (f"-{hours} hours",)
73
+ ).fetchall()
74
+ return [dict(r) for r in rows]
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Entities module."""
2
3
  import time
3
4
  from db._core import get_db, _multi_word_like
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Episodic module."""
2
3
  import datetime, time, json
3
4
  from db._core import get_db, now_epoch, _multi_word_like
@@ -568,17 +569,35 @@ def get_orphan_sessions(ttl_seconds: int = 900) -> list[dict]:
568
569
 
569
570
 
570
571
  def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = False,
571
- domain: str = '') -> list[dict]:
572
+ domain: str = '', include_automated: bool = False) -> list[dict]:
572
573
  """Read session diary entries.
573
574
 
574
575
  - session_id: returns entries for that specific session
575
576
  - last_day: returns ALL entries from the most recent day (multi-terminal aware)
576
577
  - last_n: returns last N entries (default)
577
578
  - domain: filter by project context (nexo, other)
579
+ - include_automated: if False (default), excludes automated sessions (auto-close,
580
+ cron diaries, etc.). Only returns human-interactive sessions.
581
+ Email sessions (user sends email, NEXO responds) ARE included — they're real interactions.
578
582
  """
579
583
  conn = get_db()
580
584
  domain_clause = " AND domain = ?" if domain else ""
581
585
  domain_params = (domain,) if domain else ()
586
+ # By default, filter out automated sessions so startup shows human sessions only.
587
+ # Keeps: interactive sessions + auto-closed sessions that had real user interaction.
588
+ # An auto-close is human if it has heartbeats > 0 (heartbeat only fires on user messages).
589
+ # Excludes: cron jobs, auto-closed crons (0 heartbeats or "Minimal diary").
590
+ if include_automated:
591
+ source_clause = ""
592
+ else:
593
+ source_clause = (
594
+ " AND ("
595
+ " (source = 'claude' AND summary NOT LIKE '[AUTO-%')"
596
+ " OR (source = 'auto-close'"
597
+ " AND mental_state NOT LIKE '%0 heartbeats%'"
598
+ " AND mental_state NOT LIKE '%Minimal diary%')"
599
+ ")"
600
+ )
582
601
 
583
602
  if session_id:
584
603
  rows = conn.execute(
@@ -586,25 +605,25 @@ def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = F
586
605
  (session_id,) + domain_params
587
606
  ).fetchall()
588
607
  elif last_day:
589
- # Get all entries from the most recent calendar day
608
+ # Get all entries from the most recent calendar day (human sessions only)
590
609
  if domain:
591
610
  latest = conn.execute(
592
- "SELECT date(created_at) as day FROM session_diary WHERE domain = ? ORDER BY created_at DESC LIMIT 1",
611
+ f"SELECT date(created_at) as day FROM session_diary WHERE domain = ?{source_clause} ORDER BY created_at DESC LIMIT 1",
593
612
  (domain,)
594
613
  ).fetchone()
595
614
  else:
596
615
  latest = conn.execute(
597
- "SELECT date(created_at) as day FROM session_diary ORDER BY created_at DESC LIMIT 1"
616
+ f"SELECT date(created_at) as day FROM session_diary WHERE 1=1{source_clause} ORDER BY created_at DESC LIMIT 1"
598
617
  ).fetchone()
599
618
  if not latest:
600
619
  return []
601
620
  rows = conn.execute(
602
- f"SELECT * FROM session_diary WHERE date(created_at) = ?{domain_clause} ORDER BY created_at DESC",
621
+ f"SELECT * FROM session_diary WHERE date(created_at) = ?{domain_clause}{source_clause} ORDER BY created_at DESC",
603
622
  (latest['day'],) + domain_params
604
623
  ).fetchall()
605
624
  else:
606
625
  rows = conn.execute(
607
- f"SELECT * FROM session_diary WHERE 1=1{domain_clause} ORDER BY created_at DESC LIMIT ?",
626
+ f"SELECT * FROM session_diary WHERE 1=1{domain_clause}{source_clause} ORDER BY created_at DESC LIMIT ?",
608
627
  domain_params + (last_n,)
609
628
  ).fetchall()
610
629
  return [dict(r) for r in rows]
@@ -732,6 +751,22 @@ def recall(query: str, days: int = 30) -> list[dict]:
732
751
  """, [cutoff_str] + params).fetchall()
733
752
  results.extend([dict(r) for r in rows])
734
753
 
754
+ # Skills
755
+ try:
756
+ frag, params = _multi_word_like(query, ["name", "description", "tags", "trigger_patterns"])
757
+ rows = conn.execute(f"""
758
+ SELECT id, created_at, 'skill' AS source,
759
+ name AS title,
760
+ (COALESCE(description,'') || ' | ' || COALESCE(tags,'') || ' | ' || COALESCE(trigger_patterns,'')) AS snippet,
761
+ level AS category, 0 AS rank
762
+ FROM skills
763
+ WHERE created_at >= ? AND ({frag})
764
+ ORDER BY trust_score DESC LIMIT 10
765
+ """, [cutoff_str] + params).fetchall()
766
+ results.extend([dict(r) for r in rows])
767
+ except Exception:
768
+ pass # Table may not exist yet during migration
769
+
735
770
  results.sort(key=lambda r: r.get('created_at', ''), reverse=True)
736
771
  return results[:20]
737
772
 
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Learnings module."""
2
3
  import re, time
3
4
  from db._core import get_db, now_epoch
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Reminders module."""
2
3
  import sqlite3, time, datetime
3
4
  from datetime import timedelta
package/src/db/_schema.py CHANGED
@@ -295,7 +295,69 @@ def _m15_core_rules_tables(conn):
295
295
  conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_active ON core_rules(is_active)")
296
296
 
297
297
 
298
+ def _m16_skills_tables(conn):
299
+ """Skill Auto-Creation system — reusable procedures extracted from complex tasks.
300
+
301
+ Skills are procedural knowledge (step-by-step how-tos) vs learnings which are
302
+ declarative (don't do X). Pipeline: trace → draft → published, fully autonomous.
303
+ Trust score with decay controls quality without human approval gates.
304
+ """
305
+ conn.execute("""
306
+ CREATE TABLE IF NOT EXISTS skills (
307
+ id TEXT PRIMARY KEY,
308
+ name TEXT NOT NULL,
309
+ description TEXT DEFAULT '',
310
+ level TEXT NOT NULL DEFAULT 'trace',
311
+ trust_score INTEGER NOT NULL DEFAULT 50,
312
+ file_path TEXT DEFAULT '',
313
+ tags TEXT DEFAULT '[]',
314
+ trigger_patterns TEXT DEFAULT '[]',
315
+ source_sessions TEXT DEFAULT '[]',
316
+ linked_learnings TEXT DEFAULT '[]',
317
+ use_count INTEGER DEFAULT 0,
318
+ success_count INTEGER DEFAULT 0,
319
+ fail_count INTEGER DEFAULT 0,
320
+ created_at TEXT DEFAULT (datetime('now')),
321
+ last_used_at TEXT DEFAULT NULL,
322
+ updated_at TEXT DEFAULT (datetime('now'))
323
+ )
324
+ """)
325
+ conn.execute("""
326
+ CREATE TABLE IF NOT EXISTS skill_usage (
327
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
328
+ skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
329
+ session_id TEXT DEFAULT '',
330
+ success INTEGER NOT NULL DEFAULT 1,
331
+ context TEXT DEFAULT '',
332
+ notes TEXT DEFAULT '',
333
+ created_at TEXT DEFAULT (datetime('now'))
334
+ )
335
+ """)
336
+ _migrate_add_index(conn, "idx_skills_level", "skills", "level")
337
+ _migrate_add_index(conn, "idx_skills_trust", "skills", "trust_score")
338
+ _migrate_add_index(conn, "idx_skills_last_used", "skills", "last_used_at")
339
+ _migrate_add_index(conn, "idx_skill_usage_skill_id", "skill_usage", "skill_id")
340
+ _migrate_add_index(conn, "idx_skill_usage_created", "skill_usage", "created_at")
341
+
342
+
298
343
  # Migration registry — APPEND ONLY, never reorder or delete
344
+ def _m17_cron_runs(conn):
345
+ conn.execute("""
346
+ CREATE TABLE IF NOT EXISTS cron_runs (
347
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
348
+ cron_id TEXT NOT NULL,
349
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
350
+ ended_at TEXT,
351
+ exit_code INTEGER,
352
+ summary TEXT DEFAULT '',
353
+ error TEXT DEFAULT '',
354
+ duration_secs REAL
355
+ )
356
+ """)
357
+ _migrate_add_index(conn, "idx_cron_runs_cron_id", "cron_runs", "cron_id")
358
+ _migrate_add_index(conn, "idx_cron_runs_started", "cron_runs", "started_at")
359
+
360
+
299
361
  MIGRATIONS = [
300
362
  (1, "learnings_columns", _m1_learnings_columns),
301
363
  (2, "followups_reasoning", _m2_followups_reasoning),
@@ -312,6 +374,8 @@ MIGRATIONS = [
312
374
  (13, "claude_session_id", _m13_claude_session_id),
313
375
  (14, "learnings_priority_weight", _m14_learnings_priority_weight),
314
376
  (15, "core_rules_tables", _m15_core_rules_tables),
377
+ (16, "skills_tables", _m16_skills_tables),
378
+ (17, "cron_runs", _m17_cron_runs),
315
379
  ]
316
380
 
317
381
 
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Sessions module."""
2
3
  import time, secrets, string, sqlite3
3
4
  from datetime import datetime