nexo-brain 7.33.0 → 7.34.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 +1 -1
- package/package.json +1 -1
- package/src/db/__init__.py +2 -1
- package/src/db/_episodic.py +32 -0
- package/src/db/_protocol.py +35 -0
- package/src/db/_schema.py +105 -0
- package/src/local_context/usage_events.py +2 -0
- package/src/message_batch_preview.py +290 -0
- package/src/plugins/protocol.py +195 -3
- package/src/ppr.py +473 -0
- package/src/pre_answer_router.py +239 -3
- package/src/pre_answer_runtime.py +156 -1
- package/src/resolution_cache.py +1119 -0
- package/src/scripts/deep-sleep/apply_findings.py +86 -9
- package/src/scripts/deep-sleep/rewrite.py +625 -0
- package/src/scripts/nexo-deep-sleep.sh +10 -0
- package/src/scripts/nexo-morning-agent.py +43 -2
- package/src/self_error_detector.py +414 -0
- package/src/semantic_layers.py +30 -3
- package/templates/core-prompts/morning-agent.md +3 -0
package/src/plugins/protocol.py
CHANGED
|
@@ -157,7 +157,10 @@ def _requires_external_real_world_check(task: dict, *parts: str) -> bool:
|
|
|
157
157
|
if str(task.get("task_type") or "").strip() not in ACTION_TASKS:
|
|
158
158
|
return False
|
|
159
159
|
text = _external_real_world_text(task, *parts)
|
|
160
|
-
return any(
|
|
160
|
+
return any(
|
|
161
|
+
_contains_external_action_keyword(text, keyword)
|
|
162
|
+
for keyword in EXTERNAL_REAL_WORLD_ACTION_KEYWORDS
|
|
163
|
+
)
|
|
161
164
|
|
|
162
165
|
|
|
163
166
|
def _has_external_real_world_evidence(text: str) -> bool:
|
|
@@ -169,6 +172,18 @@ def _has_external_real_world_evidence(text: str) -> bool:
|
|
|
169
172
|
return has_verify_verb and has_artifact
|
|
170
173
|
|
|
171
174
|
|
|
175
|
+
def _contains_external_action_keyword(text: str, keyword: str) -> bool:
|
|
176
|
+
clean_text = str(text or "").lower()
|
|
177
|
+
clean_keyword = str(keyword or "").lower().strip()
|
|
178
|
+
if not clean_text or not clean_keyword:
|
|
179
|
+
return False
|
|
180
|
+
return re.search(
|
|
181
|
+
rf"(?<![a-z0-9]){re.escape(clean_keyword)}(?![a-z0-9])",
|
|
182
|
+
clean_text,
|
|
183
|
+
re.IGNORECASE,
|
|
184
|
+
) is not None
|
|
185
|
+
|
|
186
|
+
|
|
172
187
|
ACTION_TASKS = {"edit", "execute", "delegate"}
|
|
173
188
|
RESPONSE_TASKS = {"answer", "analyze"}
|
|
174
189
|
_GUARD_TOUCH_DEBT_TYPES = {
|
|
@@ -1146,13 +1161,16 @@ def _capture_learning(
|
|
|
1146
1161
|
content: str,
|
|
1147
1162
|
reasoning: str,
|
|
1148
1163
|
priority: str = "high",
|
|
1164
|
+
prevention: str = "",
|
|
1165
|
+
applies_to_override: str = "",
|
|
1166
|
+
source_authority: str = "explicit_instruction",
|
|
1149
1167
|
) -> dict:
|
|
1150
1168
|
from tools_learnings import find_conflicting_active_learning, handle_learning_add
|
|
1151
1169
|
|
|
1152
1170
|
clean_title = (title or "").strip()[:120]
|
|
1153
1171
|
clean_content = (content or "").strip()
|
|
1154
1172
|
clean_reasoning = (reasoning or f"Captured from protocol task {task_id}").strip()
|
|
1155
|
-
applies_to = ",".join(effective_files)
|
|
1173
|
+
applies_to = applies_to_override.strip() if applies_to_override.strip() else ",".join(effective_files)
|
|
1156
1174
|
if not clean_title or not clean_content:
|
|
1157
1175
|
return {"ok": False, "error": "insufficient context for learning capture"}
|
|
1158
1176
|
|
|
@@ -1168,9 +1186,11 @@ def _capture_learning(
|
|
|
1168
1186
|
title=clean_title,
|
|
1169
1187
|
content=clean_content,
|
|
1170
1188
|
reasoning=clean_reasoning,
|
|
1189
|
+
prevention=prevention,
|
|
1171
1190
|
applies_to=applies_to,
|
|
1172
1191
|
priority=priority,
|
|
1173
1192
|
supersedes_id=supersedes_id,
|
|
1193
|
+
source_authority=source_authority,
|
|
1174
1194
|
)
|
|
1175
1195
|
match = re.search(r"Learning #(\d+) added", response)
|
|
1176
1196
|
if match:
|
|
@@ -1180,6 +1200,20 @@ def _capture_learning(
|
|
|
1180
1200
|
"response": response,
|
|
1181
1201
|
"superseded_id": supersedes_id or None,
|
|
1182
1202
|
}
|
|
1203
|
+
# A near/exact duplicate is a SUCCESSFUL no-op merge — the learning already
|
|
1204
|
+
# exists and no duplicate row was created (handle_learning_add returns
|
|
1205
|
+
# "already exists" / "resolved as merge"). Treat it as success so idempotent
|
|
1206
|
+
# re-captures (e.g. the same self-detected error twice) do not report a
|
|
1207
|
+
# phantom learning_ok=False in the close-response telemetry.
|
|
1208
|
+
dedup = re.search(r"Learning #(\d+) (?:already exists|resolved as merge)", response)
|
|
1209
|
+
if dedup:
|
|
1210
|
+
return {
|
|
1211
|
+
"ok": True,
|
|
1212
|
+
"deduped": True,
|
|
1213
|
+
"id": int(dedup.group(1)),
|
|
1214
|
+
"response": response,
|
|
1215
|
+
"superseded_id": supersedes_id or None,
|
|
1216
|
+
}
|
|
1183
1217
|
return {
|
|
1184
1218
|
"ok": False,
|
|
1185
1219
|
"error": response,
|
|
@@ -1217,6 +1251,136 @@ def _auto_capture_learning(task: dict, task_id: str, effective_files: list[str],
|
|
|
1217
1251
|
)
|
|
1218
1252
|
|
|
1219
1253
|
|
|
1254
|
+
# ── Forgotten-step followup detector (objective omission markers) ──────
|
|
1255
|
+
_FORGOTTEN_STEP_FOLLOWUP_RE = re.compile(
|
|
1256
|
+
r"\b(?:forgot|forgotten|missed|omitted|never (?:created|added|set up|configured|deployed|ran)|"
|
|
1257
|
+
r"missing (?:the )?(?:cron|step|trigger|hook|migration|index|webhook|deploy)|"
|
|
1258
|
+
r"olvid[éeè]|me olvid[éeè]|falt[óoa]ba?|no se (?:cre[óo]|configur[óo]|despleg[óo]|registr[óo]))\b",
|
|
1259
|
+
re.IGNORECASE,
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _followup_signals_forgotten_step(*descriptions: object) -> bool:
|
|
1264
|
+
"""True only when a followup description objectively states an omission.
|
|
1265
|
+
|
|
1266
|
+
A generic 'verify weekly' or 'monitor X' followup must NOT count — only an
|
|
1267
|
+
explicit 'forgot/missing/never created the cron' style description does.
|
|
1268
|
+
"""
|
|
1269
|
+
for desc in descriptions:
|
|
1270
|
+
text = str(desc or "").strip()
|
|
1271
|
+
if text and _FORGOTTEN_STEP_FOLLOWUP_RE.search(text):
|
|
1272
|
+
return True
|
|
1273
|
+
return False
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _detect_and_capture_self_error(
|
|
1277
|
+
task: dict,
|
|
1278
|
+
task_id: str,
|
|
1279
|
+
*,
|
|
1280
|
+
clean_outcome: str,
|
|
1281
|
+
closure_text: str,
|
|
1282
|
+
correction: bool,
|
|
1283
|
+
effective_files: list[str],
|
|
1284
|
+
forgotten_step_followup: bool,
|
|
1285
|
+
debts_created: list[dict],
|
|
1286
|
+
) -> dict | None:
|
|
1287
|
+
"""Ola 2 — auto-detect that a PRIOR own action was wrong and learn from it.
|
|
1288
|
+
|
|
1289
|
+
Runs AFTER the current task is closed. Compares it against recently
|
|
1290
|
+
closed-as-done tasks; on high-confidence objective evidence it creates a
|
|
1291
|
+
learning with a concrete prevention rule (source_authority=code_test_evidence,
|
|
1292
|
+
NOT a Francisco correction). On low confidence it records a low-confidence
|
|
1293
|
+
candidate as an INFO protocol_debt — never a learning. Best-effort: any
|
|
1294
|
+
failure returns None and never blocks the close.
|
|
1295
|
+
|
|
1296
|
+
Returns a small dict describing what happened (for the close response), or
|
|
1297
|
+
None when nothing was detected / on error.
|
|
1298
|
+
"""
|
|
1299
|
+
try:
|
|
1300
|
+
import self_error_detector as sed
|
|
1301
|
+
from db import list_recent_closed_tasks
|
|
1302
|
+
|
|
1303
|
+
# Only closes that actually claim progress can host / reveal a self-error.
|
|
1304
|
+
if clean_outcome not in {"done", "partial"}:
|
|
1305
|
+
return None
|
|
1306
|
+
|
|
1307
|
+
prior_tasks = list_recent_closed_tasks(
|
|
1308
|
+
outcome="done",
|
|
1309
|
+
exclude_task_id=task_id,
|
|
1310
|
+
within_days=sed.LOOKBACK_DAYS,
|
|
1311
|
+
limit=sed.MAX_PRIOR_TASKS,
|
|
1312
|
+
)
|
|
1313
|
+
if not prior_tasks:
|
|
1314
|
+
# Nothing previously declared done → cannot have a revealed self-error
|
|
1315
|
+
# from file overlap. A forgotten-step followup alone is candidate-only.
|
|
1316
|
+
if not forgotten_step_followup:
|
|
1317
|
+
return None
|
|
1318
|
+
|
|
1319
|
+
evaluation = sed.evaluate_self_error(
|
|
1320
|
+
current_task=task,
|
|
1321
|
+
prior_tasks=prior_tasks,
|
|
1322
|
+
closure_text=closure_text,
|
|
1323
|
+
correction_happened=correction,
|
|
1324
|
+
forgotten_step_followup=forgotten_step_followup,
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
decision = evaluation.get("decision")
|
|
1328
|
+
if decision == "none":
|
|
1329
|
+
return None
|
|
1330
|
+
|
|
1331
|
+
if decision == "candidate":
|
|
1332
|
+
# Low-confidence: record a quiet INFO candidate, NEVER a learning.
|
|
1333
|
+
# Reuses the existing open-debt dedup so the same candidate does not
|
|
1334
|
+
# pile up across repeated closes of the same task.
|
|
1335
|
+
debt = _ensure_open_debt(
|
|
1336
|
+
task.get("session_id", ""),
|
|
1337
|
+
task_id,
|
|
1338
|
+
"self_error_candidate",
|
|
1339
|
+
severity="info",
|
|
1340
|
+
evidence=(
|
|
1341
|
+
f"Low-confidence self-error candidate (confidence="
|
|
1342
|
+
f"{evaluation.get('confidence')}, signal={evaluation.get('signal')}). "
|
|
1343
|
+
f"{'; '.join(evaluation.get('reasons') or [])[:400]}"
|
|
1344
|
+
),
|
|
1345
|
+
debts=debts_created,
|
|
1346
|
+
)
|
|
1347
|
+
return {
|
|
1348
|
+
"decision": "candidate",
|
|
1349
|
+
"confidence": evaluation.get("confidence"),
|
|
1350
|
+
"signal": evaluation.get("signal"),
|
|
1351
|
+
"debt_id": debt.get("id"),
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
# decision == "fire": create the learning with a concrete prevention.
|
|
1355
|
+
payload = sed.build_self_error_learning(current_task=task, evaluation=evaluation)
|
|
1356
|
+
learning = _capture_learning(
|
|
1357
|
+
task,
|
|
1358
|
+
task_id,
|
|
1359
|
+
effective_files,
|
|
1360
|
+
category=payload["category"],
|
|
1361
|
+
title=payload["title"],
|
|
1362
|
+
content=payload["content"],
|
|
1363
|
+
reasoning=payload["reasoning"],
|
|
1364
|
+
priority="high",
|
|
1365
|
+
prevention=payload["prevention"],
|
|
1366
|
+
applies_to_override=payload["applies_to"],
|
|
1367
|
+
source_authority=payload["source_authority"],
|
|
1368
|
+
)
|
|
1369
|
+
return {
|
|
1370
|
+
"decision": "fire",
|
|
1371
|
+
"confidence": evaluation.get("confidence"),
|
|
1372
|
+
"signal": evaluation.get("signal"),
|
|
1373
|
+
"prior_task_id": evaluation.get("prior_task_id"),
|
|
1374
|
+
"overlap_files": evaluation.get("overlap_files"),
|
|
1375
|
+
"learning_ok": bool(learning.get("ok")),
|
|
1376
|
+
"learning_id": learning.get("id"),
|
|
1377
|
+
"learning_error": None if learning.get("ok") else learning.get("error"),
|
|
1378
|
+
}
|
|
1379
|
+
except Exception:
|
|
1380
|
+
# Self-error detection is strictly best-effort; never break a close.
|
|
1381
|
+
return None
|
|
1382
|
+
|
|
1383
|
+
|
|
1220
1384
|
def _append_debt_ref(debts: list[dict], debt: dict, *, debt_type: str, severity: str):
|
|
1221
1385
|
debt_id = debt.get("id")
|
|
1222
1386
|
if debt_id and any(item.get("id") == debt_id for item in debts):
|
|
@@ -2642,6 +2806,25 @@ def handle_task_close(
|
|
|
2642
2806
|
followup_id=created_followup_id,
|
|
2643
2807
|
outcome_notes=outcome_notes,
|
|
2644
2808
|
)
|
|
2809
|
+
|
|
2810
|
+
# ── Ola 2: auto-detect a PRIOR own action that this close reveals as
|
|
2811
|
+
# wrong (e.g. code shipped earlier but the cron was never created). On
|
|
2812
|
+
# high-confidence objective evidence, capture an immediate learning +
|
|
2813
|
+
# prevention rule (source_authority=code_test_evidence, not a Francisco
|
|
2814
|
+
# correction); on low confidence, only a quiet INFO candidate. Strictly
|
|
2815
|
+
# best-effort — runs after the task is already persisted-closed.
|
|
2816
|
+
self_error = _detect_and_capture_self_error(
|
|
2817
|
+
task,
|
|
2818
|
+
task_id,
|
|
2819
|
+
clean_outcome=clean_outcome,
|
|
2820
|
+
closure_text=closure_text,
|
|
2821
|
+
correction=correction,
|
|
2822
|
+
effective_files=effective_files,
|
|
2823
|
+
forgotten_step_followup=_followup_signals_forgotten_step(
|
|
2824
|
+
followup_description, outcome_notes
|
|
2825
|
+
),
|
|
2826
|
+
debts_created=debts_created,
|
|
2827
|
+
)
|
|
2645
2828
|
capture_context_event(
|
|
2646
2829
|
event_type=f"protocol_task_{clean_outcome}",
|
|
2647
2830
|
title=(task.get("goal") or task_id)[:160],
|
|
@@ -2723,10 +2906,17 @@ def handle_task_close(
|
|
|
2723
2906
|
pass # Drive detection is best-effort
|
|
2724
2907
|
|
|
2725
2908
|
open_debts = list_protocol_debts(status="open", task_id=task_id, limit=20)
|
|
2909
|
+
# The self-error CANDIDATE debt is an informational, non-actionable signal
|
|
2910
|
+
# (low confidence; recorded for audit/dedup, never a learning). It must not
|
|
2911
|
+
# flip an otherwise-clean close into "done_with_debts" — that would be the
|
|
2912
|
+
# exact kind of noise/debt Francisco rejects.
|
|
2913
|
+
status_debts = [
|
|
2914
|
+
debt for debt in open_debts if debt.get("debt_type") != "self_error_candidate"
|
|
2915
|
+
]
|
|
2726
2916
|
|
|
2727
2917
|
status = "clean"
|
|
2728
2918
|
next_action = "Task closed cleanly."
|
|
2729
|
-
if
|
|
2919
|
+
if status_debts:
|
|
2730
2920
|
if clean_outcome == "done":
|
|
2731
2921
|
status = "done_with_debts"
|
|
2732
2922
|
next_action = "Task closed as done, but resolve the open protocol debt next."
|
|
@@ -2778,6 +2968,8 @@ def handle_task_close(
|
|
|
2778
2968
|
"memory_event": memory_event,
|
|
2779
2969
|
"memory_event_ok": bool(memory_event and memory_event.get("ok")),
|
|
2780
2970
|
}
|
|
2971
|
+
if self_error:
|
|
2972
|
+
response["self_error"] = self_error
|
|
2781
2973
|
if durable_checkpoint:
|
|
2782
2974
|
response["durable_checkpoint"] = durable_checkpoint
|
|
2783
2975
|
return json.dumps(response, ensure_ascii=False, indent=2)
|