nexo-brain 2.3.0 → 2.3.2

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 (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. package/tests/test_migrations.py +0 -137
@@ -1,364 +0,0 @@
1
- """NEXO DB — Schema module."""
2
- from db._core import get_db
3
- from db._fts import _migrate_add_column, _migrate_add_index
4
-
5
- # ── Formal Migration System ─────────────────────────────────────
6
- #
7
- # Each migration is (version, name, callable). Migrations run once
8
- # and are tracked in schema_migrations. The version number MUST be
9
- # strictly increasing. Add new migrations at the end of the list.
10
- #
11
- # For users upgrading via npm/git, init_db() calls run_migrations()
12
- # automatically — no manual steps needed.
13
-
14
- def _m1_learnings_columns(conn):
15
- _migrate_add_column(conn, "learnings", "reasoning", "TEXT")
16
- _migrate_add_column(conn, "learnings", "prevention", "TEXT DEFAULT ''")
17
- _migrate_add_column(conn, "learnings", "applies_to", "TEXT DEFAULT ''")
18
- _migrate_add_column(conn, "learnings", "status", "TEXT DEFAULT 'active'")
19
- _migrate_add_column(conn, "learnings", "review_due_at", "REAL")
20
- _migrate_add_column(conn, "learnings", "last_reviewed_at", "REAL")
21
-
22
- def _m2_followups_reasoning(conn):
23
- _migrate_add_column(conn, "followups", "reasoning", "TEXT")
24
- _migrate_add_column(conn, "task_history", "reasoning", "TEXT")
25
-
26
- def _m3_decisions_review(conn):
27
- _migrate_add_column(conn, "decisions", "status", "TEXT DEFAULT 'pending_review'")
28
- _migrate_add_column(conn, "decisions", "review_due_at", "TEXT")
29
- _migrate_add_column(conn, "decisions", "last_reviewed_at", "TEXT")
30
- _migrate_add_index(conn, "idx_decisions_domain", "decisions", "domain")
31
- _migrate_add_index(conn, "idx_decisions_created", "decisions", "created_at")
32
- _migrate_add_index(conn, "idx_decisions_review_due", "decisions", "review_due_at")
33
-
34
- def _m4_session_diary_columns(conn):
35
- _migrate_add_index(conn, "idx_session_diary_sid", "session_diary", "session_id")
36
- _migrate_add_column(conn, "session_diary", "mental_state", "TEXT")
37
- _migrate_add_column(conn, "session_diary", "domain", "TEXT")
38
- _migrate_add_column(conn, "session_diary", "user_signals", "TEXT")
39
- _migrate_add_column(conn, "session_diary", "self_critique", "TEXT")
40
-
41
- def _m5_change_log_indexes(conn):
42
- _migrate_add_index(conn, "idx_change_log_created", "change_log", "created_at")
43
- _migrate_add_index(conn, "idx_change_log_files", "change_log", "files")
44
- _migrate_add_index(conn, "idx_learnings_status", "learnings", "status")
45
- _migrate_add_index(conn, "idx_learnings_review_due", "learnings", "review_due_at")
46
-
47
- def _m6_error_guard_tables(conn):
48
- conn.execute("""
49
- CREATE TABLE IF NOT EXISTS error_repetitions (
50
- id INTEGER PRIMARY KEY AUTOINCREMENT,
51
- new_learning_id INTEGER NOT NULL,
52
- original_learning_id INTEGER NOT NULL,
53
- similarity REAL NOT NULL,
54
- area TEXT NOT NULL,
55
- created_at TEXT DEFAULT (datetime('now'))
56
- )
57
- """)
58
- conn.execute("""
59
- CREATE TABLE IF NOT EXISTS guard_checks (
60
- id INTEGER PRIMARY KEY AUTOINCREMENT,
61
- session_id TEXT,
62
- files TEXT,
63
- area TEXT,
64
- learnings_returned INTEGER DEFAULT 0,
65
- blocking_rules_returned INTEGER DEFAULT 0,
66
- created_at TEXT DEFAULT (datetime('now'))
67
- )
68
- """)
69
- _migrate_add_index(conn, "idx_error_repetitions_area", "error_repetitions", "area")
70
- _migrate_add_index(conn, "idx_guard_checks_session", "guard_checks", "session_id")
71
-
72
- def _m7_diary_source_and_draft(conn):
73
- _migrate_add_column(conn, "session_diary", "source", "TEXT DEFAULT 'claude'")
74
- conn.execute("""
75
- CREATE TABLE IF NOT EXISTS session_diary_draft (
76
- sid TEXT PRIMARY KEY,
77
- summary_draft TEXT DEFAULT '',
78
- tasks_seen TEXT DEFAULT '[]',
79
- change_ids TEXT DEFAULT '[]',
80
- decision_ids TEXT DEFAULT '[]',
81
- last_context_hint TEXT DEFAULT '',
82
- heartbeat_count INTEGER DEFAULT 0,
83
- created_at TEXT DEFAULT (datetime('now')),
84
- updated_at TEXT DEFAULT (datetime('now'))
85
- )
86
- """)
87
-
88
-
89
- def _m8_adaptive_log_and_somatic(conn):
90
- conn.execute("""
91
- CREATE TABLE IF NOT EXISTS adaptive_log (
92
- id INTEGER PRIMARY KEY AUTOINCREMENT,
93
- timestamp TEXT DEFAULT (datetime('now')),
94
- mode TEXT NOT NULL,
95
- tension_score REAL NOT NULL,
96
- sig_vibe REAL DEFAULT 0,
97
- sig_corrections REAL DEFAULT 0,
98
- sig_brevity REAL DEFAULT 0,
99
- sig_topic REAL DEFAULT 0,
100
- sig_tool_errors REAL DEFAULT 0,
101
- sig_git_diff REAL DEFAULT 0,
102
- context_hint TEXT DEFAULT '',
103
- feedback_event TEXT DEFAULT NULL,
104
- feedback_delta INTEGER DEFAULT NULL,
105
- feedback_ts TEXT DEFAULT NULL
106
- )
107
- """)
108
- conn.execute("CREATE INDEX IF NOT EXISTS idx_adaptive_log_ts ON adaptive_log(timestamp)")
109
- conn.execute("""
110
- CREATE TABLE IF NOT EXISTS somatic_events (
111
- id INTEGER PRIMARY KEY AUTOINCREMENT,
112
- timestamp TEXT DEFAULT (datetime('now')),
113
- target TEXT NOT NULL,
114
- target_type TEXT NOT NULL,
115
- event_type TEXT NOT NULL,
116
- delta REAL NOT NULL,
117
- source TEXT DEFAULT '',
118
- projected INTEGER DEFAULT 0
119
- )
120
- """)
121
- conn.execute("CREATE INDEX IF NOT EXISTS idx_somatic_events_target ON somatic_events(target)")
122
- conn.execute("CREATE INDEX IF NOT EXISTS idx_somatic_events_projected ON somatic_events(projected)")
123
-
124
-
125
- def _m11_artifact_registry(conn):
126
- """Artifact Registry — structured index of things NEXO creates/deploys.
127
-
128
- Solves 'recent work amnesia': services, dashboards, scripts, APIs that
129
- NEXO builds but can't find hours later because semantic search fails on
130
- operational vocabulary mismatches (e.g., 'backend' vs 'FastAPI localhost:6174').
131
-
132
- Design informed by 3-way AI debate (GPT-5.4 + Gemini 3.1 Pro + Claude Opus 4.6).
133
- Key insight: operational facts need first-class structured storage, not just
134
- vector embeddings buried in prose diaries.
135
- """
136
- conn.execute("""
137
- CREATE TABLE IF NOT EXISTS artifact_registry (
138
- id INTEGER PRIMARY KEY AUTOINCREMENT,
139
- kind TEXT NOT NULL,
140
- canonical_name TEXT NOT NULL,
141
- aliases TEXT DEFAULT '[]',
142
- description TEXT DEFAULT '',
143
- uri TEXT DEFAULT '',
144
- ports TEXT DEFAULT '[]',
145
- paths TEXT DEFAULT '[]',
146
- run_cmd TEXT DEFAULT '',
147
- repo TEXT DEFAULT '',
148
- domain TEXT DEFAULT '',
149
- state TEXT DEFAULT 'active',
150
- session_id TEXT DEFAULT '',
151
- created_at TEXT DEFAULT (datetime('now')),
152
- last_touched_at TEXT DEFAULT (datetime('now')),
153
- last_verified_at TEXT DEFAULT NULL,
154
- metadata TEXT DEFAULT '{}'
155
- )
156
- """)
157
- conn.execute("""
158
- CREATE TABLE IF NOT EXISTS artifact_aliases (
159
- id INTEGER PRIMARY KEY AUTOINCREMENT,
160
- artifact_id INTEGER NOT NULL REFERENCES artifact_registry(id) ON DELETE CASCADE,
161
- phrase TEXT NOT NULL,
162
- source TEXT DEFAULT 'manual',
163
- confidence REAL DEFAULT 1.0,
164
- created_at TEXT DEFAULT (datetime('now')),
165
- UNIQUE(artifact_id, phrase)
166
- )
167
- """)
168
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_state ON artifact_registry(state)")
169
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_kind ON artifact_registry(kind)")
170
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_domain ON artifact_registry(domain)")
171
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_last_touched ON artifact_registry(last_touched_at)")
172
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_aliases_phrase ON artifact_aliases(phrase)")
173
- conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_aliases_aid ON artifact_aliases(artifact_id)")
174
-
175
-
176
- def _m10_diary_archive(conn):
177
- """Permanent diary archive — diaries are never truly deleted, just moved here."""
178
- conn.execute("""
179
- CREATE TABLE IF NOT EXISTS diary_archive (
180
- id INTEGER PRIMARY KEY,
181
- session_id TEXT NOT NULL,
182
- created_at TEXT NOT NULL,
183
- decisions TEXT NOT NULL,
184
- discarded TEXT,
185
- pending TEXT,
186
- context_next TEXT,
187
- summary TEXT NOT NULL,
188
- mental_state TEXT,
189
- domain TEXT,
190
- user_signals TEXT,
191
- self_critique TEXT DEFAULT '',
192
- source TEXT DEFAULT 'claude',
193
- archived_at TEXT DEFAULT (datetime('now'))
194
- )
195
- """)
196
- conn.execute("""
197
- CREATE INDEX IF NOT EXISTS idx_diary_archive_created
198
- ON diary_archive (created_at)
199
- """)
200
- conn.execute("""
201
- CREATE INDEX IF NOT EXISTS idx_diary_archive_domain
202
- ON diary_archive (domain)
203
- """)
204
-
205
-
206
- def _m9_maintenance_schedule(conn):
207
- conn.execute("""
208
- CREATE TABLE IF NOT EXISTS maintenance_schedule (
209
- task_name TEXT PRIMARY KEY,
210
- interval_hours REAL NOT NULL,
211
- last_run_at TEXT DEFAULT NULL,
212
- last_duration_ms INTEGER DEFAULT 0,
213
- run_count INTEGER DEFAULT 0
214
- )
215
- """)
216
- tasks = [
217
- ('cognitive_decay', 20), ('synthesis', 20), ('self_audit', 144),
218
- ('weight_learning', 20), ('somatic_projection', 20), ('somatic_decay', 20),
219
- ('graph_maintenance', 48),
220
- ]
221
- for name, hours in tasks:
222
- conn.execute(
223
- "INSERT OR IGNORE INTO maintenance_schedule (task_name, interval_hours) VALUES (?, ?)",
224
- (name, hours)
225
- )
226
-
227
-
228
- def _m12_session_checkpoints(conn):
229
- """Session checkpoints for intelligent auto-compaction.
230
-
231
- PreCompact saves a checkpoint; PostCompact reads it to re-inject a
232
- Core Memory Block that preserves continuity after context compression.
233
- """
234
- conn.execute("""
235
- CREATE TABLE IF NOT EXISTS session_checkpoints (
236
- sid TEXT PRIMARY KEY,
237
- task TEXT DEFAULT '',
238
- task_status TEXT DEFAULT 'active',
239
- active_files TEXT DEFAULT '[]',
240
- current_goal TEXT DEFAULT '',
241
- decisions_summary TEXT DEFAULT '',
242
- errors_found TEXT DEFAULT '',
243
- reasoning_thread TEXT DEFAULT '',
244
- next_step TEXT DEFAULT '',
245
- compaction_count INTEGER DEFAULT 0,
246
- created_at TEXT DEFAULT (datetime('now')),
247
- updated_at TEXT DEFAULT (datetime('now'))
248
- )
249
- """)
250
-
251
-
252
- def _m13_claude_session_id(conn):
253
- """Add claude_session_id to sessions for inter-terminal coordination (D+)."""
254
- _migrate_add_column(conn, "sessions", "claude_session_id", "TEXT DEFAULT ''")
255
- _migrate_add_index(conn, "idx_sessions_claude_sid", "sessions", "claude_session_id")
256
- conn.commit()
257
-
258
-
259
- def _m14_learnings_priority_weight(conn):
260
- """Add priority, weight, and guard usage tracking to learnings + followup priority."""
261
- _migrate_add_column(conn, "learnings", "priority", "TEXT DEFAULT 'medium'")
262
- _migrate_add_column(conn, "learnings", "weight", "REAL DEFAULT 0.5")
263
- _migrate_add_column(conn, "learnings", "guard_hits", "INTEGER DEFAULT 0")
264
- _migrate_add_column(conn, "learnings", "last_guard_hit_at", "REAL")
265
- _migrate_add_column(conn, "followups", "priority", "TEXT DEFAULT 'medium'")
266
-
267
-
268
- def _m15_core_rules_tables(conn):
269
- """Core rules and version tracking tables for the core_rules plugin."""
270
- conn.execute("""
271
- CREATE TABLE IF NOT EXISTS core_rules (
272
- id TEXT PRIMARY KEY,
273
- category TEXT NOT NULL,
274
- rule TEXT NOT NULL,
275
- why TEXT NOT NULL,
276
- importance INTEGER NOT NULL DEFAULT 3,
277
- type TEXT NOT NULL DEFAULT 'advisory',
278
- added_in TEXT DEFAULT '',
279
- removed_in TEXT DEFAULT NULL,
280
- is_active INTEGER NOT NULL DEFAULT 1
281
- )
282
- """)
283
- conn.execute("""
284
- CREATE TABLE IF NOT EXISTS core_rules_version (
285
- id INTEGER PRIMARY KEY,
286
- version TEXT NOT NULL DEFAULT '0.0.0',
287
- updated_at TEXT DEFAULT (datetime('now'))
288
- )
289
- """)
290
- # Seed the version row so UPDATE statements in the plugin always find it
291
- conn.execute(
292
- "INSERT OR IGNORE INTO core_rules_version (id, version) VALUES (1, '0.0.0')"
293
- )
294
- conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_category ON core_rules(category)")
295
- conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_active ON core_rules(is_active)")
296
-
297
-
298
- # Migration registry — APPEND ONLY, never reorder or delete
299
- MIGRATIONS = [
300
- (1, "learnings_columns", _m1_learnings_columns),
301
- (2, "followups_reasoning", _m2_followups_reasoning),
302
- (3, "decisions_review", _m3_decisions_review),
303
- (4, "session_diary_columns", _m4_session_diary_columns),
304
- (5, "change_log_indexes", _m5_change_log_indexes),
305
- (6, "error_guard_tables", _m6_error_guard_tables),
306
- (7, "diary_source_and_draft", _m7_diary_source_and_draft),
307
- (8, "adaptive_log_and_somatic", _m8_adaptive_log_and_somatic),
308
- (9, "maintenance_schedule", _m9_maintenance_schedule),
309
- (10, "diary_archive", _m10_diary_archive),
310
- (11, "artifact_registry", _m11_artifact_registry),
311
- (12, "session_checkpoints", _m12_session_checkpoints),
312
- (13, "claude_session_id", _m13_claude_session_id),
313
- (14, "learnings_priority_weight", _m14_learnings_priority_weight),
314
- (15, "core_rules_tables", _m15_core_rules_tables),
315
- ]
316
-
317
-
318
- def run_migrations(conn=None):
319
- """Run pending migrations. Tracks applied versions in schema_migrations.
320
-
321
- Safe to call multiple times — skips already-applied migrations.
322
- Called automatically by init_db() on every server start.
323
- """
324
- if conn is None:
325
- conn = get_db()
326
-
327
- conn.execute("""
328
- CREATE TABLE IF NOT EXISTS schema_migrations (
329
- version INTEGER PRIMARY KEY,
330
- name TEXT NOT NULL,
331
- applied_at TEXT DEFAULT (datetime('now'))
332
- )
333
- """)
334
- conn.commit()
335
-
336
- applied = {r[0] for r in conn.execute("SELECT version FROM schema_migrations").fetchall()}
337
-
338
- for version, name, fn in MIGRATIONS:
339
- if version not in applied:
340
- try:
341
- fn(conn)
342
- conn.execute(
343
- "INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
344
- (version, name)
345
- )
346
- conn.commit()
347
- except Exception as e:
348
- # Log but don't crash — partial migration is better than no server
349
- import sys
350
- print(f"[MIGRATION] v{version} ({name}) failed: {e}", file=sys.stderr)
351
-
352
- return len(MIGRATIONS) - len(applied)
353
-
354
-
355
- def get_schema_version() -> int:
356
- """Return the highest applied migration version, or 0 if none."""
357
- conn = get_db()
358
- try:
359
- row = conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()
360
- return row[0] or 0
361
- except Exception:
362
- return 0
363
-
364
-
@@ -1,300 +0,0 @@
1
- """NEXO DB — Sessions module."""
2
- import time, secrets, string, sqlite3
3
- from datetime import datetime
4
- from db._core import get_db, _gen_id, now_epoch, local_time_str, SESSION_STALE_SECONDS, MESSAGE_TTL_SECONDS, QUESTION_TTL_SECONDS
5
-
6
- # ── Session operations ──────────────────────────────────────────────
7
-
8
- def now_epoch() -> float:
9
- return time.time()
10
-
11
-
12
- def local_time_str() -> str:
13
- from datetime import datetime
14
- return datetime.now().strftime("%H:%M")
15
-
16
-
17
- def register_session(sid: str, task: str, claude_session_id: str = "") -> dict:
18
- """Register or re-register a session."""
19
- conn = get_db()
20
- now = now_epoch()
21
- conn.execute(
22
- "INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id) "
23
- "VALUES (?, ?, ?, ?, ?, ?)",
24
- (sid, task, now, now, local_time_str(), claude_session_id)
25
- )
26
- conn.commit()
27
- return {"sid": sid, "task": task}
28
-
29
-
30
- def update_session(sid: str, task: str | None) -> dict:
31
- """Update session timestamp (and task if provided). Preserves started_epoch.
32
-
33
- Args:
34
- sid: Session ID.
35
- task: New task description, or None to keep current task (keepalive touch).
36
- """
37
- conn = get_db()
38
- now = now_epoch()
39
- row = conn.execute("SELECT started_epoch, task FROM sessions WHERE sid = ?", (sid,)).fetchone()
40
- if row:
41
- effective_task = task if task is not None else row["task"]
42
- conn.execute(
43
- "UPDATE sessions SET task = ?, last_update_epoch = ?, local_time = ? WHERE sid = ?",
44
- (effective_task, now, local_time_str(), sid)
45
- )
46
- else:
47
- effective_task = task or "Unknown"
48
- conn.execute(
49
- "INSERT INTO sessions (sid, task, started_epoch, last_update_epoch, local_time) "
50
- "VALUES (?, ?, ?, ?, ?)",
51
- (sid, effective_task, now, now, local_time_str())
52
- )
53
- conn.commit()
54
- return {"sid": sid, "task": effective_task}
55
-
56
-
57
- def complete_session(sid: str):
58
- """Remove session and its tracked files."""
59
- conn = get_db()
60
- conn.execute("PRAGMA foreign_keys=ON")
61
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
62
- conn.execute("DELETE FROM sessions WHERE sid = ?", (sid,))
63
- conn.commit()
64
-
65
-
66
- def get_active_sessions() -> list[dict]:
67
- """Get all sessions updated within STALE threshold."""
68
- conn = get_db()
69
- cutoff = now_epoch() - SESSION_STALE_SECONDS
70
- rows = conn.execute(
71
- "SELECT sid, task, started_epoch, last_update_epoch, local_time "
72
- "FROM sessions WHERE last_update_epoch > ?",
73
- (cutoff,)
74
- ).fetchall()
75
- return [dict(r) for r in rows]
76
-
77
-
78
- def clean_stale_sessions() -> int:
79
- """Remove stale sessions. Returns count removed."""
80
- conn = get_db()
81
- cutoff = now_epoch() - SESSION_STALE_SECONDS
82
- stale = conn.execute(
83
- "SELECT sid FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
84
- ).fetchall()
85
- for row in stale:
86
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (row["sid"],))
87
- result = conn.execute(
88
- "DELETE FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
89
- )
90
- count = result.rowcount
91
- conn.commit()
92
- return count
93
-
94
-
95
- def search_sessions(keyword: str) -> list[dict]:
96
- """Find sessions whose task contains keyword (case-insensitive)."""
97
- conn = get_db()
98
- cutoff = now_epoch() - SESSION_STALE_SECONDS
99
- rows = conn.execute(
100
- "SELECT sid, task, last_update_epoch, local_time FROM sessions "
101
- "WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
102
- (cutoff, f"%{keyword.lower()}%")
103
- ).fetchall()
104
- return [dict(r) for r in rows]
105
-
106
-
107
- # ── File tracking ───────────────────────────────────────────────────
108
-
109
- def track_files(sid: str, paths: list[str]) -> dict:
110
- """Track files for a session. Returns conflicts if any."""
111
- conn = get_db()
112
- now = now_epoch()
113
- session = conn.execute("SELECT sid FROM sessions WHERE sid = ?", (sid,)).fetchone()
114
- if not session:
115
- return {"error": f"Session {sid} not found. Register first."}
116
-
117
- for path in paths:
118
- conn.execute(
119
- "INSERT OR IGNORE INTO tracked_files (sid, path, tracked_at) VALUES (?, ?, ?)",
120
- (sid, path, now)
121
- )
122
- conn.commit()
123
- conflicts = _check_conflicts(conn, sid)
124
- return {"tracked": paths, "conflicts": conflicts}
125
-
126
-
127
- def untrack_files(sid: str, paths: list[str] | None = None):
128
- """Untrack files. If paths is None, untrack all."""
129
- conn = get_db()
130
- if paths:
131
- for path in paths:
132
- conn.execute(
133
- "DELETE FROM tracked_files WHERE sid = ? AND path = ?",
134
- (sid, path)
135
- )
136
- else:
137
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
138
- conn.commit()
139
-
140
-
141
- def get_all_tracked_files() -> dict:
142
- """Get all tracked files grouped by session."""
143
- conn = get_db()
144
- cutoff = now_epoch() - SESSION_STALE_SECONDS
145
- rows = conn.execute(
146
- "SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
147
- "JOIN sessions s ON tf.sid = s.sid "
148
- "WHERE s.last_update_epoch > ?",
149
- (cutoff,)
150
- ).fetchall()
151
- result = {}
152
- for r in rows:
153
- sid = r["sid"]
154
- if sid not in result:
155
- result[sid] = {"task": r["task"], "files": []}
156
- result[sid]["files"].append(r["path"])
157
- return result
158
-
159
-
160
- def _check_conflicts(conn: sqlite3.Connection, sid: str) -> list[dict]:
161
- """Check if any of sid's files are tracked by other active sessions."""
162
- cutoff = now_epoch() - SESSION_STALE_SECONDS
163
- my_files = conn.execute(
164
- "SELECT path FROM tracked_files WHERE sid = ?", (sid,)
165
- ).fetchall()
166
- my_paths = {r["path"] for r in my_files}
167
- if not my_paths:
168
- return []
169
-
170
- conflicts = []
171
- others = conn.execute(
172
- "SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
173
- "JOIN sessions s ON tf.sid = s.sid "
174
- "WHERE tf.sid != ? AND s.last_update_epoch > ?",
175
- (sid, cutoff)
176
- ).fetchall()
177
- by_sid = {}
178
- for r in others:
179
- if r["path"] in my_paths:
180
- osid = r["sid"]
181
- if osid not in by_sid:
182
- by_sid[osid] = {"sid": osid, "task": r["task"], "files": []}
183
- by_sid[osid]["files"].append(r["path"])
184
- return list(by_sid.values())
185
-
186
-
187
- # ── Messages ────────────────────────────────────────────────────────
188
-
189
- def send_message(from_sid: str, to_sid: str, text: str) -> str:
190
- """Send a message. to_sid can be 'all' for broadcast."""
191
- conn = get_db()
192
- _clean_old_messages(conn)
193
- msg_id = _gen_id("msg", 6)
194
- conn.execute(
195
- "INSERT INTO messages (id, from_sid, to_sid, text, created_epoch) "
196
- "VALUES (?, ?, ?, ?, ?)",
197
- (msg_id, from_sid, to_sid, text, now_epoch())
198
- )
199
- conn.commit()
200
- return msg_id
201
-
202
-
203
- def get_inbox(sid: str) -> list[dict]:
204
- """Get unread messages for a session."""
205
- conn = get_db()
206
- _clean_old_messages(conn)
207
- rows = conn.execute(
208
- "SELECT m.id, m.from_sid, m.to_sid, m.text, m.created_epoch "
209
- "FROM messages m "
210
- "WHERE (m.to_sid = 'all' OR m.to_sid = ?) "
211
- "AND m.from_sid != ? "
212
- "AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = ?)",
213
- (sid, sid, sid)
214
- ).fetchall()
215
- for r in rows:
216
- conn.execute(
217
- "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES (?, ?)",
218
- (r["id"], sid)
219
- )
220
- conn.commit()
221
- result = [dict(r) for r in rows]
222
- return result
223
-
224
-
225
- def _clean_old_messages(conn: sqlite3.Connection):
226
- """Remove expired messages and commit immediately."""
227
- cutoff = now_epoch() - MESSAGE_TTL_SECONDS
228
- conn.execute("DELETE FROM messages WHERE created_epoch < ?", (cutoff,))
229
- conn.commit()
230
-
231
-
232
- # ── Questions ───────────────────────────────────────────────────────
233
-
234
- def ask_question(from_sid: str, to_sid: str, question: str) -> str:
235
- """Create a pending question. Returns qid."""
236
- conn = get_db()
237
- _expire_old_questions(conn)
238
- qid = _gen_id("q", 8)
239
- conn.execute(
240
- "INSERT INTO questions (qid, from_sid, to_sid, question, status, created_epoch) "
241
- "VALUES (?, ?, ?, ?, 'pending', ?)",
242
- (qid, from_sid, to_sid, question, now_epoch())
243
- )
244
- conn.commit()
245
- return qid
246
-
247
-
248
- def answer_question(qid: str, answer: str) -> dict:
249
- """Answer a pending question."""
250
- conn = get_db()
251
- row = conn.execute(
252
- "SELECT * FROM questions WHERE qid = ?", (qid,)
253
- ).fetchone()
254
- if not row:
255
- return {"error": f"Question {qid} not found"}
256
- if row["status"] != "pending":
257
- return {"error": f"Question {qid} is {row['status']}, not pending"}
258
- conn.execute(
259
- "UPDATE questions SET answer = ?, status = 'answered', answered_epoch = ? "
260
- "WHERE qid = ?",
261
- (answer, now_epoch(), qid)
262
- )
263
- conn.commit()
264
- return {"qid": qid, "status": "answered"}
265
-
266
-
267
- def get_pending_questions(sid: str) -> list[dict]:
268
- """Get pending questions addressed to this session."""
269
- conn = get_db()
270
- _expire_old_questions(conn)
271
- rows = conn.execute(
272
- "SELECT qid, from_sid, question, created_epoch FROM questions "
273
- "WHERE to_sid = ? AND status = 'pending'",
274
- (sid,)
275
- ).fetchall()
276
- conn.commit()
277
- return [dict(r) for r in rows]
278
-
279
-
280
- def check_answer(qid: str) -> dict | None:
281
- """Check if a question has been answered. Returns answer or None."""
282
- conn = get_db()
283
- row = conn.execute(
284
- "SELECT qid, answer, status FROM questions WHERE qid = ?", (qid,)
285
- ).fetchone()
286
- if not row:
287
- return None
288
- return dict(row)
289
-
290
-
291
- def _expire_old_questions(conn: sqlite3.Connection):
292
- """Mark old pending questions as expired."""
293
- cutoff = now_epoch() - QUESTION_TTL_SECONDS
294
- conn.execute(
295
- "UPDATE questions SET status = 'expired' "
296
- "WHERE status = 'pending' AND created_epoch < ?",
297
- (cutoff,)
298
- )
299
-
300
-