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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/nexo-brain.js +52 -1
- package/package.json +1 -1
- package/src/crons/sync.py +18 -4
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_entities.py +1 -1
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/agents.py +10 -3
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/schedule.py +2 -1
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/requirements.txt +1 -1
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/runtime_power.py +18 -1
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-cron-wrapper.sh +7 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup.plist +1 -1
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.dashboard.plist +1 -1
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.evolution.plist +1 -1
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.immune.plist +1 -1
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.postmortem.plist +1 -1
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.self-audit.plist +1 -1
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.synthesis.plist +1 -1
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/launchagents/com.nexo.watchdog.plist +1 -1
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/script-template.py +5 -4
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-script-template.py +2 -1
- 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")
|