nexo-brain 7.17.3 → 7.17.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.17.3",
3
+ "version": "7.17.5",
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,11 @@
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.3` is the current packaged-runtime line. 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.
21
+ Version `7.17.5` is the current packaged-runtime line. Patch release over v7.17.4 - `nexo --version --json` now returns fast machine-readable update status (`installed`, `latest`, `hasUpdate`, `unknown`, `latestSource`) while the human output keeps the legacy `nexo vX` line plus a compact latest/installed status line for Desktop compatibility.
22
+
23
+ Previously in `7.17.4`: 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.
24
+
25
+ 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
26
 
23
27
  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
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.17.3",
3
+ "version": "7.17.5",
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",
@@ -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
- instructions.append(protocol_contract)
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
- enforcement_fragment = _build_enforcement_system_prompt()
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
- try:
1243
- import sys as _sys
1244
- if str(NEXO_HOME) not in _sys.path:
1245
- _sys.path.insert(0, str(NEXO_HOME))
1246
- from enforcement_engine import run_with_enforcement
1247
- result = run_with_enforcement(
1248
- cmd,
1249
- prompt=prompt,
1250
- cwd=str(cwd_path),
1251
- env=run_env,
1252
- timeout=timeout,
1253
- )
1254
- except ImportError:
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:
package/src/cli.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  Entry points:
5
5
  nexo chat [PATH]
6
+ nexo --version [--json]
6
7
  nexo export [PATH] [--json]
7
8
  nexo import-inspect PATH [--json]
8
9
  nexo import PATH [--json]
@@ -233,17 +234,41 @@ def _version_sort_key(raw: str) -> tuple[tuple[int, ...], int, str]:
233
234
  return (tuple(parts), 1 if not suffix else 0, suffix)
234
235
 
235
236
 
236
- def _version_status_line() -> str:
237
+ def _version_status_payload() -> dict:
237
238
  installed = _get_version()
238
239
  latest = _load_latest_version_cache()
240
+ latest_source = "cache" if latest else ""
239
241
  if latest is None and _should_refresh_latest_version():
240
242
  latest = _fetch_latest_version()
243
+ latest_source = "npm" if latest else ""
241
244
  if latest and installed and _version_sort_key(latest) < _version_sort_key(installed):
242
245
  latest = installed
246
+ latest_source = "installed"
243
247
  try:
244
248
  _save_latest_version_cache(installed)
245
249
  except Exception:
246
250
  pass
251
+ has_update = bool(
252
+ latest
253
+ and installed
254
+ and _version_sort_key(latest) > _version_sort_key(installed)
255
+ )
256
+ return {
257
+ "ok": True,
258
+ "name": "nexo",
259
+ "package": LATEST_NPM_PACKAGE,
260
+ "installed": installed,
261
+ "latest": latest or "",
262
+ "hasUpdate": has_update,
263
+ "unknown": not bool(installed and latest),
264
+ "latestSource": latest_source,
265
+ }
266
+
267
+
268
+ def _version_status_line(payload: dict | None = None) -> str:
269
+ payload = payload or _version_status_payload()
270
+ installed = str(payload.get("installed") or "?").strip()
271
+ latest = str(payload.get("latest") or "").strip()
247
272
  if latest:
248
273
  return f"NEXO Latest: v{latest} | Installed: v{installed}"
249
274
  return f"NEXO Installed: v{installed}"
@@ -2823,6 +2848,7 @@ def main():
2823
2848
  parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI", add_help=False)
2824
2849
  parser.add_argument("-h", "--help", action="store_true", help="Show help")
2825
2850
  parser.add_argument("-v", "--version", action="store_true", help="Show version")
2851
+ parser.add_argument("--json", action="store_true", help="JSON output for --version")
2826
2852
  sub = parser.add_subparsers(dest="command")
2827
2853
 
2828
2854
  # -- email (Plan F1 — interactive wizard for email accounts) --
@@ -3408,7 +3434,12 @@ def main():
3408
3434
  _print_help()
3409
3435
  return 0
3410
3436
  if args.version:
3411
- print(f"nexo v{_get_version()}")
3437
+ payload = _version_status_payload()
3438
+ if args.json:
3439
+ print(json.dumps(payload, ensure_ascii=False))
3440
+ else:
3441
+ print(f"nexo v{payload.get('installed') or _get_version()}")
3442
+ print(_version_status_line(payload))
3412
3443
  return 0
3413
3444
 
3414
3445
  if args.command == "email":
@@ -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,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
193
+ allowed_tools="Read,Glob,Grep",
194
194
  )
195
195
  if result.returncode == 0:
196
196
  parsed = _extract_json(result.stdout)