nexo-brain 7.30.23 → 7.30.25

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.30.23",
3
+ "version": "7.30.25",
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.30.23` is the current packaged-runtime line. Patch release over v7.30.22 - Brain now ships Opportunity Orchestrator Phase 1 for evidence-backed proactive preparation, read-only dry-run reports, bounded proposal queues, and feedback/suppression controls.
21
+ Version `7.30.25` is the current packaged-runtime line. Patch release over v7.30.24 - Desktop-facing maintenance diagnostics now self-heal or stay informational when the runtime is healthy, and watchdog uses the canonical cognitive DB layout.
22
+
23
+ Previously in `7.30.24`: patch release over v7.30.23 - client sync now stages and verifies managed MCP providers before writing Brain-owned default MCP entries into Claude, Codex, or Desktop configs.
24
+
25
+ Previously in `7.30.23`: patch release over v7.30.22 - Brain ships Opportunity Orchestrator Phase 1 for evidence-backed proactive preparation, read-only dry-run reports, bounded proposal queues, and feedback/suppression controls.
22
26
 
23
27
  Previously in `7.30.20`: patch release over v7.30.19 - packaged installs now copy the `product_knowledge` package into the installed runtime so `nexo update` can import the new product knowledge tools.
24
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.23",
3
+ "version": "7.30.25",
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",
@@ -24,11 +24,21 @@ from bootstrap_docs import sync_client_bootstrap
24
24
  from runtime_home import resolve_nexo_home
25
25
 
26
26
  try:
27
- from managed_mcp import build_managed_server_entries, merge_json_mcp_servers, merge_toml_mcp_servers
27
+ from managed_mcp import (
28
+ build_managed_server_entries,
29
+ merge_json_mcp_servers,
30
+ merge_toml_mcp_servers,
31
+ reconcile_managed_mcp,
32
+ remove_json_nexo_managed_mcp_servers,
33
+ remove_toml_nexo_managed_mcp_servers,
34
+ )
28
35
  except Exception:
29
36
  build_managed_server_entries = None
30
37
  merge_json_mcp_servers = None
31
38
  merge_toml_mcp_servers = None
39
+ reconcile_managed_mcp = None
40
+ remove_json_nexo_managed_mcp_servers = None
41
+ remove_toml_nexo_managed_mcp_servers = None
32
42
 
33
43
  try:
34
44
  from client_preferences import (
@@ -842,7 +852,7 @@ def build_server_config(
842
852
  def _managed_mcp_entries_for(client: str, server_config: dict) -> dict:
843
853
  if str(os.environ.get("NEXO_MANAGED_MCP_DISABLE", "")).strip().lower() in {"1", "true", "yes", "on"}:
844
854
  return {}
845
- if not build_managed_server_entries:
855
+ if not build_managed_server_entries or not reconcile_managed_mcp:
846
856
  return {}
847
857
  env = server_config.get("env") if isinstance(server_config.get("env"), dict) else {}
848
858
  nexo_home = str(env.get("NEXO_HOME") or "").strip()
@@ -855,16 +865,51 @@ def _managed_mcp_entries_for(client: str, server_config: dict) -> dict:
855
865
  or ""
856
866
  ).strip()
857
867
  try:
858
- return build_managed_server_entries(
868
+ plan = reconcile_managed_mcp(
869
+ nexo_home=nexo_home,
870
+ runtime_root=runtime_root or None,
871
+ clients=[client],
872
+ apply=True,
873
+ platform=target_platform or None,
874
+ )
875
+ providers = plan.get("providers") if isinstance(plan.get("providers"), dict) else {}
876
+ if not plan.get("ok") or not providers:
877
+ return {}
878
+ entries = build_managed_server_entries(
859
879
  client=client,
860
880
  nexo_home=nexo_home,
861
881
  runtime_root=runtime_root or None,
862
882
  platform=target_platform or None,
863
883
  )
884
+ required_providers = {
885
+ str((entry.get("nexo") or {}).get("provider_id") or "").strip()
886
+ for entry in entries.values()
887
+ if isinstance(entry, dict)
888
+ }
889
+ required_providers.discard("")
890
+ if not required_providers:
891
+ return {}
892
+ for provider_id in required_providers:
893
+ state = providers.get(provider_id) if isinstance(providers.get(provider_id), dict) else {}
894
+ if state.get("status") != "healthy":
895
+ return {}
896
+ return entries
864
897
  except Exception:
865
898
  return {}
866
899
 
867
900
 
901
+ def _remove_unhealthy_json_managed_mcp(payload: dict) -> dict:
902
+ if not remove_json_nexo_managed_mcp_servers:
903
+ return payload
904
+ return remove_json_nexo_managed_mcp_servers(payload)
905
+
906
+
907
+ def _remove_unhealthy_toml_managed_mcp(payload: dict) -> dict:
908
+ if not remove_toml_nexo_managed_mcp_servers:
909
+ return payload
910
+ return remove_toml_nexo_managed_mcp_servers(payload)
911
+
912
+
868
913
  def _claude_code_settings_path(home: Path | None = None) -> Path:
869
914
  base = home or _user_home()
870
915
  return base / ".claude" / "settings.json"
@@ -1039,6 +1084,10 @@ def _sync_codex_managed_config(
1039
1084
  payload = merge_toml_mcp_servers(payload, managed_entries)
1040
1085
  codex_table = payload.setdefault("nexo", {}).setdefault("codex", {})
1041
1086
  codex_table["managed_default_mcp_count"] = len(managed_entries)
1087
+ else:
1088
+ payload = _remove_unhealthy_toml_managed_mcp(payload)
1089
+ codex_table = payload.setdefault("nexo", {}).setdefault("codex", {})
1090
+ codex_table["managed_default_mcp_count"] = 0
1042
1091
 
1043
1092
  # Ensure Codex headless crons (followup-runner, email-monitor, deep-sleep,
1044
1093
  # etc.) do not stall on approval prompts. Only set defaults when the user
@@ -1056,7 +1105,7 @@ def _sync_codex_managed_config(
1056
1105
  "hooks_enabled": True,
1057
1106
  "model": runtime_profile.get("model", ""),
1058
1107
  "reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
1059
- "managed_default_mcp_count": len(_managed_mcp_entries_for("codex", server_config)) if server_config else 0,
1108
+ "managed_default_mcp_count": len(managed_entries) if server_config else 0,
1060
1109
  }
1061
1110
 
1062
1111
 
@@ -1450,6 +1499,8 @@ def _sync_json_client(path: Path, server_config: dict, label: str, *, managed_me
1450
1499
  managed_entries = _managed_mcp_entries_for(label, server_config)
1451
1500
  if managed_entries and merge_json_mcp_servers:
1452
1501
  payload = merge_json_mcp_servers(payload, managed_entries)
1502
+ else:
1503
+ payload = _remove_unhealthy_json_managed_mcp(payload)
1453
1504
  _write_json_object(path, payload)
1454
1505
  return {
1455
1506
  "ok": True,
@@ -1537,6 +1588,8 @@ def _sync_claude_code_settings(path: Path, server_config: dict) -> dict:
1537
1588
  managed_entries = _managed_mcp_entries_for("claude_code", server_config)
1538
1589
  if managed_entries and merge_json_mcp_servers:
1539
1590
  payload = merge_json_mcp_servers(payload, managed_entries)
1591
+ else:
1592
+ payload = _remove_unhealthy_json_managed_mcp(payload)
1540
1593
  _ensure_headless_permissions(payload)
1541
1594
  _write_json_object(path, payload)
1542
1595
  return {
@@ -50,6 +50,7 @@ WATCHDOG_FRESHNESS = 3600 # 1 hour (runs every 30 min)
50
50
  RUNNER_HEALTH_FRESHNESS = 43200 # 12 hours (runner-health-check runs every 6h)
51
51
  DEFAULT_CRON_THRESHOLD = 7200 # Fallback when manifest data is unavailable
52
52
  LIVE_PROTOCOL_SESSION_FRESHNESS = 1800 # 30 minutes
53
+ AUTOMATION_TELEMETRY_USAGE_WARN_COVERAGE = 99.0
53
54
  SPECIAL_ENV_NORMALIZE_IDS = {"prevent-sleep", "tcc-approve"}
54
55
  OPTIONALS_FILE = paths.config_dir() / "optionals.json"
55
56
  SCHEDULE_FILE = paths.config_dir() / "schedule.json"
@@ -1071,6 +1072,43 @@ def _run_at_load_cron_ids() -> set[str]:
1071
1072
  return ids
1072
1073
 
1073
1074
 
1075
+ def _launchd_calendar_weekdays(schedule: dict) -> list[int]:
1076
+ raw = schedule.get("weekdays") or schedule.get("Weekdays")
1077
+ if raw is None and "weekday" in schedule:
1078
+ raw = [schedule.get("weekday")]
1079
+ if raw is None and "Weekday" in schedule:
1080
+ raw = [schedule.get("Weekday")]
1081
+ if isinstance(raw, str):
1082
+ parts = [part.strip() for part in raw.replace("+", ",").split(",")]
1083
+ elif isinstance(raw, (list, tuple, set)):
1084
+ parts = list(raw)
1085
+ else:
1086
+ return []
1087
+ selected: set[int] = set()
1088
+ for part in parts:
1089
+ try:
1090
+ selected.add(int(part) % 7)
1091
+ except Exception:
1092
+ continue
1093
+ if len(selected) >= 7:
1094
+ return []
1095
+ return [day for day in (1, 2, 3, 4, 5, 6, 0) if day in selected]
1096
+
1097
+
1098
+ def _launchd_calendar_interval(schedule: dict) -> dict | list[dict]:
1099
+ base = {}
1100
+ if "hour" in schedule:
1101
+ base["Hour"] = schedule["hour"]
1102
+ if "minute" in schedule:
1103
+ base["Minute"] = schedule["minute"]
1104
+ weekdays = _launchd_calendar_weekdays(schedule)
1105
+ if weekdays:
1106
+ if len(weekdays) == 1:
1107
+ return {**base, "Weekday": weekdays[0]}
1108
+ return [{**base, "Weekday": day} for day in weekdays]
1109
+ return base
1110
+
1111
+
1074
1112
  def _launchagent_schedule_expectations() -> dict[str, dict]:
1075
1113
  expectations = {}
1076
1114
  for cron in _enabled_manifest_crons():
@@ -1096,14 +1134,7 @@ def _launchagent_schedule_expectations() -> dict[str, dict]:
1096
1134
  expected["schedule_configured"] = True
1097
1135
  elif "schedule" in cron:
1098
1136
  schedule = resolve_declared_schedule(cron)
1099
- cal = {}
1100
- if "hour" in schedule:
1101
- cal["Hour"] = schedule["hour"]
1102
- if "minute" in schedule:
1103
- cal["Minute"] = schedule["minute"]
1104
- if "weekday" in schedule:
1105
- cal["Weekday"] = schedule["weekday"]
1106
- expected["StartCalendarInterval"] = cal
1137
+ expected["StartCalendarInterval"] = _launchd_calendar_interval(schedule)
1107
1138
  expected["RunAtLoad"] = True if should_run_at_load(cron) else None
1108
1139
  expected["schedule_configured"] = True
1109
1140
  elif should_run_at_load(cron):
@@ -3380,6 +3411,7 @@ def check_release_trace_hygiene() -> DoctorCheck:
3380
3411
  evidence=[],
3381
3412
  repair_plan=[],
3382
3413
  escalation_prompt="",
3414
+ category="operator_history",
3383
3415
  )
3384
3416
 
3385
3417
  try:
@@ -3402,6 +3434,7 @@ def check_release_trace_hygiene() -> DoctorCheck:
3402
3434
  evidence=[],
3403
3435
  repair_plan=[],
3404
3436
  escalation_prompt="",
3437
+ category="operator_history",
3405
3438
  )
3406
3439
 
3407
3440
  stale_run_samples: list[str] = []
@@ -3465,6 +3498,7 @@ def check_release_trace_hygiene() -> DoctorCheck:
3465
3498
  evidence=[str(exc)],
3466
3499
  repair_plan=["Inspect workflow_goals/workflow_runs state manually"],
3467
3500
  escalation_prompt="Release traces could not be audited, so stale audit artifacts may be hiding in the runtime.",
3501
+ category="operator_history",
3468
3502
  )
3469
3503
 
3470
3504
  evidence = [
@@ -3486,6 +3520,7 @@ def check_release_trace_hygiene() -> DoctorCheck:
3486
3520
  "Keep workflow/goal state aligned with the real shipped state after releases",
3487
3521
  ],
3488
3522
  escalation_prompt="Audit/release traces drifted away from reality, which makes shipping state look ambiguous.",
3523
+ category="operator_history",
3489
3524
  )
3490
3525
  return DoctorCheck(
3491
3526
  id="runtime.release_trace_hygiene",
@@ -3496,6 +3531,7 @@ def check_release_trace_hygiene() -> DoctorCheck:
3496
3531
  evidence=evidence,
3497
3532
  repair_plan=[],
3498
3533
  escalation_prompt="",
3534
+ category="operator_history",
3499
3535
  )
3500
3536
 
3501
3537
 
@@ -3754,7 +3790,7 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
3754
3790
  status = "healthy"
3755
3791
  severity = "info"
3756
3792
  repair_plan: list[str] = []
3757
- if usage_coverage < 100.0 and missing_usage_runs > 1:
3793
+ if usage_coverage < AUTOMATION_TELEMETRY_USAGE_WARN_COVERAGE and missing_usage_runs > 1:
3758
3794
  status = "degraded"
3759
3795
  severity = "warn"
3760
3796
  repair_plan.append("Restore backend usage parsing so automation runs always emit token telemetry")
@@ -3933,10 +3969,21 @@ def check_local_index_hygiene(fix: bool = False) -> DoctorCheck:
3933
3969
  try:
3934
3970
  from local_context import api as local_context_api
3935
3971
 
3936
- try:
3937
- result = local_context_api.local_index_hygiene(fix=fix, quick=not fix)
3938
- except TypeError:
3939
- result = local_context_api.local_index_hygiene(fix=fix)
3972
+ result = None
3973
+ for attempt in range(3):
3974
+ try:
3975
+ try:
3976
+ result = local_context_api.local_index_hygiene(fix=fix, quick=not fix)
3977
+ except TypeError:
3978
+ result = local_context_api.local_index_hygiene(fix=fix)
3979
+ break
3980
+ except sqlite3.OperationalError as exc:
3981
+ if "locked" not in str(exc).lower() or attempt >= 2:
3982
+ raise
3983
+ time.sleep(0.2 * (attempt + 1))
3984
+ if result is None:
3985
+ raise RuntimeError("local_index_hygiene returned no result")
3986
+
3940
3987
  residue = result.get("residue") or {}
3941
3988
  cleanup = result.get("cleanup") or {}
3942
3989
  privacy = result.get("privacy") or {}
@@ -3958,13 +4005,18 @@ def check_local_index_hygiene(fix: bool = False) -> DoctorCheck:
3958
4005
  "privacy_truncated=" + str(privacy_truncated),
3959
4006
  ]
3960
4007
  evidence.extend(f"root={path}" for path in suspect_roots[:5])
3961
- if residue_total == 0 and privacy_residue_total == 0 and not suspect_roots and not privacy_truncated:
4008
+ if residue_total == 0 and privacy_residue_total == 0 and not suspect_roots:
4009
+ summary = (
4010
+ "Local memory index quick scan found no residue"
4011
+ if privacy_truncated
4012
+ else "Local memory index hygiene is clean"
4013
+ )
3962
4014
  return DoctorCheck(
3963
4015
  id="runtime.local_index_hygiene",
3964
4016
  tier="runtime",
3965
4017
  status="healthy",
3966
4018
  severity="info",
3967
- summary="Local memory index hygiene is clean",
4019
+ summary=summary,
3968
4020
  evidence=evidence,
3969
4021
  repair_plan=[],
3970
4022
  )
@@ -11,7 +11,12 @@ from .catalog import (
11
11
  provider_for_capability,
12
12
  validate_catalog_lock,
13
13
  )
14
- from .client_config import merge_json_mcp_servers, merge_toml_mcp_servers
14
+ from .client_config import (
15
+ merge_json_mcp_servers,
16
+ merge_toml_mcp_servers,
17
+ remove_json_nexo_managed_mcp_servers,
18
+ remove_toml_nexo_managed_mcp_servers,
19
+ )
15
20
  from .reconcile import managed_mcp_status, reconcile_managed_mcp
16
21
 
17
22
  __all__ = [
@@ -26,6 +31,8 @@ __all__ = [
26
31
  "validate_catalog_lock",
27
32
  "merge_json_mcp_servers",
28
33
  "merge_toml_mcp_servers",
34
+ "remove_json_nexo_managed_mcp_servers",
35
+ "remove_toml_nexo_managed_mcp_servers",
29
36
  "managed_mcp_status",
30
37
  "reconcile_managed_mcp",
31
38
  ]
@@ -11,6 +11,47 @@ def _is_nexo_owned(entry: Any) -> bool:
11
11
  return isinstance(meta, dict) and meta.get("owner") == "nexo"
12
12
 
13
13
 
14
+ def _managed_servers_metadata(container: dict[str, Any]) -> dict[str, Any]:
15
+ nexo_meta = container.get("nexo") if isinstance(container.get("nexo"), dict) else {}
16
+ managed = nexo_meta.get("managed_mcp") if isinstance(nexo_meta.get("managed_mcp"), dict) else {}
17
+ servers = managed.get("servers") if isinstance(managed.get("servers"), dict) else {}
18
+ return servers
19
+
20
+
21
+ def _remove_nexo_owned_by_metadata(servers: dict[str, Any], managed_servers: dict[str, Any]) -> None:
22
+ for name, meta in list(managed_servers.items()):
23
+ if not (isinstance(meta, dict) and meta.get("owner") == "nexo"):
24
+ continue
25
+ current = servers.get(name)
26
+ if current is None or _is_nexo_owned(current) or name in servers:
27
+ servers.pop(name, None)
28
+ managed_servers.pop(name, None)
29
+
30
+
31
+ def remove_json_nexo_managed_mcp_servers(payload: dict[str, Any]) -> dict[str, Any]:
32
+ result = deepcopy(payload) if isinstance(payload, dict) else {}
33
+ servers = result.get("mcpServers")
34
+ if not isinstance(servers, dict):
35
+ return result
36
+ managed_servers = _managed_servers_metadata(result)
37
+ for name, entry in list(servers.items()):
38
+ if _is_nexo_owned(entry):
39
+ servers.pop(name, None)
40
+ managed_servers.pop(name, None)
41
+ _remove_nexo_owned_by_metadata(servers, managed_servers)
42
+ return result
43
+
44
+
45
+ def remove_toml_nexo_managed_mcp_servers(payload: dict[str, Any]) -> dict[str, Any]:
46
+ result = deepcopy(payload) if isinstance(payload, dict) else {}
47
+ servers = result.get("mcp_servers")
48
+ if not isinstance(servers, dict):
49
+ return result
50
+ managed_servers = _managed_servers_metadata(result)
51
+ _remove_nexo_owned_by_metadata(servers, managed_servers)
52
+ return result
53
+
54
+
14
55
  def merge_json_mcp_servers(payload: dict[str, Any], entries: dict[str, dict[str, Any]]) -> dict[str, Any]:
15
56
  result = deepcopy(payload) if isinstance(payload, dict) else {}
16
57
  servers = result.setdefault("mcpServers", {})
@@ -23,6 +23,8 @@ LOG_DIR="$NEXO_HOME/runtime/logs"
23
23
  [ ! -d "$LOG_DIR" ] && [ -d "$NEXO_HOME/logs" ] && LOG_DIR="$NEXO_HOME/logs"
24
24
  DATA_DIR="$NEXO_HOME/runtime/data"
25
25
  [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ] && DATA_DIR="$NEXO_HOME/data"
26
+ COGNITIVE_DIR="$NEXO_HOME/runtime/cognitive"
27
+ [ ! -d "$COGNITIVE_DIR" ] && COGNITIVE_DIR="$DATA_DIR"
26
28
  BACKUP_DIR="$NEXO_HOME/runtime/backups"
27
29
  [ ! -d "$BACKUP_DIR" ] && [ -d "$NEXO_HOME/backups" ] && BACKUP_DIR="$NEXO_HOME/backups"
28
30
  DB_PATH="$DATA_DIR/nexo.db"
@@ -841,8 +843,7 @@ for monitor in "${MONITORS[@]}"; do
841
843
  fi
842
844
  fi
843
845
  elif [ "$age" -gt $(( max_stale * 3 )) ]; then
844
- [ "$status" = "PASS" ] && status="WARN"
845
- details="${details}In-flight for ${stale_age} (long-running, process alive). "
846
+ details="${details}In-flight for ${stale_age} (long-running, process alive; observing). "
846
847
  else
847
848
  details="${details}In-flight (started ${stale_age}). "
848
849
  fi
@@ -1206,7 +1207,10 @@ fi
1206
1207
  # --- Cognitive DB check ---
1207
1208
  COG_STATUS="PASS"
1208
1209
  COG_DETAIL=""
1209
- COG_DB="$DATA_DIR/cognitive.db"
1210
+ COG_DB="$COGNITIVE_DIR/cognitive.db"
1211
+ if [ ! -f "$COG_DB" ] && [ -f "$DATA_DIR/cognitive.db" ]; then
1212
+ COG_DB="$DATA_DIR/cognitive.db"
1213
+ fi
1210
1214
  if [ -f "$COG_DB" ]; then
1211
1215
  COG_INT=$(sqlite3 "$COG_DB" "PRAGMA integrity_check;" 2>/dev/null || echo "CORRUPT")
1212
1216
  if [ "$COG_INT" != "ok" ]; then