nexo-brain 5.3.20 → 5.3.21
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/package.json +1 -1
- package/src/auto_update.py +11 -8
- 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/_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/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/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/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/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-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.catchup 2.plist +39 -0
- 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.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- 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/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,2327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
"""
|
|
4
|
+
Deep Sleep v2 -- Phase 4: Apply synthesized findings.
|
|
5
|
+
|
|
6
|
+
Reads $DATE-synthesis.json and executes actions:
|
|
7
|
+
- learning_add: inserts learnings into nexo.db
|
|
8
|
+
- followup_create: inserts followups into nexo.db
|
|
9
|
+
- morning_briefing_item: writes to morning briefing file
|
|
10
|
+
|
|
11
|
+
All actions are idempotent (dedupe_key checked against last 7 days),
|
|
12
|
+
backed up before mutation, and logged to $DATE-applied.json.
|
|
13
|
+
|
|
14
|
+
Environment variables:
|
|
15
|
+
NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
|
|
16
|
+
"""
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sqlite3
|
|
22
|
+
import sys
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from datetime import datetime, timedelta
|
|
25
|
+
from difflib import SequenceMatcher
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
29
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parents[2])))
|
|
30
|
+
if str(NEXO_CODE) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
32
|
+
|
|
33
|
+
import db as nexo_db
|
|
34
|
+
|
|
35
|
+
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
36
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
37
|
+
COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
|
|
38
|
+
OPERATIONS_DIR = NEXO_HOME / "operations"
|
|
39
|
+
BACKUP_DIR = DEEP_SLEEP_DIR # backups stored alongside outputs
|
|
40
|
+
|
|
41
|
+
STOPWORDS = {
|
|
42
|
+
"the", "a", "an", "and", "or", "but", "with", "for", "from", "into", "onto",
|
|
43
|
+
"that", "this", "these", "those", "have", "has", "had", "will", "would",
|
|
44
|
+
"could", "should", "must", "need", "needs", "your", "their", "there", "here",
|
|
45
|
+
"about", "before", "after", "during", "through", "without", "within", "while",
|
|
46
|
+
"que", "con", "para", "por", "los", "las", "una", "uno", "sobre", "desde",
|
|
47
|
+
"cuando", "como", "pero", "todo", "toda", "cada", "into", "across", "using",
|
|
48
|
+
}
|
|
49
|
+
CONCRETE_ACTION_VERBS = {
|
|
50
|
+
"add", "implement", "create", "write", "build", "introduce", "enforce",
|
|
51
|
+
"automate", "validate", "check", "verify", "guard", "fix", "migrate",
|
|
52
|
+
"review", "reconcile", "pin", "sync", "instrument",
|
|
53
|
+
}
|
|
54
|
+
NEGATION_PATTERNS = (
|
|
55
|
+
"do not", "don't", "never", "avoid", "skip", "without", "forbid", "forbidden",
|
|
56
|
+
"disable", "disabled", "remove", "ban", "bypass",
|
|
57
|
+
)
|
|
58
|
+
CONTRADICTION_PAIRS = (
|
|
59
|
+
("enable", "disable"),
|
|
60
|
+
("use", "avoid"),
|
|
61
|
+
("add", "remove"),
|
|
62
|
+
("allow", "forbid"),
|
|
63
|
+
("always", "never"),
|
|
64
|
+
("before", "after"),
|
|
65
|
+
("require", "skip"),
|
|
66
|
+
("validate", "bypass"),
|
|
67
|
+
("include", "exclude"),
|
|
68
|
+
)
|
|
69
|
+
TABLE_COLUMNS_CACHE: dict[tuple[str, str], set[str]] = {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_run_id(target_date: str) -> str:
|
|
73
|
+
"""Generate a unique run ID for this execution."""
|
|
74
|
+
ts = datetime.now().strftime("%H%M%S")
|
|
75
|
+
return f"{target_date}-{ts}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _decode_json_object(value) -> dict:
|
|
79
|
+
if isinstance(value, dict):
|
|
80
|
+
return value
|
|
81
|
+
if isinstance(value, str):
|
|
82
|
+
stripped = value.strip()
|
|
83
|
+
if not stripped:
|
|
84
|
+
return {}
|
|
85
|
+
try:
|
|
86
|
+
parsed = json.loads(stripped)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
return {}
|
|
89
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _top_followups_by_impact(limit: int = 5) -> list[dict]:
|
|
94
|
+
if not NEXO_DB.exists():
|
|
95
|
+
return []
|
|
96
|
+
try:
|
|
97
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
98
|
+
conn.row_factory = sqlite3.Row
|
|
99
|
+
columns = {str(row[1]) for row in conn.execute("PRAGMA table_info(followups)").fetchall()}
|
|
100
|
+
if "impact_score" not in columns:
|
|
101
|
+
conn.close()
|
|
102
|
+
return []
|
|
103
|
+
impact_factors_sql = ", impact_factors" if "impact_factors" in columns else ""
|
|
104
|
+
rows = [
|
|
105
|
+
dict(row)
|
|
106
|
+
for row in conn.execute(
|
|
107
|
+
f"""SELECT id, description, date, priority, impact_score{impact_factors_sql}
|
|
108
|
+
FROM followups
|
|
109
|
+
WHERE status IN ('PENDING', 'ACTIVE', 'WAITING', 'BLOCKED')
|
|
110
|
+
ORDER BY
|
|
111
|
+
CASE WHEN COALESCE(impact_score, 0) > 0 THEN 0 ELSE 1 END ASC,
|
|
112
|
+
COALESCE(impact_score, 0) DESC,
|
|
113
|
+
CASE WHEN date IS NULL OR date = '' THEN 1 ELSE 0 END ASC,
|
|
114
|
+
date ASC
|
|
115
|
+
LIMIT ?""",
|
|
116
|
+
(max(1, int(limit)),),
|
|
117
|
+
).fetchall()
|
|
118
|
+
]
|
|
119
|
+
conn.close()
|
|
120
|
+
except Exception:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
for row in rows:
|
|
124
|
+
factors = _decode_json_object(row.get("impact_factors"))
|
|
125
|
+
row["impact_factors"] = factors
|
|
126
|
+
row["impact_reasoning"] = str(factors.get("reasoning") or "").strip()
|
|
127
|
+
return rows
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _read_impact_summary() -> dict:
|
|
131
|
+
path = NEXO_HOME / "coordination" / "impact-scorer-summary.json"
|
|
132
|
+
if not path.exists():
|
|
133
|
+
return {}
|
|
134
|
+
try:
|
|
135
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
136
|
+
except Exception:
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_recent_dedupe_keys(target_date: str, days: int = 7) -> set[str]:
|
|
141
|
+
"""Load dedupe_keys from applied files in the last N days."""
|
|
142
|
+
keys = set()
|
|
143
|
+
base_date = datetime.strptime(target_date, "%Y-%m-%d")
|
|
144
|
+
for i in range(days):
|
|
145
|
+
d = (base_date - timedelta(days=i)).strftime("%Y-%m-%d")
|
|
146
|
+
applied_file = DEEP_SLEEP_DIR / f"{d}-applied.json"
|
|
147
|
+
if applied_file.exists():
|
|
148
|
+
try:
|
|
149
|
+
with open(applied_file) as f:
|
|
150
|
+
data = json.load(f)
|
|
151
|
+
for action in data.get("applied_actions", []):
|
|
152
|
+
dk = action.get("dedupe_key", "")
|
|
153
|
+
if dk:
|
|
154
|
+
keys.add(dk)
|
|
155
|
+
except (json.JSONDecodeError, KeyError):
|
|
156
|
+
continue
|
|
157
|
+
return keys
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def backup_db(db_path: Path, run_id: str) -> Path | None:
|
|
161
|
+
"""Create a backup of a database before mutations."""
|
|
162
|
+
if not db_path.exists():
|
|
163
|
+
return None
|
|
164
|
+
backup_path = BACKUP_DIR / f"{run_id}-backup-{db_path.name}"
|
|
165
|
+
try:
|
|
166
|
+
import shutil
|
|
167
|
+
shutil.copy2(str(db_path), str(backup_path))
|
|
168
|
+
return backup_path
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f" [apply] Warning: backup failed for {db_path.name}: {e}", file=sys.stderr)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _table_columns(db_path: Path, table: str) -> set[str]:
|
|
175
|
+
cache_key = (str(db_path), table)
|
|
176
|
+
if cache_key in TABLE_COLUMNS_CACHE:
|
|
177
|
+
return TABLE_COLUMNS_CACHE[cache_key]
|
|
178
|
+
if not db_path.exists():
|
|
179
|
+
TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
180
|
+
return set()
|
|
181
|
+
try:
|
|
182
|
+
conn = sqlite3.connect(str(db_path))
|
|
183
|
+
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
184
|
+
conn.close()
|
|
185
|
+
except Exception:
|
|
186
|
+
TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
187
|
+
return set()
|
|
188
|
+
cols = {str(row[1]) for row in rows}
|
|
189
|
+
TABLE_COLUMNS_CACHE[cache_key] = cols
|
|
190
|
+
return cols
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _row_dict(row) -> dict:
|
|
194
|
+
if row is None:
|
|
195
|
+
return {}
|
|
196
|
+
if isinstance(row, sqlite3.Row):
|
|
197
|
+
return dict(row)
|
|
198
|
+
return dict(zip(row.keys(), row)) if hasattr(row, "keys") else dict(row)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _normalize_text(value: str) -> str:
|
|
202
|
+
text = str(value or "").lower()
|
|
203
|
+
text = re.sub(r"https?://\S+", " ", text)
|
|
204
|
+
text = re.sub(r"[^a-z0-9_/\-\s]+", " ", text)
|
|
205
|
+
text = re.sub(r"\s+", " ", text)
|
|
206
|
+
return text.strip()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _tokenize(value: str) -> list[str]:
|
|
210
|
+
tokens = re.findall(r"[a-z0-9_/-]+", _normalize_text(value))
|
|
211
|
+
return [token for token in tokens if len(token) > 2 and token not in STOPWORDS]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _text_similarity(left: str, right: str) -> float:
|
|
215
|
+
normalized_left = _normalize_text(left)
|
|
216
|
+
normalized_right = _normalize_text(right)
|
|
217
|
+
if not normalized_left or not normalized_right:
|
|
218
|
+
return 0.0
|
|
219
|
+
if normalized_left == normalized_right:
|
|
220
|
+
return 1.0
|
|
221
|
+
|
|
222
|
+
left_tokens = set(_tokenize(normalized_left))
|
|
223
|
+
right_tokens = set(_tokenize(normalized_right))
|
|
224
|
+
shared = left_tokens & right_tokens
|
|
225
|
+
if not shared:
|
|
226
|
+
return SequenceMatcher(None, normalized_left, normalized_right).ratio()
|
|
227
|
+
|
|
228
|
+
seq = SequenceMatcher(None, normalized_left, normalized_right).ratio()
|
|
229
|
+
jaccard = len(shared) / len(left_tokens | right_tokens) if (left_tokens or right_tokens) else 0.0
|
|
230
|
+
overlap = len(shared) / min(len(left_tokens), len(right_tokens)) if min(len(left_tokens), len(right_tokens)) else 0.0
|
|
231
|
+
containment = (
|
|
232
|
+
1.0
|
|
233
|
+
if normalized_left in normalized_right or normalized_right in normalized_left
|
|
234
|
+
else 0.0
|
|
235
|
+
)
|
|
236
|
+
return round(max((seq * 0.45) + (jaccard * 0.2) + (overlap * 0.35), overlap, (containment * 0.8) + (seq * 0.2)), 4)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _is_concrete_action(text: str) -> bool:
|
|
240
|
+
tokens = set(_tokenize(text))
|
|
241
|
+
return bool(tokens & CONCRETE_ACTION_VERBS)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _prefer_due_date(current_value, new_value) -> str:
|
|
245
|
+
current = _parse_any_datetime(current_value)
|
|
246
|
+
new = _parse_any_datetime(new_value)
|
|
247
|
+
if new and (not current or new <= current):
|
|
248
|
+
return str(new_value or "")
|
|
249
|
+
return str(current_value or "")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _append_note(base: str, note: str) -> str:
|
|
253
|
+
base = str(base or "").strip()
|
|
254
|
+
note = str(note or "").strip()
|
|
255
|
+
if not note:
|
|
256
|
+
return base
|
|
257
|
+
if not base:
|
|
258
|
+
return note
|
|
259
|
+
if note.lower() in base.lower():
|
|
260
|
+
return base
|
|
261
|
+
return f"{base}\n\n{note}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _contains_negation(text: str) -> bool:
|
|
265
|
+
lowered = _normalize_text(text)
|
|
266
|
+
return any(token in lowered for token in NEGATION_PATTERNS)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _negated_action_verbs(text: str) -> set[str]:
|
|
270
|
+
lowered = _normalize_text(text)
|
|
271
|
+
matches = set()
|
|
272
|
+
for pattern in (r"(?:never|avoid|skip|disable|remove|forbid|bypass)\s+([a-z0-9_-]+)", r"(?:do not|don't)\s+([a-z0-9_-]+)"):
|
|
273
|
+
matches.update(re.findall(pattern, lowered))
|
|
274
|
+
return {match for match in matches if len(match) > 2}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _looks_contradictory(existing_text: str, new_text: str) -> bool:
|
|
278
|
+
existing_norm = _normalize_text(existing_text)
|
|
279
|
+
new_norm = _normalize_text(new_text)
|
|
280
|
+
if not existing_norm or not new_norm:
|
|
281
|
+
return False
|
|
282
|
+
existing_tokens = set(_tokenize(existing_norm))
|
|
283
|
+
new_tokens = set(_tokenize(new_norm))
|
|
284
|
+
if len(existing_tokens & new_tokens) < 3:
|
|
285
|
+
return False
|
|
286
|
+
existing_negated_verbs = _negated_action_verbs(existing_norm)
|
|
287
|
+
new_negated_verbs = _negated_action_verbs(new_norm)
|
|
288
|
+
if existing_negated_verbs & new_tokens and not existing_negated_verbs & new_negated_verbs:
|
|
289
|
+
return True
|
|
290
|
+
if new_negated_verbs & existing_tokens and not existing_negated_verbs & new_negated_verbs:
|
|
291
|
+
return True
|
|
292
|
+
if _contains_negation(existing_norm) != _contains_negation(new_norm):
|
|
293
|
+
return True
|
|
294
|
+
for positive, negative in CONTRADICTION_PAIRS:
|
|
295
|
+
existing_has_pair = positive in existing_norm or negative in existing_norm
|
|
296
|
+
new_has_pair = positive in new_norm or negative in new_norm
|
|
297
|
+
if existing_has_pair and new_has_pair:
|
|
298
|
+
if (positive in existing_norm and negative in new_norm) or (negative in existing_norm and positive in new_norm):
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _fetch_open_followups() -> list[dict]:
|
|
304
|
+
if not NEXO_DB.exists():
|
|
305
|
+
return []
|
|
306
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
307
|
+
conn.row_factory = sqlite3.Row
|
|
308
|
+
cols = _table_columns(NEXO_DB, "followups")
|
|
309
|
+
reasoning_sql = ", reasoning" if "reasoning" in cols else ""
|
|
310
|
+
verification_sql = ", verification" if "verification" in cols else ""
|
|
311
|
+
impact_sql = ", impact_score" if "impact_score" in cols else ""
|
|
312
|
+
try:
|
|
313
|
+
rows = conn.execute(
|
|
314
|
+
"SELECT id, description, date, status"
|
|
315
|
+
f"{verification_sql}{reasoning_sql}{impact_sql} "
|
|
316
|
+
"FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
317
|
+
"AND status NOT IN ('DELETED','archived','blocked','waiting','CANCELLED')"
|
|
318
|
+
).fetchall()
|
|
319
|
+
finally:
|
|
320
|
+
conn.close()
|
|
321
|
+
return [dict(row) for row in rows]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _find_similar_followup(description: str, threshold: float = 0.58) -> dict | None:
|
|
325
|
+
candidates = []
|
|
326
|
+
query = str(description or "").strip()
|
|
327
|
+
if not query:
|
|
328
|
+
return None
|
|
329
|
+
query_tokens = set(_tokenize(query))
|
|
330
|
+
for row in _fetch_open_followups():
|
|
331
|
+
haystack = " ".join(
|
|
332
|
+
[
|
|
333
|
+
str(row.get("description", "") or ""),
|
|
334
|
+
str(row.get("verification", "") or ""),
|
|
335
|
+
str(row.get("reasoning", "") or ""),
|
|
336
|
+
]
|
|
337
|
+
)
|
|
338
|
+
haystack_tokens = set(_tokenize(haystack))
|
|
339
|
+
if len(query_tokens & haystack_tokens) < 2 and _normalize_text(query) not in _normalize_text(haystack):
|
|
340
|
+
continue
|
|
341
|
+
score = _text_similarity(query, haystack)
|
|
342
|
+
if score >= threshold:
|
|
343
|
+
candidates.append({**row, "_similarity": score})
|
|
344
|
+
if not candidates:
|
|
345
|
+
return None
|
|
346
|
+
candidates.sort(key=lambda item: item["_similarity"], reverse=True)
|
|
347
|
+
return candidates[0]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _touch_existing_followup(
|
|
351
|
+
existing: dict,
|
|
352
|
+
*,
|
|
353
|
+
description: str,
|
|
354
|
+
date: str = "",
|
|
355
|
+
reasoning_note: str = "",
|
|
356
|
+
status: str = "",
|
|
357
|
+
) -> dict:
|
|
358
|
+
cols = _table_columns(NEXO_DB, "followups")
|
|
359
|
+
if not cols:
|
|
360
|
+
return {"success": False, "error": "followups table not found"}
|
|
361
|
+
|
|
362
|
+
updates: dict[str, object] = {}
|
|
363
|
+
existing_description = str(existing.get("description", "") or "")
|
|
364
|
+
if _is_concrete_action(description) and not _is_concrete_action(existing_description):
|
|
365
|
+
updates["description"] = description
|
|
366
|
+
preferred_date = _prefer_due_date(existing.get("date", ""), date)
|
|
367
|
+
if preferred_date and preferred_date != str(existing.get("date", "") or "") and "date" in cols:
|
|
368
|
+
updates["date"] = preferred_date
|
|
369
|
+
desired_status = (status or "").strip()
|
|
370
|
+
if desired_status and "status" in cols and desired_status != str(existing.get("status", "") or ""):
|
|
371
|
+
updates["status"] = desired_status
|
|
372
|
+
note = reasoning_note or "Deep Sleep matched this followup semantically."
|
|
373
|
+
changed = False
|
|
374
|
+
if updates:
|
|
375
|
+
result = nexo_db.update_followup(
|
|
376
|
+
str(existing["id"]),
|
|
377
|
+
history_actor="deep-sleep",
|
|
378
|
+
history_event="updated",
|
|
379
|
+
history_note=note,
|
|
380
|
+
**updates,
|
|
381
|
+
)
|
|
382
|
+
if result.get("error"):
|
|
383
|
+
return {"success": False, "error": result["error"]}
|
|
384
|
+
changed = True
|
|
385
|
+
elif note:
|
|
386
|
+
note_result = nexo_db.add_followup_note(str(existing["id"]), note, actor="deep-sleep")
|
|
387
|
+
if note_result.get("error"):
|
|
388
|
+
return {"success": False, "error": note_result["error"]}
|
|
389
|
+
changed = True
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"success": True,
|
|
393
|
+
"id": existing["id"],
|
|
394
|
+
"outcome": "matched_existing_followup",
|
|
395
|
+
"similarity": existing.get("_similarity", 1.0),
|
|
396
|
+
"updated_existing": changed,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _fetch_learning_candidates(category: str = "") -> list[dict]:
|
|
401
|
+
if not NEXO_DB.exists():
|
|
402
|
+
return []
|
|
403
|
+
cols = _table_columns(NEXO_DB, "learnings")
|
|
404
|
+
if not cols:
|
|
405
|
+
return []
|
|
406
|
+
select_fields = ["id", "category", "title", "content", "created_at", "updated_at"]
|
|
407
|
+
for optional in ("reasoning", "prevention", "applies_to", "status", "review_due_at", "last_reviewed_at", "weight", "priority"):
|
|
408
|
+
if optional in cols:
|
|
409
|
+
select_fields.append(optional)
|
|
410
|
+
query = f"SELECT {', '.join(select_fields)} FROM learnings"
|
|
411
|
+
params: list[object] = []
|
|
412
|
+
if category and "category" in cols:
|
|
413
|
+
query += " WHERE category = ?"
|
|
414
|
+
params.append(category)
|
|
415
|
+
query += " ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 240"
|
|
416
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
417
|
+
conn.row_factory = sqlite3.Row
|
|
418
|
+
try:
|
|
419
|
+
rows = conn.execute(query, tuple(params)).fetchall()
|
|
420
|
+
finally:
|
|
421
|
+
conn.close()
|
|
422
|
+
return [dict(row) for row in rows]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _find_learning_match(category: str, title: str, content: str) -> dict | None:
|
|
426
|
+
candidates = []
|
|
427
|
+
new_text = " ".join([str(title or ""), str(content or "")]).strip()
|
|
428
|
+
for row in _fetch_learning_candidates(category):
|
|
429
|
+
existing_text = " ".join([str(row.get("title", "") or ""), str(row.get("content", "") or "")])
|
|
430
|
+
similarity = _text_similarity(new_text, existing_text)
|
|
431
|
+
if similarity < 0.58:
|
|
432
|
+
continue
|
|
433
|
+
contradiction = _looks_contradictory(existing_text, new_text)
|
|
434
|
+
candidates.append({**row, "_similarity": similarity, "_contradiction": contradiction})
|
|
435
|
+
if not candidates:
|
|
436
|
+
return None
|
|
437
|
+
candidates.sort(
|
|
438
|
+
key=lambda item: (item["_contradiction"], item["_similarity"], item.get("updated_at", 0) or item.get("created_at", 0)),
|
|
439
|
+
reverse=True,
|
|
440
|
+
)
|
|
441
|
+
return candidates[0]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _update_learning_row(learning_id: int, updates: dict[str, object]) -> None:
|
|
445
|
+
if not updates:
|
|
446
|
+
return
|
|
447
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
448
|
+
set_clause = ", ".join(f"{column} = ?" for column in updates)
|
|
449
|
+
conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", list(updates.values()) + [learning_id])
|
|
450
|
+
conn.commit()
|
|
451
|
+
conn.close()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _bump_weight(existing_value, amount: float) -> float:
|
|
455
|
+
try:
|
|
456
|
+
base = float(existing_value or 0)
|
|
457
|
+
except Exception:
|
|
458
|
+
base = 0.0
|
|
459
|
+
return round(min(10.0, base + amount), 2)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _flag_learning_contradiction(existing: dict, category: str, title: str, content: str) -> dict:
|
|
463
|
+
review_description = (
|
|
464
|
+
f"Reconcile contradictory learning in {category or 'general'}: "
|
|
465
|
+
f"review existing learning #{existing.get('id')} ('{existing.get('title', '')}') "
|
|
466
|
+
f"against new Deep Sleep finding '{title}'. Produce one canonical rule, update guardrails, and remove ambiguity."
|
|
467
|
+
)
|
|
468
|
+
followup_result = create_followup(
|
|
469
|
+
description=review_description,
|
|
470
|
+
date="",
|
|
471
|
+
reasoning_note=f"Contradiction detected against learning #{existing.get('id')}: {content[:240]}",
|
|
472
|
+
)
|
|
473
|
+
return {
|
|
474
|
+
"success": followup_result.get("success", False),
|
|
475
|
+
"id": existing.get("id"),
|
|
476
|
+
"outcome": "contradiction_review",
|
|
477
|
+
"similarity": existing.get("_similarity", 0.0),
|
|
478
|
+
"review_followup_id": followup_result.get("id"),
|
|
479
|
+
"followup_result": followup_result,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def add_learning(category: str, title: str, content: str) -> dict:
|
|
484
|
+
"""Add a learning to nexo.db. Returns result dict."""
|
|
485
|
+
if not NEXO_DB.exists():
|
|
486
|
+
return {"success": False, "error": "nexo.db not found"}
|
|
487
|
+
try:
|
|
488
|
+
existing = _find_learning_match(category, title, content)
|
|
489
|
+
if existing:
|
|
490
|
+
similarity = existing.get("_similarity", 0.0)
|
|
491
|
+
if existing.get("_contradiction"):
|
|
492
|
+
return _flag_learning_contradiction(existing, category, title, content)
|
|
493
|
+
|
|
494
|
+
updates: dict[str, object] = {}
|
|
495
|
+
columns = _table_columns(NEXO_DB, "learnings")
|
|
496
|
+
if "updated_at" in columns:
|
|
497
|
+
updates["updated_at"] = datetime.now().timestamp()
|
|
498
|
+
|
|
499
|
+
existing_title = _normalize_text(existing.get("title", ""))
|
|
500
|
+
existing_content = _normalize_text(existing.get("content", ""))
|
|
501
|
+
incoming_title = _normalize_text(title)
|
|
502
|
+
incoming_content = _normalize_text(content)
|
|
503
|
+
|
|
504
|
+
if similarity >= 0.95 and (
|
|
505
|
+
existing_title == incoming_title
|
|
506
|
+
or existing_content == incoming_content
|
|
507
|
+
or incoming_content in existing_content
|
|
508
|
+
or existing_content in incoming_content
|
|
509
|
+
):
|
|
510
|
+
if "weight" in columns:
|
|
511
|
+
updates["weight"] = _bump_weight(existing.get("weight"), 0.1)
|
|
512
|
+
if "last_reviewed_at" in columns:
|
|
513
|
+
updates["last_reviewed_at"] = datetime.now().timestamp()
|
|
514
|
+
if "reasoning" in columns:
|
|
515
|
+
updates["reasoning"] = _append_note(
|
|
516
|
+
existing.get("reasoning", ""),
|
|
517
|
+
f"Reconfirmed by Deep Sleep on {datetime.now().strftime('%Y-%m-%d')}.",
|
|
518
|
+
)
|
|
519
|
+
_update_learning_row(existing["id"], updates)
|
|
520
|
+
return {
|
|
521
|
+
"success": True,
|
|
522
|
+
"id": existing["id"],
|
|
523
|
+
"outcome": "duplicate_learning",
|
|
524
|
+
"similarity": similarity,
|
|
525
|
+
"updated_existing": bool(updates),
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if similarity >= 0.58:
|
|
529
|
+
if "weight" in columns:
|
|
530
|
+
updates["weight"] = _bump_weight(existing.get("weight"), 0.25)
|
|
531
|
+
if "reasoning" in columns:
|
|
532
|
+
updates["reasoning"] = _append_note(
|
|
533
|
+
existing.get("reasoning", ""),
|
|
534
|
+
f"Deep Sleep reinforcement ({datetime.now().strftime('%Y-%m-%d')}): {title}. {content[:240]}",
|
|
535
|
+
)
|
|
536
|
+
elif "content" in columns and content and content not in str(existing.get("content", "")):
|
|
537
|
+
updates["content"] = _append_note(
|
|
538
|
+
existing.get("content", ""),
|
|
539
|
+
f"Reinforced by Deep Sleep: {content[:240]}",
|
|
540
|
+
)
|
|
541
|
+
_update_learning_row(existing["id"], updates)
|
|
542
|
+
return {
|
|
543
|
+
"success": True,
|
|
544
|
+
"id": existing["id"],
|
|
545
|
+
"outcome": "reinforced_learning",
|
|
546
|
+
"similarity": similarity,
|
|
547
|
+
"updated_existing": bool(updates),
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
now = datetime.now().timestamp()
|
|
551
|
+
columns = _table_columns(NEXO_DB, "learnings")
|
|
552
|
+
payload = {
|
|
553
|
+
"category": category,
|
|
554
|
+
"title": title,
|
|
555
|
+
"content": content,
|
|
556
|
+
"created_at": now,
|
|
557
|
+
"updated_at": now,
|
|
558
|
+
}
|
|
559
|
+
if "reasoning" in columns:
|
|
560
|
+
payload["reasoning"] = "Deep Sleep v2 overnight analysis"
|
|
561
|
+
if "status" in columns:
|
|
562
|
+
payload["status"] = "active"
|
|
563
|
+
insert_columns = [column for column in payload if column in columns]
|
|
564
|
+
values = [payload[column] for column in insert_columns]
|
|
565
|
+
|
|
566
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
567
|
+
cursor = conn.execute(
|
|
568
|
+
f"INSERT INTO learnings ({', '.join(insert_columns)}) VALUES ({', '.join('?' for _ in insert_columns)})",
|
|
569
|
+
values,
|
|
570
|
+
)
|
|
571
|
+
learning_id = cursor.lastrowid
|
|
572
|
+
conn.commit()
|
|
573
|
+
conn.close()
|
|
574
|
+
return {"success": True, "id": learning_id, "outcome": "new_learning"}
|
|
575
|
+
except Exception as e:
|
|
576
|
+
return {"success": False, "error": str(e)}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def create_followup(description: str, date: str = "", reasoning_note: str = "", status: str = "PENDING") -> dict:
|
|
580
|
+
"""Create a followup in nexo.db. Returns result dict."""
|
|
581
|
+
if not NEXO_DB.exists():
|
|
582
|
+
return {"success": False, "error": "nexo.db not found"}
|
|
583
|
+
try:
|
|
584
|
+
desired_status = (status or "PENDING").strip() or "PENDING"
|
|
585
|
+
is_abandoned = description.strip().startswith("[Abandoned]")
|
|
586
|
+
if not is_abandoned:
|
|
587
|
+
matched = _find_similar_followup(description)
|
|
588
|
+
if matched:
|
|
589
|
+
return _touch_existing_followup(
|
|
590
|
+
matched,
|
|
591
|
+
description=description,
|
|
592
|
+
date=date,
|
|
593
|
+
reasoning_note=reasoning_note or "Deep Sleep matched this followup semantically.",
|
|
594
|
+
status=desired_status,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Generate a deterministic ID — content fingerprint, not security-sensitive.
|
|
598
|
+
fid = "NF-DS-" + hashlib.md5(description.encode(), usedforsecurity=False).hexdigest()[:8].upper()
|
|
599
|
+
existing = nexo_db.get_followup(fid)
|
|
600
|
+
if existing:
|
|
601
|
+
return _touch_existing_followup(
|
|
602
|
+
existing,
|
|
603
|
+
description=description,
|
|
604
|
+
date=date,
|
|
605
|
+
reasoning_note=reasoning_note or "Deep Sleep revisited this deterministic followup.",
|
|
606
|
+
status=desired_status,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
followup_result = nexo_db.create_followup(
|
|
610
|
+
id=fid,
|
|
611
|
+
description=description,
|
|
612
|
+
date=date or None,
|
|
613
|
+
verification="",
|
|
614
|
+
status=desired_status,
|
|
615
|
+
reasoning=reasoning_note or "Deep Sleep v2 overnight analysis",
|
|
616
|
+
recurrence=None,
|
|
617
|
+
)
|
|
618
|
+
if followup_result.get("error"):
|
|
619
|
+
return {"success": False, "error": followup_result["error"]}
|
|
620
|
+
if desired_status != "PENDING":
|
|
621
|
+
nexo_db.add_followup_note(
|
|
622
|
+
fid,
|
|
623
|
+
f"Deep Sleep created this followup directly as {desired_status}.",
|
|
624
|
+
actor="deep-sleep",
|
|
625
|
+
)
|
|
626
|
+
return {"success": True, "id": fid, "outcome": "new_followup"}
|
|
627
|
+
except Exception as e:
|
|
628
|
+
return {"success": False, "error": str(e)}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def update_calibration_mood(synthesis: dict) -> dict:
|
|
632
|
+
"""Update mood in calibration.json based on emotional analysis."""
|
|
633
|
+
calibration_file = NEXO_HOME / "brain" / "calibration.json"
|
|
634
|
+
if not calibration_file.exists():
|
|
635
|
+
return {"success": False, "error": "calibration.json not found"}
|
|
636
|
+
|
|
637
|
+
emotional_day = synthesis.get("emotional_day", {})
|
|
638
|
+
if not emotional_day:
|
|
639
|
+
return {"success": False, "error": "no emotional_day data"}
|
|
640
|
+
|
|
641
|
+
try:
|
|
642
|
+
cal = json.loads(calibration_file.read_text())
|
|
643
|
+
|
|
644
|
+
# Add/update mood history
|
|
645
|
+
if "mood_history" not in cal:
|
|
646
|
+
cal["mood_history"] = []
|
|
647
|
+
|
|
648
|
+
cal["mood_history"].append({
|
|
649
|
+
"date": synthesis.get("date", ""),
|
|
650
|
+
"score": emotional_day.get("mood_score", 0.5),
|
|
651
|
+
"arc": emotional_day.get("mood_arc", ""),
|
|
652
|
+
"triggers": emotional_day.get("recurring_triggers", {}),
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
# Keep last 30 days
|
|
656
|
+
cal["mood_history"] = cal["mood_history"][-30:]
|
|
657
|
+
|
|
658
|
+
# Apply calibration recommendation automatically
|
|
659
|
+
rec = emotional_day.get("calibration_recommendation")
|
|
660
|
+
if rec and rec != "null":
|
|
661
|
+
applied_changes = []
|
|
662
|
+
|
|
663
|
+
# Parse and apply known calibration adjustments
|
|
664
|
+
rec_lower = rec.lower()
|
|
665
|
+
personality = cal.get("personality", {})
|
|
666
|
+
|
|
667
|
+
# Autonomy adjustments
|
|
668
|
+
if "autonomy" in rec_lower or "autonomía" in rec_lower:
|
|
669
|
+
if any(w in rec_lower for w in ["full", "más autonomía", "subir", "increase"]):
|
|
670
|
+
personality["autonomy"] = "full"
|
|
671
|
+
applied_changes.append("autonomy → full")
|
|
672
|
+
elif any(w in rec_lower for w in ["conservative", "reducir", "bajar"]):
|
|
673
|
+
personality["autonomy"] = "conservative"
|
|
674
|
+
applied_changes.append("autonomy → conservative")
|
|
675
|
+
|
|
676
|
+
# Communication adjustments
|
|
677
|
+
if any(w in rec_lower for w in ["concis", "breve", "shorter", "telegráf"]):
|
|
678
|
+
personality["communication"] = "concise"
|
|
679
|
+
applied_changes.append("communication → concise")
|
|
680
|
+
elif any(w in rec_lower for w in ["detail", "explicar más", "más contexto"]):
|
|
681
|
+
personality["communication"] = "detailed"
|
|
682
|
+
applied_changes.append("communication → detailed")
|
|
683
|
+
|
|
684
|
+
# Proactivity adjustments
|
|
685
|
+
if any(w in rec_lower for w in ["más proactiv", "proactive", "anticipar"]):
|
|
686
|
+
personality["proactivity"] = "proactive"
|
|
687
|
+
applied_changes.append("proactivity → proactive")
|
|
688
|
+
|
|
689
|
+
cal["personality"] = personality
|
|
690
|
+
|
|
691
|
+
# Log the recommendation and what was applied
|
|
692
|
+
if "calibration_log" not in cal:
|
|
693
|
+
cal["calibration_log"] = []
|
|
694
|
+
cal["calibration_log"].append({
|
|
695
|
+
"date": synthesis.get("date", ""),
|
|
696
|
+
"recommendation": rec,
|
|
697
|
+
"applied": applied_changes if applied_changes else ["noted, no auto-applicable changes"],
|
|
698
|
+
})
|
|
699
|
+
cal["calibration_log"] = cal["calibration_log"][-20:]
|
|
700
|
+
|
|
701
|
+
calibration_file.write_text(json.dumps(cal, indent=2, ensure_ascii=False))
|
|
702
|
+
changes_str = ", ".join(applied_changes) if rec and applied_changes else "none"
|
|
703
|
+
return {"success": True, "mood_score": emotional_day.get("mood_score"), "calibration_applied": changes_str}
|
|
704
|
+
except Exception as e:
|
|
705
|
+
return {"success": False, "error": str(e)}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def calibrate_trust_score(synthesis: dict, target_date: str) -> dict:
|
|
709
|
+
"""Set the daily trust score from Deep Sleep analysis.
|
|
710
|
+
|
|
711
|
+
This is the authoritative score for the day — replaces incremental
|
|
712
|
+
adjustments with a holistic evaluation of the entire day.
|
|
713
|
+
"""
|
|
714
|
+
trust_cal = synthesis.get("trust_calibration")
|
|
715
|
+
if not trust_cal or "score" not in trust_cal:
|
|
716
|
+
return {"success": False, "error": "no trust_calibration in synthesis"}
|
|
717
|
+
|
|
718
|
+
score = max(0, min(100, trust_cal["score"]))
|
|
719
|
+
reasoning = trust_cal.get("reasoning", "Deep Sleep calibration")
|
|
720
|
+
trend = trust_cal.get("trend", "stable")
|
|
721
|
+
highlights = trust_cal.get("highlights", [])
|
|
722
|
+
lowlights = trust_cal.get("lowlights", [])
|
|
723
|
+
|
|
724
|
+
context = (
|
|
725
|
+
f"Deep Sleep {target_date} | trend: {trend} | "
|
|
726
|
+
f"highlights: {', '.join(highlights[:3])} | "
|
|
727
|
+
f"lowlights: {', '.join(lowlights[:3])}"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
try:
|
|
731
|
+
# Get current score for delta calculation
|
|
732
|
+
db = sqlite3.connect(str(COGNITIVE_DB))
|
|
733
|
+
row = db.execute(
|
|
734
|
+
"SELECT score FROM trust_score ORDER BY id DESC LIMIT 1"
|
|
735
|
+
).fetchone()
|
|
736
|
+
old_score = row[0] if row else 50.0
|
|
737
|
+
delta = score - old_score
|
|
738
|
+
|
|
739
|
+
db.execute(
|
|
740
|
+
"INSERT INTO trust_score (score, event, delta, context) VALUES (?, ?, ?, ?)",
|
|
741
|
+
(score, f"deep_sleep_calibration: {reasoning[:200]}", delta, context[:500])
|
|
742
|
+
)
|
|
743
|
+
db.commit()
|
|
744
|
+
db.close()
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
"success": True,
|
|
748
|
+
"old_score": old_score,
|
|
749
|
+
"new_score": score,
|
|
750
|
+
"delta": delta,
|
|
751
|
+
"trend": trend,
|
|
752
|
+
}
|
|
753
|
+
except Exception as e:
|
|
754
|
+
return {"success": False, "error": str(e)}
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def create_skill(skill_data: dict) -> dict:
|
|
758
|
+
"""Create a personal Skill v2 definition and sync it into SQLite."""
|
|
759
|
+
try:
|
|
760
|
+
from db import materialize_personal_skill_definition
|
|
761
|
+
|
|
762
|
+
skill_id = skill_data.get("id", "")
|
|
763
|
+
if not skill_id:
|
|
764
|
+
# Content fingerprint, not security-sensitive.
|
|
765
|
+
skill_id = "SK-DS-" + hashlib.md5(
|
|
766
|
+
skill_data.get("name", "").encode(), usedforsecurity=False
|
|
767
|
+
).hexdigest()[:8].upper()
|
|
768
|
+
|
|
769
|
+
execution_level = skill_data.get("execution_level", "")
|
|
770
|
+
scriptable = bool(skill_data.get("scriptable"))
|
|
771
|
+
mode = skill_data.get("mode", "")
|
|
772
|
+
if not mode:
|
|
773
|
+
if scriptable and execution_level == "read-only":
|
|
774
|
+
mode = "hybrid"
|
|
775
|
+
else:
|
|
776
|
+
mode = "guide"
|
|
777
|
+
|
|
778
|
+
approval_required = bool(skill_data.get("approval_required", execution_level in {"local", "remote"}))
|
|
779
|
+
script_body = str(skill_data.get("script_body", "") or "")
|
|
780
|
+
executable_entry = str(skill_data.get("executable_entry", "") or "")
|
|
781
|
+
|
|
782
|
+
result = materialize_personal_skill_definition(
|
|
783
|
+
{
|
|
784
|
+
"id": skill_id,
|
|
785
|
+
"name": skill_data.get("name", ""),
|
|
786
|
+
"description": skill_data.get("description", ""),
|
|
787
|
+
"level": skill_data.get("level", "draft"),
|
|
788
|
+
"mode": mode,
|
|
789
|
+
"execution_level": execution_level if mode != "guide" else "none",
|
|
790
|
+
"approval_required": approval_required,
|
|
791
|
+
"tags": skill_data.get("tags", []),
|
|
792
|
+
"trigger_patterns": skill_data.get("trigger_patterns", []),
|
|
793
|
+
"source_sessions": skill_data.get("source_sessions", []),
|
|
794
|
+
"steps": skill_data.get("steps", []),
|
|
795
|
+
"gotchas": skill_data.get("gotchas", []),
|
|
796
|
+
"params_schema": skill_data.get("params_schema", skill_data.get("candidate_params", {})),
|
|
797
|
+
"command_template": skill_data.get("command_template", {}),
|
|
798
|
+
"executable_entry": executable_entry,
|
|
799
|
+
"script_body": script_body,
|
|
800
|
+
"content": skill_data.get("content", ""),
|
|
801
|
+
}
|
|
802
|
+
)
|
|
803
|
+
if "error" in result:
|
|
804
|
+
return {"success": False, "error": result["error"], "id": skill_id}
|
|
805
|
+
return {"success": True, "id": result["id"], "name": result.get("name", "")}
|
|
806
|
+
except Exception as e:
|
|
807
|
+
return {"success": False, "error": str(e)}
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def create_abandoned_followups(synthesis: dict) -> list[dict]:
|
|
811
|
+
"""Create followups for truly abandoned projects."""
|
|
812
|
+
results = []
|
|
813
|
+
abandoned = synthesis.get("abandoned_projects", [])
|
|
814
|
+
for proj in abandoned:
|
|
815
|
+
if proj.get("has_followup"):
|
|
816
|
+
continue
|
|
817
|
+
rec = proj.get("recommendation", "")
|
|
818
|
+
if "ignore" in rec.lower():
|
|
819
|
+
continue
|
|
820
|
+
result = create_followup(
|
|
821
|
+
description=f"[Abandoned] {proj.get('description', '')}",
|
|
822
|
+
date="", # No date — it's a discovered gap
|
|
823
|
+
reasoning_note="Deep Sleep marked this as abandoned. Keep it as archived history, not active work.",
|
|
824
|
+
status="archived",
|
|
825
|
+
)
|
|
826
|
+
results.append(result)
|
|
827
|
+
return results
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
|
|
831
|
+
if not db_path.exists():
|
|
832
|
+
return []
|
|
833
|
+
try:
|
|
834
|
+
conn = sqlite3.connect(str(db_path))
|
|
835
|
+
conn.row_factory = sqlite3.Row
|
|
836
|
+
rows = conn.execute(query, params).fetchall()
|
|
837
|
+
conn.close()
|
|
838
|
+
return [dict(row) for row in rows]
|
|
839
|
+
except Exception:
|
|
840
|
+
return []
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _parse_any_datetime(value) -> datetime | None:
|
|
844
|
+
if value in (None, ""):
|
|
845
|
+
return None
|
|
846
|
+
try:
|
|
847
|
+
if isinstance(value, (int, float)) or (isinstance(value, str) and str(value).strip().isdigit()):
|
|
848
|
+
return datetime.fromtimestamp(float(value))
|
|
849
|
+
except Exception:
|
|
850
|
+
return None
|
|
851
|
+
raw = str(value).strip()
|
|
852
|
+
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
|
|
853
|
+
try:
|
|
854
|
+
return datetime.strptime(raw[:19], fmt)
|
|
855
|
+
except Exception:
|
|
856
|
+
continue
|
|
857
|
+
try:
|
|
858
|
+
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
859
|
+
return dt.replace(tzinfo=None)
|
|
860
|
+
except Exception:
|
|
861
|
+
return None
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _load_project_aliases() -> dict[str, set[str]]:
|
|
865
|
+
atlas_path = NEXO_HOME / "brain" / "project-atlas.json"
|
|
866
|
+
if not atlas_path.is_file():
|
|
867
|
+
return {}
|
|
868
|
+
try:
|
|
869
|
+
payload = json.loads(atlas_path.read_text())
|
|
870
|
+
except Exception:
|
|
871
|
+
return {}
|
|
872
|
+
if not isinstance(payload, dict):
|
|
873
|
+
return {}
|
|
874
|
+
aliases: dict[str, set[str]] = {}
|
|
875
|
+
for key, value in payload.items():
|
|
876
|
+
if str(key).startswith("_"):
|
|
877
|
+
continue
|
|
878
|
+
canonical = str(key).strip().lower()
|
|
879
|
+
alias_set = {canonical, canonical.replace("-", " "), canonical.replace("_", " ")}
|
|
880
|
+
if isinstance(value, dict):
|
|
881
|
+
for alias in value.get("aliases", []) or []:
|
|
882
|
+
alias_value = str(alias or "").strip().lower()
|
|
883
|
+
if alias_value:
|
|
884
|
+
alias_set.add(alias_value)
|
|
885
|
+
alias_set.add(alias_value.replace("-", " "))
|
|
886
|
+
aliases[canonical] = {item for item in alias_set if item}
|
|
887
|
+
return aliases
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _match_projects(text: str, alias_map: dict[str, set[str]]) -> set[str]:
|
|
891
|
+
haystack = str(text or "").strip().lower()
|
|
892
|
+
if not haystack:
|
|
893
|
+
return set()
|
|
894
|
+
matches: set[str] = set()
|
|
895
|
+
for canonical, aliases in alias_map.items():
|
|
896
|
+
for alias in sorted(aliases, key=len, reverse=True):
|
|
897
|
+
if alias and alias in haystack:
|
|
898
|
+
matches.add(canonical)
|
|
899
|
+
break
|
|
900
|
+
return matches
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _priority_weight(value) -> float:
|
|
904
|
+
lowered = str(value or "").strip().lower()
|
|
905
|
+
if lowered in {"critical", "urgent"}:
|
|
906
|
+
return 4.0
|
|
907
|
+
if lowered == "high":
|
|
908
|
+
return 3.0
|
|
909
|
+
if lowered == "medium":
|
|
910
|
+
return 2.0
|
|
911
|
+
if lowered == "low":
|
|
912
|
+
return 1.0
|
|
913
|
+
return 1.5
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _project_weighting_window(target_date: str, *, window_days: int) -> list[dict]:
|
|
917
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
918
|
+
window_start = target_day - timedelta(days=max(0, window_days - 1))
|
|
919
|
+
alias_map = _load_project_aliases()
|
|
920
|
+
scoreboard: dict[str, dict] = {}
|
|
921
|
+
|
|
922
|
+
def normalize_project(project: str) -> str:
|
|
923
|
+
lowered = str(project or "").strip().lower()
|
|
924
|
+
if not lowered:
|
|
925
|
+
return ""
|
|
926
|
+
matched = _match_projects(lowered, alias_map)
|
|
927
|
+
if matched:
|
|
928
|
+
return sorted(matched)[0]
|
|
929
|
+
return lowered
|
|
930
|
+
|
|
931
|
+
def bump(project: str, score: float, signal_key: str, reason: str) -> None:
|
|
932
|
+
canonical = normalize_project(project)
|
|
933
|
+
if not canonical:
|
|
934
|
+
return
|
|
935
|
+
slot = scoreboard.setdefault(
|
|
936
|
+
canonical,
|
|
937
|
+
{
|
|
938
|
+
"project": canonical,
|
|
939
|
+
"score": 0.0,
|
|
940
|
+
"signals": {
|
|
941
|
+
"diary_sessions": 0,
|
|
942
|
+
"learnings": 0,
|
|
943
|
+
"followups": 0,
|
|
944
|
+
"decisions": 0,
|
|
945
|
+
},
|
|
946
|
+
"reasons": [],
|
|
947
|
+
},
|
|
948
|
+
)
|
|
949
|
+
slot["score"] += score
|
|
950
|
+
slot["signals"][signal_key] += 1
|
|
951
|
+
if reason and reason not in slot["reasons"]:
|
|
952
|
+
slot["reasons"].append(reason)
|
|
953
|
+
|
|
954
|
+
diary_rows = _safe_query(
|
|
955
|
+
NEXO_DB,
|
|
956
|
+
"SELECT created_at, summary, self_critique, domain FROM session_diary ORDER BY created_at DESC",
|
|
957
|
+
)
|
|
958
|
+
for row in diary_rows:
|
|
959
|
+
created = _parse_any_datetime(row.get("created_at"))
|
|
960
|
+
if not created or created < window_start or created > target_day + timedelta(days=1):
|
|
961
|
+
continue
|
|
962
|
+
recency_bonus = 1.4 if (target_day - created).days <= 7 else 1.0
|
|
963
|
+
matched = _match_projects(
|
|
964
|
+
" ".join(
|
|
965
|
+
[
|
|
966
|
+
str(row.get("summary", "") or ""),
|
|
967
|
+
str(row.get("self_critique", "") or ""),
|
|
968
|
+
]
|
|
969
|
+
),
|
|
970
|
+
alias_map,
|
|
971
|
+
)
|
|
972
|
+
domain = normalize_project(str(row.get("domain", "") or ""))
|
|
973
|
+
if domain:
|
|
974
|
+
matched.add(domain)
|
|
975
|
+
for project in matched:
|
|
976
|
+
bump(project, 3.0 * recency_bonus, "diary_sessions", "recent diary activity")
|
|
977
|
+
|
|
978
|
+
learning_rows = _safe_query(
|
|
979
|
+
NEXO_DB,
|
|
980
|
+
"SELECT title, content, applies_to, priority, weight, updated_at, created_at FROM learnings "
|
|
981
|
+
"ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 180",
|
|
982
|
+
)
|
|
983
|
+
for row in learning_rows:
|
|
984
|
+
when = _parse_any_datetime(row.get("updated_at") or row.get("created_at"))
|
|
985
|
+
if when and when < window_start:
|
|
986
|
+
continue
|
|
987
|
+
matched = _match_projects(
|
|
988
|
+
" ".join(
|
|
989
|
+
[
|
|
990
|
+
str(row.get("applies_to", "") or ""),
|
|
991
|
+
str(row.get("title", "") or ""),
|
|
992
|
+
str(row.get("content", "") or ""),
|
|
993
|
+
]
|
|
994
|
+
),
|
|
995
|
+
alias_map,
|
|
996
|
+
)
|
|
997
|
+
if not matched:
|
|
998
|
+
continue
|
|
999
|
+
score = 1.0 + _priority_weight(row.get("priority")) + min(2.0, max(0.0, float(row.get("weight", 0) or 0)))
|
|
1000
|
+
for project in matched:
|
|
1001
|
+
bump(project, score, "learnings", "recent leverage-bearing learning")
|
|
1002
|
+
|
|
1003
|
+
followup_rows = _safe_query(
|
|
1004
|
+
NEXO_DB,
|
|
1005
|
+
"SELECT description, date, status, priority, created_at, updated_at, reasoning, impact_score FROM followups "
|
|
1006
|
+
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 160",
|
|
1007
|
+
)
|
|
1008
|
+
for row in followup_rows:
|
|
1009
|
+
matched = _match_projects(
|
|
1010
|
+
" ".join(
|
|
1011
|
+
[
|
|
1012
|
+
str(row.get("description", "") or ""),
|
|
1013
|
+
str(row.get("reasoning", "") or ""),
|
|
1014
|
+
]
|
|
1015
|
+
),
|
|
1016
|
+
alias_map,
|
|
1017
|
+
)
|
|
1018
|
+
if not matched:
|
|
1019
|
+
continue
|
|
1020
|
+
overdue_bonus = 0.0
|
|
1021
|
+
due_dt = _parse_any_datetime(row.get("date"))
|
|
1022
|
+
if due_dt and due_dt <= target_day:
|
|
1023
|
+
overdue_bonus = 1.5
|
|
1024
|
+
impact_bonus = min(4.0, max(0.0, float(row.get("impact_score", 0) or 0)) / 25.0)
|
|
1025
|
+
score = 1.5 + _priority_weight(row.get("priority")) + overdue_bonus + impact_bonus
|
|
1026
|
+
for project in matched:
|
|
1027
|
+
bump(project, score, "followups", "open followup pressure")
|
|
1028
|
+
|
|
1029
|
+
decision_rows = _safe_query(
|
|
1030
|
+
NEXO_DB,
|
|
1031
|
+
"SELECT domain, outcome, status, reasoning, created_at, review_due_at FROM decisions "
|
|
1032
|
+
"ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 160",
|
|
1033
|
+
)
|
|
1034
|
+
for row in decision_rows:
|
|
1035
|
+
when = _parse_any_datetime(row.get("created_at") or row.get("review_due_at"))
|
|
1036
|
+
if when and when < window_start:
|
|
1037
|
+
continue
|
|
1038
|
+
matched = _match_projects(
|
|
1039
|
+
" ".join(
|
|
1040
|
+
[
|
|
1041
|
+
str(row.get("reasoning", "") or ""),
|
|
1042
|
+
str(row.get("outcome", "") or ""),
|
|
1043
|
+
str(row.get("status", "") or ""),
|
|
1044
|
+
]
|
|
1045
|
+
),
|
|
1046
|
+
alias_map,
|
|
1047
|
+
)
|
|
1048
|
+
domain = normalize_project(str(row.get("domain", "") or ""))
|
|
1049
|
+
if domain:
|
|
1050
|
+
matched.add(domain)
|
|
1051
|
+
if not matched:
|
|
1052
|
+
continue
|
|
1053
|
+
outcome = str(row.get("outcome", "") or "").lower()
|
|
1054
|
+
status = str(row.get("status", "") or "").lower()
|
|
1055
|
+
score = 2.5
|
|
1056
|
+
if any(token in outcome for token in ("fail", "error", "blocked", "regression")):
|
|
1057
|
+
score += 2.0
|
|
1058
|
+
if status in {"pending", "blocked", "open"}:
|
|
1059
|
+
score += 1.5
|
|
1060
|
+
for project in matched:
|
|
1061
|
+
bump(project, score, "decisions", "recent decision pressure")
|
|
1062
|
+
|
|
1063
|
+
ranked = sorted(scoreboard.values(), key=lambda item: item["score"], reverse=True)
|
|
1064
|
+
for item in ranked:
|
|
1065
|
+
item["score"] = round(item["score"], 2)
|
|
1066
|
+
item["reasons"] = item["reasons"][:4]
|
|
1067
|
+
return ranked[:8]
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _load_period_syntheses(target_date: str, *, window_days: int) -> list[dict]:
|
|
1071
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
1072
|
+
syntheses: list[dict] = []
|
|
1073
|
+
for offset in range(window_days):
|
|
1074
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
1075
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-synthesis.json"
|
|
1076
|
+
if not path.is_file():
|
|
1077
|
+
continue
|
|
1078
|
+
try:
|
|
1079
|
+
payload = json.loads(path.read_text())
|
|
1080
|
+
except Exception:
|
|
1081
|
+
continue
|
|
1082
|
+
if isinstance(payload, dict):
|
|
1083
|
+
syntheses.append(payload)
|
|
1084
|
+
syntheses.reverse()
|
|
1085
|
+
return syntheses
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _load_period_extractions(target_date: str, *, window_days: int) -> list[dict]:
|
|
1089
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
1090
|
+
payloads: list[dict] = []
|
|
1091
|
+
for offset in range(window_days):
|
|
1092
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
1093
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-extractions.json"
|
|
1094
|
+
if not path.is_file():
|
|
1095
|
+
continue
|
|
1096
|
+
try:
|
|
1097
|
+
payload = json.loads(path.read_text())
|
|
1098
|
+
except Exception:
|
|
1099
|
+
continue
|
|
1100
|
+
if isinstance(payload, dict):
|
|
1101
|
+
payloads.append(payload)
|
|
1102
|
+
payloads.reverse()
|
|
1103
|
+
return payloads
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def _load_period_applied_logs(target_date: str, *, window_days: int) -> list[dict]:
|
|
1107
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
1108
|
+
payloads: list[dict] = []
|
|
1109
|
+
for offset in range(window_days):
|
|
1110
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
1111
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-applied.json"
|
|
1112
|
+
if not path.is_file():
|
|
1113
|
+
continue
|
|
1114
|
+
try:
|
|
1115
|
+
payload = json.loads(path.read_text())
|
|
1116
|
+
except Exception:
|
|
1117
|
+
continue
|
|
1118
|
+
if isinstance(payload, dict):
|
|
1119
|
+
payloads.append(payload)
|
|
1120
|
+
payloads.reverse()
|
|
1121
|
+
return payloads
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _safe_pct(numerator: float, denominator: float) -> float | None:
|
|
1125
|
+
if denominator <= 0:
|
|
1126
|
+
return None
|
|
1127
|
+
return round((numerator / denominator) * 100.0, 1)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _aggregate_protocol_summary(extractions: list[dict]) -> dict:
|
|
1131
|
+
totals = {
|
|
1132
|
+
"sessions": 0,
|
|
1133
|
+
"guard_check": {"required": 0, "executed": 0},
|
|
1134
|
+
"heartbeat": {"total": 0, "with_context": 0},
|
|
1135
|
+
"change_log": {"edits": 0, "logged": 0},
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
for payload in extractions:
|
|
1139
|
+
for item in payload.get("extractions", []) or []:
|
|
1140
|
+
if not isinstance(item, dict) or item.get("error"):
|
|
1141
|
+
continue
|
|
1142
|
+
totals["sessions"] += 1
|
|
1143
|
+
protocol_summary = item.get("protocol_summary") or {}
|
|
1144
|
+
for key in ("guard_check", "heartbeat", "change_log"):
|
|
1145
|
+
current = protocol_summary.get(key) or {}
|
|
1146
|
+
if key == "guard_check":
|
|
1147
|
+
totals[key]["required"] += int(current.get("required", 0) or 0)
|
|
1148
|
+
totals[key]["executed"] += int(current.get("executed", 0) or 0)
|
|
1149
|
+
elif key == "heartbeat":
|
|
1150
|
+
totals[key]["total"] += int(current.get("total", 0) or 0)
|
|
1151
|
+
totals[key]["with_context"] += int(current.get("with_context", 0) or 0)
|
|
1152
|
+
else:
|
|
1153
|
+
totals[key]["edits"] += int(current.get("edits", 0) or 0)
|
|
1154
|
+
totals[key]["logged"] += int(current.get("logged", 0) or 0)
|
|
1155
|
+
|
|
1156
|
+
guard_pct = _safe_pct(totals["guard_check"]["executed"], totals["guard_check"]["required"])
|
|
1157
|
+
heartbeat_pct = _safe_pct(totals["heartbeat"]["with_context"], totals["heartbeat"]["total"])
|
|
1158
|
+
change_pct = _safe_pct(totals["change_log"]["logged"], totals["change_log"]["edits"])
|
|
1159
|
+
available = [value for value in (guard_pct, heartbeat_pct, change_pct) if value is not None]
|
|
1160
|
+
|
|
1161
|
+
totals["guard_check"]["compliance_pct"] = guard_pct
|
|
1162
|
+
totals["heartbeat"]["compliance_pct"] = heartbeat_pct
|
|
1163
|
+
totals["change_log"]["compliance_pct"] = change_pct
|
|
1164
|
+
totals["overall_compliance_pct"] = round(sum(available) / len(available), 1) if available else None
|
|
1165
|
+
return totals
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def _aggregate_delivery_metrics(applied_logs: list[dict]) -> dict:
|
|
1169
|
+
totals = {
|
|
1170
|
+
"runs": len(applied_logs),
|
|
1171
|
+
"applied_actions": 0,
|
|
1172
|
+
"deferred_actions": 0,
|
|
1173
|
+
"skipped_dedupe": 0,
|
|
1174
|
+
"errors": 0,
|
|
1175
|
+
"engineering_followups": 0,
|
|
1176
|
+
"followup_dedupe_matches": 0,
|
|
1177
|
+
"learning_reinforcements": 0,
|
|
1178
|
+
"learning_duplicate_skips": 0,
|
|
1179
|
+
"learning_contradiction_reviews": 0,
|
|
1180
|
+
}
|
|
1181
|
+
for payload in applied_logs:
|
|
1182
|
+
stats = payload.get("stats") or {}
|
|
1183
|
+
totals["applied_actions"] += int(stats.get("applied", 0) or 0)
|
|
1184
|
+
totals["deferred_actions"] += int(stats.get("deferred", 0) or 0)
|
|
1185
|
+
totals["skipped_dedupe"] += int(stats.get("skipped_dedupe", 0) or 0)
|
|
1186
|
+
totals["errors"] += int(stats.get("errors", 0) or 0)
|
|
1187
|
+
for action in payload.get("applied_actions", []) or []:
|
|
1188
|
+
details = action.get("details") or {}
|
|
1189
|
+
if action.get("action_type") == "followup_create":
|
|
1190
|
+
description = str(details.get("description", "") or "") + " " + str(details.get("reasoning", "") or "")
|
|
1191
|
+
if "engineering" in description.lower() or "guardrail" in description.lower():
|
|
1192
|
+
totals["engineering_followups"] += 1
|
|
1193
|
+
if details.get("outcome") == "matched_existing_followup":
|
|
1194
|
+
totals["followup_dedupe_matches"] += 1
|
|
1195
|
+
elif action.get("action_type") == "learning_add":
|
|
1196
|
+
outcome = str(details.get("outcome", "") or "")
|
|
1197
|
+
if outcome == "reinforced_learning":
|
|
1198
|
+
totals["learning_reinforcements"] += 1
|
|
1199
|
+
elif outcome == "duplicate_learning":
|
|
1200
|
+
totals["learning_duplicate_skips"] += 1
|
|
1201
|
+
elif outcome == "contradiction_review":
|
|
1202
|
+
totals["learning_contradiction_reviews"] += 1
|
|
1203
|
+
|
|
1204
|
+
attempted = totals["applied_actions"] + totals["deferred_actions"] + totals["skipped_dedupe"] + totals["errors"]
|
|
1205
|
+
totals["dedupe_rate_pct"] = _safe_pct(totals["skipped_dedupe"], attempted)
|
|
1206
|
+
totals["error_rate_pct"] = _safe_pct(totals["errors"], attempted)
|
|
1207
|
+
return totals
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _semantic_duplicate_metrics(items: list[tuple[str, str]], *, threshold: float = 0.82) -> dict:
|
|
1211
|
+
filtered = [(item_id, _normalize_text(text)) for item_id, text in items if _normalize_text(text)]
|
|
1212
|
+
if len(filtered) < 2:
|
|
1213
|
+
return {
|
|
1214
|
+
"cluster_count": 0,
|
|
1215
|
+
"duplicate_items": 0,
|
|
1216
|
+
"duplicate_excess": 0,
|
|
1217
|
+
"sample_clusters": [],
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
used: set[int] = set()
|
|
1221
|
+
clusters: list[list[tuple[str, str]]] = []
|
|
1222
|
+
for index, (item_id, text) in enumerate(filtered):
|
|
1223
|
+
if index in used:
|
|
1224
|
+
continue
|
|
1225
|
+
cluster = [(item_id, text)]
|
|
1226
|
+
for other_index in range(index + 1, len(filtered)):
|
|
1227
|
+
if other_index in used:
|
|
1228
|
+
continue
|
|
1229
|
+
other_id, other_text = filtered[other_index]
|
|
1230
|
+
if _text_similarity(text, other_text) >= threshold:
|
|
1231
|
+
cluster.append((other_id, other_text))
|
|
1232
|
+
used.add(other_index)
|
|
1233
|
+
if len(cluster) > 1:
|
|
1234
|
+
used.add(index)
|
|
1235
|
+
clusters.append(cluster)
|
|
1236
|
+
|
|
1237
|
+
return {
|
|
1238
|
+
"cluster_count": len(clusters),
|
|
1239
|
+
"duplicate_items": sum(len(cluster) for cluster in clusters),
|
|
1240
|
+
"duplicate_excess": sum(max(0, len(cluster) - 1) for cluster in clusters),
|
|
1241
|
+
"sample_clusters": [
|
|
1242
|
+
[item_id for item_id, _ in cluster[:4]]
|
|
1243
|
+
for cluster in clusters[:5]
|
|
1244
|
+
],
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _followup_deduplication_metrics() -> dict:
|
|
1249
|
+
cols = _table_columns(NEXO_DB, "followups")
|
|
1250
|
+
if "description" not in cols:
|
|
1251
|
+
return {
|
|
1252
|
+
"open_followups": 0,
|
|
1253
|
+
"duplicate_clusters": 0,
|
|
1254
|
+
"duplicate_open_followups": 0,
|
|
1255
|
+
"duplicate_rate_pct": None,
|
|
1256
|
+
"sample_clusters": [],
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
select_cols = ["description"]
|
|
1260
|
+
if "id" in cols:
|
|
1261
|
+
select_cols.append("id")
|
|
1262
|
+
if "status" in cols:
|
|
1263
|
+
select_cols.append("status")
|
|
1264
|
+
|
|
1265
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1266
|
+
conn.row_factory = sqlite3.Row
|
|
1267
|
+
rows = [dict(row) for row in conn.execute(f"SELECT {', '.join(select_cols)} FROM followups").fetchall()]
|
|
1268
|
+
conn.close()
|
|
1269
|
+
|
|
1270
|
+
open_rows = []
|
|
1271
|
+
for row in rows:
|
|
1272
|
+
status = str(row.get("status", "pending") or "pending").strip().lower()
|
|
1273
|
+
if status in {"done", "completed", "cancelled", "resolved"}:
|
|
1274
|
+
continue
|
|
1275
|
+
identifier = str(row.get("id") or row.get("description") or f"followup-{len(open_rows)+1}")
|
|
1276
|
+
open_rows.append((identifier, str(row.get("description", "") or "")))
|
|
1277
|
+
|
|
1278
|
+
duplicates = _semantic_duplicate_metrics(open_rows)
|
|
1279
|
+
return {
|
|
1280
|
+
"open_followups": len(open_rows),
|
|
1281
|
+
"duplicate_clusters": duplicates["cluster_count"],
|
|
1282
|
+
"duplicate_open_followups": duplicates["duplicate_excess"],
|
|
1283
|
+
"duplicate_rate_pct": _safe_pct(duplicates["duplicate_excess"], len(open_rows)),
|
|
1284
|
+
"sample_clusters": duplicates["sample_clusters"],
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _learning_consolidation_metrics() -> dict:
|
|
1289
|
+
cols = _table_columns(NEXO_DB, "learnings")
|
|
1290
|
+
if not {"title", "content"}.issubset(cols):
|
|
1291
|
+
return {
|
|
1292
|
+
"active_learnings": 0,
|
|
1293
|
+
"weak_active_learnings": 0,
|
|
1294
|
+
"duplicate_clusters": 0,
|
|
1295
|
+
"duplicate_active_learnings": 0,
|
|
1296
|
+
"noise_pressure": 0,
|
|
1297
|
+
"noise_rate_pct": None,
|
|
1298
|
+
"sample_clusters": [],
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
select_cols = ["title", "content"]
|
|
1302
|
+
if "id" in cols:
|
|
1303
|
+
select_cols.append("id")
|
|
1304
|
+
for field in ("status", "weight", "reasoning", "prevention", "applies_to", "guard_hits"):
|
|
1305
|
+
if field in cols:
|
|
1306
|
+
select_cols.append(field)
|
|
1307
|
+
|
|
1308
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1309
|
+
conn.row_factory = sqlite3.Row
|
|
1310
|
+
rows = [dict(row) for row in conn.execute(f"SELECT {', '.join(select_cols)} FROM learnings").fetchall()]
|
|
1311
|
+
conn.close()
|
|
1312
|
+
|
|
1313
|
+
active_rows = []
|
|
1314
|
+
weak_active = 0
|
|
1315
|
+
for row in rows:
|
|
1316
|
+
status = str(row.get("status", "active") or "active").strip().lower()
|
|
1317
|
+
if status != "active":
|
|
1318
|
+
continue
|
|
1319
|
+
active_rows.append(row)
|
|
1320
|
+
weight = row.get("weight")
|
|
1321
|
+
reasoning = str(row.get("reasoning", "") or "").strip()
|
|
1322
|
+
prevention = str(row.get("prevention", "") or "").strip()
|
|
1323
|
+
guard_hits = int(row.get("guard_hits", 0) or 0)
|
|
1324
|
+
applies_to = str(row.get("applies_to", "") or "").strip()
|
|
1325
|
+
if isinstance(weight, (int, float)) and float(weight) < 1.0:
|
|
1326
|
+
weak_active += 1
|
|
1327
|
+
elif not reasoning and not prevention:
|
|
1328
|
+
weak_active += 1
|
|
1329
|
+
elif applies_to and guard_hits <= 0:
|
|
1330
|
+
weak_active += 1
|
|
1331
|
+
|
|
1332
|
+
duplicates = _semantic_duplicate_metrics(
|
|
1333
|
+
[
|
|
1334
|
+
(str(row.get("id") or f"learning-{index}"), f"{row.get('title', '')} {row.get('content', '')}")
|
|
1335
|
+
for index, row in enumerate(active_rows, 1)
|
|
1336
|
+
],
|
|
1337
|
+
threshold=0.8,
|
|
1338
|
+
)
|
|
1339
|
+
noise_pressure = weak_active + duplicates["duplicate_excess"]
|
|
1340
|
+
return {
|
|
1341
|
+
"active_learnings": len(active_rows),
|
|
1342
|
+
"weak_active_learnings": weak_active,
|
|
1343
|
+
"duplicate_clusters": duplicates["cluster_count"],
|
|
1344
|
+
"duplicate_active_learnings": duplicates["duplicate_excess"],
|
|
1345
|
+
"noise_pressure": noise_pressure,
|
|
1346
|
+
"noise_rate_pct": _safe_pct(noise_pressure, len(active_rows)),
|
|
1347
|
+
"sample_clusters": duplicates["sample_clusters"],
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _load_previous_period_summary(kind: str, label: str) -> dict | None:
|
|
1352
|
+
pattern = f"*-{kind}-summary.json"
|
|
1353
|
+
candidates: list[tuple[str, Path]] = []
|
|
1354
|
+
for path in DEEP_SLEEP_DIR.glob(pattern):
|
|
1355
|
+
try:
|
|
1356
|
+
payload = json.loads(path.read_text())
|
|
1357
|
+
except Exception:
|
|
1358
|
+
continue
|
|
1359
|
+
candidate_label = str(payload.get("label", "") or "")
|
|
1360
|
+
if candidate_label and candidate_label < label:
|
|
1361
|
+
candidates.append((candidate_label, path))
|
|
1362
|
+
if not candidates:
|
|
1363
|
+
return None
|
|
1364
|
+
_, path = sorted(candidates, key=lambda item: item[0])[-1]
|
|
1365
|
+
try:
|
|
1366
|
+
payload = json.loads(path.read_text())
|
|
1367
|
+
except Exception:
|
|
1368
|
+
return None
|
|
1369
|
+
return payload if isinstance(payload, dict) else None
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def _build_project_pulse(top_projects: list[dict], previous_summary: dict | None) -> list[dict]:
|
|
1373
|
+
previous_scores: dict[str, float] = {}
|
|
1374
|
+
if previous_summary:
|
|
1375
|
+
for item in previous_summary.get("project_pulse", []) or previous_summary.get("top_projects", []) or []:
|
|
1376
|
+
project = str(item.get("project", "") or "")
|
|
1377
|
+
if project:
|
|
1378
|
+
previous_scores[project] = float(item.get("score", 0) or 0)
|
|
1379
|
+
|
|
1380
|
+
pulse: list[dict] = []
|
|
1381
|
+
for item in top_projects:
|
|
1382
|
+
project = str(item.get("project", "") or "")
|
|
1383
|
+
score = float(item.get("score", 0) or 0)
|
|
1384
|
+
previous_score = previous_scores.get(project, 0.0)
|
|
1385
|
+
delta = round(score - previous_score, 2)
|
|
1386
|
+
if score >= 18:
|
|
1387
|
+
status = "critical"
|
|
1388
|
+
elif score >= 10:
|
|
1389
|
+
status = "elevated"
|
|
1390
|
+
else:
|
|
1391
|
+
status = "watch"
|
|
1392
|
+
if delta >= 2.0:
|
|
1393
|
+
trend = "rising"
|
|
1394
|
+
elif delta <= -2.0:
|
|
1395
|
+
trend = "cooling"
|
|
1396
|
+
else:
|
|
1397
|
+
trend = "steady"
|
|
1398
|
+
pulse.append(
|
|
1399
|
+
{
|
|
1400
|
+
"project": project,
|
|
1401
|
+
"score": round(score, 2),
|
|
1402
|
+
"delta_vs_previous": delta,
|
|
1403
|
+
"trend": trend,
|
|
1404
|
+
"status": status,
|
|
1405
|
+
"signals": item.get("signals", {}),
|
|
1406
|
+
"reasons": item.get("reasons", []),
|
|
1407
|
+
}
|
|
1408
|
+
)
|
|
1409
|
+
return pulse
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _build_period_trend(summary: dict, previous_summary: dict | None) -> dict:
|
|
1413
|
+
if not previous_summary:
|
|
1414
|
+
return {
|
|
1415
|
+
"has_previous": False,
|
|
1416
|
+
"avg_mood_delta": None,
|
|
1417
|
+
"avg_trust_delta": None,
|
|
1418
|
+
"total_corrections_delta": None,
|
|
1419
|
+
"protocol_compliance_delta": None,
|
|
1420
|
+
"followup_duplicate_open_delta": None,
|
|
1421
|
+
"followup_duplicate_rate_delta": None,
|
|
1422
|
+
"learning_noise_delta": None,
|
|
1423
|
+
"learning_noise_rate_delta": None,
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
current_protocol = summary.get("protocol_summary", {}).get("overall_compliance_pct")
|
|
1427
|
+
previous_protocol = (previous_summary.get("protocol_summary") or {}).get("overall_compliance_pct")
|
|
1428
|
+
current_mood = summary.get("avg_mood_score")
|
|
1429
|
+
previous_mood = previous_summary.get("avg_mood_score")
|
|
1430
|
+
current_trust = summary.get("avg_trust_score")
|
|
1431
|
+
previous_trust = previous_summary.get("avg_trust_score")
|
|
1432
|
+
current_followup = (summary.get("followup_deduplication") or {}).get("duplicate_open_followups")
|
|
1433
|
+
previous_followup = (previous_summary.get("followup_deduplication") or {}).get("duplicate_open_followups")
|
|
1434
|
+
current_followup_rate = (summary.get("followup_deduplication") or {}).get("duplicate_rate_pct")
|
|
1435
|
+
previous_followup_rate = (previous_summary.get("followup_deduplication") or {}).get("duplicate_rate_pct")
|
|
1436
|
+
current_learning_noise = (summary.get("learning_consolidation") or {}).get("noise_pressure")
|
|
1437
|
+
previous_learning_noise = (previous_summary.get("learning_consolidation") or {}).get("noise_pressure")
|
|
1438
|
+
current_learning_rate = (summary.get("learning_consolidation") or {}).get("noise_rate_pct")
|
|
1439
|
+
previous_learning_rate = (previous_summary.get("learning_consolidation") or {}).get("noise_rate_pct")
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
"has_previous": True,
|
|
1443
|
+
"avg_mood_delta": round(current_mood - previous_mood, 3) if isinstance(current_mood, (int, float)) and isinstance(previous_mood, (int, float)) else None,
|
|
1444
|
+
"avg_trust_delta": round(current_trust - previous_trust, 1) if isinstance(current_trust, (int, float)) and isinstance(previous_trust, (int, float)) else None,
|
|
1445
|
+
"total_corrections_delta": int(summary.get("total_corrections", 0) or 0) - int(previous_summary.get("total_corrections", 0) or 0),
|
|
1446
|
+
"protocol_compliance_delta": round(current_protocol - previous_protocol, 1) if isinstance(current_protocol, (int, float)) and isinstance(previous_protocol, (int, float)) else None,
|
|
1447
|
+
"followup_duplicate_open_delta": int(current_followup or 0) - int(previous_followup or 0) if current_followup is not None or previous_followup is not None else None,
|
|
1448
|
+
"followup_duplicate_rate_delta": round(float(current_followup_rate) - float(previous_followup_rate), 1) if isinstance(current_followup_rate, (int, float)) and isinstance(previous_followup_rate, (int, float)) else None,
|
|
1449
|
+
"learning_noise_delta": int(current_learning_noise or 0) - int(previous_learning_noise or 0) if current_learning_noise is not None or previous_learning_noise is not None else None,
|
|
1450
|
+
"learning_noise_rate_delta": round(float(current_learning_rate) - float(previous_learning_rate), 1) if isinstance(current_learning_rate, (int, float)) and isinstance(previous_learning_rate, (int, float)) else None,
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, window_days: int) -> dict:
|
|
1455
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
1456
|
+
window_start = (target_day - timedelta(days=max(0, window_days - 1))).strftime("%Y-%m-%d")
|
|
1457
|
+
label = (
|
|
1458
|
+
f"{target_day.isocalendar().year}-W{target_day.isocalendar().week:02d}"
|
|
1459
|
+
if kind == "weekly"
|
|
1460
|
+
else target_day.strftime("%Y-%m")
|
|
1461
|
+
)
|
|
1462
|
+
syntheses = _load_period_syntheses(target_date, window_days=window_days)
|
|
1463
|
+
extractions = _load_period_extractions(target_date, window_days=window_days)
|
|
1464
|
+
applied_logs = _load_period_applied_logs(target_date, window_days=window_days)
|
|
1465
|
+
if not any(item.get("date") == target_date for item in syntheses):
|
|
1466
|
+
syntheses.append(synthesis)
|
|
1467
|
+
|
|
1468
|
+
mood_scores = []
|
|
1469
|
+
trust_scores = []
|
|
1470
|
+
total_corrections = 0
|
|
1471
|
+
pattern_counter: Counter[str] = Counter()
|
|
1472
|
+
agenda_counter: Counter[str] = Counter()
|
|
1473
|
+
for item in syntheses:
|
|
1474
|
+
mood = item.get("emotional_day", {}).get("mood_score")
|
|
1475
|
+
if isinstance(mood, (int, float)):
|
|
1476
|
+
mood_scores.append(float(mood))
|
|
1477
|
+
trust = item.get("trust_calibration", {}).get("score")
|
|
1478
|
+
if isinstance(trust, (int, float)):
|
|
1479
|
+
trust_scores.append(float(trust))
|
|
1480
|
+
total_corrections += int(item.get("productivity_day", {}).get("total_corrections", 0) or 0)
|
|
1481
|
+
for pattern in item.get("cross_session_patterns", []) or []:
|
|
1482
|
+
text = str(pattern.get("pattern", "") or "").strip()
|
|
1483
|
+
if text:
|
|
1484
|
+
pattern_counter[text] += 1
|
|
1485
|
+
for agenda in item.get("morning_agenda", []) or []:
|
|
1486
|
+
title = str(agenda.get("title", "") or "").strip()
|
|
1487
|
+
if title:
|
|
1488
|
+
agenda_counter[title] += 1
|
|
1489
|
+
|
|
1490
|
+
top_projects = _project_weighting_window(target_date, window_days=window_days)
|
|
1491
|
+
avg_mood = round(sum(mood_scores) / len(mood_scores), 3) if mood_scores else None
|
|
1492
|
+
avg_trust = round(sum(trust_scores) / len(trust_scores), 1) if trust_scores else None
|
|
1493
|
+
top_patterns = [
|
|
1494
|
+
{"pattern": pattern, "count": count}
|
|
1495
|
+
for pattern, count in pattern_counter.most_common(6)
|
|
1496
|
+
]
|
|
1497
|
+
recurring_agenda = [
|
|
1498
|
+
{"title": title, "count": count}
|
|
1499
|
+
for title, count in agenda_counter.most_common(6)
|
|
1500
|
+
]
|
|
1501
|
+
protocol_summary = _aggregate_protocol_summary(extractions)
|
|
1502
|
+
delivery_metrics = _aggregate_delivery_metrics(applied_logs)
|
|
1503
|
+
followup_deduplication = _followup_deduplication_metrics()
|
|
1504
|
+
learning_consolidation = _learning_consolidation_metrics()
|
|
1505
|
+
previous_summary = _load_previous_period_summary(kind, label)
|
|
1506
|
+
project_pulse = _build_project_pulse(top_projects, previous_summary)
|
|
1507
|
+
|
|
1508
|
+
summary_parts = [f"{len(syntheses)} Deep Sleep run(s)"]
|
|
1509
|
+
if top_projects:
|
|
1510
|
+
summary_parts.append(f"top focus: {top_projects[0]['project']}")
|
|
1511
|
+
if top_patterns:
|
|
1512
|
+
summary_parts.append(f"recurring pattern: {top_patterns[0]['pattern']}")
|
|
1513
|
+
if avg_trust is not None:
|
|
1514
|
+
summary_parts.append(f"avg trust {avg_trust:.1f}")
|
|
1515
|
+
if protocol_summary.get("overall_compliance_pct") is not None:
|
|
1516
|
+
summary_parts.append(f"protocol {protocol_summary['overall_compliance_pct']:.1f}%")
|
|
1517
|
+
summary = " | ".join(summary_parts)
|
|
1518
|
+
|
|
1519
|
+
period_summary = {
|
|
1520
|
+
"kind": kind,
|
|
1521
|
+
"label": label,
|
|
1522
|
+
"window_days": window_days,
|
|
1523
|
+
"window_start": window_start,
|
|
1524
|
+
"window_end": target_date,
|
|
1525
|
+
"generated_at": datetime.now().isoformat(),
|
|
1526
|
+
"daily_syntheses": len(syntheses),
|
|
1527
|
+
"avg_mood_score": avg_mood,
|
|
1528
|
+
"avg_trust_score": avg_trust,
|
|
1529
|
+
"total_corrections": total_corrections,
|
|
1530
|
+
"top_projects": top_projects,
|
|
1531
|
+
"project_pulse": project_pulse,
|
|
1532
|
+
"top_patterns": top_patterns,
|
|
1533
|
+
"recurring_agenda": recurring_agenda,
|
|
1534
|
+
"protocol_summary": protocol_summary,
|
|
1535
|
+
"delivery_metrics": delivery_metrics,
|
|
1536
|
+
"followup_deduplication": followup_deduplication,
|
|
1537
|
+
"learning_consolidation": learning_consolidation,
|
|
1538
|
+
"summary": summary,
|
|
1539
|
+
}
|
|
1540
|
+
period_summary["trend"] = _build_period_trend(period_summary, previous_summary)
|
|
1541
|
+
return period_summary
|
|
1542
|
+
|
|
1543
|
+
def _render_period_summary_markdown(summary: dict) -> str:
|
|
1544
|
+
lines = [
|
|
1545
|
+
f"# {summary.get('kind', 'period').title()} Deep Sleep Summary — {summary.get('label', '')}",
|
|
1546
|
+
"",
|
|
1547
|
+
f"- Window: {summary.get('window_start', '')} -> {summary.get('window_end', '')}",
|
|
1548
|
+
f"- Deep Sleep runs: {summary.get('daily_syntheses', 0)}",
|
|
1549
|
+
]
|
|
1550
|
+
if summary.get("avg_mood_score") is not None:
|
|
1551
|
+
lines.append(f"- Avg mood score: {summary['avg_mood_score']:.2f}")
|
|
1552
|
+
if summary.get("avg_trust_score") is not None:
|
|
1553
|
+
lines.append(f"- Avg trust score: {summary['avg_trust_score']:.1f}")
|
|
1554
|
+
lines.append(f"- Total corrections: {summary.get('total_corrections', 0)}")
|
|
1555
|
+
lines.append("")
|
|
1556
|
+
if summary.get("summary"):
|
|
1557
|
+
lines.append(f"> {summary['summary']}")
|
|
1558
|
+
lines.append("")
|
|
1559
|
+
|
|
1560
|
+
protocol_summary = summary.get("protocol_summary") or {}
|
|
1561
|
+
if protocol_summary:
|
|
1562
|
+
lines.append("## Protocol Compliance")
|
|
1563
|
+
lines.append("")
|
|
1564
|
+
overall = protocol_summary.get("overall_compliance_pct")
|
|
1565
|
+
if overall is not None:
|
|
1566
|
+
lines.append(f"- Overall compliance: {overall:.1f}%")
|
|
1567
|
+
guard = protocol_summary.get("guard_check", {})
|
|
1568
|
+
heartbeat = protocol_summary.get("heartbeat", {})
|
|
1569
|
+
change_log = protocol_summary.get("change_log", {})
|
|
1570
|
+
if guard:
|
|
1571
|
+
lines.append(
|
|
1572
|
+
f"- guard_check: {guard.get('executed', 0)}/{guard.get('required', 0)}"
|
|
1573
|
+
+ (f" ({guard['compliance_pct']:.1f}%)" if guard.get("compliance_pct") is not None else "")
|
|
1574
|
+
)
|
|
1575
|
+
if heartbeat:
|
|
1576
|
+
lines.append(
|
|
1577
|
+
f"- heartbeat with context: {heartbeat.get('with_context', 0)}/{heartbeat.get('total', 0)}"
|
|
1578
|
+
+ (f" ({heartbeat['compliance_pct']:.1f}%)" if heartbeat.get("compliance_pct") is not None else "")
|
|
1579
|
+
)
|
|
1580
|
+
if change_log:
|
|
1581
|
+
lines.append(
|
|
1582
|
+
f"- change_log after edits: {change_log.get('logged', 0)}/{change_log.get('edits', 0)}"
|
|
1583
|
+
+ (f" ({change_log['compliance_pct']:.1f}%)" if change_log.get("compliance_pct") is not None else "")
|
|
1584
|
+
)
|
|
1585
|
+
lines.append("")
|
|
1586
|
+
|
|
1587
|
+
delivery_metrics = summary.get("delivery_metrics") or {}
|
|
1588
|
+
if delivery_metrics:
|
|
1589
|
+
lines.append("## Loop Output")
|
|
1590
|
+
lines.append("")
|
|
1591
|
+
lines.append(f"- Applied actions: {delivery_metrics.get('applied_actions', 0)}")
|
|
1592
|
+
lines.append(f"- Deferred actions: {delivery_metrics.get('deferred_actions', 0)}")
|
|
1593
|
+
lines.append(f"- Dedupe skips: {delivery_metrics.get('skipped_dedupe', 0)}")
|
|
1594
|
+
lines.append(f"- Engineering followups: {delivery_metrics.get('engineering_followups', 0)}")
|
|
1595
|
+
if delivery_metrics.get("dedupe_rate_pct") is not None:
|
|
1596
|
+
lines.append(f"- Dedupe rate: {delivery_metrics['dedupe_rate_pct']:.1f}%")
|
|
1597
|
+
if delivery_metrics.get("error_rate_pct") is not None:
|
|
1598
|
+
lines.append(f"- Error rate: {delivery_metrics['error_rate_pct']:.1f}%")
|
|
1599
|
+
lines.append(f"- Followup dedupe matches: {delivery_metrics.get('followup_dedupe_matches', 0)}")
|
|
1600
|
+
lines.append(f"- Learning reinforcements: {delivery_metrics.get('learning_reinforcements', 0)}")
|
|
1601
|
+
lines.append(f"- Learning duplicate skips: {delivery_metrics.get('learning_duplicate_skips', 0)}")
|
|
1602
|
+
lines.append(f"- Learning contradiction reviews: {delivery_metrics.get('learning_contradiction_reviews', 0)}")
|
|
1603
|
+
lines.append("")
|
|
1604
|
+
|
|
1605
|
+
followup_deduplication = summary.get("followup_deduplication") or {}
|
|
1606
|
+
learning_consolidation = summary.get("learning_consolidation") or {}
|
|
1607
|
+
if followup_deduplication or learning_consolidation:
|
|
1608
|
+
lines.append("## Prevention Quality")
|
|
1609
|
+
lines.append("")
|
|
1610
|
+
if followup_deduplication:
|
|
1611
|
+
lines.append(f"- Open followups: {followup_deduplication.get('open_followups', 0)}")
|
|
1612
|
+
lines.append(f"- Duplicate open followups: {followup_deduplication.get('duplicate_open_followups', 0)}")
|
|
1613
|
+
if followup_deduplication.get("duplicate_rate_pct") is not None:
|
|
1614
|
+
lines.append(f"- Duplicate followup rate: {followup_deduplication['duplicate_rate_pct']:.1f}%")
|
|
1615
|
+
if learning_consolidation:
|
|
1616
|
+
lines.append(f"- Active learnings: {learning_consolidation.get('active_learnings', 0)}")
|
|
1617
|
+
lines.append(f"- Learning noise pressure: {learning_consolidation.get('noise_pressure', 0)}")
|
|
1618
|
+
if learning_consolidation.get("noise_rate_pct") is not None:
|
|
1619
|
+
lines.append(f"- Learning noise rate: {learning_consolidation['noise_rate_pct']:.1f}%")
|
|
1620
|
+
lines.append("")
|
|
1621
|
+
|
|
1622
|
+
if summary.get("top_projects"):
|
|
1623
|
+
lines.append("## Top Projects")
|
|
1624
|
+
lines.append("")
|
|
1625
|
+
for item in summary["top_projects"][:5]:
|
|
1626
|
+
lines.append(f"- **{item['project']}** — score {item['score']}")
|
|
1627
|
+
if item.get("reasons"):
|
|
1628
|
+
lines.append(f" Reasons: {', '.join(item['reasons'])}")
|
|
1629
|
+
lines.append("")
|
|
1630
|
+
|
|
1631
|
+
if summary.get("project_pulse"):
|
|
1632
|
+
lines.append("## Project Pulse")
|
|
1633
|
+
lines.append("")
|
|
1634
|
+
for item in summary["project_pulse"][:5]:
|
|
1635
|
+
delta = item.get("delta_vs_previous")
|
|
1636
|
+
delta_label = ""
|
|
1637
|
+
if isinstance(delta, (int, float)):
|
|
1638
|
+
delta_label = f" | Δ {delta:+.2f}"
|
|
1639
|
+
lines.append(
|
|
1640
|
+
f"- **{item['project']}** — {item.get('status', 'watch')} / {item.get('trend', 'steady')}"
|
|
1641
|
+
f" | score {item.get('score', 0)}{delta_label}"
|
|
1642
|
+
)
|
|
1643
|
+
lines.append("")
|
|
1644
|
+
|
|
1645
|
+
if summary.get("top_patterns"):
|
|
1646
|
+
lines.append("## Recurring Patterns")
|
|
1647
|
+
lines.append("")
|
|
1648
|
+
for item in summary["top_patterns"][:5]:
|
|
1649
|
+
lines.append(f"- {item['pattern']} ({item['count']}x)")
|
|
1650
|
+
lines.append("")
|
|
1651
|
+
|
|
1652
|
+
if summary.get("recurring_agenda"):
|
|
1653
|
+
lines.append("## Recurring Agenda")
|
|
1654
|
+
lines.append("")
|
|
1655
|
+
for item in summary["recurring_agenda"][:5]:
|
|
1656
|
+
lines.append(f"- {item['title']} ({item['count']}x)")
|
|
1657
|
+
lines.append("")
|
|
1658
|
+
|
|
1659
|
+
trend = summary.get("trend") or {}
|
|
1660
|
+
if trend.get("has_previous"):
|
|
1661
|
+
lines.append("## Trend vs Previous")
|
|
1662
|
+
lines.append("")
|
|
1663
|
+
if trend.get("avg_mood_delta") is not None:
|
|
1664
|
+
lines.append(f"- Mood delta: {trend['avg_mood_delta']:+.3f}")
|
|
1665
|
+
if trend.get("avg_trust_delta") is not None:
|
|
1666
|
+
lines.append(f"- Trust delta: {trend['avg_trust_delta']:+.1f}")
|
|
1667
|
+
if trend.get("total_corrections_delta") is not None:
|
|
1668
|
+
lines.append(f"- Corrections delta: {trend['total_corrections_delta']:+d}")
|
|
1669
|
+
if trend.get("protocol_compliance_delta") is not None:
|
|
1670
|
+
lines.append(f"- Protocol delta: {trend['protocol_compliance_delta']:+.1f}%")
|
|
1671
|
+
if trend.get("followup_duplicate_open_delta") is not None:
|
|
1672
|
+
lines.append(f"- Duplicate followups delta: {trend['followup_duplicate_open_delta']:+d}")
|
|
1673
|
+
if trend.get("followup_duplicate_rate_delta") is not None:
|
|
1674
|
+
lines.append(f"- Duplicate followup rate delta: {trend['followup_duplicate_rate_delta']:+.1f}%")
|
|
1675
|
+
if trend.get("learning_noise_delta") is not None:
|
|
1676
|
+
lines.append(f"- Learning noise delta: {trend['learning_noise_delta']:+d}")
|
|
1677
|
+
if trend.get("learning_noise_rate_delta") is not None:
|
|
1678
|
+
lines.append(f"- Learning noise rate delta: {trend['learning_noise_rate_delta']:+.1f}%")
|
|
1679
|
+
lines.append("")
|
|
1680
|
+
|
|
1681
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def write_periodic_summaries(target_date: str, synthesis: dict) -> dict:
|
|
1685
|
+
outputs: dict[str, str] = {}
|
|
1686
|
+
for kind, window_days in (("weekly", 7), ("monthly", 30)):
|
|
1687
|
+
summary = _build_period_summary(target_date, synthesis, kind=kind, window_days=window_days)
|
|
1688
|
+
label = summary["label"]
|
|
1689
|
+
json_path = DEEP_SLEEP_DIR / f"{label}-{kind}-summary.json"
|
|
1690
|
+
md_path = DEEP_SLEEP_DIR / f"{label}-{kind}-summary.md"
|
|
1691
|
+
json_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
1692
|
+
md_path.write_text(_render_period_summary_markdown(summary), encoding="utf-8")
|
|
1693
|
+
outputs[f"{kind}_json"] = str(json_path)
|
|
1694
|
+
outputs[f"{kind}_markdown"] = str(md_path)
|
|
1695
|
+
return outputs
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def generate_session_tone(synthesis: dict, target_date: str) -> dict:
|
|
1699
|
+
"""Generate emotional tone guidance for next session startup.
|
|
1700
|
+
|
|
1701
|
+
This is the 'psychology' layer — tells NEXO how to behave emotionally
|
|
1702
|
+
based on yesterday's analysis. Read by startup hook to adapt greeting.
|
|
1703
|
+
"""
|
|
1704
|
+
emotional = synthesis.get("emotional_day", {})
|
|
1705
|
+
productivity = synthesis.get("productivity_day", {})
|
|
1706
|
+
patterns = synthesis.get("cross_session_patterns", [])
|
|
1707
|
+
abandoned = synthesis.get("abandoned_projects", [])
|
|
1708
|
+
mood_score = emotional.get("mood_score", 0.5)
|
|
1709
|
+
corrections = productivity.get("total_corrections", 0)
|
|
1710
|
+
proactivity = productivity.get("overall_proactivity", "mixed")
|
|
1711
|
+
|
|
1712
|
+
tone = {
|
|
1713
|
+
"date": target_date,
|
|
1714
|
+
"mood_yesterday": mood_score,
|
|
1715
|
+
"approach": "neutral",
|
|
1716
|
+
"opening_style": "normal",
|
|
1717
|
+
"acknowledge_mistakes": False,
|
|
1718
|
+
"mistakes_to_own": [],
|
|
1719
|
+
"motivational": False,
|
|
1720
|
+
"reduce_load": False,
|
|
1721
|
+
"suggested_greeting_context": "",
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
# Agent made many mistakes yesterday → own it, apologize, show learning
|
|
1725
|
+
if corrections > 5:
|
|
1726
|
+
tone["acknowledge_mistakes"] = True
|
|
1727
|
+
tone["opening_style"] = "humble"
|
|
1728
|
+
# Collect what went wrong
|
|
1729
|
+
high_patterns = [p["pattern"] for p in patterns if p.get("severity") == "high"]
|
|
1730
|
+
tone["mistakes_to_own"] = high_patterns[:3]
|
|
1731
|
+
tone["suggested_greeting_context"] = (
|
|
1732
|
+
f"Yesterday the agent needed {corrections} corrections. "
|
|
1733
|
+
f"Acknowledge specific mistakes, show what was learned, "
|
|
1734
|
+
f"and demonstrate improvement from the first interaction."
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
# User had a bad day → supportive, less pressure
|
|
1738
|
+
if mood_score < 0.4:
|
|
1739
|
+
tone["approach"] = "supportive"
|
|
1740
|
+
tone["motivational"] = True
|
|
1741
|
+
tone["reduce_load"] = True
|
|
1742
|
+
frustration_triggers = emotional.get("recurring_triggers", {}).get("frustration", [])
|
|
1743
|
+
tone["suggested_greeting_context"] += (
|
|
1744
|
+
f" User had a tough day (mood {mood_score:.0%}). "
|
|
1745
|
+
f"Be supportive, acknowledge the difficulty, and propose a lighter start. "
|
|
1746
|
+
f"Avoid these frustration triggers: {', '.join(frustration_triggers[:3])}."
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
# User had a great day → reinforce, push momentum
|
|
1750
|
+
elif mood_score > 0.7:
|
|
1751
|
+
tone["approach"] = "energetic"
|
|
1752
|
+
tone["motivational"] = True
|
|
1753
|
+
flow_triggers = emotional.get("recurring_triggers", {}).get("flow", [])
|
|
1754
|
+
tone["suggested_greeting_context"] += (
|
|
1755
|
+
f" User had a great day (mood {mood_score:.0%}). "
|
|
1756
|
+
f"Reinforce the momentum. Reference yesterday's wins. "
|
|
1757
|
+
f"Propose ambitious next steps. Flow triggers: {', '.join(flow_triggers[:3])}."
|
|
1758
|
+
)
|
|
1759
|
+
|
|
1760
|
+
# Agent was too reactive → be proactive today
|
|
1761
|
+
if proactivity == "reactive":
|
|
1762
|
+
tone["approach"] = "proactive"
|
|
1763
|
+
tone["suggested_greeting_context"] += (
|
|
1764
|
+
" Agent was too reactive yesterday — today lead with proposals, "
|
|
1765
|
+
"don't wait for instructions."
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
# There are abandoned projects → gently bring up
|
|
1769
|
+
if abandoned:
|
|
1770
|
+
truly_abandoned = [a for a in abandoned if not a.get("has_followup")]
|
|
1771
|
+
if truly_abandoned:
|
|
1772
|
+
tone["suggested_greeting_context"] += (
|
|
1773
|
+
f" {len(truly_abandoned)} project(s) were started but not finished. "
|
|
1774
|
+
f"Offer to pick them up today without pressure."
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
return tone
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def write_morning_briefing(target_date: str, synthesis: dict) -> Path:
|
|
1781
|
+
"""Write the morning briefing file from synthesis data."""
|
|
1782
|
+
briefing_dir = OPERATIONS_DIR
|
|
1783
|
+
briefing_dir.mkdir(parents=True, exist_ok=True)
|
|
1784
|
+
briefing_file = briefing_dir / "morning-briefing.md"
|
|
1785
|
+
|
|
1786
|
+
# Generate session tone for startup
|
|
1787
|
+
tone = generate_session_tone(synthesis, target_date)
|
|
1788
|
+
tone_file = briefing_dir / "session-tone.json"
|
|
1789
|
+
tone_file.write_text(json.dumps(tone, indent=2, ensure_ascii=False))
|
|
1790
|
+
|
|
1791
|
+
lines = [
|
|
1792
|
+
f"# Morning Briefing -- {target_date}",
|
|
1793
|
+
f"_Generated by Deep Sleep at {datetime.now().strftime('%H:%M')}_",
|
|
1794
|
+
""
|
|
1795
|
+
]
|
|
1796
|
+
|
|
1797
|
+
# Summary
|
|
1798
|
+
summary = synthesis.get("summary", "")
|
|
1799
|
+
if summary:
|
|
1800
|
+
lines.append(f"> {summary}")
|
|
1801
|
+
lines.append("")
|
|
1802
|
+
|
|
1803
|
+
# Morning agenda
|
|
1804
|
+
agenda = synthesis.get("morning_agenda", [])
|
|
1805
|
+
if agenda:
|
|
1806
|
+
lines.append("## Agenda")
|
|
1807
|
+
lines.append("")
|
|
1808
|
+
for item in agenda:
|
|
1809
|
+
priority = item.get("priority", "?")
|
|
1810
|
+
title = item.get("title", "")
|
|
1811
|
+
desc = item.get("description", "")
|
|
1812
|
+
item_type = item.get("type", "")
|
|
1813
|
+
lines.append(f"### {priority}. {title}")
|
|
1814
|
+
if item_type:
|
|
1815
|
+
lines.append(f"_Type: {item_type}_")
|
|
1816
|
+
lines.append(desc)
|
|
1817
|
+
if item.get("context"):
|
|
1818
|
+
lines.append(f"\n> {item['context']}")
|
|
1819
|
+
lines.append("")
|
|
1820
|
+
|
|
1821
|
+
top_impact = _top_followups_by_impact(limit=5)
|
|
1822
|
+
if top_impact:
|
|
1823
|
+
lines.append("## Top by Impact")
|
|
1824
|
+
lines.append("")
|
|
1825
|
+
for item in top_impact:
|
|
1826
|
+
impact = float(item.get("impact_score") or 0.0)
|
|
1827
|
+
due = item.get("date") or "—"
|
|
1828
|
+
reason = item.get("impact_reasoning") or "impact score persisted without explicit reasoning"
|
|
1829
|
+
lines.append(
|
|
1830
|
+
f"- **{item.get('id', '?')}** [{impact:.1f}] {item.get('description', '')} "
|
|
1831
|
+
f"(due {due}, priority {item.get('priority') or 'medium'})"
|
|
1832
|
+
)
|
|
1833
|
+
lines.append(f" Why: {reason}")
|
|
1834
|
+
lines.append("")
|
|
1835
|
+
|
|
1836
|
+
impact_summary = _read_impact_summary()
|
|
1837
|
+
top_changes = [item for item in (impact_summary.get("top_changes") or []) if abs(float(item.get("delta") or 0.0)) >= 1.0]
|
|
1838
|
+
if top_changes:
|
|
1839
|
+
lines.append("## Impact Queue Changes")
|
|
1840
|
+
lines.append("")
|
|
1841
|
+
for item in top_changes[:5]:
|
|
1842
|
+
delta = float(item.get("delta") or 0.0)
|
|
1843
|
+
direction = "+" if delta >= 0 else ""
|
|
1844
|
+
lines.append(
|
|
1845
|
+
f"- **{item.get('id', '?')}** {direction}{delta:.1f} -> {float(item.get('impact_score') or 0.0):.1f} "
|
|
1846
|
+
f"({item.get('impact_reasoning') or 'score recalculated'})"
|
|
1847
|
+
)
|
|
1848
|
+
lines.append("")
|
|
1849
|
+
|
|
1850
|
+
# Emotional day
|
|
1851
|
+
emotional = synthesis.get("emotional_day", {})
|
|
1852
|
+
if emotional:
|
|
1853
|
+
mood_score = emotional.get("mood_score", 0.5)
|
|
1854
|
+
mood_bar = "🟢" if mood_score >= 0.7 else "🟡" if mood_score >= 0.4 else "🔴"
|
|
1855
|
+
lines.append(f"## Mood {mood_bar} {mood_score:.0%}")
|
|
1856
|
+
lines.append("")
|
|
1857
|
+
if emotional.get("mood_arc"):
|
|
1858
|
+
lines.append(emotional["mood_arc"])
|
|
1859
|
+
triggers = emotional.get("recurring_triggers", {})
|
|
1860
|
+
if triggers.get("frustration"):
|
|
1861
|
+
lines.append(f"**Frustration triggers:** {', '.join(triggers['frustration'])}")
|
|
1862
|
+
if triggers.get("flow"):
|
|
1863
|
+
lines.append(f"**Flow triggers:** {', '.join(triggers['flow'])}")
|
|
1864
|
+
if emotional.get("calibration_recommendation"):
|
|
1865
|
+
lines.append(f"\n💡 **Recommendation:** {emotional['calibration_recommendation']}")
|
|
1866
|
+
lines.append("")
|
|
1867
|
+
|
|
1868
|
+
# Productivity
|
|
1869
|
+
productivity = synthesis.get("productivity_day", {})
|
|
1870
|
+
if productivity:
|
|
1871
|
+
lines.append("## Productivity")
|
|
1872
|
+
lines.append("")
|
|
1873
|
+
lines.append(f"- Corrections needed: {productivity.get('total_corrections', '?')}")
|
|
1874
|
+
lines.append(f"- Proactivity: {productivity.get('overall_proactivity', '?')}")
|
|
1875
|
+
if productivity.get("tool_insights"):
|
|
1876
|
+
lines.append(f"- Tools: {productivity['tool_insights']}")
|
|
1877
|
+
inefficiencies = productivity.get("systemic_inefficiencies", [])
|
|
1878
|
+
if inefficiencies:
|
|
1879
|
+
lines.append(f"- Issues: {', '.join(inefficiencies)}")
|
|
1880
|
+
lines.append("")
|
|
1881
|
+
|
|
1882
|
+
# Abandoned projects
|
|
1883
|
+
abandoned = synthesis.get("abandoned_projects", [])
|
|
1884
|
+
if abandoned:
|
|
1885
|
+
truly_abandoned = [a for a in abandoned if not a.get("has_followup")]
|
|
1886
|
+
if truly_abandoned:
|
|
1887
|
+
lines.append("## Abandoned Projects")
|
|
1888
|
+
lines.append("")
|
|
1889
|
+
for a in truly_abandoned:
|
|
1890
|
+
lines.append(f"- {a.get('description', '?')}")
|
|
1891
|
+
if a.get("recommendation"):
|
|
1892
|
+
lines.append(f" → {a['recommendation']}")
|
|
1893
|
+
lines.append("")
|
|
1894
|
+
|
|
1895
|
+
# Cross-session patterns
|
|
1896
|
+
patterns = synthesis.get("cross_session_patterns", [])
|
|
1897
|
+
if patterns:
|
|
1898
|
+
lines.append("## Patterns Detected")
|
|
1899
|
+
lines.append("")
|
|
1900
|
+
for p in patterns:
|
|
1901
|
+
severity = p.get("severity", "")
|
|
1902
|
+
lines.append(f"- **[{severity}]** {p.get('pattern', '')}")
|
|
1903
|
+
sessions = p.get("sessions", [])
|
|
1904
|
+
if sessions:
|
|
1905
|
+
lines.append(f" Sessions: {', '.join(sessions)}")
|
|
1906
|
+
lines.append("")
|
|
1907
|
+
|
|
1908
|
+
# Draft actions (things that need user decision)
|
|
1909
|
+
draft_actions = [
|
|
1910
|
+
a for a in synthesis.get("actions", [])
|
|
1911
|
+
if a.get("action_class") == "draft_for_morning"
|
|
1912
|
+
]
|
|
1913
|
+
if draft_actions:
|
|
1914
|
+
lines.append("## Items for Review")
|
|
1915
|
+
lines.append("")
|
|
1916
|
+
for a in draft_actions:
|
|
1917
|
+
confidence = a.get("confidence", 0)
|
|
1918
|
+
lines.append(f"- **{a.get('action_type', '')}** (confidence: {confidence:.0%})")
|
|
1919
|
+
content = a.get("content", {})
|
|
1920
|
+
if isinstance(content, dict):
|
|
1921
|
+
title = content.get("title", content.get("description", ""))
|
|
1922
|
+
lines.append(f" {title}")
|
|
1923
|
+
evidence = a.get("evidence", [])
|
|
1924
|
+
if evidence and isinstance(evidence, list):
|
|
1925
|
+
for ev in evidence[:2]:
|
|
1926
|
+
quote = ev.get("quote", "")
|
|
1927
|
+
if quote:
|
|
1928
|
+
lines.append(f' > "{quote}"')
|
|
1929
|
+
lines.append("")
|
|
1930
|
+
|
|
1931
|
+
# Context packets
|
|
1932
|
+
packets = synthesis.get("context_packets", [])
|
|
1933
|
+
if packets:
|
|
1934
|
+
lines.append("## Context for Today's Work")
|
|
1935
|
+
lines.append("")
|
|
1936
|
+
for p in packets:
|
|
1937
|
+
lines.append(f"### {p.get('topic', 'Unknown')}")
|
|
1938
|
+
lines.append(f"**Last state:** {p.get('last_state', 'N/A')}")
|
|
1939
|
+
files = p.get("key_files", [])
|
|
1940
|
+
if files:
|
|
1941
|
+
lines.append(f"**Files:** {', '.join(files)}")
|
|
1942
|
+
questions = p.get("open_questions", [])
|
|
1943
|
+
if questions:
|
|
1944
|
+
lines.append("**Open questions:**")
|
|
1945
|
+
for q in questions:
|
|
1946
|
+
lines.append(f" - {q}")
|
|
1947
|
+
lines.append("")
|
|
1948
|
+
|
|
1949
|
+
briefing_file.write_text("\n".join(lines), encoding="utf-8")
|
|
1950
|
+
return briefing_file
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def apply_action(action: dict, run_id: str) -> dict:
|
|
1954
|
+
"""Apply a single action and return the result log."""
|
|
1955
|
+
action_type = action.get("action_type", "")
|
|
1956
|
+
action_class = action.get("action_class", "")
|
|
1957
|
+
content = action.get("content", {})
|
|
1958
|
+
dedupe_key = action.get("dedupe_key", "")
|
|
1959
|
+
|
|
1960
|
+
# Content fingerprint, not security-sensitive.
|
|
1961
|
+
applied_id = f"{run_id}-{hashlib.md5(dedupe_key.encode(), usedforsecurity=False).hexdigest()[:8]}"
|
|
1962
|
+
|
|
1963
|
+
log_entry = {
|
|
1964
|
+
"applied_action_id": applied_id,
|
|
1965
|
+
"action_type": action_type,
|
|
1966
|
+
"action_class": action_class,
|
|
1967
|
+
"dedupe_key": dedupe_key,
|
|
1968
|
+
"timestamp": datetime.now().isoformat(),
|
|
1969
|
+
"status": "skipped",
|
|
1970
|
+
"details": {}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
# Only auto_apply actions get executed
|
|
1974
|
+
if action_class != "auto_apply":
|
|
1975
|
+
log_entry["status"] = "deferred_to_morning"
|
|
1976
|
+
log_entry["details"] = {"reason": "action_class is not auto_apply"}
|
|
1977
|
+
return log_entry
|
|
1978
|
+
|
|
1979
|
+
if not isinstance(content, dict):
|
|
1980
|
+
log_entry["status"] = "error"
|
|
1981
|
+
log_entry["details"] = {"error": "content is not a dict"}
|
|
1982
|
+
return log_entry
|
|
1983
|
+
|
|
1984
|
+
if action_type == "learning_add":
|
|
1985
|
+
result = add_learning(
|
|
1986
|
+
category=content.get("category", "process"),
|
|
1987
|
+
title=content.get("title", "Deep Sleep finding"),
|
|
1988
|
+
content=content.get("content", content.get("description", ""))
|
|
1989
|
+
)
|
|
1990
|
+
log_entry["status"] = "applied" if result.get("success") else "error"
|
|
1991
|
+
log_entry["details"] = result
|
|
1992
|
+
|
|
1993
|
+
elif action_type == "followup_create":
|
|
1994
|
+
result = create_followup(
|
|
1995
|
+
description=content.get("description", content.get("title", "")),
|
|
1996
|
+
date=content.get("date", ""),
|
|
1997
|
+
reasoning_note=content.get("reasoning", content.get("why", "")),
|
|
1998
|
+
)
|
|
1999
|
+
log_entry["status"] = "applied" if result.get("success") else "error"
|
|
2000
|
+
log_entry["details"] = result
|
|
2001
|
+
|
|
2002
|
+
elif action_type == "skill_create":
|
|
2003
|
+
result = create_skill(content)
|
|
2004
|
+
log_entry["status"] = "applied" if result.get("success") else "error"
|
|
2005
|
+
log_entry["details"] = result
|
|
2006
|
+
|
|
2007
|
+
elif action_type == "morning_briefing_item":
|
|
2008
|
+
# These are included in the briefing file, not applied separately
|
|
2009
|
+
log_entry["status"] = "included_in_briefing"
|
|
2010
|
+
|
|
2011
|
+
elif action_type == "code_change":
|
|
2012
|
+
# Closes Fase 2 item 5: deep sleep can now hand a concrete code
|
|
2013
|
+
# change to the evolution apply pipeline. We do NOT touch any source
|
|
2014
|
+
# file directly here — that would bypass the sandbox/snapshot/rollback
|
|
2015
|
+
# safety net that lives in nexo-evolution-run.py:execute_auto_proposal.
|
|
2016
|
+
# Instead we persist the proposal to evolution_log with status=accepted
|
|
2017
|
+
# and a proposal_payload, so the next evolution cycle picks it up via
|
|
2018
|
+
# _apply_accepted_proposals (added in Fase 2 item 1, m38).
|
|
2019
|
+
result = apply_code_change_action(content, dedupe_key)
|
|
2020
|
+
log_entry["status"] = "applied" if result.get("success") else "error"
|
|
2021
|
+
log_entry["details"] = result
|
|
2022
|
+
|
|
2023
|
+
else:
|
|
2024
|
+
log_entry["status"] = "unknown_type"
|
|
2025
|
+
log_entry["details"] = {"error": f"Unknown action_type: {action_type}"}
|
|
2026
|
+
|
|
2027
|
+
return log_entry
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
def apply_code_change_action(content: dict, dedupe_key: str) -> dict:
|
|
2031
|
+
"""Stage a code_change finding into evolution_log for the next cycle.
|
|
2032
|
+
|
|
2033
|
+
Required content keys:
|
|
2034
|
+
dimension: evolution dimension (reliability/safety/etc)
|
|
2035
|
+
action: short human-readable description of what to do
|
|
2036
|
+
reasoning: why deep sleep proposed this change
|
|
2037
|
+
changes: list of {file, operation, search?, content} entries
|
|
2038
|
+
matching nexo-evolution-run.py:apply_change semantics
|
|
2039
|
+
|
|
2040
|
+
Optional:
|
|
2041
|
+
scope: 'local' | 'public' (default 'local')
|
|
2042
|
+
classification: 'propose' | 'auto' (default 'propose')
|
|
2043
|
+
|
|
2044
|
+
Idempotent: if a row with the same dedupe_key already exists in
|
|
2045
|
+
evolution_log (matched against proposal_payload extras.dedupe_key), the
|
|
2046
|
+
function returns success without inserting a duplicate.
|
|
2047
|
+
|
|
2048
|
+
Returns: {success: bool, evolution_log_id: int|None, reason: str|None,
|
|
2049
|
+
skipped_duplicate: bool}
|
|
2050
|
+
"""
|
|
2051
|
+
if not isinstance(content, dict):
|
|
2052
|
+
return {"success": False, "reason": "content must be a dict"}
|
|
2053
|
+
|
|
2054
|
+
dimension = (content.get("dimension") or "").strip()
|
|
2055
|
+
action_text = (content.get("action") or content.get("title") or "").strip()
|
|
2056
|
+
reasoning = (content.get("reasoning") or content.get("why") or "").strip()
|
|
2057
|
+
changes = content.get("changes") or []
|
|
2058
|
+
scope = (content.get("scope") or "local").strip() or "local"
|
|
2059
|
+
classification = (content.get("classification") or "propose").strip() or "propose"
|
|
2060
|
+
|
|
2061
|
+
if not dimension:
|
|
2062
|
+
return {"success": False, "reason": "missing dimension"}
|
|
2063
|
+
if not action_text:
|
|
2064
|
+
return {"success": False, "reason": "missing action"}
|
|
2065
|
+
if not isinstance(changes, list) or not changes:
|
|
2066
|
+
return {"success": False, "reason": "missing or empty changes array"}
|
|
2067
|
+
for idx, change in enumerate(changes):
|
|
2068
|
+
if not isinstance(change, dict):
|
|
2069
|
+
return {"success": False, "reason": f"changes[{idx}] is not a dict"}
|
|
2070
|
+
if not change.get("file"):
|
|
2071
|
+
return {"success": False, "reason": f"changes[{idx}] missing file"}
|
|
2072
|
+
if not change.get("operation"):
|
|
2073
|
+
return {"success": False, "reason": f"changes[{idx}] missing operation"}
|
|
2074
|
+
|
|
2075
|
+
payload = {
|
|
2076
|
+
"classification": classification,
|
|
2077
|
+
"dimension": dimension,
|
|
2078
|
+
"action": action_text,
|
|
2079
|
+
"reasoning": reasoning,
|
|
2080
|
+
"scope": scope,
|
|
2081
|
+
"changes": changes,
|
|
2082
|
+
"extras": {
|
|
2083
|
+
"source": "deep_sleep",
|
|
2084
|
+
"dedupe_key": dedupe_key,
|
|
2085
|
+
},
|
|
2086
|
+
}
|
|
2087
|
+
payload_json = json.dumps(payload, ensure_ascii=False)
|
|
2088
|
+
|
|
2089
|
+
try:
|
|
2090
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
2091
|
+
except Exception as e:
|
|
2092
|
+
return {"success": False, "reason": f"cannot open nexo.db: {e}"}
|
|
2093
|
+
|
|
2094
|
+
try:
|
|
2095
|
+
# Idempotency: skip if any prior row already staged this dedupe_key.
|
|
2096
|
+
existing = conn.execute(
|
|
2097
|
+
"SELECT id FROM evolution_log "
|
|
2098
|
+
"WHERE proposal_payload IS NOT NULL "
|
|
2099
|
+
" AND proposal_payload LIKE ? "
|
|
2100
|
+
"ORDER BY id DESC LIMIT 1",
|
|
2101
|
+
(f'%"dedupe_key": "{dedupe_key}"%',),
|
|
2102
|
+
).fetchone()
|
|
2103
|
+
if existing:
|
|
2104
|
+
return {
|
|
2105
|
+
"success": True,
|
|
2106
|
+
"skipped_duplicate": True,
|
|
2107
|
+
"evolution_log_id": int(existing[0]),
|
|
2108
|
+
"reason": "already staged in evolution_log",
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
cur = conn.execute(
|
|
2112
|
+
"INSERT INTO evolution_log "
|
|
2113
|
+
"(cycle_number, dimension, proposal, classification, reasoning, status, proposal_payload) "
|
|
2114
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
2115
|
+
(
|
|
2116
|
+
0, # cycle_number 0 = staged from outside the regular cycle
|
|
2117
|
+
dimension,
|
|
2118
|
+
action_text,
|
|
2119
|
+
classification,
|
|
2120
|
+
reasoning or "deep sleep proposed code change",
|
|
2121
|
+
"accepted",
|
|
2122
|
+
payload_json,
|
|
2123
|
+
),
|
|
2124
|
+
)
|
|
2125
|
+
try:
|
|
2126
|
+
conn.commit()
|
|
2127
|
+
except Exception:
|
|
2128
|
+
pass
|
|
2129
|
+
return {
|
|
2130
|
+
"success": True,
|
|
2131
|
+
"skipped_duplicate": False,
|
|
2132
|
+
"evolution_log_id": int(cur.lastrowid),
|
|
2133
|
+
}
|
|
2134
|
+
except Exception as e:
|
|
2135
|
+
return {"success": False, "reason": f"insert failed: {e}"}
|
|
2136
|
+
finally:
|
|
2137
|
+
try:
|
|
2138
|
+
conn.close()
|
|
2139
|
+
except Exception:
|
|
2140
|
+
pass
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
def main():
|
|
2144
|
+
target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
2145
|
+
|
|
2146
|
+
synthesis_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
2147
|
+
if not synthesis_file.exists():
|
|
2148
|
+
print(f"[apply] No synthesis file for {target_date}. Run synthesize.py first.")
|
|
2149
|
+
sys.exit(1)
|
|
2150
|
+
|
|
2151
|
+
with open(synthesis_file) as f:
|
|
2152
|
+
synthesis = json.load(f)
|
|
2153
|
+
|
|
2154
|
+
run_id = generate_run_id(target_date)
|
|
2155
|
+
actions = synthesis.get("actions", [])
|
|
2156
|
+
print(f"[apply] Phase 4: Applying findings for {target_date} (run: {run_id})")
|
|
2157
|
+
print(f"[apply] Actions to process: {len(actions)}")
|
|
2158
|
+
|
|
2159
|
+
# Load recent dedupe keys for idempotency
|
|
2160
|
+
existing_keys = load_recent_dedupe_keys(target_date)
|
|
2161
|
+
print(f"[apply] Existing dedupe keys (7d): {len(existing_keys)}")
|
|
2162
|
+
|
|
2163
|
+
# Backup databases before mutations
|
|
2164
|
+
auto_apply_count = sum(1 for a in actions if a.get("action_class") == "auto_apply")
|
|
2165
|
+
if auto_apply_count > 0:
|
|
2166
|
+
print("[apply] Creating database backups...")
|
|
2167
|
+
nexo_backup = backup_db(NEXO_DB, run_id)
|
|
2168
|
+
cog_backup = backup_db(COGNITIVE_DB, run_id)
|
|
2169
|
+
if nexo_backup:
|
|
2170
|
+
print(f" Backup: {nexo_backup}")
|
|
2171
|
+
if cog_backup:
|
|
2172
|
+
print(f" Backup: {cog_backup}")
|
|
2173
|
+
|
|
2174
|
+
# Process actions
|
|
2175
|
+
applied_actions = []
|
|
2176
|
+
stats = {"applied": 0, "deferred": 0, "skipped_dedupe": 0, "errors": 0}
|
|
2177
|
+
|
|
2178
|
+
for action in actions:
|
|
2179
|
+
dedupe_key = action.get("dedupe_key", "")
|
|
2180
|
+
|
|
2181
|
+
# Idempotency check
|
|
2182
|
+
if dedupe_key and dedupe_key in existing_keys:
|
|
2183
|
+
applied_actions.append({
|
|
2184
|
+
"applied_action_id": f"{run_id}-deduped",
|
|
2185
|
+
"action_type": action.get("action_type"),
|
|
2186
|
+
"dedupe_key": dedupe_key,
|
|
2187
|
+
"status": "skipped_dedupe",
|
|
2188
|
+
"timestamp": datetime.now().isoformat()
|
|
2189
|
+
})
|
|
2190
|
+
stats["skipped_dedupe"] += 1
|
|
2191
|
+
continue
|
|
2192
|
+
|
|
2193
|
+
result = apply_action(action, run_id)
|
|
2194
|
+
applied_actions.append(result)
|
|
2195
|
+
|
|
2196
|
+
if result["status"] == "applied":
|
|
2197
|
+
stats["applied"] += 1
|
|
2198
|
+
print(f" Applied: {action.get('action_type')} -- {action.get('content', {}).get('title', '')[:50]}")
|
|
2199
|
+
elif result["status"] == "deferred_to_morning":
|
|
2200
|
+
stats["deferred"] += 1
|
|
2201
|
+
elif result["status"] == "error":
|
|
2202
|
+
stats["errors"] += 1
|
|
2203
|
+
print(f" Error: {result.get('details', {}).get('error', 'unknown')}", file=sys.stderr)
|
|
2204
|
+
|
|
2205
|
+
# Update mood in calibration.json
|
|
2206
|
+
print("[apply] Updating mood/calibration...")
|
|
2207
|
+
mood_result = update_calibration_mood(synthesis)
|
|
2208
|
+
if mood_result.get("success"):
|
|
2209
|
+
stats["applied"] += 1
|
|
2210
|
+
print(f" Mood score: {mood_result.get('mood_score', '?')}")
|
|
2211
|
+
else:
|
|
2212
|
+
print(f" Mood skip: {mood_result.get('error', '?')}")
|
|
2213
|
+
|
|
2214
|
+
# Calibrate trust score (authoritative daily score from Deep Sleep)
|
|
2215
|
+
print("[apply] Calibrating trust score...")
|
|
2216
|
+
trust_result = calibrate_trust_score(synthesis, target_date)
|
|
2217
|
+
if trust_result.get("success"):
|
|
2218
|
+
stats["applied"] += 1
|
|
2219
|
+
print(f" Trust: {trust_result['old_score']:.0f} → {trust_result['new_score']:.0f} (Δ{trust_result['delta']:+.0f}, {trust_result['trend']})")
|
|
2220
|
+
else:
|
|
2221
|
+
print(f" Trust skip: {trust_result.get('error', '?')}")
|
|
2222
|
+
|
|
2223
|
+
# Create skills from synthesis
|
|
2224
|
+
skills_data = synthesis.get("skills", [])
|
|
2225
|
+
if skills_data:
|
|
2226
|
+
print(f"[apply] Creating {len(skills_data)} skill(s)...")
|
|
2227
|
+
for skill_data in skills_data:
|
|
2228
|
+
if skill_data.get("confidence", 0) < 0.7:
|
|
2229
|
+
continue
|
|
2230
|
+
if skill_data.get("merge_with"):
|
|
2231
|
+
print(f" Skip {skill_data.get('id', '?')}: merge candidate (needs runtime merge)")
|
|
2232
|
+
continue
|
|
2233
|
+
result = create_skill(skill_data)
|
|
2234
|
+
if result.get("success"):
|
|
2235
|
+
stats["applied"] += 1
|
|
2236
|
+
print(f" Skill created: {result['id']} — {result.get('name', '')[:50]}")
|
|
2237
|
+
elif "already exists" in result.get("error", ""):
|
|
2238
|
+
stats["skipped_dedupe"] += 1
|
|
2239
|
+
else:
|
|
2240
|
+
stats["errors"] += 1
|
|
2241
|
+
print(f" Skill error: {result.get('error', 'unknown')}", file=sys.stderr)
|
|
2242
|
+
|
|
2243
|
+
evolution_candidates = synthesis.get("skill_evolution_candidates", [])
|
|
2244
|
+
if evolution_candidates:
|
|
2245
|
+
evolution_file = DEEP_SLEEP_DIR / f"{target_date}-skill-evolution-candidates.json"
|
|
2246
|
+
with open(evolution_file, "w") as f:
|
|
2247
|
+
json.dump(evolution_candidates, f, indent=2, ensure_ascii=False)
|
|
2248
|
+
print(f" Skill evolution candidates: {evolution_file}")
|
|
2249
|
+
|
|
2250
|
+
try:
|
|
2251
|
+
from skills_runtime import auto_promote_skill_evolution
|
|
2252
|
+
|
|
2253
|
+
promotion_result = auto_promote_skill_evolution()
|
|
2254
|
+
if promotion_result.get("promoted"):
|
|
2255
|
+
promotion_file = DEEP_SLEEP_DIR / f"{target_date}-skill-autopromotions.json"
|
|
2256
|
+
with open(promotion_file, "w") as f:
|
|
2257
|
+
json.dump(promotion_result, f, indent=2, ensure_ascii=False)
|
|
2258
|
+
stats["applied"] += len(promotion_result["promoted"])
|
|
2259
|
+
print(f" Skill autopromotions: {len(promotion_result['promoted'])} → {promotion_file}")
|
|
2260
|
+
except Exception as e:
|
|
2261
|
+
print(f" Skill autopromotion error: {e}", file=sys.stderr)
|
|
2262
|
+
|
|
2263
|
+
# Apply drive synthesis (investigate/dismiss/promote signals)
|
|
2264
|
+
drive_synthesis = synthesis.get("drive_synthesis", {})
|
|
2265
|
+
if drive_synthesis:
|
|
2266
|
+
print("[apply] Processing drive synthesis...")
|
|
2267
|
+
try:
|
|
2268
|
+
from db import update_drive_signal_status, reinforce_drive_signal
|
|
2269
|
+
for item in drive_synthesis.get("investigated", []):
|
|
2270
|
+
signal_id = item.get("signal_id")
|
|
2271
|
+
action_taken = item.get("action_taken", "acted")
|
|
2272
|
+
outcome = item.get("outcome", item.get("finding", ""))
|
|
2273
|
+
if signal_id and outcome:
|
|
2274
|
+
update_drive_signal_status(signal_id, action_taken, outcome[:500])
|
|
2275
|
+
stats["applied"] += 1
|
|
2276
|
+
print(f" Drive signal #{signal_id}: {action_taken}")
|
|
2277
|
+
for item in drive_synthesis.get("promoted", []):
|
|
2278
|
+
signal_id = item.get("signal_id")
|
|
2279
|
+
reason = item.get("reason", "promoted by Deep Sleep")
|
|
2280
|
+
if signal_id:
|
|
2281
|
+
reinforce_drive_signal(signal_id, f"Deep Sleep promotion: {reason}"[:500])
|
|
2282
|
+
print(f" Drive signal #{signal_id}: promoted")
|
|
2283
|
+
except Exception as e:
|
|
2284
|
+
print(f" Drive synthesis error: {e}", file=sys.stderr)
|
|
2285
|
+
|
|
2286
|
+
# Create followups for abandoned projects
|
|
2287
|
+
abandoned_results = create_abandoned_followups(synthesis)
|
|
2288
|
+
for r in abandoned_results:
|
|
2289
|
+
if r.get("success"):
|
|
2290
|
+
stats["applied"] += 1
|
|
2291
|
+
print(f" Abandoned project followup: {r.get('id')}")
|
|
2292
|
+
|
|
2293
|
+
# Write morning briefing
|
|
2294
|
+
print("[apply] Writing morning briefing...")
|
|
2295
|
+
briefing_path = write_morning_briefing(target_date, synthesis)
|
|
2296
|
+
print(f" Briefing: {briefing_path}")
|
|
2297
|
+
|
|
2298
|
+
print("[apply] Writing weekly/monthly Deep Sleep summaries...")
|
|
2299
|
+
periodic_outputs = write_periodic_summaries(target_date, synthesis)
|
|
2300
|
+
for label, path in periodic_outputs.items():
|
|
2301
|
+
print(f" {label}: {path}")
|
|
2302
|
+
|
|
2303
|
+
# Write applied log
|
|
2304
|
+
applied_log = {
|
|
2305
|
+
"date": target_date,
|
|
2306
|
+
"run_id": run_id,
|
|
2307
|
+
"applied_at": datetime.now().isoformat(),
|
|
2308
|
+
"stats": stats,
|
|
2309
|
+
"applied_actions": applied_actions,
|
|
2310
|
+
"summary": synthesis.get("summary", ""),
|
|
2311
|
+
"periodic_summaries": periodic_outputs,
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
applied_file = DEEP_SLEEP_DIR / f"{target_date}-applied.json"
|
|
2315
|
+
with open(applied_file, "w") as f:
|
|
2316
|
+
json.dump(applied_log, f, indent=2, ensure_ascii=False)
|
|
2317
|
+
|
|
2318
|
+
print(f"\n[apply] Done.")
|
|
2319
|
+
print(f" Applied: {stats['applied']}")
|
|
2320
|
+
print(f" Deferred to morning: {stats['deferred']}")
|
|
2321
|
+
print(f" Skipped (dedupe): {stats['skipped_dedupe']}")
|
|
2322
|
+
print(f" Errors: {stats['errors']}")
|
|
2323
|
+
print(f"[apply] Log: {applied_file}")
|
|
2324
|
+
|
|
2325
|
+
|
|
2326
|
+
if __name__ == "__main__":
|
|
2327
|
+
main()
|