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.
- package/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- 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
|
-
]
|