nexo-brain 5.3.23 → 5.3.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": "5.3.23",
3
+ "version": "5.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.3.23",
3
+ "version": "5.3.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",
@@ -580,7 +580,15 @@ def run_automation_prompt(
580
580
  raise AutomationBackendUnavailableError(
581
581
  "Claude Code automation backend selected but `claude` is not installed."
582
582
  )
583
- cmd = [claude_bin, "-p", prompt]
583
+ # Headless claude -p does NOT reliably honour permissions.allow from
584
+ # settings.json for MCP tool calls — it can stall waiting for an
585
+ # approval that will never come in non-interactive mode. All NEXO
586
+ # headless automation (followup-runner, email-monitor, deep-sleep,
587
+ # etc.) therefore must pass --dangerously-skip-permissions explicitly
588
+ # so the process actually runs instead of zombying. Interactive
589
+ # sessions (`nexo chat`) never go through this code path and keep
590
+ # their normal approval prompts.
591
+ cmd = [claude_bin, "-p", prompt, "--dangerously-skip-permissions"]
584
592
  if resolved_model:
585
593
  cmd.extend(["--model", resolved_model])
586
594
  if resolved_effort:
package/src/cli.py CHANGED
@@ -938,7 +938,21 @@ def _prompt_model_recommendations(*, interactive: bool) -> None:
938
938
  from model_defaults import detect_outdated_recommendations, client_default
939
939
 
940
940
  preferences = load_client_preferences()
941
- pending = detect_outdated_recommendations(preferences)
941
+ result = detect_outdated_recommendations(preferences)
942
+ pending = result.get("pending") or []
943
+ auto_ack = result.get("auto_ack") or {}
944
+
945
+ # Apply silent acknowledgements first (user already on current model, or
946
+ # has customized their model — either way no prompt needed). This avoids
947
+ # repeated stderr hints in cron/headless updates.
948
+ if auto_ack:
949
+ try:
950
+ existing_ack = dict(preferences.get("acknowledged_model_recommendations") or {})
951
+ existing_ack.update({k: int(v) for k, v in auto_ack.items()})
952
+ save_client_preferences(acknowledged_model_recommendations=existing_ack)
953
+ except Exception:
954
+ pass
955
+
942
956
  if not pending:
943
957
  return
944
958
 
@@ -955,7 +969,8 @@ def _prompt_model_recommendations(*, interactive: bool) -> None:
955
969
 
956
970
  updated_profiles = dict(preferences.get("client_runtime_profiles") or {})
957
971
  updated_ack = dict(preferences.get("acknowledged_model_recommendations") or {})
958
- changed = False
972
+ updated_ack.update({k: int(v) for k, v in auto_ack.items()})
973
+ changed = bool(auto_ack)
959
974
  for entry in pending:
960
975
  client = entry["client"]
961
976
  effort_str = f" / {entry['current_effort']}" if entry["current_effort"] else ""
@@ -126,9 +126,22 @@ def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
126
126
 
127
127
  def detect_outdated_recommendations(
128
128
  preferences: dict,
129
- ) -> list[dict]:
130
- """Return a list of clients whose user is on an older recommendation and
131
- hasn't acknowledged the current one. Entries shape:
129
+ ) -> dict:
130
+ """Classify clients into "needs interactive prompt" vs "silent ack".
131
+
132
+ Returns a dict with two keys:
133
+ - ``pending``: list of entries that require an interactive prompt
134
+ because the user is still on an older NEXO-recommended model and a
135
+ newer recommendation is available.
136
+ - ``auto_ack``: mapping of ``{client: recommendation_version}`` that
137
+ should be acknowledged silently without prompting (because either
138
+ the user already matches the current recommended model, or they
139
+ have customized their model and we must respect that silently).
140
+
141
+ Saving the ``auto_ack`` entries prevents repeated stderr spam in
142
+ non-interactive (cron/headless) update runs.
143
+
144
+ ``pending`` entry shape:
132
145
  {
133
146
  "client": "codex",
134
147
  "current_model": "gpt-5.9",
@@ -139,7 +152,8 @@ def detect_outdated_recommendations(
139
152
  "display_name": "GPT-5.9 with high reasoning",
140
153
  }
141
154
  """
142
- out: list[dict] = []
155
+ pending: list[dict] = []
156
+ auto_ack: dict[str, int] = {}
143
157
  profiles = preferences.get("client_runtime_profiles") or {}
144
158
  acknowledged = preferences.get("acknowledged_model_recommendations") or {}
145
159
  for client in ("claude_code", "codex"):
@@ -153,15 +167,22 @@ def detect_outdated_recommendations(
153
167
  ack_v = int(acknowledged.get(client) or 0)
154
168
  if current_v <= ack_v:
155
169
  continue
156
- # Only offer upgrade if user is on a prior NEXO default.
157
- # If they customized, respect their choice silently.
170
+ # User customized their model entirely (not a previously recommended
171
+ # NEXO default) respect their choice and record ack silently so we
172
+ # don't log a hint every `nexo update`.
158
173
  if not was_nexo_default(client, user_model):
174
+ auto_ack[client] = current_v
159
175
  continue
160
- # No change needed if they already match current (race: install wrote
161
- # the new default but ack wasn't bumped). Treat as auto-acknowledged.
162
- if user_model == default["model"] and user_effort == default["reasoning_effort"]:
176
+ # User is already on the current recommended model. Even if their
177
+ # reasoning_effort differs (e.g. they picked "max" at onboarding and
178
+ # the JSON stores ""), nothing to migrate — their effort is a
179
+ # personal choice layered on top of the recommended model.
180
+ if user_model == default["model"]:
181
+ auto_ack[client] = current_v
163
182
  continue
164
- out.append({
183
+ # User is on a prior NEXO default and the current recommendation is
184
+ # a different model → offer interactive upgrade.
185
+ pending.append({
165
186
  "client": client,
166
187
  "current_model": default["model"],
167
188
  "current_effort": default["reasoning_effort"],
@@ -170,4 +191,4 @@ def detect_outdated_recommendations(
170
191
  "user_effort": user_effort,
171
192
  "display_name": default.get("display_name") or default["model"],
172
193
  })
173
- return out
194
+ return {"pending": pending, "auto_ack": auto_ack}
@@ -442,9 +442,17 @@ def _restore_code_tree(backup_dir: str) -> str | None:
442
442
 
443
443
  def _normalize_preferences_for_client_sync() -> dict:
444
444
  from client_preferences import normalize_client_preferences
445
+ from model_defaults import heal_runtime_profiles
445
446
 
446
447
  schedule_path = NEXO_HOME / "config" / "schedule.json"
447
448
  schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
449
+ # Heal invalid models (e.g. Claude-family written into codex profile by
450
+ # earlier buggy versions). Must run BEFORE normalize so the healed values
451
+ # propagate into preferences and downstream client config files.
452
+ existing_profiles = schedule_payload.get("client_runtime_profiles") or {}
453
+ healed_profiles, _heal_messages = heal_runtime_profiles(existing_profiles)
454
+ if _heal_messages:
455
+ schedule_payload["client_runtime_profiles"] = healed_profiles
448
456
  normalized_preferences = normalize_client_preferences(schedule_payload)
449
457
  if normalized_preferences != {
450
458
  key: schedule_payload.get(key)
@@ -902,9 +910,20 @@ def handle_update(remote: str = "origin", branch: str = "main", progress_fn=None
902
910
  _emit_progress(progress_fn, "Refreshing shared client configs...")
903
911
  from client_sync import sync_all_clients
904
912
  from client_preferences import normalize_client_preferences
913
+ from model_defaults import heal_runtime_profiles
905
914
 
906
915
  schedule_path = NEXO_HOME / "config" / "schedule.json"
907
916
  schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
917
+ # Heal Claude-family models written into Codex profile by earlier
918
+ # buggy versions. Must run BEFORE normalize so healed values
919
+ # propagate into the saved preferences.
920
+ existing_profiles = schedule_payload.get("client_runtime_profiles") or {}
921
+ healed_profiles, heal_messages = heal_runtime_profiles(existing_profiles)
922
+ if heal_messages:
923
+ schedule_payload["client_runtime_profiles"] = healed_profiles
924
+ for msg in heal_messages:
925
+ _emit_progress(progress_fn, msg)
926
+ steps_done.append("model-heal")
908
927
  normalized_preferences = normalize_client_preferences(schedule_payload)
909
928
  if normalized_preferences != {
910
929
  key: schedule_payload.get(key)