nexo-brain 2.1.0 → 2.3.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 (297) hide show
  1. package/README.md +7 -7
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +1 -1
  4. package/scripts/migrate-to-unified 2.sh +813 -0
  5. package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
  6. package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
  7. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  8. package/scripts/nexo-preflight.sh +236 -0
  9. package/scripts/pre-commit-check 2.sh +55 -0
  10. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  12. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  17. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  21. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  22. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  26. package/src/auto_close_sessions 2.py +159 -0
  27. package/src/auto_update 2.py +634 -0
  28. package/src/auto_update.py +25 -0
  29. package/src/claim_graph 2.py +323 -0
  30. package/src/cognitive/__init__ 2.py +62 -0
  31. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  32. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  33. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  35. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  36. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  40. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  45. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  47. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  48. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  49. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  50. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  51. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  52. package/src/cognitive/_core 2.py +567 -0
  53. package/src/cognitive/_decay 2.py +382 -0
  54. package/src/cognitive/_ingest 2.py +892 -0
  55. package/src/cognitive/_memory 2.py +912 -0
  56. package/src/cognitive/_search 2.py +949 -0
  57. package/src/cognitive/_trust 2.py +464 -0
  58. package/src/cognitive/_trust.py +10 -36
  59. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  60. package/src/crons/manifest 2.json +106 -0
  61. package/src/crons/manifest.json +6 -13
  62. package/src/crons/sync 2.py +217 -0
  63. package/src/crons/sync.py +151 -6
  64. package/src/dashboard/__init__ 2.py +0 -0
  65. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  66. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  67. package/src/dashboard/app 2.py +789 -0
  68. package/src/db/__init__ 2.py +89 -0
  69. package/src/db/__init__.py +13 -0
  70. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  71. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  73. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  74. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  75. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  76. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  77. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  78. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  79. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  80. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  111. package/src/db/_core 2.py +417 -0
  112. package/src/db/_credentials 2.py +124 -0
  113. package/src/db/_cron_runs.py +74 -0
  114. package/src/db/_entities 2.py +178 -0
  115. package/src/db/_episodic 2.py +738 -0
  116. package/src/db/_episodic.py +40 -6
  117. package/src/db/_evolution 2.py +54 -0
  118. package/src/db/_fts 2.py +406 -0
  119. package/src/db/_learnings 2.py +168 -0
  120. package/src/db/_reminders 2.py +338 -0
  121. package/src/db/_schema 2.py +364 -0
  122. package/src/db/_schema.py +64 -0
  123. package/src/db/_sessions 2.py +300 -0
  124. package/src/db/_skills.py +514 -0
  125. package/src/db/_tasks 2.py +91 -0
  126. package/src/evolution_cycle 2.py +266 -0
  127. package/src/hnsw_index 2.py +254 -0
  128. package/src/hooks/auto_capture 2.py +208 -0
  129. package/src/hooks/caffeinate-guard 2.sh +8 -0
  130. package/src/hooks/capture-session 2.sh +21 -0
  131. package/src/hooks/capture-session.sh +2 -0
  132. package/src/hooks/capture-tool-logs 2.sh +127 -0
  133. package/src/hooks/capture-tool-logs.sh +3 -2
  134. package/src/hooks/daily-briefing-check 2.sh +33 -0
  135. package/src/hooks/inbox-hook 2.sh +76 -0
  136. package/src/hooks/inbox-hook.sh +3 -2
  137. package/src/hooks/post-compact 2.sh +148 -0
  138. package/src/hooks/post-compact.sh +1 -1
  139. package/src/hooks/pre-compact 2.sh +151 -0
  140. package/src/hooks/pre-compact.sh +1 -1
  141. package/src/hooks/session-start 2.sh +268 -0
  142. package/src/hooks/session-start.sh +6 -3
  143. package/src/hooks/session-stop 2.sh +140 -0
  144. package/src/hooks/session-stop.sh +14 -102
  145. package/src/kg_populate 2.py +290 -0
  146. package/src/knowledge_graph 2.py +257 -0
  147. package/src/maintenance 2.py +59 -0
  148. package/src/migrate_embeddings 2.py +122 -0
  149. package/src/plugin_loader 2.py +202 -0
  150. package/src/plugins/__init__ 2.py +0 -0
  151. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.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__/adaptive_mode.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  183. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  185. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  187. package/src/plugins/adaptive_mode 2.py +805 -0
  188. package/src/plugins/agents 2.py +52 -0
  189. package/src/plugins/artifact_registry 2.py +450 -0
  190. package/src/plugins/backup 2.py +104 -0
  191. package/src/plugins/cognitive_memory 2.py +564 -0
  192. package/src/plugins/core_rules 2.py +252 -0
  193. package/src/plugins/cortex 2.py +299 -0
  194. package/src/plugins/entities 2.py +67 -0
  195. package/src/plugins/episodic_memory 2.py +533 -0
  196. package/src/plugins/episodic_memory.py +5 -3
  197. package/src/plugins/evolution 2.py +115 -0
  198. package/src/plugins/guard 2.py +746 -0
  199. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  200. package/src/plugins/preferences 2.py +47 -0
  201. package/src/plugins/schedule.py +212 -0
  202. package/src/plugins/skills.py +264 -0
  203. package/src/plugins/update 2.py +256 -0
  204. package/src/requirements 2.txt +12 -0
  205. package/src/rules/__init__ 2.py +0 -0
  206. package/src/rules/core-rules 2.json +331 -0
  207. package/src/rules/migrate 2.py +207 -0
  208. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  229. package/src/scripts/check-context 2.py +264 -0
  230. package/src/scripts/deep-sleep/apply_findings.py +168 -8
  231. package/src/scripts/deep-sleep/collect.py +33 -11
  232. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  233. package/src/scripts/deep-sleep/extract.py +80 -8
  234. package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
  235. package/src/scripts/deep-sleep/synthesize.py +3 -1
  236. package/src/scripts/nexo-auto-update 2.py +6 -0
  237. package/src/scripts/nexo-backup 2.sh +25 -0
  238. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  239. package/src/scripts/nexo-catchup 2.py +242 -0
  240. package/src/scripts/nexo-catchup.py +65 -29
  241. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  242. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  243. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  244. package/src/scripts/nexo-daily-self-audit.py +4 -2
  245. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  246. package/src/scripts/nexo-deep-sleep.sh +66 -77
  247. package/src/scripts/nexo-evolution-run 2.py +597 -0
  248. package/src/scripts/nexo-evolution-run.py +13 -0
  249. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  250. package/src/scripts/nexo-immune 2.py +927 -0
  251. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  252. package/src/scripts/nexo-install 2.py +6 -0
  253. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  254. package/src/scripts/nexo-learning-housekeep.py +156 -1
  255. package/src/scripts/nexo-learning-validator 2.py +207 -0
  256. package/src/scripts/nexo-learning-validator.py +19 -0
  257. package/src/scripts/nexo-migrate 2.py +232 -0
  258. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  259. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  260. package/src/scripts/nexo-pre-commit 2.py +120 -0
  261. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  262. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  263. package/src/scripts/nexo-reflection 2.py +253 -0
  264. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  265. package/src/scripts/nexo-send-email 2.py +25 -0
  266. package/src/scripts/nexo-send-reply 2.py +178 -0
  267. package/src/scripts/nexo-sleep 2.py +592 -0
  268. package/src/scripts/nexo-sleep.py +16 -11
  269. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  270. package/src/scripts/nexo-synthesis 2.py +253 -0
  271. package/src/scripts/nexo-synthesis.py +46 -3
  272. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  273. package/src/scripts/nexo-update 2.sh +161 -0
  274. package/src/scripts/nexo-watchdog 2.sh +878 -0
  275. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  276. package/src/scripts/nexo-watchdog.sh +72 -19
  277. package/src/server 2.py +733 -0
  278. package/src/server.py +11 -2
  279. package/src/storage_router 2.py +32 -0
  280. package/src/tools_coordination 2.py +102 -0
  281. package/src/tools_credentials 2.py +68 -0
  282. package/src/tools_learnings 2.py +220 -0
  283. package/src/tools_menu 2.py +227 -0
  284. package/src/tools_reminders 2.py +86 -0
  285. package/src/tools_reminders_crud 2.py +159 -0
  286. package/src/tools_reminders_crud.py +7 -0
  287. package/src/tools_sessions 2.py +476 -0
  288. package/src/tools_task_history 2.py +57 -0
  289. package/templates/CLAUDE.md 2.template +63 -0
  290. package/templates/openclaw 2.json +13 -0
  291. package/tests/__init__ 2.py +0 -0
  292. package/tests/conftest 2.py +71 -0
  293. package/tests/test_cognitive 2.py +205 -0
  294. package/tests/test_knowledge_graph 2.py +140 -0
  295. package/tests/test_migrations 2.py +137 -0
  296. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  297. /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -0
@@ -0,0 +1,300 @@
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
+