nexo-brain 7.17.3 → 7.17.4
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 +3 -1
- package/package.json +1 -1
- package/src/agent_runner.py +77 -16
- package/src/doctor/providers/runtime.py +158 -0
- package/src/scripts/check-context.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
3
|
+
"version": "7.17.4",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.17.
|
|
21
|
+
Version `7.17.4` is the current packaged-runtime line. Corrective patch over v7.17.3 - automation runners now keep full NEXO discipline for real background agents while strict JSON children stay clean, and runtime doctor/metrics expose caller coverage and Guardian injection telemetry instead of hiding blind spots.
|
|
22
|
+
|
|
23
|
+
Previously in `7.17.3`: corrective patch over v7.17.2 - standalone Brain install/update no longer aborts when the Desktop-only `qwen3-0.6b-q4-local-presence` model is not bundled or already cached locally. Required Brain warmups stay strict; only the optional local-presence GGUF now degrades cleanly.
|
|
22
24
|
|
|
23
25
|
Previously in `7.17.2`: patch release over v7.17.1 - email-monitor now guards its `/tmp/nexo-*` draft buffers before writing, morning-agent closes interrupted/stale briefing claims deterministically, and Codex managed config migrates from the legacy `codex_hooks` flag to `[features].hooks`.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
3
|
+
"version": "7.17.4",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/agent_runner.py
CHANGED
|
@@ -273,6 +273,7 @@ def _record_automation_end(
|
|
|
273
273
|
{
|
|
274
274
|
"warnings": (telemetry or {}).get("warnings") or [],
|
|
275
275
|
"raw": (telemetry or {}).get("raw") or {},
|
|
276
|
+
"automation_contract": (telemetry or {}).get("automation_contract") or "",
|
|
276
277
|
},
|
|
277
278
|
ensure_ascii=False,
|
|
278
279
|
)
|
|
@@ -922,10 +923,11 @@ def _build_codex_prompt(
|
|
|
922
923
|
output_format: str = "",
|
|
923
924
|
append_system_prompt: str = "",
|
|
924
925
|
allowed_tools: str = "",
|
|
926
|
+
include_protocol_contract: bool = True,
|
|
925
927
|
) -> str:
|
|
926
|
-
protocol_contract = render_core_prompt("codex-protocol-contract")
|
|
927
928
|
instructions: list[str] = []
|
|
928
|
-
|
|
929
|
+
if include_protocol_contract:
|
|
930
|
+
instructions.append(render_core_prompt("codex-protocol-contract"))
|
|
929
931
|
if append_system_prompt:
|
|
930
932
|
instructions.append(f"SYSTEM INSTRUCTIONS:\n{append_system_prompt}")
|
|
931
933
|
if output_format and output_format.lower() == "text":
|
|
@@ -1047,6 +1049,35 @@ BARE_MODE_SAFE_CALLERS: frozenset[str] = frozenset({
|
|
|
1047
1049
|
"deep-sleep/synthesize",
|
|
1048
1050
|
})
|
|
1049
1051
|
|
|
1052
|
+
# Execution contracts keep background agents disciplined without polluting
|
|
1053
|
+
# machine-only child calls that must return strict JSON.
|
|
1054
|
+
AUTOMATION_CONTRACT_FULL_NEXO_AGENT = "full_nexo_agent"
|
|
1055
|
+
AUTOMATION_CONTRACT_STRICT_CHILD = "strict_child_json"
|
|
1056
|
+
AUTOMATION_CONTRACT_PUBLIC_CHILD = "public_isolated_child"
|
|
1057
|
+
AUTOMATION_CONTRACT_DEFAULT = AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1058
|
+
|
|
1059
|
+
FULL_NEXO_AGENT_CALLERS: frozenset[str] = frozenset({
|
|
1060
|
+
"catchup/morning",
|
|
1061
|
+
"daily_self_audit",
|
|
1062
|
+
"email_monitor",
|
|
1063
|
+
"evolution/run",
|
|
1064
|
+
"followup_runner",
|
|
1065
|
+
"immune/scan",
|
|
1066
|
+
"postmortem_consolidator",
|
|
1067
|
+
"sleep/nightly",
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
STRICT_CHILD_CALLERS: frozenset[str] = frozenset({
|
|
1071
|
+
"automation_probe",
|
|
1072
|
+
"check_context",
|
|
1073
|
+
"deep-sleep/extract",
|
|
1074
|
+
"deep-sleep/synthesize",
|
|
1075
|
+
"learning_validator",
|
|
1076
|
+
"morning_agent",
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
PUBLIC_CHILD_CALLERS: frozenset[str] = frozenset()
|
|
1080
|
+
|
|
1050
1081
|
MACHINE_ONLY_LANGUAGE_CONTRACT_CALLERS: frozenset[str] = frozenset({
|
|
1051
1082
|
"automation_probe",
|
|
1052
1083
|
"check_context",
|
|
@@ -1054,6 +1085,19 @@ MACHINE_ONLY_LANGUAGE_CONTRACT_CALLERS: frozenset[str] = frozenset({
|
|
|
1054
1085
|
})
|
|
1055
1086
|
|
|
1056
1087
|
|
|
1088
|
+
def _automation_contract_for_caller(caller: str) -> str:
|
|
1089
|
+
clean = str(caller or "").strip()
|
|
1090
|
+
if clean in STRICT_CHILD_CALLERS:
|
|
1091
|
+
return AUTOMATION_CONTRACT_STRICT_CHILD
|
|
1092
|
+
if clean in PUBLIC_CHILD_CALLERS:
|
|
1093
|
+
return AUTOMATION_CONTRACT_PUBLIC_CHILD
|
|
1094
|
+
return AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _caller_uses_global_discipline(caller: str) -> bool:
|
|
1098
|
+
return _automation_contract_for_caller(caller) == AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1099
|
+
|
|
1100
|
+
|
|
1057
1101
|
def _should_apply_operator_language_contract(caller: str) -> bool:
|
|
1058
1102
|
clean = str(caller or "").strip()
|
|
1059
1103
|
if not clean:
|
|
@@ -1140,7 +1184,8 @@ def run_automation_prompt(
|
|
|
1140
1184
|
if mapped_effort and not reasoning_effort:
|
|
1141
1185
|
reasoning_effort = mapped_effort
|
|
1142
1186
|
|
|
1143
|
-
|
|
1187
|
+
automation_contract = _automation_contract_for_caller(caller)
|
|
1188
|
+
enforcement_fragment = _build_enforcement_system_prompt() if _caller_uses_global_discipline(caller) else ""
|
|
1144
1189
|
if enforcement_fragment:
|
|
1145
1190
|
if append_system_prompt:
|
|
1146
1191
|
append_system_prompt = append_system_prompt + "\n\n" + enforcement_fragment
|
|
@@ -1239,19 +1284,29 @@ def run_automation_prompt(
|
|
|
1239
1284
|
if allowed_tools:
|
|
1240
1285
|
cmd.extend(["--allowedTools", allowed_tools])
|
|
1241
1286
|
cmd.extend(extra_args)
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1287
|
+
if _caller_uses_global_discipline(caller):
|
|
1288
|
+
try:
|
|
1289
|
+
import sys as _sys
|
|
1290
|
+
if str(NEXO_HOME) not in _sys.path:
|
|
1291
|
+
_sys.path.insert(0, str(NEXO_HOME))
|
|
1292
|
+
from enforcement_engine import run_with_enforcement
|
|
1293
|
+
result = run_with_enforcement(
|
|
1294
|
+
cmd,
|
|
1295
|
+
prompt=prompt,
|
|
1296
|
+
cwd=str(cwd_path),
|
|
1297
|
+
env=run_env,
|
|
1298
|
+
timeout=timeout,
|
|
1299
|
+
)
|
|
1300
|
+
except ImportError:
|
|
1301
|
+
result = subprocess.run(
|
|
1302
|
+
cmd,
|
|
1303
|
+
cwd=str(cwd_path),
|
|
1304
|
+
capture_output=True,
|
|
1305
|
+
text=True,
|
|
1306
|
+
timeout=timeout,
|
|
1307
|
+
env=run_env,
|
|
1308
|
+
)
|
|
1309
|
+
else:
|
|
1255
1310
|
result = subprocess.run(
|
|
1256
1311
|
cmd,
|
|
1257
1312
|
cwd=str(cwd_path),
|
|
@@ -1264,6 +1319,7 @@ def run_automation_prompt(
|
|
|
1264
1319
|
result.stdout or "",
|
|
1265
1320
|
requested_output_format=requested_output_format,
|
|
1266
1321
|
)
|
|
1322
|
+
telemetry["automation_contract"] = automation_contract
|
|
1267
1323
|
recorded, record_error = _record_automation_run(
|
|
1268
1324
|
backend=selected_backend,
|
|
1269
1325
|
task_profile=task_profile,
|
|
@@ -1323,6 +1379,7 @@ def run_automation_prompt(
|
|
|
1323
1379
|
output_format=output_format,
|
|
1324
1380
|
append_system_prompt=append_system_prompt,
|
|
1325
1381
|
allowed_tools=allowed_tools,
|
|
1382
|
+
include_protocol_contract=_caller_uses_global_discipline(caller),
|
|
1326
1383
|
)
|
|
1327
1384
|
)
|
|
1328
1385
|
result = subprocess.run(
|
|
@@ -1340,6 +1397,7 @@ def run_automation_prompt(
|
|
|
1340
1397
|
final_stdout=stdout,
|
|
1341
1398
|
model=resolved_model,
|
|
1342
1399
|
)
|
|
1400
|
+
telemetry["automation_contract"] = automation_contract
|
|
1343
1401
|
recorded, record_error = _record_automation_run(
|
|
1344
1402
|
backend=selected_backend,
|
|
1345
1403
|
task_profile=task_profile,
|
|
@@ -1351,6 +1409,9 @@ def run_automation_prompt(
|
|
|
1351
1409
|
returncode=result.returncode,
|
|
1352
1410
|
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
1353
1411
|
telemetry=telemetry,
|
|
1412
|
+
caller=caller,
|
|
1413
|
+
session_type="headless",
|
|
1414
|
+
resonance_tier=resonance_tier,
|
|
1354
1415
|
)
|
|
1355
1416
|
stderr = result.stderr or ""
|
|
1356
1417
|
if not recorded:
|
|
@@ -55,6 +55,15 @@ OPTIONALS_FILE = paths.config_dir() / "optionals.json"
|
|
|
55
55
|
SCHEDULE_FILE = paths.config_dir() / "schedule.json"
|
|
56
56
|
PACKAGE_JSON = NEXO_CODE / "package.json"
|
|
57
57
|
CHANGELOG_FILE = NEXO_CODE / "CHANGELOG.md"
|
|
58
|
+
CORE_AUTOMATION_CALLERS_BY_CRON = {
|
|
59
|
+
"catchup": ("catchup/morning",),
|
|
60
|
+
"deep-sleep": ("deep-sleep/extract", "deep-sleep/synthesize"),
|
|
61
|
+
"email-monitor": ("email_monitor",),
|
|
62
|
+
"evolution": ("evolution/run",),
|
|
63
|
+
"followup-runner": ("followup_runner",),
|
|
64
|
+
"morning-agent": ("morning_agent",),
|
|
65
|
+
"sleep": ("sleep/nightly",),
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
|
|
60
69
|
def _evolution_objective_payload() -> dict:
|
|
@@ -3676,6 +3685,154 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3676
3685
|
)
|
|
3677
3686
|
|
|
3678
3687
|
|
|
3688
|
+
def check_automation_caller_coverage(days: int = 7) -> DoctorCheck:
|
|
3689
|
+
"""Compare core cron activity with caller-attributed automation rows.
|
|
3690
|
+
|
|
3691
|
+
This is a support signal, not proof of failure: a cron can legitimately
|
|
3692
|
+
have no model call when there is no work. It still catches the regression
|
|
3693
|
+
class where a runner branch records rows without caller/session context.
|
|
3694
|
+
"""
|
|
3695
|
+
db_path = paths.db_path()
|
|
3696
|
+
if not db_path.is_file():
|
|
3697
|
+
return DoctorCheck(
|
|
3698
|
+
id="runtime.automation_caller_coverage",
|
|
3699
|
+
tier="runtime",
|
|
3700
|
+
status="degraded",
|
|
3701
|
+
severity="warn",
|
|
3702
|
+
summary="Automation caller coverage DB is missing",
|
|
3703
|
+
evidence=[str(db_path)],
|
|
3704
|
+
repair_plan=["Run NEXO once so migrations create the shared runtime DB"],
|
|
3705
|
+
escalation_prompt="Support cannot compare core cron activity with automation caller traces.",
|
|
3706
|
+
)
|
|
3707
|
+
|
|
3708
|
+
expected_crons = tuple(CORE_AUTOMATION_CALLERS_BY_CRON)
|
|
3709
|
+
placeholders = ",".join("?" for _ in expected_crons)
|
|
3710
|
+
|
|
3711
|
+
try:
|
|
3712
|
+
conn = sqlite3.connect(str(db_path), timeout=2)
|
|
3713
|
+
conn.row_factory = sqlite3.Row
|
|
3714
|
+
try:
|
|
3715
|
+
tables = {
|
|
3716
|
+
row["name"]
|
|
3717
|
+
for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
|
3718
|
+
}
|
|
3719
|
+
missing_tables = [name for name in ("cron_runs", "automation_runs") if name not in tables]
|
|
3720
|
+
if missing_tables:
|
|
3721
|
+
return DoctorCheck(
|
|
3722
|
+
id="runtime.automation_caller_coverage",
|
|
3723
|
+
tier="runtime",
|
|
3724
|
+
status="degraded",
|
|
3725
|
+
severity="warn",
|
|
3726
|
+
summary="Automation caller coverage schema is incomplete",
|
|
3727
|
+
evidence=[f"missing_tables={','.join(missing_tables)}"],
|
|
3728
|
+
repair_plan=["Run NEXO migrations before trusting automation caller coverage"],
|
|
3729
|
+
escalation_prompt="Core automation activity cannot be matched to caller-attributed model runs.",
|
|
3730
|
+
)
|
|
3731
|
+
cron_rows = conn.execute(
|
|
3732
|
+
f"""
|
|
3733
|
+
SELECT cron_id, COUNT(*) AS runs, MAX(started_at) AS last_started
|
|
3734
|
+
FROM cron_runs
|
|
3735
|
+
WHERE started_at >= datetime('now', ?)
|
|
3736
|
+
AND cron_id IN ({placeholders})
|
|
3737
|
+
GROUP BY cron_id
|
|
3738
|
+
ORDER BY cron_id
|
|
3739
|
+
""",
|
|
3740
|
+
(f"-{days} days", *expected_crons),
|
|
3741
|
+
).fetchall()
|
|
3742
|
+
caller_rows = conn.execute(
|
|
3743
|
+
"""
|
|
3744
|
+
SELECT caller, COUNT(*) AS runs,
|
|
3745
|
+
MAX(COALESCE(started_at, created_at)) AS last_seen
|
|
3746
|
+
FROM automation_runs
|
|
3747
|
+
WHERE COALESCE(started_at, created_at) >= datetime('now', ?)
|
|
3748
|
+
GROUP BY caller
|
|
3749
|
+
""",
|
|
3750
|
+
(f"-{days} days",),
|
|
3751
|
+
).fetchall()
|
|
3752
|
+
finally:
|
|
3753
|
+
conn.close()
|
|
3754
|
+
except Exception as exc:
|
|
3755
|
+
return DoctorCheck(
|
|
3756
|
+
id="runtime.automation_caller_coverage",
|
|
3757
|
+
tier="runtime",
|
|
3758
|
+
status="degraded",
|
|
3759
|
+
severity="warn",
|
|
3760
|
+
summary="Automation caller coverage is unreadable",
|
|
3761
|
+
evidence=[str(exc)],
|
|
3762
|
+
repair_plan=["Inspect runtime DB cron_runs and automation_runs tables"],
|
|
3763
|
+
escalation_prompt="Support cannot verify whether core automations are leaving caller traces.",
|
|
3764
|
+
)
|
|
3765
|
+
|
|
3766
|
+
caller_counts = {str(row["caller"] or ""): int(row["runs"] or 0) for row in caller_rows}
|
|
3767
|
+
caller_last_seen = {str(row["caller"] or ""): str(row["last_seen"] or "") for row in caller_rows}
|
|
3768
|
+
cron_rows = list(cron_rows)
|
|
3769
|
+
|
|
3770
|
+
evidence = [f"window={days}d", f"core_crons_seen={len(cron_rows)}"]
|
|
3771
|
+
missing_matches: list[str] = []
|
|
3772
|
+
for row in cron_rows:
|
|
3773
|
+
cron_id = str(row["cron_id"] or "")
|
|
3774
|
+
expected_callers = CORE_AUTOMATION_CALLERS_BY_CRON.get(cron_id, ())
|
|
3775
|
+
matched = [caller for caller in expected_callers if caller_counts.get(caller, 0) > 0]
|
|
3776
|
+
if matched:
|
|
3777
|
+
evidence.append(
|
|
3778
|
+
f"{cron_id}: cron_runs={int(row['runs'] or 0)} caller_runs="
|
|
3779
|
+
+ ",".join(f"{caller}:{caller_counts[caller]}" for caller in matched)
|
|
3780
|
+
)
|
|
3781
|
+
else:
|
|
3782
|
+
expected = "|".join(expected_callers)
|
|
3783
|
+
missing_matches.append(cron_id)
|
|
3784
|
+
evidence.append(
|
|
3785
|
+
f"{cron_id}: cron_runs={int(row['runs'] or 0)} no_matching_caller={expected} "
|
|
3786
|
+
f"last_started={row['last_started']}"
|
|
3787
|
+
)
|
|
3788
|
+
|
|
3789
|
+
empty_caller_runs = caller_counts.get("", 0)
|
|
3790
|
+
if empty_caller_runs:
|
|
3791
|
+
evidence.append(f"empty_caller_runs={empty_caller_runs}")
|
|
3792
|
+
for caller in sorted(caller_counts):
|
|
3793
|
+
if caller:
|
|
3794
|
+
evidence.append(f"caller_seen={caller}:{caller_counts[caller]} last={caller_last_seen.get(caller, '')}")
|
|
3795
|
+
|
|
3796
|
+
if not cron_rows:
|
|
3797
|
+
return DoctorCheck(
|
|
3798
|
+
id="runtime.automation_caller_coverage",
|
|
3799
|
+
tier="runtime",
|
|
3800
|
+
status="healthy",
|
|
3801
|
+
severity="info",
|
|
3802
|
+
summary="No recent core automation cron activity to match",
|
|
3803
|
+
evidence=evidence,
|
|
3804
|
+
repair_plan=[],
|
|
3805
|
+
escalation_prompt="",
|
|
3806
|
+
)
|
|
3807
|
+
|
|
3808
|
+
status = "healthy"
|
|
3809
|
+
severity = "info"
|
|
3810
|
+
repair_plan: list[str] = []
|
|
3811
|
+
if empty_caller_runs or missing_matches:
|
|
3812
|
+
status = "degraded"
|
|
3813
|
+
severity = "warn"
|
|
3814
|
+
repair_plan.append(
|
|
3815
|
+
"If the cron had work, inspect run_automation_prompt caller attribution; if it was a no-op tick, no user action is needed"
|
|
3816
|
+
)
|
|
3817
|
+
|
|
3818
|
+
return DoctorCheck(
|
|
3819
|
+
id="runtime.automation_caller_coverage",
|
|
3820
|
+
tier="runtime",
|
|
3821
|
+
status=status,
|
|
3822
|
+
severity=severity,
|
|
3823
|
+
summary=(
|
|
3824
|
+
"Core automation caller coverage looks healthy"
|
|
3825
|
+
if status == "healthy"
|
|
3826
|
+
else "Core automation caller coverage needs review"
|
|
3827
|
+
),
|
|
3828
|
+
evidence=evidence,
|
|
3829
|
+
repair_plan=repair_plan,
|
|
3830
|
+
escalation_prompt=(
|
|
3831
|
+
"A core automation cron ran without a matching caller-attributed automation row in the selected window."
|
|
3832
|
+
) if status != "healthy" else "",
|
|
3833
|
+
)
|
|
3834
|
+
|
|
3835
|
+
|
|
3679
3836
|
def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
3680
3837
|
"""Run all runtime-tier checks. Read-only by default."""
|
|
3681
3838
|
return [
|
|
@@ -3695,6 +3852,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
|
3695
3852
|
safe_check(check_client_assumption_regressions),
|
|
3696
3853
|
safe_check(check_protocol_compliance),
|
|
3697
3854
|
safe_check(check_automation_telemetry),
|
|
3855
|
+
safe_check(check_automation_caller_coverage),
|
|
3698
3856
|
safe_check(check_state_watchers),
|
|
3699
3857
|
safe_check(check_release_artifact_sync),
|
|
3700
3858
|
safe_check(check_release_trace_hygiene),
|
|
@@ -190,7 +190,7 @@ def smart_check(action_description: str, context: str = "") -> dict:
|
|
|
190
190
|
timeout=300,
|
|
191
191
|
output_format="text",
|
|
192
192
|
append_system_prompt=render_core_prompt("json-object-only"),
|
|
193
|
-
allowed_tools="Read,
|
|
193
|
+
allowed_tools="Read,Glob,Grep",
|
|
194
194
|
)
|
|
195
195
|
if result.returncode == 0:
|
|
196
196
|
parsed = _extract_json(result.stdout)
|