nexo-brain 5.5.6 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/auto_update.py +69 -0
- package/src/client_sync.py +65 -1
- package/src/model_defaults.json +5 -5
- package/src/model_defaults.py +33 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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
|
|
|
@@ -830,7 +832,7 @@ If you want the shell or Python wrappers instead of raw MCP tools:
|
|
|
830
832
|
- [docs/reference-verticals.md](docs/reference-verticals.md)
|
|
831
833
|
- [compare/README.md](compare/README.md)
|
|
832
834
|
|
|
833
|
-
The model you pick during install is used everywhere — interactive sessions, automation scripts, and all task profiles. Change it once in your preferences and every part of the system follows. Default: `Opus 4.
|
|
835
|
+
The model you pick during install is used everywhere — interactive sessions, automation scripts, and all task profiles. Change it once in your preferences and every part of the system follows. Default: `Opus 4.7 with 1M context`.
|
|
834
836
|
|
|
835
837
|
Or use the shell alias created during install (e.g. `atlas`), which now runs `nexo chat .` so it opens the terminal client you pick for that session, with the last-used option shown first.
|
|
836
838
|
|
|
@@ -911,7 +913,7 @@ The Doctor system reads existing health artifacts (immune, watchdog, self-audit)
|
|
|
911
913
|
- **macOS or Linux** (Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install))
|
|
912
914
|
- **Node.js 18+** (for the installer)
|
|
913
915
|
- **Claude Code is the primary recommended client.** It remains the most mature NEXO path: native hooks, the most battle-tested automation contract, and the clearest parity with historical production behavior.
|
|
914
|
-
- **Model:** You pick your model during install and every component uses it. Default is `Opus 4.
|
|
916
|
+
- **Model:** You pick your model during install and every component uses it. Default is `Opus 4.7 with 1M context`. Scripts and automation profiles read from a single preference — no hardcoded model strings.
|
|
915
917
|
- Python 3, Homebrew, and the selected required client/backend can be installed automatically when NEXO has a supported installer path for that dependency.
|
|
916
918
|
|
|
917
919
|
## Architecture
|
|
@@ -1020,7 +1022,7 @@ NEXO Brain is designed as an MCP server. Claude Code remains the primary recomme
|
|
|
1020
1022
|
npx nexo-brain
|
|
1021
1023
|
```
|
|
1022
1024
|
|
|
1023
|
-
All 150+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically. The recommended Claude profile is `Opus 4.
|
|
1025
|
+
All 150+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically. The recommended Claude profile is `Opus 4.7 with 1M context`.
|
|
1024
1026
|
|
|
1025
1027
|
### Claude Desktop
|
|
1026
1028
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -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)
|
package/src/client_sync.py
CHANGED
|
@@ -62,7 +62,7 @@ except Exception:
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
|
|
65
|
-
_default_model = "claude-opus-4-
|
|
65
|
+
_default_model = "claude-opus-4-7[1m]"
|
|
66
66
|
defaults = {
|
|
67
67
|
"claude_code": {"model": _default_model, "reasoning_effort": ""},
|
|
68
68
|
"codex": {"model": _default_model, "reasoning_effort": ""},
|
|
@@ -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,
|
package/src/model_defaults.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"claude_code": {
|
|
4
|
-
"model": "claude-opus-4-
|
|
5
|
-
"reasoning_effort": "",
|
|
6
|
-
"display_name": "Opus 4.
|
|
7
|
-
"recommendation_version":
|
|
8
|
-
"previous_defaults": []
|
|
4
|
+
"model": "claude-opus-4-7[1m]",
|
|
5
|
+
"reasoning_effort": "max",
|
|
6
|
+
"display_name": "Opus 4.7 with 1M context",
|
|
7
|
+
"recommendation_version": 2,
|
|
8
|
+
"previous_defaults": ["claude-opus-4-6[1m]"]
|
|
9
9
|
},
|
|
10
10
|
"codex": {
|
|
11
11
|
"model": "gpt-5.4",
|
package/src/model_defaults.py
CHANGED
|
@@ -20,11 +20,11 @@ from typing import Any
|
|
|
20
20
|
_FALLBACK: dict[str, Any] = {
|
|
21
21
|
"schema_version": 1,
|
|
22
22
|
"claude_code": {
|
|
23
|
-
"model": "claude-opus-4-
|
|
24
|
-
"reasoning_effort": "",
|
|
25
|
-
"display_name": "Opus 4.
|
|
26
|
-
"recommendation_version":
|
|
27
|
-
"previous_defaults": [],
|
|
23
|
+
"model": "claude-opus-4-7[1m]",
|
|
24
|
+
"reasoning_effort": "max",
|
|
25
|
+
"display_name": "Opus 4.7 with 1M context",
|
|
26
|
+
"recommendation_version": 2,
|
|
27
|
+
"previous_defaults": ["claude-opus-4-6[1m]"],
|
|
28
28
|
},
|
|
29
29
|
"codex": {
|
|
30
30
|
"model": "gpt-5.4",
|
|
@@ -99,15 +99,20 @@ def looks_like_claude_model(model: str) -> bool:
|
|
|
99
99
|
return str(model or "").strip().lower().startswith(_CLAUDE_MODEL_PREFIXES)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
_OPUS_46_PREFIX = "claude-opus-4-6"
|
|
103
|
+
|
|
104
|
+
|
|
102
105
|
def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
|
|
103
106
|
"""Detect and repair invalid models in client_runtime_profiles. Returns
|
|
104
|
-
(healed_profiles_dict, list_of_heal_messages).
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
(healed_profiles_dict, list_of_heal_messages). Handles two cases:
|
|
108
|
+
1. Claude-family model in the codex profile (historical bug).
|
|
109
|
+
2. Opus 4.6 → 4.7 auto-migration for claude_code users on a NEXO default."""
|
|
107
110
|
if not isinstance(profiles, dict):
|
|
108
111
|
return profiles, []
|
|
109
112
|
healed = dict(profiles)
|
|
110
113
|
messages: list[str] = []
|
|
114
|
+
|
|
115
|
+
# --- Codex heal (historical bug: Claude model in codex slot) ---
|
|
111
116
|
codex_profile = healed.get("codex") if isinstance(healed.get("codex"), dict) else None
|
|
112
117
|
if codex_profile is not None:
|
|
113
118
|
current = str(codex_profile.get("model") or "").strip()
|
|
@@ -121,6 +126,26 @@ def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
|
|
|
121
126
|
f"Healed Codex profile: model '{current}' → '{default['model']}' "
|
|
122
127
|
f"(Claude models are invalid for Codex)."
|
|
123
128
|
)
|
|
129
|
+
|
|
130
|
+
# --- Opus 4.6 → 4.7 auto-migration for claude_code ---
|
|
131
|
+
cc_profile = healed.get("claude_code") if isinstance(healed.get("claude_code"), dict) else None
|
|
132
|
+
if cc_profile is not None:
|
|
133
|
+
cc_model = str(cc_profile.get("model") or "").strip()
|
|
134
|
+
if cc_model.startswith(_OPUS_46_PREFIX):
|
|
135
|
+
default = client_default("claude_code")
|
|
136
|
+
suffix = cc_model[len(_OPUS_46_PREFIX):]
|
|
137
|
+
new_model = f"claude-opus-4-7{suffix}"
|
|
138
|
+
old_effort = str(cc_profile.get("reasoning_effort") or "").strip()
|
|
139
|
+
new_effort = default["reasoning_effort"]
|
|
140
|
+
healed["claude_code"] = dict(cc_profile)
|
|
141
|
+
healed["claude_code"]["model"] = new_model
|
|
142
|
+
if old_effort in ("", "xhigh", "enabled"):
|
|
143
|
+
healed["claude_code"]["reasoning_effort"] = new_effort
|
|
144
|
+
messages.append(
|
|
145
|
+
f"Auto-migrated Claude Code: '{cc_model}' → '{new_model}', "
|
|
146
|
+
f"effort '{old_effort or '(empty)'}' → '{healed['claude_code']['reasoning_effort']}'."
|
|
147
|
+
)
|
|
148
|
+
|
|
124
149
|
return healed, messages
|
|
125
150
|
|
|
126
151
|
|