nexo-brain 2.6.21 → 3.0.0
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/README.md +72 -20
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +296 -8
- package/src/cli.py +209 -4
- package/src/client_preferences.py +115 -0
- package/src/client_sync.py +202 -2
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +264 -0
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/dashboard.html +59 -1
- package/src/dashboard/templates/protocol.html +199 -0
- package/src/db/__init__.py +23 -1
- package/src/db/_learnings.py +31 -4
- package/src/db/_personal_scripts.py +12 -0
- package/src/db/_protocol.py +303 -0
- package/src/db/_schema.py +248 -0
- package/src/db/_watchers.py +173 -0
- package/src/db/_workflow.py +952 -0
- package/src/doctor/providers/runtime.py +1095 -3
- package/src/evolution_cycle.py +62 -0
- package/src/hook_guardrails.py +308 -0
- package/src/hooks/protocol-guardrail.sh +10 -0
- package/src/nexo_sdk.py +103 -0
- package/src/plugins/cognitive_memory.py +18 -0
- package/src/plugins/cortex.py +55 -35
- package/src/plugins/guard.py +132 -16
- package/src/plugins/protocol.py +911 -0
- package/src/plugins/schedule.py +40 -6
- package/src/plugins/simple_api.py +103 -0
- package/src/plugins/skills.py +67 -0
- package/src/plugins/state_watchers.py +79 -0
- package/src/plugins/workflow.py +588 -0
- package/src/public_contribution.py +86 -12
- package/src/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +482 -2
- package/src/scripts/deep-sleep/collect.py +49 -4
- package/src/scripts/nexo-agent-run.py +2 -0
- package/src/scripts/nexo-daily-self-audit.py +843 -5
- package/src/scripts/nexo-evolution-run.py +343 -1
- package/src/server.py +92 -6
- package/src/skills_runtime.py +151 -0
- package/src/state_watchers_runtime.py +334 -0
- package/src/tools_learnings.py +345 -7
- package/src/tools_sessions.py +183 -0
- package/templates/CLAUDE.md.template +9 -1
- package/templates/CODEX.AGENTS.md.template +10 -2
|
@@ -992,6 +992,372 @@ def _load_period_syntheses(target_date: str, *, window_days: int) -> list[dict]:
|
|
|
992
992
|
return syntheses
|
|
993
993
|
|
|
994
994
|
|
|
995
|
+
def _load_period_extractions(target_date: str, *, window_days: int) -> list[dict]:
|
|
996
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
997
|
+
payloads: list[dict] = []
|
|
998
|
+
for offset in range(window_days):
|
|
999
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
1000
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-extractions.json"
|
|
1001
|
+
if not path.is_file():
|
|
1002
|
+
continue
|
|
1003
|
+
try:
|
|
1004
|
+
payload = json.loads(path.read_text())
|
|
1005
|
+
except Exception:
|
|
1006
|
+
continue
|
|
1007
|
+
if isinstance(payload, dict):
|
|
1008
|
+
payloads.append(payload)
|
|
1009
|
+
payloads.reverse()
|
|
1010
|
+
return payloads
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _load_period_applied_logs(target_date: str, *, window_days: int) -> list[dict]:
|
|
1014
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
1015
|
+
payloads: list[dict] = []
|
|
1016
|
+
for offset in range(window_days):
|
|
1017
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
1018
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-applied.json"
|
|
1019
|
+
if not path.is_file():
|
|
1020
|
+
continue
|
|
1021
|
+
try:
|
|
1022
|
+
payload = json.loads(path.read_text())
|
|
1023
|
+
except Exception:
|
|
1024
|
+
continue
|
|
1025
|
+
if isinstance(payload, dict):
|
|
1026
|
+
payloads.append(payload)
|
|
1027
|
+
payloads.reverse()
|
|
1028
|
+
return payloads
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _safe_pct(numerator: float, denominator: float) -> float | None:
|
|
1032
|
+
if denominator <= 0:
|
|
1033
|
+
return None
|
|
1034
|
+
return round((numerator / denominator) * 100.0, 1)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _aggregate_protocol_summary(extractions: list[dict]) -> dict:
|
|
1038
|
+
totals = {
|
|
1039
|
+
"sessions": 0,
|
|
1040
|
+
"guard_check": {"required": 0, "executed": 0},
|
|
1041
|
+
"heartbeat": {"total": 0, "with_context": 0},
|
|
1042
|
+
"change_log": {"edits": 0, "logged": 0},
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for payload in extractions:
|
|
1046
|
+
for item in payload.get("extractions", []) or []:
|
|
1047
|
+
if not isinstance(item, dict) or item.get("error"):
|
|
1048
|
+
continue
|
|
1049
|
+
totals["sessions"] += 1
|
|
1050
|
+
protocol_summary = item.get("protocol_summary") or {}
|
|
1051
|
+
for key in ("guard_check", "heartbeat", "change_log"):
|
|
1052
|
+
current = protocol_summary.get(key) or {}
|
|
1053
|
+
if key == "guard_check":
|
|
1054
|
+
totals[key]["required"] += int(current.get("required", 0) or 0)
|
|
1055
|
+
totals[key]["executed"] += int(current.get("executed", 0) or 0)
|
|
1056
|
+
elif key == "heartbeat":
|
|
1057
|
+
totals[key]["total"] += int(current.get("total", 0) or 0)
|
|
1058
|
+
totals[key]["with_context"] += int(current.get("with_context", 0) or 0)
|
|
1059
|
+
else:
|
|
1060
|
+
totals[key]["edits"] += int(current.get("edits", 0) or 0)
|
|
1061
|
+
totals[key]["logged"] += int(current.get("logged", 0) or 0)
|
|
1062
|
+
|
|
1063
|
+
guard_pct = _safe_pct(totals["guard_check"]["executed"], totals["guard_check"]["required"])
|
|
1064
|
+
heartbeat_pct = _safe_pct(totals["heartbeat"]["with_context"], totals["heartbeat"]["total"])
|
|
1065
|
+
change_pct = _safe_pct(totals["change_log"]["logged"], totals["change_log"]["edits"])
|
|
1066
|
+
available = [value for value in (guard_pct, heartbeat_pct, change_pct) if value is not None]
|
|
1067
|
+
|
|
1068
|
+
totals["guard_check"]["compliance_pct"] = guard_pct
|
|
1069
|
+
totals["heartbeat"]["compliance_pct"] = heartbeat_pct
|
|
1070
|
+
totals["change_log"]["compliance_pct"] = change_pct
|
|
1071
|
+
totals["overall_compliance_pct"] = round(sum(available) / len(available), 1) if available else None
|
|
1072
|
+
return totals
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _aggregate_delivery_metrics(applied_logs: list[dict]) -> dict:
|
|
1076
|
+
totals = {
|
|
1077
|
+
"runs": len(applied_logs),
|
|
1078
|
+
"applied_actions": 0,
|
|
1079
|
+
"deferred_actions": 0,
|
|
1080
|
+
"skipped_dedupe": 0,
|
|
1081
|
+
"errors": 0,
|
|
1082
|
+
"engineering_followups": 0,
|
|
1083
|
+
"followup_dedupe_matches": 0,
|
|
1084
|
+
"learning_reinforcements": 0,
|
|
1085
|
+
"learning_duplicate_skips": 0,
|
|
1086
|
+
"learning_contradiction_reviews": 0,
|
|
1087
|
+
}
|
|
1088
|
+
for payload in applied_logs:
|
|
1089
|
+
stats = payload.get("stats") or {}
|
|
1090
|
+
totals["applied_actions"] += int(stats.get("applied", 0) or 0)
|
|
1091
|
+
totals["deferred_actions"] += int(stats.get("deferred", 0) or 0)
|
|
1092
|
+
totals["skipped_dedupe"] += int(stats.get("skipped_dedupe", 0) or 0)
|
|
1093
|
+
totals["errors"] += int(stats.get("errors", 0) or 0)
|
|
1094
|
+
for action in payload.get("applied_actions", []) or []:
|
|
1095
|
+
details = action.get("details") or {}
|
|
1096
|
+
if action.get("action_type") == "followup_create":
|
|
1097
|
+
description = str(details.get("description", "") or "") + " " + str(details.get("reasoning", "") or "")
|
|
1098
|
+
if "engineering" in description.lower() or "guardrail" in description.lower():
|
|
1099
|
+
totals["engineering_followups"] += 1
|
|
1100
|
+
if details.get("outcome") == "matched_existing_followup":
|
|
1101
|
+
totals["followup_dedupe_matches"] += 1
|
|
1102
|
+
elif action.get("action_type") == "learning_add":
|
|
1103
|
+
outcome = str(details.get("outcome", "") or "")
|
|
1104
|
+
if outcome == "reinforced_learning":
|
|
1105
|
+
totals["learning_reinforcements"] += 1
|
|
1106
|
+
elif outcome == "duplicate_learning":
|
|
1107
|
+
totals["learning_duplicate_skips"] += 1
|
|
1108
|
+
elif outcome == "contradiction_review":
|
|
1109
|
+
totals["learning_contradiction_reviews"] += 1
|
|
1110
|
+
|
|
1111
|
+
attempted = totals["applied_actions"] + totals["deferred_actions"] + totals["skipped_dedupe"] + totals["errors"]
|
|
1112
|
+
totals["dedupe_rate_pct"] = _safe_pct(totals["skipped_dedupe"], attempted)
|
|
1113
|
+
totals["error_rate_pct"] = _safe_pct(totals["errors"], attempted)
|
|
1114
|
+
return totals
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _semantic_duplicate_metrics(items: list[tuple[str, str]], *, threshold: float = 0.82) -> dict:
|
|
1118
|
+
filtered = [(item_id, _normalize_text(text)) for item_id, text in items if _normalize_text(text)]
|
|
1119
|
+
if len(filtered) < 2:
|
|
1120
|
+
return {
|
|
1121
|
+
"cluster_count": 0,
|
|
1122
|
+
"duplicate_items": 0,
|
|
1123
|
+
"duplicate_excess": 0,
|
|
1124
|
+
"sample_clusters": [],
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
used: set[int] = set()
|
|
1128
|
+
clusters: list[list[tuple[str, str]]] = []
|
|
1129
|
+
for index, (item_id, text) in enumerate(filtered):
|
|
1130
|
+
if index in used:
|
|
1131
|
+
continue
|
|
1132
|
+
cluster = [(item_id, text)]
|
|
1133
|
+
for other_index in range(index + 1, len(filtered)):
|
|
1134
|
+
if other_index in used:
|
|
1135
|
+
continue
|
|
1136
|
+
other_id, other_text = filtered[other_index]
|
|
1137
|
+
if _text_similarity(text, other_text) >= threshold:
|
|
1138
|
+
cluster.append((other_id, other_text))
|
|
1139
|
+
used.add(other_index)
|
|
1140
|
+
if len(cluster) > 1:
|
|
1141
|
+
used.add(index)
|
|
1142
|
+
clusters.append(cluster)
|
|
1143
|
+
|
|
1144
|
+
return {
|
|
1145
|
+
"cluster_count": len(clusters),
|
|
1146
|
+
"duplicate_items": sum(len(cluster) for cluster in clusters),
|
|
1147
|
+
"duplicate_excess": sum(max(0, len(cluster) - 1) for cluster in clusters),
|
|
1148
|
+
"sample_clusters": [
|
|
1149
|
+
[item_id for item_id, _ in cluster[:4]]
|
|
1150
|
+
for cluster in clusters[:5]
|
|
1151
|
+
],
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _followup_deduplication_metrics() -> dict:
|
|
1156
|
+
cols = _table_columns(NEXO_DB, "followups")
|
|
1157
|
+
if "description" not in cols:
|
|
1158
|
+
return {
|
|
1159
|
+
"open_followups": 0,
|
|
1160
|
+
"duplicate_clusters": 0,
|
|
1161
|
+
"duplicate_open_followups": 0,
|
|
1162
|
+
"duplicate_rate_pct": None,
|
|
1163
|
+
"sample_clusters": [],
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
select_cols = ["description"]
|
|
1167
|
+
if "id" in cols:
|
|
1168
|
+
select_cols.append("id")
|
|
1169
|
+
if "status" in cols:
|
|
1170
|
+
select_cols.append("status")
|
|
1171
|
+
|
|
1172
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1173
|
+
conn.row_factory = sqlite3.Row
|
|
1174
|
+
rows = [dict(row) for row in conn.execute(f"SELECT {', '.join(select_cols)} FROM followups").fetchall()]
|
|
1175
|
+
conn.close()
|
|
1176
|
+
|
|
1177
|
+
open_rows = []
|
|
1178
|
+
for row in rows:
|
|
1179
|
+
status = str(row.get("status", "pending") or "pending").strip().lower()
|
|
1180
|
+
if status in {"done", "completed", "cancelled", "resolved"}:
|
|
1181
|
+
continue
|
|
1182
|
+
identifier = str(row.get("id") or row.get("description") or f"followup-{len(open_rows)+1}")
|
|
1183
|
+
open_rows.append((identifier, str(row.get("description", "") or "")))
|
|
1184
|
+
|
|
1185
|
+
duplicates = _semantic_duplicate_metrics(open_rows)
|
|
1186
|
+
return {
|
|
1187
|
+
"open_followups": len(open_rows),
|
|
1188
|
+
"duplicate_clusters": duplicates["cluster_count"],
|
|
1189
|
+
"duplicate_open_followups": duplicates["duplicate_excess"],
|
|
1190
|
+
"duplicate_rate_pct": _safe_pct(duplicates["duplicate_excess"], len(open_rows)),
|
|
1191
|
+
"sample_clusters": duplicates["sample_clusters"],
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def _learning_consolidation_metrics() -> dict:
|
|
1196
|
+
cols = _table_columns(NEXO_DB, "learnings")
|
|
1197
|
+
if not {"title", "content"}.issubset(cols):
|
|
1198
|
+
return {
|
|
1199
|
+
"active_learnings": 0,
|
|
1200
|
+
"weak_active_learnings": 0,
|
|
1201
|
+
"duplicate_clusters": 0,
|
|
1202
|
+
"duplicate_active_learnings": 0,
|
|
1203
|
+
"noise_pressure": 0,
|
|
1204
|
+
"noise_rate_pct": None,
|
|
1205
|
+
"sample_clusters": [],
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
select_cols = ["title", "content"]
|
|
1209
|
+
if "id" in cols:
|
|
1210
|
+
select_cols.append("id")
|
|
1211
|
+
for field in ("status", "weight", "reasoning", "prevention", "applies_to", "guard_hits"):
|
|
1212
|
+
if field in cols:
|
|
1213
|
+
select_cols.append(field)
|
|
1214
|
+
|
|
1215
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1216
|
+
conn.row_factory = sqlite3.Row
|
|
1217
|
+
rows = [dict(row) for row in conn.execute(f"SELECT {', '.join(select_cols)} FROM learnings").fetchall()]
|
|
1218
|
+
conn.close()
|
|
1219
|
+
|
|
1220
|
+
active_rows = []
|
|
1221
|
+
weak_active = 0
|
|
1222
|
+
for row in rows:
|
|
1223
|
+
status = str(row.get("status", "active") or "active").strip().lower()
|
|
1224
|
+
if status != "active":
|
|
1225
|
+
continue
|
|
1226
|
+
active_rows.append(row)
|
|
1227
|
+
weight = row.get("weight")
|
|
1228
|
+
reasoning = str(row.get("reasoning", "") or "").strip()
|
|
1229
|
+
prevention = str(row.get("prevention", "") or "").strip()
|
|
1230
|
+
guard_hits = int(row.get("guard_hits", 0) or 0)
|
|
1231
|
+
applies_to = str(row.get("applies_to", "") or "").strip()
|
|
1232
|
+
if isinstance(weight, (int, float)) and float(weight) < 1.0:
|
|
1233
|
+
weak_active += 1
|
|
1234
|
+
elif not reasoning and not prevention:
|
|
1235
|
+
weak_active += 1
|
|
1236
|
+
elif applies_to and guard_hits <= 0:
|
|
1237
|
+
weak_active += 1
|
|
1238
|
+
|
|
1239
|
+
duplicates = _semantic_duplicate_metrics(
|
|
1240
|
+
[
|
|
1241
|
+
(str(row.get("id") or f"learning-{index}"), f"{row.get('title', '')} {row.get('content', '')}")
|
|
1242
|
+
for index, row in enumerate(active_rows, 1)
|
|
1243
|
+
],
|
|
1244
|
+
threshold=0.8,
|
|
1245
|
+
)
|
|
1246
|
+
noise_pressure = weak_active + duplicates["duplicate_excess"]
|
|
1247
|
+
return {
|
|
1248
|
+
"active_learnings": len(active_rows),
|
|
1249
|
+
"weak_active_learnings": weak_active,
|
|
1250
|
+
"duplicate_clusters": duplicates["cluster_count"],
|
|
1251
|
+
"duplicate_active_learnings": duplicates["duplicate_excess"],
|
|
1252
|
+
"noise_pressure": noise_pressure,
|
|
1253
|
+
"noise_rate_pct": _safe_pct(noise_pressure, len(active_rows)),
|
|
1254
|
+
"sample_clusters": duplicates["sample_clusters"],
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _load_previous_period_summary(kind: str, label: str) -> dict | None:
|
|
1259
|
+
pattern = f"*-{kind}-summary.json"
|
|
1260
|
+
candidates: list[tuple[str, Path]] = []
|
|
1261
|
+
for path in DEEP_SLEEP_DIR.glob(pattern):
|
|
1262
|
+
try:
|
|
1263
|
+
payload = json.loads(path.read_text())
|
|
1264
|
+
except Exception:
|
|
1265
|
+
continue
|
|
1266
|
+
candidate_label = str(payload.get("label", "") or "")
|
|
1267
|
+
if candidate_label and candidate_label < label:
|
|
1268
|
+
candidates.append((candidate_label, path))
|
|
1269
|
+
if not candidates:
|
|
1270
|
+
return None
|
|
1271
|
+
_, path = sorted(candidates, key=lambda item: item[0])[-1]
|
|
1272
|
+
try:
|
|
1273
|
+
payload = json.loads(path.read_text())
|
|
1274
|
+
except Exception:
|
|
1275
|
+
return None
|
|
1276
|
+
return payload if isinstance(payload, dict) else None
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _build_project_pulse(top_projects: list[dict], previous_summary: dict | None) -> list[dict]:
|
|
1280
|
+
previous_scores: dict[str, float] = {}
|
|
1281
|
+
if previous_summary:
|
|
1282
|
+
for item in previous_summary.get("project_pulse", []) or previous_summary.get("top_projects", []) or []:
|
|
1283
|
+
project = str(item.get("project", "") or "")
|
|
1284
|
+
if project:
|
|
1285
|
+
previous_scores[project] = float(item.get("score", 0) or 0)
|
|
1286
|
+
|
|
1287
|
+
pulse: list[dict] = []
|
|
1288
|
+
for item in top_projects:
|
|
1289
|
+
project = str(item.get("project", "") or "")
|
|
1290
|
+
score = float(item.get("score", 0) or 0)
|
|
1291
|
+
previous_score = previous_scores.get(project, 0.0)
|
|
1292
|
+
delta = round(score - previous_score, 2)
|
|
1293
|
+
if score >= 18:
|
|
1294
|
+
status = "critical"
|
|
1295
|
+
elif score >= 10:
|
|
1296
|
+
status = "elevated"
|
|
1297
|
+
else:
|
|
1298
|
+
status = "watch"
|
|
1299
|
+
if delta >= 2.0:
|
|
1300
|
+
trend = "rising"
|
|
1301
|
+
elif delta <= -2.0:
|
|
1302
|
+
trend = "cooling"
|
|
1303
|
+
else:
|
|
1304
|
+
trend = "steady"
|
|
1305
|
+
pulse.append(
|
|
1306
|
+
{
|
|
1307
|
+
"project": project,
|
|
1308
|
+
"score": round(score, 2),
|
|
1309
|
+
"delta_vs_previous": delta,
|
|
1310
|
+
"trend": trend,
|
|
1311
|
+
"status": status,
|
|
1312
|
+
"signals": item.get("signals", {}),
|
|
1313
|
+
"reasons": item.get("reasons", []),
|
|
1314
|
+
}
|
|
1315
|
+
)
|
|
1316
|
+
return pulse
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _build_period_trend(summary: dict, previous_summary: dict | None) -> dict:
|
|
1320
|
+
if not previous_summary:
|
|
1321
|
+
return {
|
|
1322
|
+
"has_previous": False,
|
|
1323
|
+
"avg_mood_delta": None,
|
|
1324
|
+
"avg_trust_delta": None,
|
|
1325
|
+
"total_corrections_delta": None,
|
|
1326
|
+
"protocol_compliance_delta": None,
|
|
1327
|
+
"followup_duplicate_open_delta": None,
|
|
1328
|
+
"followup_duplicate_rate_delta": None,
|
|
1329
|
+
"learning_noise_delta": None,
|
|
1330
|
+
"learning_noise_rate_delta": None,
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
current_protocol = summary.get("protocol_summary", {}).get("overall_compliance_pct")
|
|
1334
|
+
previous_protocol = (previous_summary.get("protocol_summary") or {}).get("overall_compliance_pct")
|
|
1335
|
+
current_mood = summary.get("avg_mood_score")
|
|
1336
|
+
previous_mood = previous_summary.get("avg_mood_score")
|
|
1337
|
+
current_trust = summary.get("avg_trust_score")
|
|
1338
|
+
previous_trust = previous_summary.get("avg_trust_score")
|
|
1339
|
+
current_followup = (summary.get("followup_deduplication") or {}).get("duplicate_open_followups")
|
|
1340
|
+
previous_followup = (previous_summary.get("followup_deduplication") or {}).get("duplicate_open_followups")
|
|
1341
|
+
current_followup_rate = (summary.get("followup_deduplication") or {}).get("duplicate_rate_pct")
|
|
1342
|
+
previous_followup_rate = (previous_summary.get("followup_deduplication") or {}).get("duplicate_rate_pct")
|
|
1343
|
+
current_learning_noise = (summary.get("learning_consolidation") or {}).get("noise_pressure")
|
|
1344
|
+
previous_learning_noise = (previous_summary.get("learning_consolidation") or {}).get("noise_pressure")
|
|
1345
|
+
current_learning_rate = (summary.get("learning_consolidation") or {}).get("noise_rate_pct")
|
|
1346
|
+
previous_learning_rate = (previous_summary.get("learning_consolidation") or {}).get("noise_rate_pct")
|
|
1347
|
+
|
|
1348
|
+
return {
|
|
1349
|
+
"has_previous": True,
|
|
1350
|
+
"avg_mood_delta": round(current_mood - previous_mood, 3) if isinstance(current_mood, (int, float)) and isinstance(previous_mood, (int, float)) else None,
|
|
1351
|
+
"avg_trust_delta": round(current_trust - previous_trust, 1) if isinstance(current_trust, (int, float)) and isinstance(previous_trust, (int, float)) else None,
|
|
1352
|
+
"total_corrections_delta": int(summary.get("total_corrections", 0) or 0) - int(previous_summary.get("total_corrections", 0) or 0),
|
|
1353
|
+
"protocol_compliance_delta": round(current_protocol - previous_protocol, 1) if isinstance(current_protocol, (int, float)) and isinstance(previous_protocol, (int, float)) else None,
|
|
1354
|
+
"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,
|
|
1355
|
+
"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,
|
|
1356
|
+
"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,
|
|
1357
|
+
"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,
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
|
|
995
1361
|
def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, window_days: int) -> dict:
|
|
996
1362
|
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
997
1363
|
window_start = (target_day - timedelta(days=max(0, window_days - 1))).strftime("%Y-%m-%d")
|
|
@@ -1001,6 +1367,8 @@ def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, windo
|
|
|
1001
1367
|
else target_day.strftime("%Y-%m")
|
|
1002
1368
|
)
|
|
1003
1369
|
syntheses = _load_period_syntheses(target_date, window_days=window_days)
|
|
1370
|
+
extractions = _load_period_extractions(target_date, window_days=window_days)
|
|
1371
|
+
applied_logs = _load_period_applied_logs(target_date, window_days=window_days)
|
|
1004
1372
|
if not any(item.get("date") == target_date for item in syntheses):
|
|
1005
1373
|
syntheses.append(synthesis)
|
|
1006
1374
|
|
|
@@ -1037,6 +1405,12 @@ def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, windo
|
|
|
1037
1405
|
{"title": title, "count": count}
|
|
1038
1406
|
for title, count in agenda_counter.most_common(6)
|
|
1039
1407
|
]
|
|
1408
|
+
protocol_summary = _aggregate_protocol_summary(extractions)
|
|
1409
|
+
delivery_metrics = _aggregate_delivery_metrics(applied_logs)
|
|
1410
|
+
followup_deduplication = _followup_deduplication_metrics()
|
|
1411
|
+
learning_consolidation = _learning_consolidation_metrics()
|
|
1412
|
+
previous_summary = _load_previous_period_summary(kind, label)
|
|
1413
|
+
project_pulse = _build_project_pulse(top_projects, previous_summary)
|
|
1040
1414
|
|
|
1041
1415
|
summary_parts = [f"{len(syntheses)} Deep Sleep run(s)"]
|
|
1042
1416
|
if top_projects:
|
|
@@ -1045,9 +1419,11 @@ def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, windo
|
|
|
1045
1419
|
summary_parts.append(f"recurring pattern: {top_patterns[0]['pattern']}")
|
|
1046
1420
|
if avg_trust is not None:
|
|
1047
1421
|
summary_parts.append(f"avg trust {avg_trust:.1f}")
|
|
1422
|
+
if protocol_summary.get("overall_compliance_pct") is not None:
|
|
1423
|
+
summary_parts.append(f"protocol {protocol_summary['overall_compliance_pct']:.1f}%")
|
|
1048
1424
|
summary = " | ".join(summary_parts)
|
|
1049
1425
|
|
|
1050
|
-
|
|
1426
|
+
period_summary = {
|
|
1051
1427
|
"kind": kind,
|
|
1052
1428
|
"label": label,
|
|
1053
1429
|
"window_days": window_days,
|
|
@@ -1059,11 +1435,17 @@ def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, windo
|
|
|
1059
1435
|
"avg_trust_score": avg_trust,
|
|
1060
1436
|
"total_corrections": total_corrections,
|
|
1061
1437
|
"top_projects": top_projects,
|
|
1438
|
+
"project_pulse": project_pulse,
|
|
1062
1439
|
"top_patterns": top_patterns,
|
|
1063
1440
|
"recurring_agenda": recurring_agenda,
|
|
1441
|
+
"protocol_summary": protocol_summary,
|
|
1442
|
+
"delivery_metrics": delivery_metrics,
|
|
1443
|
+
"followup_deduplication": followup_deduplication,
|
|
1444
|
+
"learning_consolidation": learning_consolidation,
|
|
1064
1445
|
"summary": summary,
|
|
1065
1446
|
}
|
|
1066
|
-
|
|
1447
|
+
period_summary["trend"] = _build_period_trend(period_summary, previous_summary)
|
|
1448
|
+
return period_summary
|
|
1067
1449
|
|
|
1068
1450
|
def _render_period_summary_markdown(summary: dict) -> str:
|
|
1069
1451
|
lines = [
|
|
@@ -1082,6 +1464,68 @@ def _render_period_summary_markdown(summary: dict) -> str:
|
|
|
1082
1464
|
lines.append(f"> {summary['summary']}")
|
|
1083
1465
|
lines.append("")
|
|
1084
1466
|
|
|
1467
|
+
protocol_summary = summary.get("protocol_summary") or {}
|
|
1468
|
+
if protocol_summary:
|
|
1469
|
+
lines.append("## Protocol Compliance")
|
|
1470
|
+
lines.append("")
|
|
1471
|
+
overall = protocol_summary.get("overall_compliance_pct")
|
|
1472
|
+
if overall is not None:
|
|
1473
|
+
lines.append(f"- Overall compliance: {overall:.1f}%")
|
|
1474
|
+
guard = protocol_summary.get("guard_check", {})
|
|
1475
|
+
heartbeat = protocol_summary.get("heartbeat", {})
|
|
1476
|
+
change_log = protocol_summary.get("change_log", {})
|
|
1477
|
+
if guard:
|
|
1478
|
+
lines.append(
|
|
1479
|
+
f"- guard_check: {guard.get('executed', 0)}/{guard.get('required', 0)}"
|
|
1480
|
+
+ (f" ({guard['compliance_pct']:.1f}%)" if guard.get("compliance_pct") is not None else "")
|
|
1481
|
+
)
|
|
1482
|
+
if heartbeat:
|
|
1483
|
+
lines.append(
|
|
1484
|
+
f"- heartbeat with context: {heartbeat.get('with_context', 0)}/{heartbeat.get('total', 0)}"
|
|
1485
|
+
+ (f" ({heartbeat['compliance_pct']:.1f}%)" if heartbeat.get("compliance_pct") is not None else "")
|
|
1486
|
+
)
|
|
1487
|
+
if change_log:
|
|
1488
|
+
lines.append(
|
|
1489
|
+
f"- change_log after edits: {change_log.get('logged', 0)}/{change_log.get('edits', 0)}"
|
|
1490
|
+
+ (f" ({change_log['compliance_pct']:.1f}%)" if change_log.get("compliance_pct") is not None else "")
|
|
1491
|
+
)
|
|
1492
|
+
lines.append("")
|
|
1493
|
+
|
|
1494
|
+
delivery_metrics = summary.get("delivery_metrics") or {}
|
|
1495
|
+
if delivery_metrics:
|
|
1496
|
+
lines.append("## Loop Output")
|
|
1497
|
+
lines.append("")
|
|
1498
|
+
lines.append(f"- Applied actions: {delivery_metrics.get('applied_actions', 0)}")
|
|
1499
|
+
lines.append(f"- Deferred actions: {delivery_metrics.get('deferred_actions', 0)}")
|
|
1500
|
+
lines.append(f"- Dedupe skips: {delivery_metrics.get('skipped_dedupe', 0)}")
|
|
1501
|
+
lines.append(f"- Engineering followups: {delivery_metrics.get('engineering_followups', 0)}")
|
|
1502
|
+
if delivery_metrics.get("dedupe_rate_pct") is not None:
|
|
1503
|
+
lines.append(f"- Dedupe rate: {delivery_metrics['dedupe_rate_pct']:.1f}%")
|
|
1504
|
+
if delivery_metrics.get("error_rate_pct") is not None:
|
|
1505
|
+
lines.append(f"- Error rate: {delivery_metrics['error_rate_pct']:.1f}%")
|
|
1506
|
+
lines.append(f"- Followup dedupe matches: {delivery_metrics.get('followup_dedupe_matches', 0)}")
|
|
1507
|
+
lines.append(f"- Learning reinforcements: {delivery_metrics.get('learning_reinforcements', 0)}")
|
|
1508
|
+
lines.append(f"- Learning duplicate skips: {delivery_metrics.get('learning_duplicate_skips', 0)}")
|
|
1509
|
+
lines.append(f"- Learning contradiction reviews: {delivery_metrics.get('learning_contradiction_reviews', 0)}")
|
|
1510
|
+
lines.append("")
|
|
1511
|
+
|
|
1512
|
+
followup_deduplication = summary.get("followup_deduplication") or {}
|
|
1513
|
+
learning_consolidation = summary.get("learning_consolidation") or {}
|
|
1514
|
+
if followup_deduplication or learning_consolidation:
|
|
1515
|
+
lines.append("## Prevention Quality")
|
|
1516
|
+
lines.append("")
|
|
1517
|
+
if followup_deduplication:
|
|
1518
|
+
lines.append(f"- Open followups: {followup_deduplication.get('open_followups', 0)}")
|
|
1519
|
+
lines.append(f"- Duplicate open followups: {followup_deduplication.get('duplicate_open_followups', 0)}")
|
|
1520
|
+
if followup_deduplication.get("duplicate_rate_pct") is not None:
|
|
1521
|
+
lines.append(f"- Duplicate followup rate: {followup_deduplication['duplicate_rate_pct']:.1f}%")
|
|
1522
|
+
if learning_consolidation:
|
|
1523
|
+
lines.append(f"- Active learnings: {learning_consolidation.get('active_learnings', 0)}")
|
|
1524
|
+
lines.append(f"- Learning noise pressure: {learning_consolidation.get('noise_pressure', 0)}")
|
|
1525
|
+
if learning_consolidation.get("noise_rate_pct") is not None:
|
|
1526
|
+
lines.append(f"- Learning noise rate: {learning_consolidation['noise_rate_pct']:.1f}%")
|
|
1527
|
+
lines.append("")
|
|
1528
|
+
|
|
1085
1529
|
if summary.get("top_projects"):
|
|
1086
1530
|
lines.append("## Top Projects")
|
|
1087
1531
|
lines.append("")
|
|
@@ -1091,6 +1535,20 @@ def _render_period_summary_markdown(summary: dict) -> str:
|
|
|
1091
1535
|
lines.append(f" Reasons: {', '.join(item['reasons'])}")
|
|
1092
1536
|
lines.append("")
|
|
1093
1537
|
|
|
1538
|
+
if summary.get("project_pulse"):
|
|
1539
|
+
lines.append("## Project Pulse")
|
|
1540
|
+
lines.append("")
|
|
1541
|
+
for item in summary["project_pulse"][:5]:
|
|
1542
|
+
delta = item.get("delta_vs_previous")
|
|
1543
|
+
delta_label = ""
|
|
1544
|
+
if isinstance(delta, (int, float)):
|
|
1545
|
+
delta_label = f" | Δ {delta:+.2f}"
|
|
1546
|
+
lines.append(
|
|
1547
|
+
f"- **{item['project']}** — {item.get('status', 'watch')} / {item.get('trend', 'steady')}"
|
|
1548
|
+
f" | score {item.get('score', 0)}{delta_label}"
|
|
1549
|
+
)
|
|
1550
|
+
lines.append("")
|
|
1551
|
+
|
|
1094
1552
|
if summary.get("top_patterns"):
|
|
1095
1553
|
lines.append("## Recurring Patterns")
|
|
1096
1554
|
lines.append("")
|
|
@@ -1105,6 +1563,28 @@ def _render_period_summary_markdown(summary: dict) -> str:
|
|
|
1105
1563
|
lines.append(f"- {item['title']} ({item['count']}x)")
|
|
1106
1564
|
lines.append("")
|
|
1107
1565
|
|
|
1566
|
+
trend = summary.get("trend") or {}
|
|
1567
|
+
if trend.get("has_previous"):
|
|
1568
|
+
lines.append("## Trend vs Previous")
|
|
1569
|
+
lines.append("")
|
|
1570
|
+
if trend.get("avg_mood_delta") is not None:
|
|
1571
|
+
lines.append(f"- Mood delta: {trend['avg_mood_delta']:+.3f}")
|
|
1572
|
+
if trend.get("avg_trust_delta") is not None:
|
|
1573
|
+
lines.append(f"- Trust delta: {trend['avg_trust_delta']:+.1f}")
|
|
1574
|
+
if trend.get("total_corrections_delta") is not None:
|
|
1575
|
+
lines.append(f"- Corrections delta: {trend['total_corrections_delta']:+d}")
|
|
1576
|
+
if trend.get("protocol_compliance_delta") is not None:
|
|
1577
|
+
lines.append(f"- Protocol delta: {trend['protocol_compliance_delta']:+.1f}%")
|
|
1578
|
+
if trend.get("followup_duplicate_open_delta") is not None:
|
|
1579
|
+
lines.append(f"- Duplicate followups delta: {trend['followup_duplicate_open_delta']:+d}")
|
|
1580
|
+
if trend.get("followup_duplicate_rate_delta") is not None:
|
|
1581
|
+
lines.append(f"- Duplicate followup rate delta: {trend['followup_duplicate_rate_delta']:+.1f}%")
|
|
1582
|
+
if trend.get("learning_noise_delta") is not None:
|
|
1583
|
+
lines.append(f"- Learning noise delta: {trend['learning_noise_delta']:+d}")
|
|
1584
|
+
if trend.get("learning_noise_rate_delta") is not None:
|
|
1585
|
+
lines.append(f"- Learning noise rate delta: {trend['learning_noise_rate_delta']:+.1f}%")
|
|
1586
|
+
lines.append("")
|
|
1587
|
+
|
|
1108
1588
|
return "\n".join(lines).rstrip() + "\n"
|
|
1109
1589
|
|
|
1110
1590
|
|
|
@@ -25,6 +25,7 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", ""))
|
|
|
25
25
|
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
26
26
|
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
27
27
|
COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
|
|
28
|
+
_TABLE_COLUMNS_CACHE: dict[tuple[str, str], set[str]] = {}
|
|
28
29
|
|
|
29
30
|
MIN_USER_MESSAGES = 3 # Skip trivial sessions
|
|
30
31
|
|
|
@@ -304,6 +305,33 @@ def safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
|
|
|
304
305
|
return []
|
|
305
306
|
|
|
306
307
|
|
|
308
|
+
def _table_columns(db_path: Path, table_name: str) -> set[str]:
|
|
309
|
+
cache_key = (str(db_path), table_name)
|
|
310
|
+
cached = _TABLE_COLUMNS_CACHE.get(cache_key)
|
|
311
|
+
if cached is not None:
|
|
312
|
+
return cached
|
|
313
|
+
if not db_path.exists():
|
|
314
|
+
_TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
315
|
+
return set()
|
|
316
|
+
try:
|
|
317
|
+
conn = sqlite3.connect(str(db_path))
|
|
318
|
+
conn.row_factory = sqlite3.Row
|
|
319
|
+
rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
|
|
320
|
+
conn.close()
|
|
321
|
+
except Exception:
|
|
322
|
+
_TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
323
|
+
return set()
|
|
324
|
+
columns = {str(row["name"]) for row in rows}
|
|
325
|
+
_TABLE_COLUMNS_CACHE[cache_key] = columns
|
|
326
|
+
return columns
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _optional_column_sql(db_path: Path, table_name: str, column_name: str, default_sql: str = "''") -> str:
|
|
330
|
+
if column_name in _table_columns(db_path, table_name):
|
|
331
|
+
return column_name
|
|
332
|
+
return f"{default_sql} AS {column_name}"
|
|
333
|
+
|
|
334
|
+
|
|
307
335
|
def collect_followups() -> list[dict]:
|
|
308
336
|
"""Active followups from nexo.db."""
|
|
309
337
|
return safe_query(
|
|
@@ -513,9 +541,13 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
|
|
|
513
541
|
for project in candidates:
|
|
514
542
|
bump(project, 3.0 * recency_bonus, "diary_sessions", "recent session diary activity")
|
|
515
543
|
|
|
544
|
+
learning_priority_sql = _optional_column_sql(NEXO_DB, "learnings", "priority", "'medium'")
|
|
545
|
+
learning_weight_sql = _optional_column_sql(NEXO_DB, "learnings", "weight", "0")
|
|
546
|
+
learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
|
|
516
547
|
learning_rows = safe_query(
|
|
517
548
|
NEXO_DB,
|
|
518
|
-
"SELECT category, title, content, created_at, updated_at,
|
|
549
|
+
f"SELECT category, title, content, created_at, updated_at, {learning_priority_sql}, "
|
|
550
|
+
f"{learning_weight_sql}, {learning_applies_sql} FROM learnings "
|
|
519
551
|
"ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 160",
|
|
520
552
|
)
|
|
521
553
|
for row in learning_rows:
|
|
@@ -535,9 +567,12 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
|
|
|
535
567
|
for project in matched:
|
|
536
568
|
bump(project, score, "learnings", "recent leverage-bearing learning")
|
|
537
569
|
|
|
570
|
+
followup_priority_sql = _optional_column_sql(NEXO_DB, "followups", "priority", "'medium'")
|
|
571
|
+
followup_reasoning_sql = _optional_column_sql(NEXO_DB, "followups", "reasoning", "''")
|
|
538
572
|
followup_rows = safe_query(
|
|
539
573
|
NEXO_DB,
|
|
540
|
-
"SELECT id, description, date, status,
|
|
574
|
+
f"SELECT id, description, date, status, {followup_priority_sql}, created_at, updated_at, "
|
|
575
|
+
f"{followup_reasoning_sql} FROM followups "
|
|
541
576
|
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 120",
|
|
542
577
|
)
|
|
543
578
|
for row in followup_rows:
|
|
@@ -565,9 +600,13 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
|
|
|
565
600
|
for project in matched:
|
|
566
601
|
bump(project, score, "followups", "open followup pressure")
|
|
567
602
|
|
|
603
|
+
decision_status_sql = _optional_column_sql(NEXO_DB, "decisions", "status", "''")
|
|
604
|
+
decision_reasoning_sql = _optional_column_sql(NEXO_DB, "decisions", "reasoning", "''")
|
|
605
|
+
decision_review_due_sql = _optional_column_sql(NEXO_DB, "decisions", "review_due_at", "NULL")
|
|
568
606
|
decision_rows = safe_query(
|
|
569
607
|
NEXO_DB,
|
|
570
|
-
"SELECT domain, outcome,
|
|
608
|
+
f"SELECT domain, outcome, {decision_status_sql}, {decision_reasoning_sql}, decision, based_on, created_at, "
|
|
609
|
+
f"{decision_review_due_sql} FROM decisions "
|
|
571
610
|
"ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 120",
|
|
572
611
|
)
|
|
573
612
|
for row in decision_rows:
|
|
@@ -579,6 +618,8 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
|
|
|
579
618
|
" ".join(
|
|
580
619
|
[
|
|
581
620
|
str(row.get("reasoning", "") or ""),
|
|
621
|
+
str(row.get("decision", "") or ""),
|
|
622
|
+
str(row.get("based_on", "") or ""),
|
|
582
623
|
str(row.get("outcome", "") or ""),
|
|
583
624
|
str(row.get("status", "") or ""),
|
|
584
625
|
]
|
|
@@ -647,9 +688,13 @@ def collect_long_horizon_context(
|
|
|
647
688
|
recurring_states = Counter(row["mental_state"] for row in compact_diaries if row.get("mental_state"))
|
|
648
689
|
recurring_critiques = Counter(row["self_critique"] for row in compact_diaries if row.get("self_critique"))
|
|
649
690
|
|
|
691
|
+
learning_reasoning_sql = _optional_column_sql(NEXO_DB, "learnings", "reasoning", "''")
|
|
692
|
+
learning_prevention_sql = _optional_column_sql(NEXO_DB, "learnings", "prevention", "''")
|
|
693
|
+
learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
|
|
650
694
|
learning_rows = safe_query(
|
|
651
695
|
NEXO_DB,
|
|
652
|
-
"SELECT category, title, content, created_at, updated_at,
|
|
696
|
+
f"SELECT category, title, content, created_at, updated_at, {learning_reasoning_sql}, "
|
|
697
|
+
f"{learning_prevention_sql}, {learning_applies_sql} "
|
|
653
698
|
"FROM learnings ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 120"
|
|
654
699
|
)
|
|
655
700
|
long_horizon_learnings = []
|
|
@@ -29,6 +29,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
29
29
|
parser.add_argument("--prompt", default="", help="Prompt text")
|
|
30
30
|
parser.add_argument("--prompt-file", default="", help="Read prompt text from a file")
|
|
31
31
|
parser.add_argument("--cwd", default="", help="Working directory for the backend")
|
|
32
|
+
parser.add_argument("--task-profile", default="", help="Automation task profile: default|fast|balanced|deep")
|
|
32
33
|
parser.add_argument("--model", default="", help="Backend model hint")
|
|
33
34
|
parser.add_argument("--reasoning-effort", default="", help="Backend reasoning effort/profile")
|
|
34
35
|
parser.add_argument("--timeout", type=int, default=21600, help="Timeout in seconds")
|
|
@@ -51,6 +52,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
51
52
|
result = run_automation_prompt(
|
|
52
53
|
prompt,
|
|
53
54
|
cwd=args.cwd or None,
|
|
55
|
+
task_profile=args.task_profile,
|
|
54
56
|
model=args.model,
|
|
55
57
|
reasoning_effort=args.reasoning_effort,
|
|
56
58
|
timeout=args.timeout,
|