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,805 +0,0 @@
1
- """
2
- NEXO Adaptive Personality — Dynamic mode switching based on multi-signal detection.
3
-
4
- Three modes:
5
- - FLOW: User is in the zone. Be proactive, suggest improvements, explain reasoning.
6
- - NORMAL: Default operating mode. Follow calibration settings.
7
- - TENSION: User is frustrated or under pressure. Ultra-concise, only solve, zero friction.
8
-
9
- 6 signals (weighted):
10
- - Heartbeat VIBE sentiment (0.20) — keyword-based, noisy, reduced from 0.30
11
- - Trust corrections in recent interactions (0.30) — strongest explicit signal
12
- - User message brevity relative to baseline (0.15) — relative, not absolute
13
- - Topic context — deploys/production (0.10) — with emergency override
14
- - Tool error rate (0.15) — objective friction from failed tool calls
15
- - Git diff rejection proxy (0.10) — code reverted by user since last heartbeat
16
-
17
- Design decisions from AI debate (25 Mar 2026):
18
- - Emergency keywords ("production down", "outage") bypass hysteresis
19
- - Brevity is relative to user's baseline, not absolute thresholds
20
- - Severity-weighted decay: harsh sessions decay slower than mild ones
21
- - git diff whitelist: stash/checkout-branch/rebase don't count as rejection
22
- - Manual override: nexo_adaptive_override(mode) for user control
23
- """
24
-
25
- import os
26
- import json
27
- import time
28
- import math
29
- import subprocess
30
- from datetime import datetime, timedelta
31
- from db import get_db
32
-
33
- NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
34
- ADAPTIVE_STATE_FILE = os.path.join(NEXO_HOME, "brain", "adaptive_state.json")
35
-
36
- # Mode definitions
37
- MODES = {
38
- "FLOW": {
39
- "communication_override": "detailed",
40
- "proactivity_override": "proactive",
41
- "description": "User in flow. Suggest improvements, explain reasoning.",
42
- },
43
- "NORMAL": {
44
- "communication_override": None,
45
- "proactivity_override": None,
46
- "description": "Default mode. Follow calibration settings.",
47
- },
48
- "TENSION": {
49
- "communication_override": "concise",
50
- "proactivity_override": "reactive",
51
- "description": "User under pressure. Ultra-concise, only solve, zero friction.",
52
- },
53
- }
54
-
55
- # Signal weights (rebalanced after AI debate)
56
- WEIGHTS = {
57
- "vibe": 0.20, # Reduced: keyword-based is noisy/sarcasm-prone
58
- "corrections": 0.30, # Strongest explicit signal
59
- "brevity": 0.15, # Now relative to user baseline
60
- "topic": 0.10, # Low but has emergency override
61
- "tool_errors": 0.15, # NEW: objective friction signal
62
- "git_diff": 0.10, # NEW: code rejection proxy
63
- }
64
-
65
- # Thresholds
66
- TENSION_THRESHOLD = 0.55
67
- FLOW_THRESHOLD = -0.45
68
-
69
- # Tension topics
70
- TENSION_TOPICS = [
71
- "deploy", "production", "hotfix", "rollback", "broken", "down",
72
- "crash", "urgent", "emergency", "deadline", "server", "outage",
73
- "revert", "incident", "p0", "p1", "critical", "fix asap",
74
- ]
75
-
76
- # Emergency keywords — bypass hysteresis, force TENSION immediately
77
- EMERGENCY_KEYWORDS = [
78
- "production down", "production is down", "site is down", "outage",
79
- "server down", "everything broken", "p0", "incident",
80
- "rollback now", "revert now", "emergency",
81
- ]
82
-
83
- # Git operations that are NOT code rejection (whitelist)
84
- GIT_SAFE_OPS = [
85
- "git stash", "git checkout -b", "git checkout --", "git switch",
86
- "git rebase", "git merge", "git pull", "git fetch",
87
- ]
88
-
89
-
90
- def _log_to_db(mode, score, signals, context_hint=""):
91
- """Log adaptive computation to nexo.db for weight learning."""
92
- try:
93
- conn = get_db()
94
- conn.execute(
95
- "INSERT INTO adaptive_log (mode, tension_score, sig_vibe, sig_corrections, "
96
- "sig_brevity, sig_topic, sig_tool_errors, sig_git_diff, context_hint) "
97
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
98
- (mode, score, signals["vibe"], signals["corrections"], signals["brevity"],
99
- signals["topic"], signals["tool_errors"], signals["git_diff"], context_hint[:500])
100
- )
101
- conn.commit()
102
- except Exception:
103
- pass # DB logging is best-effort, never break mode computation
104
-
105
-
106
- def _load_state():
107
- """Load adaptive state from disk."""
108
- if os.path.exists(ADAPTIVE_STATE_FILE):
109
- try:
110
- with open(ADAPTIVE_STATE_FILE) as f:
111
- return json.load(f)
112
- except (json.JSONDecodeError, IOError):
113
- pass
114
- return {
115
- "current_mode": "NORMAL",
116
- "tension_score": 0.0,
117
- "corrections_window": [],
118
- "mode_history": [],
119
- "msg_lengths": [], # Rolling window for baseline brevity
120
- "tool_errors": [], # Rolling window for error rate
121
- "last_git_hash": None, # Last known git state for diff detection
122
- "peak_tension": 0.0, # For severity-weighted decay
123
- "manual_override": None, # User manual override (expires after session)
124
- "last_updated": None,
125
- }
126
-
127
-
128
- def _save_state(state):
129
- """Save adaptive state to disk."""
130
- state["last_updated"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
131
- os.makedirs(os.path.dirname(ADAPTIVE_STATE_FILE), exist_ok=True)
132
- with open(ADAPTIVE_STATE_FILE, "w") as f:
133
- json.dump(state, f, indent=2)
134
-
135
-
136
- def _get_baseline_brevity(state, current_length):
137
- """Compute relative brevity signal based on user's rolling baseline."""
138
- lengths = state.get("msg_lengths", [])
139
- lengths.append(current_length)
140
- # Keep last 30 messages for baseline
141
- lengths = lengths[-30:]
142
- state["msg_lengths"] = lengths
143
-
144
- if len(lengths) < 5:
145
- return 0.0 # Not enough data for baseline
146
-
147
- avg = sum(lengths) / len(lengths)
148
- if avg == 0:
149
- return 0.0
150
-
151
- # How much shorter is current message vs baseline?
152
- ratio = current_length / avg
153
- if ratio < 0.3:
154
- return 0.7 # Much shorter than usual → strong tension signal
155
- elif ratio < 0.5:
156
- return 0.4 # Shorter than usual
157
- elif ratio > 2.0:
158
- return -0.5 # Much longer than usual → flow signal
159
- elif ratio > 1.5:
160
- return -0.3 # Longer than usual
161
- return 0.0
162
-
163
-
164
- def _get_tool_error_rate(state, new_error=False):
165
- """Compute tool error rate from rolling window."""
166
- errors = state.get("tool_errors", [])
167
- now = time.time()
168
- cutoff = now - 900 # 15 min window
169
-
170
- # Clean old entries
171
- errors = [e for e in errors if e["ts"] > cutoff]
172
-
173
- if new_error:
174
- errors.append({"ts": now, "error": True})
175
- else:
176
- errors.append({"ts": now, "error": False})
177
-
178
- state["tool_errors"] = errors[-20:] # Keep last 20
179
-
180
- if len(errors) < 3:
181
- return 0.0
182
-
183
- error_count = sum(1 for e in errors if e["error"])
184
- rate = error_count / len(errors)
185
-
186
- # 0% errors = 0.0, 30% = 0.5, 60%+ = 1.0
187
- if rate >= 0.6:
188
- return 1.0
189
- elif rate >= 0.3:
190
- return 0.5
191
- elif rate >= 0.15:
192
- return 0.2
193
- return 0.0
194
-
195
-
196
- def _get_git_diff_signal(state):
197
- """Check if user reverted AI-generated code since last heartbeat."""
198
- try:
199
- # Get current short hash
200
- result = subprocess.run(
201
- ["git", "rev-parse", "--short", "HEAD"],
202
- capture_output=True, text=True, timeout=5, cwd=os.getcwd()
203
- )
204
- if result.returncode != 0:
205
- return 0.0
206
-
207
- current_hash = result.stdout.strip()
208
- last_hash = state.get("last_git_hash")
209
- state["last_git_hash"] = current_hash
210
-
211
- if not last_hash:
212
- return 0.0 # First reading, no comparison
213
-
214
- # Check if there were manual reverts (unstaged changes that undo recent work)
215
- diff_result = subprocess.run(
216
- ["git", "diff", "--stat"],
217
- capture_output=True, text=True, timeout=5, cwd=os.getcwd()
218
- )
219
- if diff_result.returncode != 0:
220
- return 0.0
221
-
222
- diff_stat = diff_result.stdout.strip()
223
- if not diff_stat:
224
- return 0.0
225
-
226
- # Check recent git reflog for safe operations (stash, branch switch, etc.)
227
- reflog = subprocess.run(
228
- ["git", "reflog", "-1", "--format=%gs"],
229
- capture_output=True, text=True, timeout=5, cwd=os.getcwd()
230
- )
231
- if reflog.returncode == 0:
232
- last_action = reflog.stdout.strip().lower()
233
- for safe_op in GIT_SAFE_OPS:
234
- if safe_op.replace("git ", "") in last_action:
235
- return 0.0 # Safe operation, not a rejection
236
-
237
- # Files changed could indicate rejection — mild signal
238
- lines = diff_stat.strip().split("\n")
239
- files_changed = len(lines) - 1 # Last line is summary
240
- if files_changed >= 3:
241
- return 0.6 # Many files changed since last heartbeat
242
- elif files_changed >= 1:
243
- return 0.3
244
- return 0.0
245
-
246
- except Exception:
247
- return 0.0 # git diff is best-effort
248
-
249
-
250
- def _check_emergency(context_hint: str) -> bool:
251
- """Check for emergency keywords that bypass hysteresis."""
252
- hint_lower = context_hint.lower()
253
- return any(kw in hint_lower for kw in EMERGENCY_KEYWORDS)
254
-
255
-
256
- def compute_mode(
257
- vibe: str = "neutral",
258
- vibe_intensity: float = 0.5,
259
- recent_corrections: int = 0,
260
- user_msg_length: int = 50,
261
- context_hint: str = "",
262
- tool_had_error: bool = False,
263
- ) -> dict:
264
- """
265
- Compute the current adaptive mode from 6 weighted signals.
266
-
267
- Returns dict with: mode, score, signals, overrides, description, changed, emergency
268
- """
269
- state = _load_state()
270
- prev_mode = state["current_mode"]
271
-
272
- # Check manual override first
273
- manual = state.get("manual_override")
274
- if manual:
275
- mode_def = MODES[manual]
276
- return {
277
- "mode": manual,
278
- "score": state.get("tension_score", 0.0),
279
- "changed": False,
280
- "previous_mode": None,
281
- "manual_override": True,
282
- "signals": {},
283
- "overrides": {
284
- "communication": mode_def["communication_override"],
285
- "proactivity": mode_def["proactivity_override"],
286
- },
287
- "description": f"Manual override: {mode_def['description']}",
288
- }
289
-
290
- # Check emergency bypass
291
- is_emergency = _check_emergency(context_hint)
292
-
293
- # --- Signal 1: VIBE sentiment (0.20) ---
294
- vibe_signal = 0.0
295
- if vibe.upper() == "NEGATIVE":
296
- vibe_signal = vibe_intensity
297
- elif vibe.upper() == "POSITIVE":
298
- vibe_signal = -vibe_intensity
299
-
300
- # --- Signal 2: Corrections (0.30) ---
301
- now = time.time()
302
- cutoff = now - 900
303
- state["corrections_window"] = [
304
- t for t in state.get("corrections_window", []) if t > cutoff
305
- ]
306
- for _ in range(recent_corrections):
307
- state["corrections_window"].append(now)
308
-
309
- correction_count = len(state["corrections_window"])
310
- correction_signal = min(correction_count * 0.3, 1.0)
311
-
312
- # --- Signal 3: Relative brevity (0.15) ---
313
- brevity_signal = _get_baseline_brevity(state, user_msg_length)
314
-
315
- # --- Signal 4: Topic context (0.10) ---
316
- topic_signal = 0.0
317
- hint_lower = context_hint.lower()
318
- tension_matches = sum(1 for t in TENSION_TOPICS if t in hint_lower)
319
- if tension_matches >= 2:
320
- topic_signal = 0.8
321
- elif tension_matches == 1:
322
- topic_signal = 0.4
323
-
324
- # --- Signal 5: Tool error rate (0.15) ---
325
- tool_error_signal = _get_tool_error_rate(state, new_error=tool_had_error)
326
-
327
- # --- Signal 6: Git diff rejection (0.10) ---
328
- git_diff_signal = _get_git_diff_signal(state)
329
-
330
- # --- Weighted composite score ---
331
- # Use learned weights if available, otherwise static
332
- active_weights = state.get("learned_weights", None)
333
- if not active_weights or len(active_weights) != 6:
334
- active_weights = WEIGHTS
335
-
336
- composite = (
337
- active_weights["vibe"] * vibe_signal
338
- + active_weights["corrections"] * correction_signal
339
- + active_weights["brevity"] * brevity_signal
340
- + active_weights["topic"] * topic_signal
341
- + active_weights["tool_errors"] * tool_error_signal
342
- + active_weights["git_diff"] * git_diff_signal
343
- )
344
-
345
- # Momentum (30% of previous score for stability)
346
- prev_tension = state.get("tension_score", 0.0)
347
- smoothed = 0.7 * composite + 0.3 * prev_tension
348
-
349
- # Track peak tension for severity-weighted decay
350
- if smoothed > state.get("peak_tension", 0.0):
351
- state["peak_tension"] = smoothed
352
-
353
- # --- Determine mode ---
354
- if smoothed >= TENSION_THRESHOLD:
355
- new_mode = "TENSION"
356
- elif smoothed <= FLOW_THRESHOLD:
357
- new_mode = "FLOW"
358
- else:
359
- new_mode = "NORMAL"
360
-
361
- # --- Emergency bypass: skip hysteresis ---
362
- if is_emergency:
363
- new_mode = "TENSION"
364
- smoothed = max(smoothed, TENSION_THRESHOLD + 0.1)
365
- state["pending_mode"] = None
366
- else:
367
- # --- Hysteresis: require 2 consecutive signals to change mode ---
368
- pending_mode = state.get("pending_mode", None)
369
- if new_mode != prev_mode:
370
- if pending_mode == new_mode:
371
- state["pending_mode"] = None
372
- else:
373
- state["pending_mode"] = new_mode
374
- new_mode = prev_mode
375
- else:
376
- state["pending_mode"] = None
377
-
378
- # --- Record ---
379
- changed = new_mode != prev_mode
380
- state["current_mode"] = new_mode
381
- state["tension_score"] = smoothed
382
-
383
- state["mode_history"].append({
384
- "timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
385
- "mode": new_mode,
386
- "score": round(smoothed, 3),
387
- "changed": changed,
388
- "emergency": is_emergency,
389
- })
390
- state["mode_history"] = state["mode_history"][-50:]
391
-
392
- # Log to DB for learned weights
393
- _log_to_db(new_mode, smoothed, {
394
- "vibe": vibe_signal, "corrections": correction_signal,
395
- "brevity": brevity_signal, "topic": topic_signal,
396
- "tool_errors": tool_error_signal, "git_diff": git_diff_signal,
397
- }, context_hint)
398
-
399
- _save_state(state)
400
-
401
- mode_def = MODES[new_mode]
402
- return {
403
- "mode": new_mode,
404
- "score": round(smoothed, 3),
405
- "changed": changed,
406
- "previous_mode": prev_mode if changed else None,
407
- "emergency": is_emergency,
408
- "signals": {
409
- "vibe": round(vibe_signal, 2),
410
- "corrections": round(correction_signal, 2),
411
- "brevity": round(brevity_signal, 2),
412
- "topic": round(topic_signal, 2),
413
- "tool_errors": round(tool_error_signal, 2),
414
- "git_diff": round(git_diff_signal, 2),
415
- },
416
- "overrides": {
417
- "communication": mode_def["communication_override"],
418
- "proactivity": mode_def["proactivity_override"],
419
- },
420
- "description": mode_def["description"],
421
- "weights_source": "learned" if state.get("learned_weights") and len(state.get("learned_weights", {})) == 6 else "static",
422
- }
423
-
424
-
425
- def decay_tension(gamma: float = 0.15):
426
- """
427
- Inter-session tension decay with severity weighting.
428
-
429
- Mild sessions (peak < 0.4): gamma * 1.5 (faster decay — ~3h half-life)
430
- Normal sessions (peak 0.4-0.7): gamma * 1.0 (standard — ~4.6h half-life)
431
- Severe sessions (peak > 0.7): gamma * 0.6 (slower decay — ~7.7h half-life)
432
-
433
- After 6+ hours gap with no new signals, apply aggressive floor reset.
434
- """
435
- state = _load_state()
436
- last_updated = state.get("last_updated")
437
- if not last_updated:
438
- return
439
-
440
- try:
441
- last_dt = datetime.strptime(last_updated, "%Y-%m-%dT%H:%M:%S")
442
- except (ValueError, TypeError):
443
- return
444
-
445
- hours_elapsed = (datetime.utcnow() - last_dt).total_seconds() / 3600
446
-
447
- if hours_elapsed < 1:
448
- return
449
-
450
- old_tension = state.get("tension_score", 0.0)
451
- peak = state.get("peak_tension", abs(old_tension))
452
-
453
- # Severity-weighted gamma
454
- if peak > 0.7:
455
- effective_gamma = gamma * 0.6 # Severe: slow decay
456
- elif peak < 0.4:
457
- effective_gamma = gamma * 1.5 # Mild: fast decay
458
- else:
459
- effective_gamma = gamma # Normal
460
-
461
- new_tension = old_tension * math.exp(-effective_gamma * hours_elapsed)
462
-
463
- # Aggressive floor reset after 6+ hours (sleep reset)
464
- if hours_elapsed >= 6 and abs(new_tension) < 0.2:
465
- new_tension = 0.0
466
- state["peak_tension"] = 0.0
467
-
468
- if abs(new_tension) < 0.05:
469
- new_tension = 0.0
470
- state["current_mode"] = "NORMAL"
471
- state["pending_mode"] = None
472
- state["peak_tension"] = 0.0
473
-
474
- state["tension_score"] = round(new_tension, 4)
475
- _save_state(state)
476
-
477
- return {
478
- "old_tension": round(old_tension, 4),
479
- "new_tension": round(new_tension, 4),
480
- "peak": round(peak, 4),
481
- "effective_gamma": round(effective_gamma, 4),
482
- "hours_elapsed": round(hours_elapsed, 1),
483
- "mode": state["current_mode"],
484
- }
485
-
486
-
487
- def learn_weights(min_samples: int = 30, lookback_days: int = 30) -> dict:
488
- """Learn optimal signal weights from feedback-annotated adaptive_log entries.
489
-
490
- Uses Ridge regression with weight momentum (0.85 old + 0.15 new).
491
- Starts in shadow mode — logs what weights WOULD be without activating.
492
- After 2 weeks of shadow data, transitions to active mode.
493
- """
494
- try:
495
- conn = get_db()
496
- cutoff = (datetime.utcnow() - timedelta(days=lookback_days)).strftime("%Y-%m-%dT%H:%M:%S")
497
- rows = conn.execute(
498
- "SELECT sig_vibe, sig_corrections, sig_brevity, sig_topic, sig_tool_errors, "
499
- "sig_git_diff, feedback_delta FROM adaptive_log "
500
- "WHERE feedback_event IS NOT NULL AND timestamp >= ?",
501
- (cutoff,)
502
- ).fetchall()
503
-
504
- if len(rows) < min_samples:
505
- return {"status": "insufficient_data", "samples": len(rows), "min_required": min_samples}
506
-
507
- import numpy as np
508
- X = np.array([[r[0], r[1], r[2], r[3], r[4], r[5]] for r in rows], dtype=np.float64)
509
- y = np.array([r[6] for r in rows], dtype=np.float64)
510
-
511
- # Ridge regression (alpha=1.0 — more stable than OLS with correlated features)
512
- try:
513
- n_features = X.shape[1]
514
- XtX = X.T @ X + np.eye(n_features) * 1.0
515
- Xty = X.T @ y
516
- w = np.linalg.solve(XtX, Xty)
517
- except np.linalg.LinAlgError:
518
- return {"status": "regression_failed", "samples": len(rows)}
519
-
520
- w = np.abs(w)
521
- w = np.clip(w, 0.05, 0.50)
522
- w = w / w.sum()
523
-
524
- signal_names = ["vibe", "corrections", "brevity", "topic", "tool_errors", "git_diff"]
525
- raw_learned = {name: round(float(w[i]), 4) for i, name in enumerate(signal_names)}
526
-
527
- # Weight momentum: blend 85% old + 15% new (prevents personality whiplash)
528
- state = _load_state()
529
- old_weights = state.get("learned_weights", dict(WEIGHTS))
530
- learned = {}
531
- for name in signal_names:
532
- blended = 0.85 * old_weights.get(name, WEIGHTS[name]) + 0.15 * raw_learned[name]
533
- learned[name] = round(blended, 4)
534
- total = sum(learned.values())
535
- learned = {k: round(v / total, 4) for k, v in learned.items()}
536
-
537
- drift = {name: round(learned[name] - WEIGHTS[name], 4) for name in signal_names}
538
- max_drift = max(abs(d) for d in drift.values())
539
-
540
- # Shadow mode: first 2 weeks, only LOG without activating
541
- first_learned_date = state.get("learned_weights_first_date")
542
- if not first_learned_date:
543
- first_learned_date = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
544
- state["learned_weights_first_date"] = first_learned_date
545
-
546
- first_dt = datetime.strptime(first_learned_date, "%Y-%m-%dT%H:%M:%S")
547
- days_since_first = (datetime.utcnow() - first_dt).days
548
- is_shadow = days_since_first < 14
549
-
550
- state["shadow_weights"] = learned
551
- state["shadow_weights_date"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
552
- state["shadow_weights_samples"] = len(rows)
553
-
554
- if not is_shadow:
555
- state["learned_weights"] = learned
556
- state["learned_weights_date"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
557
- state["learned_weights_samples"] = len(rows)
558
- _save_state(state)
559
-
560
- return {
561
- "status": "shadow" if is_shadow else "active",
562
- "mode": "shadow" if is_shadow else "active",
563
- "days_in_shadow": days_since_first if is_shadow else 0,
564
- "samples": len(rows),
565
- "weights": learned,
566
- "raw_weights": raw_learned,
567
- "static_weights": dict(WEIGHTS),
568
- "drift": drift,
569
- "max_drift": max_drift,
570
- }
571
- except Exception as e:
572
- return {"status": "error", "error": str(e)}
573
-
574
-
575
- def prune_adaptive_log(max_age_days: int = 90):
576
- """Remove adaptive_log entries older than max_age_days."""
577
- try:
578
- conn = get_db()
579
- cutoff = (datetime.utcnow() - timedelta(days=max_age_days)).strftime("%Y-%m-%dT%H:%M:%S")
580
- cursor = conn.execute("DELETE FROM adaptive_log WHERE timestamp < ?", (cutoff,))
581
- conn.commit()
582
- return cursor.rowcount
583
- except Exception:
584
- return 0
585
-
586
-
587
- def check_weight_rollback() -> dict:
588
- """Check if learned weights should be rolled back.
589
- Compares correction rate in last 7 days vs 7 days before activation.
590
- Includes minimum-volume guard (skip if <10 events in either window).
591
- """
592
- state = _load_state()
593
- activation_date = state.get("learned_weights_date")
594
- if not activation_date:
595
- return {"status": "no_learned_weights"}
596
- try:
597
- conn = get_db()
598
- activation_dt = datetime.strptime(activation_date, "%Y-%m-%dT%H:%M:%S")
599
- days_since = (datetime.utcnow() - activation_dt).days
600
- if days_since < 7:
601
- return {"status": "too_early", "days_since_activation": days_since}
602
-
603
- pre_start = (activation_dt - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
604
- pre_end = activation_date
605
- pre_corrections = conn.execute(
606
- "SELECT COUNT(*) FROM adaptive_log WHERE feedback_event IN ('correction','repeated_error') "
607
- "AND timestamp BETWEEN ? AND ?", (pre_start, pre_end)
608
- ).fetchone()[0]
609
-
610
- post_start = (datetime.utcnow() - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%S")
611
- post_corrections = conn.execute(
612
- "SELECT COUNT(*) FROM adaptive_log WHERE feedback_event IN ('correction','repeated_error') "
613
- "AND timestamp >= ?", (post_start,)
614
- ).fetchone()[0]
615
-
616
- # Minimum-volume guard
617
- pre_total = conn.execute(
618
- "SELECT COUNT(*) FROM adaptive_log WHERE timestamp BETWEEN ? AND ?",
619
- (pre_start, pre_end)
620
- ).fetchone()[0]
621
- post_total = conn.execute(
622
- "SELECT COUNT(*) FROM adaptive_log WHERE timestamp >= ?", (post_start,)
623
- ).fetchone()[0]
624
- if pre_total < 10 or post_total < 10:
625
- return {"status": "low_volume", "pre_events": pre_total, "post_events": post_total,
626
- "days_since_activation": days_since}
627
-
628
- pre_rate = pre_corrections / 7
629
- post_rate = post_corrections / 7
630
-
631
- if pre_rate > 0 and post_rate >= 2 * pre_rate:
632
- state.pop("learned_weights", None)
633
- state.pop("learned_weights_date", None)
634
- state.pop("learned_weights_samples", None)
635
- state["learned_weights_rollback"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
636
- _save_state(state)
637
- return {"status": "rolled_back", "pre_rate": round(pre_rate, 2),
638
- "post_rate": round(post_rate, 2),
639
- "reason": f"Recent correction rate {post_rate:.2f}/day vs pre-activation {pre_rate:.2f}/day (>=2x)"}
640
-
641
- return {"status": "ok", "pre_rate": round(pre_rate, 2), "post_rate": round(post_rate, 2),
642
- "days_since_activation": days_since}
643
- except Exception as e:
644
- return {"status": "error", "error": str(e)}
645
-
646
-
647
- def reset_session():
648
- """
649
- Reset adaptive state for a new session.
650
- Keeps tension_score (for inter-session continuity) but clears windows.
651
- """
652
- state = _load_state()
653
- state["corrections_window"] = []
654
- state["tool_errors"] = []
655
- state["pending_mode"] = None
656
- state["manual_override"] = None # Clear manual override between sessions
657
- state["last_git_hash"] = None
658
-
659
- if abs(state.get("tension_score", 0.0)) < TENSION_THRESHOLD:
660
- state["current_mode"] = "NORMAL"
661
-
662
- _save_state(state)
663
- return state["current_mode"]
664
-
665
-
666
- # --- MCP Tool handlers ---
667
-
668
- def handle_adaptive_mode(
669
- vibe: str = "",
670
- vibe_intensity: float = 0.5,
671
- corrections: int = 0,
672
- msg_length: int = 50,
673
- context: str = "",
674
- tool_error: bool = False,
675
- ) -> str:
676
- """Get or compute the current adaptive personality mode.
677
-
678
- Call without args to get current mode. Call with signals to update.
679
- Returns: mode (FLOW/NORMAL/TENSION), score, 6-signal breakdown, and any overrides.
680
- """
681
- if not vibe and corrections == 0 and not tool_error:
682
- state = _load_state()
683
- mode = state.get("current_mode", "NORMAL")
684
- manual = state.get("manual_override")
685
- if manual:
686
- mode = manual
687
- mode_def = MODES[mode]
688
- return json.dumps({
689
- "mode": mode,
690
- "score": state.get("tension_score", 0.0),
691
- "manual_override": manual,
692
- "overrides": {
693
- "communication": mode_def["communication_override"],
694
- "proactivity": mode_def["proactivity_override"],
695
- },
696
- "description": mode_def["description"],
697
- }, indent=2)
698
-
699
- result = compute_mode(
700
- vibe=vibe,
701
- vibe_intensity=vibe_intensity,
702
- recent_corrections=corrections,
703
- user_msg_length=msg_length,
704
- context_hint=context,
705
- tool_had_error=tool_error,
706
- )
707
- return json.dumps(result, indent=2)
708
-
709
-
710
- def handle_adaptive_history(last_n: int = 10) -> str:
711
- """View recent adaptive mode transitions and score history.
712
-
713
- Args:
714
- last_n: Number of recent entries to show (default 10).
715
- """
716
- state = _load_state()
717
- history = state.get("mode_history", [])[-last_n:]
718
- return json.dumps({
719
- "current_mode": state.get("current_mode", "NORMAL"),
720
- "tension_score": state.get("tension_score", 0.0),
721
- "peak_tension": state.get("peak_tension", 0.0),
722
- "corrections_in_window": len(state.get("corrections_window", [])),
723
- "tool_errors_in_window": len(state.get("tool_errors", [])),
724
- "manual_override": state.get("manual_override"),
725
- "msg_baseline_count": len(state.get("msg_lengths", [])),
726
- "history": history,
727
- }, indent=2)
728
-
729
-
730
- def handle_adaptive_decay() -> str:
731
- """Manually trigger inter-session tension decay. Normally runs automatically during nocturnal processes."""
732
- result = decay_tension()
733
- if result:
734
- return json.dumps(result, indent=2)
735
- return "No decay needed (no previous state or too recent)."
736
-
737
-
738
- def handle_adaptive_reset() -> str:
739
- """Reset adaptive mode for a fresh session start. Keeps inter-session tension but clears correction window."""
740
- mode = reset_session()
741
- return f"Session reset. Starting mode: {mode}"
742
-
743
-
744
- def handle_adaptive_override(mode: str = "") -> str:
745
- """Manual user override of adaptive mode. Use when the system misreads your state.
746
-
747
- Args:
748
- mode: FLOW, NORMAL, TENSION, or empty to clear override.
749
- """
750
- state = _load_state()
751
-
752
- if not mode or mode.upper() == "CLEAR":
753
- state["manual_override"] = None
754
- _save_state(state)
755
- return f"Manual override cleared. Returning to auto-detection (current: {state['current_mode']})."
756
-
757
- mode_upper = mode.upper()
758
- if mode_upper not in MODES:
759
- return f"Invalid mode '{mode}'. Use FLOW, NORMAL, TENSION, or CLEAR."
760
-
761
- state["manual_override"] = mode_upper
762
- _save_state(state)
763
- mode_def = MODES[mode_upper]
764
- return f"Manual override set: {mode_upper}. {mode_def['description']} Use 'CLEAR' to return to auto-detection."
765
-
766
-
767
- def handle_adaptive_weights() -> str:
768
- """View current adaptive weights — static vs learned, training stats, drift from baseline, shadow mode status."""
769
- state = _load_state()
770
- learned = state.get("learned_weights")
771
- shadow = state.get("shadow_weights")
772
- result = {
773
- "static_weights": dict(WEIGHTS),
774
- "using": "learned" if learned and len(learned) == 6 else "static",
775
- }
776
- if learned and len(learned) == 6:
777
- result["learned_weights"] = learned
778
- result["learned_date"] = state.get("learned_weights_date", "unknown")
779
- result["learned_samples"] = state.get("learned_weights_samples", 0)
780
- result["drift"] = {k: round(learned[k] - WEIGHTS[k], 4) for k in WEIGHTS}
781
- result["max_drift"] = max(abs(d) for d in result["drift"].values())
782
- if shadow and len(shadow) == 6:
783
- result["shadow_weights"] = shadow
784
- result["shadow_date"] = state.get("shadow_weights_date")
785
- result["shadow_samples"] = state.get("shadow_weights_samples", 0)
786
- first = state.get("learned_weights_first_date")
787
- if first:
788
- days = (datetime.utcnow() - datetime.strptime(first, "%Y-%m-%dT%H:%M:%S")).days
789
- result["shadow_days"] = days
790
- result["shadow_active"] = days < 14
791
- rollback = state.get("learned_weights_rollback")
792
- if rollback:
793
- result["last_rollback"] = rollback
794
- return json.dumps(result, indent=2)
795
-
796
-
797
- # Plugin registration
798
- TOOLS = [
799
- (handle_adaptive_mode, "nexo_adaptive_mode", "Get or compute adaptive personality mode (FLOW/NORMAL/TENSION) from 6 signals"),
800
- (handle_adaptive_history, "nexo_adaptive_history", "View recent adaptive mode transitions and signal history"),
801
- (handle_adaptive_decay, "nexo_adaptive_decay", "Trigger inter-session tension decay (severity-weighted)"),
802
- (handle_adaptive_reset, "nexo_adaptive_reset", "Reset adaptive state for new session"),
803
- (handle_adaptive_override, "nexo_adaptive_override", "Manual override: force FLOW/NORMAL/TENSION or CLEAR to return to auto"),
804
- (handle_adaptive_weights, "nexo_adaptive_weights", "View adaptive weights — static vs learned, training stats, shadow mode, drift"),
805
- ]