nexo-brain 5.3.13 → 5.3.15

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 (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -1
  3. package/package.json +1 -1
  4. package/src/crons/sync.py +18 -4
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_entities.py +1 -1
  43. package/src/db/_episodic 2.py +762 -0
  44. package/src/db/_evolution 2.py +54 -0
  45. package/src/db/_fts 2.py +406 -0
  46. package/src/db/_goal_profiles 2.py +376 -0
  47. package/src/db/_hot_context 2.py +660 -0
  48. package/src/db/_outcomes 2.py +800 -0
  49. package/src/db/_personal_scripts 2.py +582 -0
  50. package/src/db/_sessions 2.py +330 -0
  51. package/src/db/_tasks 2.py +91 -0
  52. package/src/db/_watchers 2.py +173 -0
  53. package/src/doctor/formatters 2.py +52 -0
  54. package/src/doctor/models 2.py +69 -0
  55. package/src/doctor/planes 2.py +87 -0
  56. package/src/doctor/providers/__init__ 2.py +1 -0
  57. package/src/doctor/providers/deep 2.py +367 -0
  58. package/src/evolution_cycle 2.py +519 -0
  59. package/src/hooks/auto_capture 2.py +208 -0
  60. package/src/hooks/caffeinate-guard 2.sh +8 -0
  61. package/src/hooks/capture-session 2.sh +21 -0
  62. package/src/hooks/capture-tool-logs 2.sh +158 -0
  63. package/src/hooks/daily-briefing-check 2.sh +33 -0
  64. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  65. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  66. package/src/hooks/inbox-hook 2.sh +76 -0
  67. package/src/hooks/post-compact 2.sh +152 -0
  68. package/src/hooks/pre-compact 2.sh +169 -0
  69. package/src/hooks/protocol-guardrail 2.sh +10 -0
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  71. package/src/hooks/session-stop 2.sh +52 -0
  72. package/src/kg_populate 2.py +292 -0
  73. package/src/maintenance 2.py +53 -0
  74. package/src/memory_backends 2.py +71 -0
  75. package/src/migrate_embeddings 2.py +124 -0
  76. package/src/nexo_sdk 2.py +103 -0
  77. package/src/observability 2.py +199 -0
  78. package/src/plugin_loader 2.py +217 -0
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/agents.py +10 -3
  81. package/src/plugins/artifact_registry 2.py +450 -0
  82. package/src/plugins/backup 2.py +127 -0
  83. package/src/plugins/claims_tools 2.py +119 -0
  84. package/src/plugins/cognitive_memory 2.py +609 -0
  85. package/src/plugins/core_rules 2.py +252 -0
  86. package/src/plugins/cortex 2.py +1155 -0
  87. package/src/plugins/entities 2.py +67 -0
  88. package/src/plugins/episodic_memory 2.py +560 -0
  89. package/src/plugins/evolution 2.py +167 -0
  90. package/src/plugins/goal_engine 2.py +142 -0
  91. package/src/plugins/guard 2.py +862 -0
  92. package/src/plugins/impact 2.py +29 -0
  93. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  94. package/src/plugins/media_memory_tools 2.py +98 -0
  95. package/src/plugins/memory_export 2.py +196 -0
  96. package/src/plugins/outcomes 2.py +130 -0
  97. package/src/plugins/personal_scripts 2.py +117 -0
  98. package/src/plugins/preferences 2.py +47 -0
  99. package/src/plugins/protocol 2.py +1449 -0
  100. package/src/plugins/schedule.py +2 -1
  101. package/src/plugins/simple_api 2.py +106 -0
  102. package/src/plugins/skills 2.py +341 -0
  103. package/src/plugins/state_watchers 2.py +79 -0
  104. package/src/plugins/update 2.py +986 -0
  105. package/src/plugins/user_state_tools 2.py +43 -0
  106. package/src/plugins/workflow 2.py +588 -0
  107. package/src/protocol_settings 2.py +59 -0
  108. package/src/public_contribution 2.py +466 -0
  109. package/src/public_evolution_queue 2.py +241 -0
  110. package/src/requirements 2.txt +14 -0
  111. package/src/requirements.txt +1 -1
  112. package/src/retroactive_learnings 2.py +373 -0
  113. package/src/rules/__init__ 2.py +0 -0
  114. package/src/rules/core-rules 2.json +331 -0
  115. package/src/rules/migrate 2.py +207 -0
  116. package/src/runtime_power 2.py +874 -0
  117. package/src/runtime_power.py +18 -1
  118. package/src/script_registry 2.py +1559 -0
  119. package/src/scripts/check-context 2.py +272 -0
  120. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  121. package/src/scripts/deep-sleep/collect 2.py +928 -0
  122. package/src/scripts/deep-sleep/extract 2.py +330 -0
  123. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  124. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  125. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  126. package/src/scripts/nexo-agent-run 2.py +75 -0
  127. package/src/scripts/nexo-auto-update 2.py +6 -0
  128. package/src/scripts/nexo-backup 2.sh +25 -0
  129. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  130. package/src/scripts/nexo-catchup 2.py +300 -0
  131. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  132. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  133. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  134. package/src/scripts/nexo-cron-wrapper.sh +7 -0
  135. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  136. package/src/scripts/nexo-dashboard 2.sh +29 -0
  137. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  138. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  139. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  140. package/src/scripts/nexo-hook-record 2.py +42 -0
  141. package/src/scripts/nexo-immune 2.py +936 -0
  142. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  143. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  144. package/src/scripts/nexo-install 2.py +6 -0
  145. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  146. package/src/scripts/nexo-learning-validator 2.py +266 -0
  147. package/src/scripts/nexo-migrate 2.py +260 -0
  148. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  149. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  150. package/src/scripts/nexo-pre-commit 2.py +120 -0
  151. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  152. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  153. package/src/scripts/nexo-reflection 2.py +256 -0
  154. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  155. package/src/scripts/nexo-sleep 2.py +631 -0
  156. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  157. package/src/scripts/nexo-sync-clients 2.py +16 -0
  158. package/src/scripts/nexo-synthesis 2.py +475 -0
  159. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  160. package/src/scripts/nexo-update 2.sh +306 -0
  161. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  162. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  163. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  164. package/src/server 2.py +1296 -0
  165. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  166. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  167. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  168. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  169. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  170. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  171. package/src/skills/run-release-final-audit/script 2.py +259 -0
  172. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  173. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  174. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  175. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  176. package/src/skills_runtime 2.py +932 -0
  177. package/src/state_watchers_runtime 2.py +475 -0
  178. package/src/storage_router 2.py +32 -0
  179. package/src/system_catalog 2.py +786 -0
  180. package/src/tools_coordination 2.py +103 -0
  181. package/src/tools_credentials 2.py +68 -0
  182. package/src/tools_drive 2.py +487 -0
  183. package/src/tools_hot_context 2.py +163 -0
  184. package/src/tools_learnings 2.py +612 -0
  185. package/src/tools_menu 2.py +229 -0
  186. package/src/tools_reminders 2.py +88 -0
  187. package/src/tools_reminders_crud 2.py +363 -0
  188. package/src/tools_sessions 2.py +1054 -0
  189. package/src/tools_system_catalog 2.py +19 -0
  190. package/src/tools_task_history 2.py +57 -0
  191. package/src/tools_transcripts 2.py +98 -0
  192. package/src/transcript_utils 2.py +412 -0
  193. package/src/user_context 2.py +46 -0
  194. package/src/user_data_portability 2.py +328 -0
  195. package/src/user_state_model 2.py +170 -0
  196. package/templates/CLAUDE.md 2.template +108 -0
  197. package/templates/CODEX.AGENTS.md 2.template +66 -0
  198. package/templates/launchagents/README 2.md +132 -0
  199. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  200. package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
  201. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  202. package/templates/launchagents/com.nexo.catchup.plist +1 -1
  203. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  204. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  205. package/templates/launchagents/com.nexo.dashboard.plist +1 -1
  206. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  207. package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
  208. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  209. package/templates/launchagents/com.nexo.evolution.plist +1 -1
  210. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  211. package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
  212. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  213. package/templates/launchagents/com.nexo.immune.plist +1 -1
  214. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  215. package/templates/launchagents/com.nexo.postmortem.plist +1 -1
  216. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  217. package/templates/launchagents/com.nexo.self-audit.plist +1 -1
  218. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  219. package/templates/launchagents/com.nexo.synthesis.plist +1 -1
  220. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  221. package/templates/launchagents/com.nexo.watchdog.plist +1 -1
  222. package/templates/nexo_helper 2.py +301 -0
  223. package/templates/openclaw 2.json +13 -0
  224. package/templates/plugin-template 2.py +40 -0
  225. package/templates/script-template 2.py +59 -0
  226. package/templates/script-template 2.sh +13 -0
  227. package/templates/script-template.py +5 -4
  228. package/templates/skill-script-template 2.py +48 -0
  229. package/templates/skill-script-template.py +2 -1
  230. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ # NEXO PostCompact Hook — Re-inject Core Memory Block after compaction
3
+ # Reads the latest session checkpoint from SQLite and generates a structured
4
+ # context block that preserves session continuity.
5
+ set -uo pipefail
6
+
7
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
8
+ NEXO_DB="$NEXO_HOME/data/nexo.db"
9
+ mkdir -p "$NEXO_HOME/data"
10
+ TODAY=$(date +%Y-%m-%d)
11
+ LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
12
+ LOG_LINES=0
13
+ if [ -f "$LOG_FILE" ]; then
14
+ LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
15
+ fi
16
+
17
+ # Read checkpoint for the session that just compacted
18
+ # PreCompact writes the SID to /tmp/nexo-compacting-sid
19
+ TARGET_SID=""
20
+ if [ -f /tmp/nexo-compacting-sid ]; then
21
+ RAW_SID=$(cat /tmp/nexo-compacting-sid 2>/dev/null || echo "")
22
+ rm -f /tmp/nexo-compacting-sid
23
+ # Validate SID format: must be nexo-DIGITS-DIGITS
24
+ if [[ "$RAW_SID" =~ ^nexo-[0-9]+-[0-9]+$ ]]; then
25
+ TARGET_SID="$RAW_SID"
26
+ fi
27
+ fi
28
+
29
+ CHECKPOINT=""
30
+ if [ -f "$NEXO_DB" ]; then
31
+ if [ -n "$TARGET_SID" ]; then
32
+ # Read checkpoint for the specific session that compacted
33
+ CHECKPOINT=$(sqlite3 "$NEXO_DB" "
34
+ SELECT sid, task, task_status, active_files, current_goal,
35
+ decisions_summary, errors_found, reasoning_thread,
36
+ next_step, compaction_count
37
+ FROM session_checkpoints
38
+ WHERE sid = '$TARGET_SID'
39
+ " 2>/dev/null || echo "")
40
+ fi
41
+ # Fallback: if no target SID or no checkpoint found, use latest
42
+ if [ -z "$CHECKPOINT" ]; then
43
+ CHECKPOINT=$(sqlite3 "$NEXO_DB" "
44
+ SELECT sid, task, task_status, active_files, current_goal,
45
+ decisions_summary, errors_found, reasoning_thread,
46
+ next_step, compaction_count
47
+ FROM session_checkpoints
48
+ ORDER BY updated_at DESC LIMIT 1
49
+ " 2>/dev/null || echo "")
50
+ fi
51
+
52
+ if [ -n "$CHECKPOINT" ]; then
53
+ # Parse pipe-separated fields
54
+ SID=$(echo "$CHECKPOINT" | cut -d'|' -f1)
55
+ TASK=$(echo "$CHECKPOINT" | cut -d'|' -f2)
56
+ TASK_STATUS=$(echo "$CHECKPOINT" | cut -d'|' -f3)
57
+ ACTIVE_FILES=$(echo "$CHECKPOINT" | cut -d'|' -f4)
58
+ CURRENT_GOAL=$(echo "$CHECKPOINT" | cut -d'|' -f5)
59
+ DECISIONS=$(echo "$CHECKPOINT" | cut -d'|' -f6)
60
+ ERRORS=$(echo "$CHECKPOINT" | cut -d'|' -f7)
61
+ REASONING=$(echo "$CHECKPOINT" | cut -d'|' -f8)
62
+ NEXT_STEP=$(echo "$CHECKPOINT" | cut -d'|' -f9)
63
+ COMPACT_COUNT=$(echo "$CHECKPOINT" | cut -d'|' -f10)
64
+
65
+ # Increment compaction count
66
+ sqlite3 "$NEXO_DB" "
67
+ UPDATE session_checkpoints
68
+ SET compaction_count = compaction_count + 1, updated_at = datetime('now')
69
+ WHERE sid = '$SID'
70
+ " 2>/dev/null || true
71
+
72
+ # Read diary draft for extra context
73
+ DRAFT=$(sqlite3 "$NEXO_DB" "
74
+ SELECT tasks_seen, last_context_hint
75
+ FROM session_diary_draft
76
+ WHERE sid = '$SID'
77
+ " 2>/dev/null || echo "")
78
+
79
+ TASKS_SEEN=""
80
+ LAST_HINT=""
81
+ if [ -n "$DRAFT" ]; then
82
+ TASKS_SEEN=$(echo "$DRAFT" | cut -d'|' -f1)
83
+ LAST_HINT=$(echo "$DRAFT" | cut -d'|' -f2)
84
+ fi
85
+
86
+ # Build Core Memory Block
87
+ BLOCK="## SESSION CONTINUITY [auto-injected post-compaction #$((COMPACT_COUNT + 1))]"
88
+ BLOCK="$BLOCK\n**Session:** $SID"
89
+ BLOCK="$BLOCK\n**Task:** $TASK (status: $TASK_STATUS)"
90
+
91
+ if [ -n "$CURRENT_GOAL" ] && [ "$CURRENT_GOAL" != "$TASK" ]; then
92
+ BLOCK="$BLOCK\n**Goal:** $CURRENT_GOAL"
93
+ fi
94
+
95
+ if [ -n "$ACTIVE_FILES" ] && [ "$ACTIVE_FILES" != "[]" ]; then
96
+ BLOCK="$BLOCK\n**Files:** $ACTIVE_FILES"
97
+ fi
98
+
99
+ if [ -n "$DECISIONS" ]; then
100
+ BLOCK="$BLOCK\n**Decisions:** $DECISIONS"
101
+ fi
102
+
103
+ if [ -n "$ERRORS" ]; then
104
+ BLOCK="$BLOCK\n**Errors:** $ERRORS"
105
+ fi
106
+
107
+ if [ -n "$NEXT_STEP" ]; then
108
+ BLOCK="$BLOCK\n**Next:** $NEXT_STEP"
109
+ fi
110
+
111
+ if [ -n "$REASONING" ]; then
112
+ BLOCK="$BLOCK\n**Context:** $REASONING"
113
+ fi
114
+
115
+ if [ -n "$LAST_HINT" ]; then
116
+ BLOCK="$BLOCK\n**Last context:** $LAST_HINT"
117
+ fi
118
+
119
+ if [ -n "$TASKS_SEEN" ] && [ "$TASKS_SEEN" != "[]" ]; then
120
+ BLOCK="$BLOCK\n**Session tasks so far:** $TASKS_SEEN"
121
+ fi
122
+
123
+ BLOCK="$BLOCK\n**Tool logs:** ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl ($LOG_LINES entries)"
124
+ BLOCK="$BLOCK\n\n**POST-COMPACTION INSTRUCTIONS:**"
125
+ BLOCK="$BLOCK\n1. Call nexo_heartbeat with the SID above to reconnect with the session"
126
+ BLOCK="$BLOCK\n2. If you need specific lost data, query tool logs with jq"
127
+ BLOCK="$BLOCK\n3. Continue the task from where it left off — do NOT start from zero"
128
+ BLOCK="$BLOCK\n4. MCP tools (nexo_*) have all persistent state"
129
+
130
+ # Escape for JSON
131
+ BLOCK_ESCAPED=$(echo -e "$BLOCK" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
132
+
133
+ cat << HOOKEOF
134
+ {
135
+ "systemMessage": $BLOCK_ESCAPED
136
+ }
137
+ HOOKEOF
138
+ else
139
+ # No checkpoint — fallback to basic message
140
+ cat << 'HOOKEOF'
141
+ {
142
+ "systemMessage": "Post-compaction: no prior checkpoint found. Call nexo_heartbeat to reconnect session state."
143
+ }
144
+ HOOKEOF
145
+ fi
146
+ else
147
+ cat << 'HOOKEOF'
148
+ {
149
+ "systemMessage": "Post-compaction: nexo.db not found. Reconnect via nexo_startup."
150
+ }
151
+ HOOKEOF
152
+ fi
@@ -0,0 +1,169 @@
1
+ #!/bin/bash
2
+ # NEXO PreCompact Hook — Save checkpoint + inject preservation instructions
3
+ # This runs BEFORE Claude Code compacts. It:
4
+ # 1. Enriches the session checkpoint in SQLite with latest diary draft data
5
+ # 2. Injects a systemMessage telling the operator to save any WIP via MCP tools
6
+ set -uo pipefail
7
+
8
+ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
10
+ NEXO_DB="$NEXO_HOME/data/nexo.db"
11
+ mkdir -p "$NEXO_HOME/data"
12
+ TODAY=$(date +%Y-%m-%d)
13
+ LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
14
+ LOG_LINES=0
15
+ if [ -f "$LOG_FILE" ]; then
16
+ LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
17
+ fi
18
+
19
+ # Enrich checkpoint: copy diary draft context into checkpoint if exists
20
+ if [ -f "$NEXO_DB" ]; then
21
+ # Get latest active session's diary draft
22
+ LATEST_SID=$(sqlite3 "$NEXO_DB" "
23
+ SELECT sid FROM sessions ORDER BY last_update_epoch DESC LIMIT 1
24
+ " 2>/dev/null || echo "")
25
+
26
+ if [ -n "$LATEST_SID" ] && [[ "$LATEST_SID" =~ ^nexo-[0-9]+-[0-9]+$ ]]; then
27
+ # Write SID to temp file so PostCompact knows which session compacted
28
+ echo "$LATEST_SID" > /tmp/nexo-compacting-sid
29
+ # Pull diary draft data into checkpoint
30
+ sqlite3 "$NEXO_DB" "
31
+ INSERT INTO session_checkpoints (sid, task, current_goal, updated_at)
32
+ SELECT s.sid, s.task, COALESCE(d.last_context_hint, s.task), datetime('now')
33
+ FROM sessions s
34
+ LEFT JOIN session_diary_draft d ON d.sid = s.sid
35
+ WHERE s.sid = '$LATEST_SID'
36
+ ON CONFLICT(sid) DO UPDATE SET
37
+ task = excluded.task,
38
+ current_goal = CASE
39
+ WHEN excluded.current_goal != '' THEN excluded.current_goal
40
+ ELSE session_checkpoints.current_goal
41
+ END,
42
+ updated_at = datetime('now')
43
+ " 2>/dev/null || true
44
+ fi
45
+ fi
46
+
47
+ # ── Layer 2: Emergency auto-diary before compaction ──────────────────
48
+ # Write an actual session_diary entry (not draft) with mechanical summary
49
+ # This is the parachute — if the LLM never wrote a diary, at least this exists
50
+ if [ -f "$NEXO_DB" ]; then
51
+ python3 -c "
52
+ import json, sqlite3, os, sys
53
+ from datetime import datetime
54
+
55
+ db_path = '$NEXO_DB'
56
+ log_file = '$LOG_FILE'
57
+
58
+ conn = sqlite3.connect(db_path, timeout=3)
59
+ conn.row_factory = sqlite3.Row
60
+
61
+ # Get latest active session
62
+ row = conn.execute(
63
+ 'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
64
+ ).fetchone()
65
+ if not row:
66
+ conn.close()
67
+ sys.exit(0)
68
+
69
+ sid = row['sid']
70
+ task = row['task'] or 'unknown'
71
+
72
+ # Check if a real diary already exists for this session
73
+ has_diary = conn.execute(
74
+ 'SELECT id FROM session_diary WHERE session_id = ? LIMIT 1', (sid,)
75
+ ).fetchone()
76
+ if has_diary:
77
+ conn.close()
78
+ sys.exit(0) # LLM already wrote one, no need for emergency diary
79
+
80
+ # Find last diary timestamp to know where to start reading logs
81
+ last_diary = conn.execute(
82
+ 'SELECT created_at FROM session_diary ORDER BY created_at DESC LIMIT 1'
83
+ ).fetchone()
84
+ last_diary_ts = last_diary['created_at'] if last_diary else '1970-01-01T00:00:00Z'
85
+
86
+ # Read tool log entries since last diary
87
+ entries = []
88
+ modified_files = []
89
+ git_actions = []
90
+ if os.path.isfile(log_file):
91
+ with open(log_file, 'r') as f:
92
+ for line in f:
93
+ try:
94
+ e = json.loads(line.strip())
95
+ ts = e.get('timestamp', '')
96
+ if ts < last_diary_ts:
97
+ continue
98
+ name = e.get('tool_name', '?')
99
+ inp = e.get('tool_input', {}) or {}
100
+ brief = ''
101
+ if isinstance(inp, dict):
102
+ for k, v in list(inp.items())[:1]:
103
+ brief = str(v)[:80]
104
+ entries.append(f'{name}({brief})')
105
+ # Extract decisions from tool calls
106
+ if name in ('Edit', 'Write'):
107
+ fp = inp.get('file_path', inp.get('path', ''))
108
+ if fp:
109
+ modified_files.append(fp.split('/')[-1])
110
+ if name == 'Bash':
111
+ cmd = str(inp.get('command', ''))
112
+ if 'git commit' in cmd or 'git push' in cmd:
113
+ git_actions.append(cmd[:80])
114
+ except Exception:
115
+ pass
116
+
117
+ if not entries:
118
+ conn.close()
119
+ sys.exit(0)
120
+
121
+ # Build mechanical diary
122
+ tools_summary = ', '.join(entries[-30:])[:500]
123
+ summary = f'[EMERGENCY PRE-COMPACT] {len(entries)} tool calls since last diary. Tools: {tools_summary}'
124
+
125
+ decisions = ''
126
+ if modified_files:
127
+ decisions = 'Modified: ' + ', '.join(set(modified_files))[:300]
128
+ if git_actions:
129
+ decisions += (' | Git: ' + ', '.join(git_actions))[:200]
130
+ if not decisions:
131
+ decisions = 'No file modifications detected in tool logs'
132
+
133
+ pending = f'Current task: {task[:200]}'
134
+ context_next = 'COMPACTION HAPPENED. Read this diary to continue. Check session_checkpoints and tool-logs for full context.'
135
+
136
+ # Write actual session_diary entry
137
+ conn.execute('''
138
+ INSERT INTO session_diary
139
+ (session_id, decisions, discarded, pending, context_next,
140
+ mental_state, domain, user_signals, summary, source)
141
+ VALUES (?, ?, '', ?, ?, 'auto-generated', 'auto', '', ?, 'pre-compact-hook')
142
+ ''', (sid, decisions, pending, context_next, summary))
143
+ conn.commit()
144
+
145
+ # Layer 3: structured auto-flush for continuity and inspectability
146
+ try:
147
+ import os
148
+ sys.path.insert(0, os.path.abspath(os.path.join('$HOOK_DIR', '..')))
149
+ import compaction_memory
150
+ compaction_memory.record_auto_flush(
151
+ session_id=sid,
152
+ task=task,
153
+ current_goal='',
154
+ log_file=log_file,
155
+ last_diary_ts=last_diary_ts,
156
+ source='pre-compact-hook',
157
+ )
158
+ except Exception:
159
+ pass
160
+
161
+ conn.close()
162
+ " 2>/dev/null || true
163
+ fi
164
+
165
+ cat << HOOKEOF
166
+ {
167
+ "systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
168
+ }
169
+ HOOKEOF
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # NEXO PostToolUse hook — conditioned file discipline guardrail
3
+
4
+ INPUT=$(cat || true)
5
+ [ -z "$INPUT" ] && exit 0
6
+
7
+ NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
8
+ NEXO_HOOK_PHASE=post python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT" 2>/dev/null || true
9
+
10
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # NEXO PreToolUse hook — strict protocol blocking before writes/deletes
3
+
4
+ INPUT=$(cat || true)
5
+ [ -z "$INPUT" ] && exit 0
6
+
7
+ NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
8
+ NEXO_HOOK_PHASE=pre python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT"
9
+ exit $?
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # NEXO Memory Stop Hook (v8 — non-blocking, approve always)
3
+ #
4
+ # v5: used "approve" + systemMessage — AI never processed post-mortem.
5
+ # v6: used "block" — but blocked ALL sessions including trivial ones.
6
+ # v7: detects trivial sessions (<5 tool calls) and approves immediately.
7
+ # v8: NEVER blocks. The Stop hook fires after EVERY Claude response (not just
8
+ # session close), so blocking causes mid-conversation interruptions.
9
+ # Post-mortem is now handled by:
10
+ # 1. Claude detecting closing intent (any language) → diary inline
11
+ # 2. auto_close_sessions.py → promotes draft for orphan sessions
12
+ #
13
+ # This hook only refreshes the diary draft with latest data (best-effort).
14
+ set -uo pipefail
15
+
16
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
17
+
18
+ # Refresh diary draft with latest changes/decisions (best-effort)
19
+ python3 -c "
20
+ import sys, json, os
21
+ nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
22
+ nexo_code = os.environ.get('NEXO_CODE', nexo_home)
23
+ sys.path.insert(0, nexo_code)
24
+ os.environ['NEXO_SKIP_FS_INDEX'] = '1'
25
+ from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
26
+ init_db()
27
+ conn = get_db()
28
+ sessions = get_active_sessions()
29
+ for s in sessions:
30
+ sid = s['sid']
31
+ draft = get_diary_draft(sid)
32
+ if not draft:
33
+ continue
34
+ change_ids = [r[0] for r in conn.execute('SELECT id FROM change_log WHERE session_id = ?', (sid,)).fetchall()]
35
+ decision_ids = [r[0] for r in conn.execute('SELECT id FROM decisions WHERE session_id = ?', (sid,)).fetchall()]
36
+ upsert_diary_draft(
37
+ sid=sid,
38
+ tasks_seen=draft['tasks_seen'],
39
+ change_ids=json.dumps(change_ids),
40
+ decision_ids=json.dumps(decision_ids),
41
+ last_context_hint=draft['last_context_hint'],
42
+ heartbeat_count=draft['heartbeat_count'],
43
+ summary_draft=draft['summary_draft'],
44
+ )
45
+ " 2>/dev/null || true
46
+
47
+ # Always approve — never interrupt the conversation
48
+ cat << 'HOOKEOF'
49
+ {
50
+ "decision": "approve"
51
+ }
52
+ HOOKEOF
@@ -0,0 +1,292 @@
1
+ """NEXO KG Auto-Population — backfill from nexo.db + incremental hooks."""
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ from typing import Optional
7
+
8
+ import knowledge_graph as kg
9
+ from db import get_db
10
+
11
+
12
+ # ─── helpers ────────────────────────────────────────────────────────────────
13
+
14
+ def _cognitive_db():
15
+ """Direct cognitive.db connection (for somatic_markers)."""
16
+ nexo_home = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
17
+ data_dir = os.path.join(nexo_home, "data")
18
+ os.makedirs(data_dir, exist_ok=True)
19
+ path = os.path.join(data_dir, "cognitive.db")
20
+ conn = sqlite3.connect(path)
21
+ conn.row_factory = sqlite3.Row
22
+ return conn
23
+
24
+
25
+ def _parse_files(files_str: str) -> list[str]:
26
+ """Extract individual file paths from a comma/newline-separated string."""
27
+ if not files_str:
28
+ return []
29
+ parts = [p.strip() for p in files_str.replace("\n", ",").split(",")]
30
+ return [p for p in parts if p]
31
+
32
+
33
+ # ─── backfill functions ──────────────────────────────────────────────────────
34
+
35
+ def backfill_entities() -> int:
36
+ """Read entities table → create entity nodes in KG."""
37
+ db = get_db()
38
+ rows = db.execute("SELECT id, name, type, value, notes FROM entities").fetchall()
39
+ count = 0
40
+ for row in rows:
41
+ props = {}
42
+ if row["value"]:
43
+ props["value"] = row["value"]
44
+ if row["notes"]:
45
+ props["notes"] = row["notes"]
46
+ kg.upsert_node(
47
+ node_type="entity",
48
+ node_ref=f"entity:{row['id']}",
49
+ label=row["name"],
50
+ properties={"entity_type": row["type"], **props},
51
+ )
52
+ count += 1
53
+ return count
54
+
55
+
56
+ def backfill_learnings() -> int:
57
+ """Read learnings → create learning nodes + file/area edges."""
58
+ db = get_db()
59
+ rows = db.execute(
60
+ "SELECT id, category, title, applies_to FROM learnings WHERE status != 'deleted'"
61
+ ).fetchall()
62
+ count = 0
63
+ for row in rows:
64
+ learning_ref = f"learning:{row['id']}"
65
+ kg.upsert_node(
66
+ node_type="learning",
67
+ node_ref=learning_ref,
68
+ label=row["title"] or f"Learning #{row['id']}",
69
+ properties={"category": row["category"]},
70
+ )
71
+ # edge: learning → category/area
72
+ if row["category"]:
73
+ kg.upsert_edge(
74
+ source_type="learning", source_ref=learning_ref,
75
+ relation="belongs_to",
76
+ target_type="area", target_ref=f"area:{row['category']}",
77
+ weight=1.0,
78
+ )
79
+ # edge: learning → file (from applies_to)
80
+ applies = row["applies_to"] or ""
81
+ for fpath in _parse_files(applies):
82
+ if fpath:
83
+ kg.upsert_edge(
84
+ source_type="learning", source_ref=learning_ref,
85
+ relation="applies_to_file",
86
+ target_type="file", target_ref=f"file:{fpath}",
87
+ weight=0.8,
88
+ )
89
+ count += 1
90
+ return count
91
+
92
+
93
+ def backfill_changes() -> int:
94
+ """Read change_log → create file nodes + file→area edges."""
95
+ db = get_db()
96
+ rows = db.execute("SELECT id, files, what_changed FROM change_log").fetchall()
97
+ count = 0
98
+ for row in rows:
99
+ change_ref = f"change:{row['id']}"
100
+ kg.upsert_node(
101
+ node_type="change",
102
+ node_ref=change_ref,
103
+ label=f"Change #{row['id']}",
104
+ properties={"summary": (row["what_changed"] or "")[:120]},
105
+ )
106
+ for fpath in _parse_files(row["files"] or ""):
107
+ file_ref = f"file:{fpath}"
108
+ kg.upsert_node(
109
+ node_type="file",
110
+ node_ref=file_ref,
111
+ label=os.path.basename(fpath) or fpath,
112
+ )
113
+ kg.upsert_edge(
114
+ source_type="change", source_ref=change_ref,
115
+ relation="touched",
116
+ target_type="file", target_ref=file_ref,
117
+ weight=1.0,
118
+ )
119
+ count += 1
120
+ return count
121
+
122
+
123
+ def backfill_decisions() -> int:
124
+ """Read decisions → create decision nodes + decision→area edges."""
125
+ db = get_db()
126
+ rows = db.execute("SELECT id, domain, decision, status FROM decisions").fetchall()
127
+ count = 0
128
+ for row in rows:
129
+ decision_ref = f"decision:{row['id']}"
130
+ kg.upsert_node(
131
+ node_type="decision",
132
+ node_ref=decision_ref,
133
+ label=(row["decision"] or "")[:80] or f"Decision #{row['id']}",
134
+ properties={"domain": row["domain"], "status": row["status"]},
135
+ )
136
+ if row["domain"]:
137
+ kg.upsert_edge(
138
+ source_type="decision", source_ref=decision_ref,
139
+ relation="in_domain",
140
+ target_type="area", target_ref=f"area:{row['domain']}",
141
+ weight=1.0,
142
+ )
143
+ count += 1
144
+ return count
145
+
146
+
147
+ def backfill_somatic() -> int:
148
+ """Read somatic_markers from cognitive.db → create file/area nodes with risk."""
149
+ cdb = _cognitive_db()
150
+ try:
151
+ rows = cdb.execute(
152
+ "SELECT target, target_type, risk_score, incident_count FROM somatic_markers"
153
+ ).fetchall()
154
+ count = 0
155
+ for row in rows:
156
+ target_type = row["target_type"] or "file"
157
+ node_ref = f"{target_type}:{row['target']}"
158
+ kg.upsert_node(
159
+ node_type=target_type,
160
+ node_ref=node_ref,
161
+ label=os.path.basename(row["target"]) or row["target"],
162
+ properties={
163
+ "risk_score": row["risk_score"],
164
+ "incident_count": row["incident_count"],
165
+ },
166
+ )
167
+ count += 1
168
+ return count
169
+ finally:
170
+ cdb.close()
171
+
172
+
173
+ def run_full_backfill() -> dict:
174
+ """Run all backfill functions. Idempotent (upsert-based)."""
175
+ results = {}
176
+ results["entities"] = backfill_entities()
177
+ results["learnings"] = backfill_learnings()
178
+ results["changes"] = backfill_changes()
179
+ results["decisions"] = backfill_decisions()
180
+ results["somatic"] = backfill_somatic()
181
+ results["total"] = sum(results.values())
182
+ return results
183
+
184
+
185
+ # ─── incremental hooks ───────────────────────────────────────────────────────
186
+
187
+ def on_learning_add(learning_id: int, category: str, title: str, applies_to: str = "") -> None:
188
+ try:
189
+ learning_ref = f"learning:{learning_id}"
190
+ kg.upsert_node(
191
+ node_type="learning",
192
+ node_ref=learning_ref,
193
+ label=title or f"Learning #{learning_id}",
194
+ properties={"category": category},
195
+ )
196
+ if category:
197
+ kg.upsert_edge(
198
+ source_type="learning", source_ref=learning_ref,
199
+ relation="belongs_to",
200
+ target_type="area", target_ref=f"area:{category}",
201
+ weight=1.0,
202
+ )
203
+ for fpath in _parse_files(applies_to or ""):
204
+ if fpath:
205
+ kg.upsert_edge(
206
+ source_type="learning", source_ref=learning_ref,
207
+ relation="applies_to_file",
208
+ target_type="file", target_ref=f"file:{fpath}",
209
+ weight=0.8,
210
+ )
211
+ except Exception:
212
+ pass
213
+
214
+
215
+ def on_change_log(change_id: int, files: str, system: str = "") -> None:
216
+ try:
217
+ change_ref = f"change:{change_id}"
218
+ kg.upsert_node(
219
+ node_type="change",
220
+ node_ref=change_ref,
221
+ label=f"Change #{change_id}",
222
+ )
223
+ for fpath in _parse_files(files or ""):
224
+ file_ref = f"file:{fpath}"
225
+ kg.upsert_node(
226
+ node_type="file",
227
+ node_ref=file_ref,
228
+ label=os.path.basename(fpath) or fpath,
229
+ )
230
+ kg.upsert_edge(
231
+ source_type="change", source_ref=change_ref,
232
+ relation="touched",
233
+ target_type="file", target_ref=file_ref,
234
+ weight=1.0,
235
+ )
236
+ if system:
237
+ kg.upsert_edge(
238
+ source_type="change", source_ref=change_ref,
239
+ relation="in_system",
240
+ target_type="area", target_ref=f"area:{system}",
241
+ weight=1.0,
242
+ )
243
+ except Exception:
244
+ pass
245
+
246
+
247
+ def on_decision_log(decision_id: int, domain: str, decision_text: str) -> None:
248
+ try:
249
+ decision_ref = f"decision:{decision_id}"
250
+ kg.upsert_node(
251
+ node_type="decision",
252
+ node_ref=decision_ref,
253
+ label=(decision_text or "")[:80] or f"Decision #{decision_id}",
254
+ properties={"domain": domain},
255
+ )
256
+ if domain:
257
+ kg.upsert_edge(
258
+ source_type="decision", source_ref=decision_ref,
259
+ relation="in_domain",
260
+ target_type="area", target_ref=f"area:{domain}",
261
+ weight=1.0,
262
+ )
263
+ except Exception:
264
+ pass
265
+
266
+
267
+ def on_entity_create(entity_id: int, name: str, entity_type: str) -> None:
268
+ try:
269
+ kg.upsert_node(
270
+ node_type="entity",
271
+ node_ref=f"entity:{entity_id}",
272
+ label=name,
273
+ properties={"entity_type": entity_type},
274
+ )
275
+ except Exception:
276
+ pass
277
+
278
+
279
+ # ─── main ────────────────────────────────────────────────────────────────────
280
+
281
+ if __name__ == "__main__":
282
+ print("Running full KG backfill...")
283
+ results = run_full_backfill()
284
+ print("\nBackfill complete:")
285
+ for key, val in results.items():
286
+ if key != "total":
287
+ print(f" {key:12s}: {val:4d} records")
288
+ print(f" {'TOTAL':12s}: {results['total']:4d} nodes/edges processed")
289
+
290
+ # Show KG stats
291
+ s = kg.stats()
292
+ print(f"\nKG state: {s['nodes']} nodes, {s['edges_active']} active edges")