nexo-brain 5.6.0 → 5.6.1

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.6.0",
3
+ "version": "5.6.1",
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 `5.6.0` is the current packaged-runtime line: default model upgrade from Opus 4.6 to **Opus 4.7** with `reasoning_effort: "max"` (the new highest tier). Auto-migration on `nexo update` silently upgrades existing users from `claude-opus-4-6*` to `claude-opus-4-7` preserving the 1M context suffix. Codex profiles are untouched.
21
+ Version `5.6.1` is the current packaged-runtime line: two small-but-sharp fixes in the `nexo update` path. 0-byte `.db` orphans from interrupted installs are now purged from `~/.nexo/` and `~/.nexo/data/` before the pre-update backup runs, so backup validation no longer trips over empty shells. And when `heal_runtime_profiles()` migrates the `claude_code` default (e.g. Opus 4.6 4.7), the new `sync_claude_code_model()` helper also updates the `model` field in `~/.claude/settings.json` the file Claude Code actually reads — so the boot model matches NEXO's recommendation on the next launch.
22
+
23
+ Previously in `5.6.0`: default model upgrade from Opus 4.6 to **Opus 4.7** with `reasoning_effort: "max"` (the new highest tier). Auto-migration on `nexo update` silently upgrades existing users from `claude-opus-4-6*` to `claude-opus-4-7` preserving the 1M context suffix. Codex profiles are untouched.
22
24
 
23
25
  Previously in `5.5.5`: data-loss guardrails + automatic self-heal. The updater now refuses to capture an already-wiped `nexo.db` into a `pre-update-*` snapshot (validated `sqlite3.backup` + pre-flight wipe guard + post-migration row-count gate), and an auto-heal restores `data/nexo.db` from the newest hourly backup on the next server boot when a wipe is detected. New `nexo recover` CLI + `nexo_recover` MCP tool.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.6.0",
3
+ "version": "5.6.1",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain \u2014 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",
@@ -836,10 +836,53 @@ def _self_heal_if_wiped() -> dict | None:
836
836
  }
837
837
 
838
838
 
839
+ def _purge_zero_byte_db_files() -> list[Path]:
840
+ """Delete 0-byte .db files in NEXO_HOME and its ``data/`` subdir.
841
+
842
+ These are orphans from interrupted installs / aborted ``sqlite3.connect``
843
+ calls. They break backup validation by (a) masking the real DB during
844
+ :func:`_find_primary_db_path` selection when two ``nexo.db`` paths
845
+ coexist, and (b) being copied into the backup as empty shells that later
846
+ confuse :func:`_restore_dbs` on rollback.
847
+
848
+ Never touches SRC_DIR (the repo checkout) or the ``backups/`` tree.
849
+ Returns the list of removed paths for logging; failures are swallowed
850
+ so backup never aborts because of orphan cleanup.
851
+ """
852
+ removed: list[Path] = []
853
+ scan_dirs: list[Path] = []
854
+ if NEXO_HOME.is_dir():
855
+ scan_dirs.append(NEXO_HOME)
856
+ if DATA_DIR.is_dir() and DATA_DIR != NEXO_HOME:
857
+ scan_dirs.append(DATA_DIR)
858
+ for scan_dir in scan_dirs:
859
+ try:
860
+ candidates = [f for f in scan_dir.glob("*.db") if f.is_file()]
861
+ except Exception:
862
+ continue
863
+ for path in candidates:
864
+ try:
865
+ if path.stat().st_size != 0:
866
+ continue
867
+ except Exception:
868
+ continue
869
+ try:
870
+ path.unlink()
871
+ removed.append(path)
872
+ _log(f"Purged zero-byte DB orphan: {path}")
873
+ except Exception as e:
874
+ _log(f"Failed to purge zero-byte DB {path}: {e}")
875
+ return removed
876
+
877
+
839
878
  def _backup_dbs() -> str | None:
840
879
  """Snapshot all .db files before migration. Returns backup dir or None."""
841
880
  import sqlite3
842
881
  import time as _time
882
+ # Drop 0-byte .db orphans first — they mask the real DB during primary
883
+ # path selection and turn into empty shells in the backup, breaking both
884
+ # validation and rollback paths. Safe no-op when there are none.
885
+ _purge_zero_byte_db_files()
843
886
  timestamp = _time.strftime("%Y-%m-%d-%H%M%S")
844
887
  backup_dir = NEXO_HOME / "backups" / f"pre-autoupdate-{timestamp}"
845
888
 
@@ -2271,6 +2314,32 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
2271
2314
  for msg in heal_messages:
2272
2315
  _emit_progress(progress_fn, msg)
2273
2316
  actions.append("model-heal")
2317
+ # Claude Code reads the default model from ~/.claude/settings.json,
2318
+ # not from client_runtime_profiles. If the heal migrated the
2319
+ # claude_code model (e.g. Opus 4.6 → 4.7) the internal profile is
2320
+ # now correct but Claude Code keeps booting on the old model until
2321
+ # settings.json is also updated. Propagate conservatively: only
2322
+ # touch settings.json when it already has a "model" field.
2323
+ existing_cc = existing_profiles.get("claude_code") if isinstance(existing_profiles.get("claude_code"), dict) else None
2324
+ healed_cc = healed_profiles.get("claude_code") if isinstance(healed_profiles.get("claude_code"), dict) else None
2325
+ old_cc_model = str((existing_cc or {}).get("model") or "")
2326
+ new_cc_model = str((healed_cc or {}).get("model") or "")
2327
+ if new_cc_model and new_cc_model != old_cc_model:
2328
+ try:
2329
+ from client_sync import sync_claude_code_model
2330
+ sync_result = sync_claude_code_model(new_cc_model)
2331
+ if sync_result.get("action") == "updated":
2332
+ _emit_progress(
2333
+ progress_fn,
2334
+ f"Synced Claude Code settings.json model → '{new_cc_model}'.",
2335
+ )
2336
+ actions.append("claude-settings-model")
2337
+ elif not sync_result.get("ok"):
2338
+ actions.append(
2339
+ f"claude-settings-model-warning:{sync_result.get('reason', 'unknown')}"
2340
+ )
2341
+ except Exception as e:
2342
+ actions.append(f"claude-settings-model-warning:{e}")
2274
2343
  normalized_preferences = normalize_client_preferences(schedule_payload)
2275
2344
  if normalized_preferences != {
2276
2345
  key: schedule_payload.get(key)
@@ -802,6 +802,70 @@ def _sync_claude_code_settings(path: Path, server_config: dict) -> dict:
802
802
  }
803
803
 
804
804
 
805
+ def sync_claude_code_model(
806
+ model: str,
807
+ *,
808
+ user_home: str | os.PathLike[str] | None = None,
809
+ ) -> dict:
810
+ """Propagate the NEXO-recommended ``model`` into ``~/.claude/settings.json``.
811
+
812
+ Claude Code actually reads the default model from ``settings.json``, not
813
+ from NEXO's internal ``client_runtime_profiles``. When a NEXO update
814
+ migrates a user's recommended default (e.g. Opus 4.6 → 4.7) the internal
815
+ profile changes but Claude Code keeps booting on the old model until this
816
+ file is rewritten.
817
+
818
+ Intentionally conservative — do NOT seed a field the user never had:
819
+ - ``settings.json`` missing → skip.
820
+ - ``settings.json`` exists but has no
821
+ top-level ``"model"`` key → skip.
822
+ - Current value already equals ``model`` → skip (no write).
823
+ - JSON read/write error → ``ok=False``, skip.
824
+ - Otherwise replace the value.
825
+
826
+ Returns a small result dict: ``{ok, action, path, reason?, previous_model?, new_model?}``.
827
+ ``action`` is ``"updated"`` only when the file was actually rewritten.
828
+ """
829
+ result: dict = {"ok": True, "action": "skipped", "path": ""}
830
+ if not model:
831
+ result["reason"] = "empty model"
832
+ return result
833
+ home_path = Path(user_home).expanduser() if user_home else None
834
+ path = _claude_code_settings_path(home_path)
835
+ result["path"] = str(path)
836
+ if not path.is_file():
837
+ result["reason"] = "settings.json missing"
838
+ return result
839
+ try:
840
+ raw = path.read_text()
841
+ payload = json.loads(raw) if raw.strip() else {}
842
+ except Exception as e:
843
+ result["ok"] = False
844
+ result["reason"] = f"read failed: {e}"
845
+ return result
846
+ if not isinstance(payload, dict):
847
+ result["reason"] = "settings.json is not a JSON object"
848
+ return result
849
+ if "model" not in payload:
850
+ result["reason"] = "no model field in settings.json"
851
+ return result
852
+ current = payload.get("model")
853
+ if current == model:
854
+ result["reason"] = "already matches"
855
+ return result
856
+ payload["model"] = model
857
+ try:
858
+ _write_json_object(path, payload)
859
+ except Exception as e:
860
+ result["ok"] = False
861
+ result["reason"] = f"write failed: {e}"
862
+ return result
863
+ result["action"] = "updated"
864
+ result["previous_model"] = current
865
+ result["new_model"] = model
866
+ return result
867
+
868
+
805
869
  def sync_claude_code(
806
870
  *,
807
871
  nexo_home: str | os.PathLike[str] | None = None,