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,290 +0,0 @@
1
- """NEXO KG Auto-Population — backfill from nexo.db + incremental hooks."""
2
-
3
- import json
4
- import os
5
- import sqlite3
6
- from typing import Optional
7
-
8
- import knowledge_graph as kg
9
- from db import get_db
10
-
11
-
12
- # ─── helpers ────────────────────────────────────────────────────────────────
13
-
14
- def _cognitive_db():
15
- """Direct cognitive.db connection (for somatic_markers)."""
16
- nexo_home = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
17
- data_dir = os.path.join(nexo_home, "data")
18
- os.makedirs(data_dir, exist_ok=True)
19
- path = os.path.join(data_dir, "cognitive.db")
20
- conn = sqlite3.connect(path)
21
- conn.row_factory = sqlite3.Row
22
- return conn
23
-
24
-
25
- def _parse_files(files_str: str) -> list[str]:
26
- """Extract individual file paths from a comma/newline-separated string."""
27
- if not files_str:
28
- return []
29
- parts = [p.strip() for p in files_str.replace("\n", ",").split(",")]
30
- return [p for p in parts if p]
31
-
32
-
33
- # ─── backfill functions ──────────────────────────────────────────────────────
34
-
35
- def backfill_entities() -> int:
36
- """Read entities table → create entity nodes in KG."""
37
- db = get_db()
38
- rows = db.execute("SELECT id, name, type, value, notes FROM entities").fetchall()
39
- count = 0
40
- for row in rows:
41
- props = {}
42
- if row["value"]:
43
- props["value"] = row["value"]
44
- if row["notes"]:
45
- props["notes"] = row["notes"]
46
- kg.upsert_node(
47
- node_type="entity",
48
- node_ref=f"entity:{row['id']}",
49
- label=row["name"],
50
- properties={"entity_type": row["type"], **props},
51
- )
52
- count += 1
53
- return count
54
-
55
-
56
- def backfill_learnings() -> int:
57
- """Read learnings → create learning nodes + file/area edges."""
58
- db = get_db()
59
- rows = db.execute(
60
- "SELECT id, category, title, applies_to FROM learnings WHERE status != 'deleted'"
61
- ).fetchall()
62
- count = 0
63
- for row in rows:
64
- learning_ref = f"learning:{row['id']}"
65
- kg.upsert_node(
66
- node_type="learning",
67
- node_ref=learning_ref,
68
- label=row["title"] or f"Learning #{row['id']}",
69
- properties={"category": row["category"]},
70
- )
71
- # edge: learning → category/area
72
- if row["category"]:
73
- kg.upsert_edge(
74
- source_type="learning", source_ref=learning_ref,
75
- relation="belongs_to",
76
- target_type="area", target_ref=f"area:{row['category']}",
77
- weight=1.0,
78
- )
79
- # edge: learning → file (from applies_to)
80
- applies = row["applies_to"] or ""
81
- for fpath in _parse_files(applies):
82
- if fpath:
83
- kg.upsert_edge(
84
- source_type="learning", source_ref=learning_ref,
85
- relation="applies_to_file",
86
- target_type="file", target_ref=f"file:{fpath}",
87
- weight=0.8,
88
- )
89
- count += 1
90
- return count
91
-
92
-
93
- def backfill_changes() -> int:
94
- """Read change_log → create file nodes + file→area edges."""
95
- db = get_db()
96
- rows = db.execute("SELECT id, files, what_changed FROM change_log").fetchall()
97
- count = 0
98
- for row in rows:
99
- change_ref = f"change:{row['id']}"
100
- kg.upsert_node(
101
- node_type="change",
102
- node_ref=change_ref,
103
- label=f"Change #{row['id']}",
104
- properties={"summary": (row["what_changed"] or "")[:120]},
105
- )
106
- for fpath in _parse_files(row["files"] or ""):
107
- file_ref = f"file:{fpath}"
108
- kg.upsert_node(
109
- node_type="file",
110
- node_ref=file_ref,
111
- label=os.path.basename(fpath) or fpath,
112
- )
113
- kg.upsert_edge(
114
- source_type="change", source_ref=change_ref,
115
- relation="touched",
116
- target_type="file", target_ref=file_ref,
117
- weight=1.0,
118
- )
119
- count += 1
120
- return count
121
-
122
-
123
- def backfill_decisions() -> int:
124
- """Read decisions → create decision nodes + decision→area edges."""
125
- db = get_db()
126
- rows = db.execute("SELECT id, domain, decision, status FROM decisions").fetchall()
127
- count = 0
128
- for row in rows:
129
- decision_ref = f"decision:{row['id']}"
130
- kg.upsert_node(
131
- node_type="decision",
132
- node_ref=decision_ref,
133
- label=(row["decision"] or "")[:80] or f"Decision #{row['id']}",
134
- properties={"domain": row["domain"], "status": row["status"]},
135
- )
136
- if row["domain"]:
137
- kg.upsert_edge(
138
- source_type="decision", source_ref=decision_ref,
139
- relation="in_domain",
140
- target_type="area", target_ref=f"area:{row['domain']}",
141
- weight=1.0,
142
- )
143
- count += 1
144
- return count
145
-
146
-
147
- def backfill_somatic() -> int:
148
- """Read somatic_markers from cognitive.db → create file/area nodes with risk."""
149
- cdb = _cognitive_db()
150
- rows = cdb.execute(
151
- "SELECT target, target_type, risk_score, incident_count FROM somatic_markers"
152
- ).fetchall()
153
- count = 0
154
- for row in rows:
155
- target_type = row["target_type"] or "file"
156
- node_ref = f"{target_type}:{row['target']}"
157
- kg.upsert_node(
158
- node_type=target_type,
159
- node_ref=node_ref,
160
- label=os.path.basename(row["target"]) or row["target"],
161
- properties={
162
- "risk_score": row["risk_score"],
163
- "incident_count": row["incident_count"],
164
- },
165
- )
166
- count += 1
167
- cdb.close()
168
- return count
169
-
170
-
171
- def run_full_backfill() -> dict:
172
- """Run all backfill functions. Idempotent (upsert-based)."""
173
- results = {}
174
- results["entities"] = backfill_entities()
175
- results["learnings"] = backfill_learnings()
176
- results["changes"] = backfill_changes()
177
- results["decisions"] = backfill_decisions()
178
- results["somatic"] = backfill_somatic()
179
- results["total"] = sum(results.values())
180
- return results
181
-
182
-
183
- # ─── incremental hooks ───────────────────────────────────────────────────────
184
-
185
- def on_learning_add(learning_id: int, category: str, title: str, applies_to: str = "") -> None:
186
- try:
187
- learning_ref = f"learning:{learning_id}"
188
- kg.upsert_node(
189
- node_type="learning",
190
- node_ref=learning_ref,
191
- label=title or f"Learning #{learning_id}",
192
- properties={"category": category},
193
- )
194
- if category:
195
- kg.upsert_edge(
196
- source_type="learning", source_ref=learning_ref,
197
- relation="belongs_to",
198
- target_type="area", target_ref=f"area:{category}",
199
- weight=1.0,
200
- )
201
- for fpath in _parse_files(applies_to or ""):
202
- if fpath:
203
- kg.upsert_edge(
204
- source_type="learning", source_ref=learning_ref,
205
- relation="applies_to_file",
206
- target_type="file", target_ref=f"file:{fpath}",
207
- weight=0.8,
208
- )
209
- except Exception:
210
- pass
211
-
212
-
213
- def on_change_log(change_id: int, files: str, system: str = "") -> None:
214
- try:
215
- change_ref = f"change:{change_id}"
216
- kg.upsert_node(
217
- node_type="change",
218
- node_ref=change_ref,
219
- label=f"Change #{change_id}",
220
- )
221
- for fpath in _parse_files(files or ""):
222
- file_ref = f"file:{fpath}"
223
- kg.upsert_node(
224
- node_type="file",
225
- node_ref=file_ref,
226
- label=os.path.basename(fpath) or fpath,
227
- )
228
- kg.upsert_edge(
229
- source_type="change", source_ref=change_ref,
230
- relation="touched",
231
- target_type="file", target_ref=file_ref,
232
- weight=1.0,
233
- )
234
- if system:
235
- kg.upsert_edge(
236
- source_type="change", source_ref=change_ref,
237
- relation="in_system",
238
- target_type="area", target_ref=f"area:{system}",
239
- weight=1.0,
240
- )
241
- except Exception:
242
- pass
243
-
244
-
245
- def on_decision_log(decision_id: int, domain: str, decision_text: str) -> None:
246
- try:
247
- decision_ref = f"decision:{decision_id}"
248
- kg.upsert_node(
249
- node_type="decision",
250
- node_ref=decision_ref,
251
- label=(decision_text or "")[:80] or f"Decision #{decision_id}",
252
- properties={"domain": domain},
253
- )
254
- if domain:
255
- kg.upsert_edge(
256
- source_type="decision", source_ref=decision_ref,
257
- relation="in_domain",
258
- target_type="area", target_ref=f"area:{domain}",
259
- weight=1.0,
260
- )
261
- except Exception:
262
- pass
263
-
264
-
265
- def on_entity_create(entity_id: int, name: str, entity_type: str) -> None:
266
- try:
267
- kg.upsert_node(
268
- node_type="entity",
269
- node_ref=f"entity:{entity_id}",
270
- label=name,
271
- properties={"entity_type": entity_type},
272
- )
273
- except Exception:
274
- pass
275
-
276
-
277
- # ─── main ────────────────────────────────────────────────────────────────────
278
-
279
- if __name__ == "__main__":
280
- print("Running full KG backfill...")
281
- results = run_full_backfill()
282
- print("\nBackfill complete:")
283
- for key, val in results.items():
284
- if key != "total":
285
- print(f" {key:12s}: {val:4d} records")
286
- print(f" {'TOTAL':12s}: {results['total']:4d} nodes/edges processed")
287
-
288
- # Show KG stats
289
- s = kg.stats()
290
- print(f"\nKG state: {s['nodes']} nodes, {s['edges_active']} active edges")
@@ -1,257 +0,0 @@
1
- """NEXO Knowledge Graph — Bi-temporal entity-relationship graph on SQLite."""
2
-
3
- import json
4
- from datetime import datetime, timezone
5
- from typing import Optional
6
- import os
7
-
8
-
9
- def _get_db():
10
- """Get cognitive.db connection (KG lives in cognitive.db)."""
11
- import cognitive
12
- return cognitive._get_db()
13
-
14
-
15
- def upsert_node(node_type: str, node_ref: str, label: str, properties: dict = None) -> int:
16
- db = _get_db()
17
- props_json = json.dumps(properties or {})
18
- existing = db.execute(
19
- "SELECT id FROM kg_nodes WHERE node_type = ? AND node_ref = ?",
20
- (node_type, node_ref)
21
- ).fetchone()
22
- if existing:
23
- db.execute("UPDATE kg_nodes SET label = ?, properties = ? WHERE id = ?",
24
- (label, props_json, existing["id"]))
25
- db.commit()
26
- return existing["id"]
27
- cursor = db.execute(
28
- "INSERT INTO kg_nodes (node_type, node_ref, label, properties) VALUES (?, ?, ?, ?)",
29
- (node_type, node_ref, label, props_json))
30
- db.commit()
31
- return cursor.lastrowid
32
-
33
-
34
- def get_node(node_type: str, node_ref: str) -> Optional[dict]:
35
- db = _get_db()
36
- row = db.execute("SELECT * FROM kg_nodes WHERE node_type = ? AND node_ref = ?",
37
- (node_type, node_ref)).fetchone()
38
- return dict(row) if row else None
39
-
40
-
41
- def get_node_by_id(node_id: int) -> Optional[dict]:
42
- db = _get_db()
43
- row = db.execute("SELECT * FROM kg_nodes WHERE id = ?", (node_id,)).fetchone()
44
- return dict(row) if row else None
45
-
46
-
47
- def upsert_edge(source_type: str, source_ref: str, relation: str,
48
- target_type: str, target_ref: str,
49
- weight: float = 1.0, confidence: float = 1.0,
50
- source_memory_id: str = "", properties: dict = None) -> dict:
51
- db = _get_db()
52
- source_node = get_node(source_type, source_ref)
53
- target_node = get_node(target_type, target_ref)
54
- if not source_node:
55
- source_node = {"id": upsert_node(source_type, source_ref, source_ref)}
56
- if not target_node:
57
- target_node = {"id": upsert_node(target_type, target_ref, target_ref)}
58
- source_id = source_node["id"]
59
- target_id = target_node["id"]
60
- props_json = json.dumps(properties or {})
61
- now = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
62
- existing = db.execute(
63
- "SELECT id, weight, confidence, properties FROM kg_edges "
64
- "WHERE source_id = ? AND target_id = ? AND relation = ? AND valid_until IS NULL",
65
- (source_id, target_id, relation)).fetchone()
66
- if existing:
67
- if (abs(existing["weight"] - weight) < 0.01 and
68
- abs(existing["confidence"] - confidence) < 0.01 and
69
- existing["properties"] == props_json):
70
- return {"action": "NOOP", "edge_id": existing["id"]}
71
- db.execute("UPDATE kg_edges SET valid_until = ? WHERE id = ?", (now, existing["id"]))
72
- cursor = db.execute(
73
- "INSERT INTO kg_edges (source_id, target_id, relation, weight, confidence, "
74
- "valid_from, source_memory_id, properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
75
- (source_id, target_id, relation, weight, confidence, now, source_memory_id, props_json))
76
- db.commit()
77
- return {"action": "UPDATE", "edge_id": cursor.lastrowid}
78
- cursor = db.execute(
79
- "INSERT INTO kg_edges (source_id, target_id, relation, weight, confidence, "
80
- "valid_from, source_memory_id, properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
81
- (source_id, target_id, relation, weight, confidence, now, source_memory_id, props_json))
82
- db.commit()
83
- return {"action": "ADD", "edge_id": cursor.lastrowid}
84
-
85
-
86
- def delete_edge(source_type: str, source_ref: str, relation: str,
87
- target_type: str, target_ref: str) -> bool:
88
- db = _get_db()
89
- source = get_node(source_type, source_ref)
90
- target = get_node(target_type, target_ref)
91
- if not source or not target:
92
- return False
93
- now = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
94
- cursor = db.execute(
95
- "UPDATE kg_edges SET valid_until = ? WHERE source_id = ? AND target_id = ? "
96
- "AND relation = ? AND valid_until IS NULL",
97
- (now, source["id"], target["id"], relation))
98
- db.commit()
99
- return cursor.rowcount > 0
100
-
101
-
102
- def get_neighbors(node_id: int, relation: str = None, active_only: bool = True) -> list:
103
- db = _get_db()
104
- conditions = ["(e.source_id = ? OR e.target_id = ?)"]
105
- params = [node_id, node_id]
106
- if active_only:
107
- conditions.append("e.valid_until IS NULL")
108
- if relation:
109
- conditions.append("e.relation = ?")
110
- params.append(relation)
111
- where = " AND ".join(conditions)
112
- rows = db.execute(f"""
113
- SELECT e.*, n.node_type, n.node_ref, n.label,
114
- CASE WHEN e.source_id = ? THEN 'outgoing' ELSE 'incoming' END as direction
115
- FROM kg_edges e
116
- JOIN kg_nodes n ON n.id = CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END
117
- WHERE {where}
118
- ORDER BY e.weight DESC
119
- """, [node_id, node_id] + params).fetchall()
120
- return [dict(r) for r in rows]
121
-
122
-
123
- def traverse(start_id: int, max_depth: int = 3, relation_filter: str = None,
124
- active_only: bool = True) -> dict:
125
- visited_nodes = set()
126
- visited_edges = set()
127
- result_nodes = []
128
- result_edges = []
129
- queue = [(start_id, 0)]
130
- while queue:
131
- current_id, depth = queue.pop(0)
132
- if current_id in visited_nodes or depth > max_depth:
133
- continue
134
- visited_nodes.add(current_id)
135
- node = get_node_by_id(current_id)
136
- if node:
137
- node["depth"] = depth
138
- result_nodes.append(node)
139
- neighbors = get_neighbors(current_id, relation=relation_filter, active_only=active_only)
140
- for n in neighbors:
141
- edge_id = n["id"]
142
- if edge_id not in visited_edges:
143
- visited_edges.add(edge_id)
144
- result_edges.append({
145
- "id": edge_id, "source_id": n["source_id"], "target_id": n["target_id"],
146
- "relation": n["relation"], "weight": n["weight"],
147
- "valid_from": n["valid_from"], "valid_until": n["valid_until"],
148
- })
149
- neighbor_id = n["target_id"] if n["source_id"] == current_id else n["source_id"]
150
- if neighbor_id not in visited_nodes and depth + 1 <= max_depth:
151
- queue.append((neighbor_id, depth + 1))
152
- return {"nodes": result_nodes, "edges": result_edges}
153
-
154
-
155
- def shortest_path(from_id: int, to_id: int, max_depth: int = 6) -> Optional[list]:
156
- if from_id == to_id:
157
- return [from_id]
158
- visited = {from_id}
159
- queue = [(from_id, [from_id])]
160
- while queue:
161
- current, path = queue.pop(0)
162
- if len(path) > max_depth:
163
- continue
164
- neighbors = get_neighbors(current, active_only=True)
165
- for n in neighbors:
166
- nid = n["target_id"] if n["source_id"] == current else n["source_id"]
167
- if nid == to_id:
168
- return path + [nid]
169
- if nid not in visited:
170
- visited.add(nid)
171
- queue.append((nid, path + [nid]))
172
- return None
173
-
174
-
175
- def merge_nodes(keep_id: int, merge_id: int) -> int:
176
- db = _get_db()
177
- db.execute("UPDATE kg_edges SET source_id = ? WHERE source_id = ?", (keep_id, merge_id))
178
- db.execute("UPDATE kg_edges SET target_id = ? WHERE target_id = ?", (keep_id, merge_id))
179
- # Clean up self-loops created by merge
180
- db.execute("DELETE FROM kg_edges WHERE source_id = target_id")
181
- db.execute("DELETE FROM kg_nodes WHERE id = ?", (merge_id,))
182
- db.commit()
183
- return keep_id
184
-
185
-
186
- def query_at(node_id: int, timestamp: str, relation: str = None) -> list:
187
- db = _get_db()
188
- conditions = ["(e.source_id = ? OR e.target_id = ?)",
189
- "e.valid_from <= ?",
190
- "(e.valid_until IS NULL OR e.valid_until >= ?)"]
191
- params = [node_id, node_id, timestamp, timestamp]
192
- if relation:
193
- conditions.append("e.relation = ?")
194
- params.append(relation)
195
- where = " AND ".join(conditions)
196
- rows = db.execute(f"""
197
- SELECT e.*, n.node_type, n.node_ref, n.label
198
- FROM kg_edges e
199
- JOIN kg_nodes n ON n.id = CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END
200
- WHERE {where}
201
- """, [node_id] + params).fetchall()
202
- return [dict(r) for r in rows]
203
-
204
-
205
- def timeline(node_id: int, relation: str = None) -> list:
206
- db = _get_db()
207
- conditions = ["(e.source_id = ? OR e.target_id = ?)"]
208
- params = [node_id, node_id]
209
- if relation:
210
- conditions.append("e.relation = ?")
211
- params.append(relation)
212
- where = " AND ".join(conditions)
213
- rows = db.execute(f"""
214
- SELECT e.*, n.node_type, n.node_ref, n.label
215
- FROM kg_edges e
216
- JOIN kg_nodes n ON n.id = CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END
217
- WHERE {where}
218
- ORDER BY e.valid_from
219
- """, [node_id] + params).fetchall()
220
- return [dict(r) for r in rows]
221
-
222
-
223
- def stats() -> dict:
224
- db = _get_db()
225
- nodes = db.execute("SELECT COUNT(*) FROM kg_nodes").fetchone()[0]
226
- edges_active = db.execute("SELECT COUNT(*) FROM kg_edges WHERE valid_until IS NULL").fetchone()[0]
227
- edges_historical = db.execute("SELECT COUNT(*) FROM kg_edges WHERE valid_until IS NOT NULL").fetchone()[0]
228
- type_counts = {}
229
- for row in db.execute("SELECT node_type, COUNT(*) as cnt FROM kg_nodes GROUP BY node_type").fetchall():
230
- type_counts[row["node_type"]] = row["cnt"]
231
- relation_counts = {}
232
- for row in db.execute(
233
- "SELECT relation, COUNT(*) as cnt FROM kg_edges WHERE valid_until IS NULL GROUP BY relation"
234
- ).fetchall():
235
- relation_counts[row["relation"]] = row["cnt"]
236
- most_connected = []
237
- for row in db.execute("""
238
- SELECT n.id, n.label, n.node_type, COUNT(e.id) as connections
239
- FROM kg_nodes n
240
- LEFT JOIN kg_edges e ON (e.source_id = n.id OR e.target_id = n.id) AND e.valid_until IS NULL
241
- GROUP BY n.id ORDER BY connections DESC LIMIT 10
242
- """).fetchall():
243
- most_connected.append(dict(row))
244
- return {
245
- "nodes": nodes, "edges_active": edges_active, "edges_historical": edges_historical,
246
- "node_types": type_counts, "relation_types": relation_counts,
247
- "most_connected": most_connected,
248
- }
249
-
250
-
251
- def extract_subgraph(center_id: int, depth: int = 2) -> dict:
252
- graph = traverse(center_id, max_depth=depth)
253
- d3_nodes = [{"id": n["id"], "label": n["label"], "type": n["node_type"],
254
- "depth": n.get("depth", 0)} for n in graph["nodes"]]
255
- d3_edges = [{"source": e["source_id"], "target": e["target_id"],
256
- "relation": e["relation"], "weight": e["weight"]} for e in graph["edges"]]
257
- return {"nodes": d3_nodes, "edges": d3_edges}
@@ -1,59 +0,0 @@
1
- """Opportunistic maintenance — run overdue tasks on MCP startup."""
2
-
3
- import time
4
- from datetime import datetime
5
- from db import get_db
6
-
7
-
8
- def check_and_run_overdue():
9
- conn = get_db()
10
- rows = conn.execute("SELECT task_name, interval_hours, last_run_at FROM maintenance_schedule").fetchall()
11
- ran = []
12
- for row in rows:
13
- task = row["task_name"]
14
- interval = row["interval_hours"]
15
- last_run = row["last_run_at"]
16
- if last_run:
17
- try:
18
- last_dt = datetime.strptime(last_run, "%Y-%m-%dT%H:%M:%S")
19
- hours_since = (datetime.now(datetime.timezone.utc).replace(tzinfo=None) - last_dt).total_seconds() / 3600
20
- if hours_since < interval:
21
- continue
22
- except (ValueError, TypeError):
23
- pass
24
- start = time.time()
25
- try:
26
- _run_task(task)
27
- duration_ms = int((time.time() - start) * 1000)
28
- conn.execute(
29
- "UPDATE maintenance_schedule SET last_run_at = ?, last_duration_ms = ?, "
30
- "run_count = run_count + 1 WHERE task_name = ?",
31
- (datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S"), duration_ms, task))
32
- conn.commit()
33
- ran.append({"task": task, "duration_ms": duration_ms})
34
- except Exception as e:
35
- ran.append({"task": task, "error": str(e)})
36
- return ran
37
-
38
-
39
- def _run_task(task_name: str):
40
- import cognitive
41
- if task_name == "cognitive_decay":
42
- cognitive.apply_decay()
43
- cognitive.promote_stm_to_ltm()
44
- cognitive.gc_stm()
45
- elif task_name == "somatic_decay":
46
- cognitive.somatic_nightly_decay()
47
- elif task_name == "somatic_projection":
48
- cognitive.somatic_project_events()
49
- elif task_name == "weight_learning":
50
- try:
51
- import sys, os
52
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "plugins"))
53
- from adaptive_mode import learn_weights, prune_adaptive_log
54
- learn_weights()
55
- prune_adaptive_log()
56
- except Exception:
57
- pass
58
- elif task_name == "graph_maintenance":
59
- pass # Future: orphan cleanup, consolidation