nexo-brain 5.5.5 → 5.6.0
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/client_sync.py +1 -1
- package/src/model_defaults.json +5 -5
- package/src/model_defaults.py +33 -8
- package/src/plugins/backup.py +72 -5
- package/src/user_data_portability.py +35 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
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.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.
|
|
22
|
+
|
|
23
|
+
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.
|
|
22
24
|
|
|
23
25
|
Previously in `5.5.4`: Deep Sleep no longer blocks on unparseable sessions — reduced retries, added a JSON escape hatch, and unified the automation subprocess timeout to 3h across all scripts via a single shared constant.
|
|
24
26
|
|
|
@@ -828,7 +830,7 @@ If you want the shell or Python wrappers instead of raw MCP tools:
|
|
|
828
830
|
- [docs/reference-verticals.md](docs/reference-verticals.md)
|
|
829
831
|
- [compare/README.md](compare/README.md)
|
|
830
832
|
|
|
831
|
-
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.
|
|
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.7 with 1M context`.
|
|
832
834
|
|
|
833
835
|
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.
|
|
834
836
|
|
|
@@ -909,7 +911,7 @@ The Doctor system reads existing health artifacts (immune, watchdog, self-audit)
|
|
|
909
911
|
- **macOS or Linux** (Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install))
|
|
910
912
|
- **Node.js 18+** (for the installer)
|
|
911
913
|
- **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.
|
|
912
|
-
- **Model:** You pick your model during install and every component uses it. Default is `Opus 4.
|
|
914
|
+
- **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.
|
|
913
915
|
- Python 3, Homebrew, and the selected required client/backend can be installed automatically when NEXO has a supported installer path for that dependency.
|
|
914
916
|
|
|
915
917
|
## Architecture
|
|
@@ -1018,7 +1020,7 @@ NEXO Brain is designed as an MCP server. Claude Code remains the primary recomme
|
|
|
1018
1020
|
npx nexo-brain
|
|
1019
1021
|
```
|
|
1020
1022
|
|
|
1021
|
-
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.
|
|
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.7 with 1M context`.
|
|
1022
1024
|
|
|
1023
1025
|
### Claude Desktop
|
|
1024
1026
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
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/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": ""},
|
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
|
|
package/src/plugins/backup.py
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
"""Backup plugin — hourly SQLite backups with 7-day retention.
|
|
1
|
+
"""Backup plugin — hourly SQLite backups with 7-day retention.
|
|
2
|
+
|
|
3
|
+
v5.5.6: all three tools are rate-limited in-process so that a runaway MCP
|
|
4
|
+
client (tool-use loop in Claude Code, buggy Desktop handler, etc.) cannot
|
|
5
|
+
hammer ``sqlite3.Connection.backup()`` hundreds of times in minutes. The
|
|
6
|
+
v5.5.4 incident where an external loop caused ~8.5 GB of file-backed writes
|
|
7
|
+
in 37 minutes and corrupted nexo.db when the OS finally killed the process
|
|
8
|
+
is the exact scenario this limit prevents at the tool boundary — in addition
|
|
9
|
+
to the v5.5.5 self-heal that recovers from that class of wipe.
|
|
10
|
+
"""
|
|
11
|
+
import glob
|
|
2
12
|
import os
|
|
3
13
|
import shutil
|
|
14
|
+
import sqlite3
|
|
15
|
+
import threading
|
|
4
16
|
import time
|
|
5
|
-
|
|
17
|
+
|
|
6
18
|
from db import get_db
|
|
7
19
|
|
|
8
20
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
@@ -11,15 +23,63 @@ BACKUP_DIR = os.path.join(NEXO_HOME, "backups")
|
|
|
11
23
|
|
|
12
24
|
RETENTION_DAYS = 7
|
|
13
25
|
|
|
26
|
+
# ── Rate limits (v5.5.6) ────────────────────────────────────────────
|
|
27
|
+
# Minimum seconds between successive calls to each destructive/expensive
|
|
28
|
+
# backup tool. Overridable per-tool via env var for tests or deliberate
|
|
29
|
+
# recovery scenarios (NEXO_BACKUP_MIN_INTERVAL_SECS, etc.).
|
|
30
|
+
BACKUP_NOW_MIN_INTERVAL_SECS = int(
|
|
31
|
+
os.environ.get("NEXO_BACKUP_MIN_INTERVAL_SECS", "30")
|
|
32
|
+
)
|
|
33
|
+
BACKUP_RESTORE_MIN_INTERVAL_SECS = int(
|
|
34
|
+
os.environ.get("NEXO_BACKUP_RESTORE_MIN_INTERVAL_SECS", "60")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_rate_limit_lock = threading.Lock()
|
|
38
|
+
_last_call_ts: dict[str, float] = {
|
|
39
|
+
"backup_now": 0.0,
|
|
40
|
+
"backup_restore": 0.0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_rate_limit(tool: str, min_interval: int) -> str | None:
|
|
45
|
+
"""Return a rate-limit error string if the tool is called too soon, else None."""
|
|
46
|
+
now = time.time()
|
|
47
|
+
with _rate_limit_lock:
|
|
48
|
+
last = _last_call_ts.get(tool, 0.0)
|
|
49
|
+
elapsed = now - last
|
|
50
|
+
if last > 0 and elapsed < min_interval:
|
|
51
|
+
remaining = int(min_interval - elapsed)
|
|
52
|
+
return (
|
|
53
|
+
f"Rate-limited: {tool} called {int(elapsed)}s ago "
|
|
54
|
+
f"(min {min_interval}s between calls). Wait {remaining}s. "
|
|
55
|
+
"If you are seeing this message repeatedly, a client may be stuck in a "
|
|
56
|
+
"tool-use loop — check NEXO transcripts and kill the runaway session."
|
|
57
|
+
)
|
|
58
|
+
_last_call_ts[tool] = now
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _reset_rate_limit_state_for_tests() -> None:
|
|
63
|
+
"""Test hook: clear all tracked call timestamps."""
|
|
64
|
+
with _rate_limit_lock:
|
|
65
|
+
for key in _last_call_ts:
|
|
66
|
+
_last_call_ts[key] = 0.0
|
|
67
|
+
|
|
14
68
|
|
|
15
69
|
def handle_backup_now() -> str:
|
|
16
|
-
"""Create an immediate backup of the NEXO database.
|
|
70
|
+
"""Create an immediate backup of the NEXO database.
|
|
71
|
+
|
|
72
|
+
Rate-limited to one call every BACKUP_NOW_MIN_INTERVAL_SECS (default 30 s).
|
|
73
|
+
"""
|
|
74
|
+
err = _check_rate_limit("backup_now", BACKUP_NOW_MIN_INTERVAL_SECS)
|
|
75
|
+
if err is not None:
|
|
76
|
+
return err
|
|
77
|
+
|
|
17
78
|
os.makedirs(BACKUP_DIR, exist_ok=True)
|
|
18
79
|
timestamp = time.strftime("%Y-%m-%d-%H%M")
|
|
19
80
|
dest = os.path.join(BACKUP_DIR, f"nexo-{timestamp}.db")
|
|
20
81
|
|
|
21
82
|
# Use SQLite backup API for consistency
|
|
22
|
-
import sqlite3
|
|
23
83
|
src_conn = sqlite3.connect(DB_PATH)
|
|
24
84
|
try:
|
|
25
85
|
dst_conn = sqlite3.connect(dest)
|
|
@@ -56,16 +116,23 @@ def handle_backup_list() -> str:
|
|
|
56
116
|
def handle_backup_restore(filename: str) -> str:
|
|
57
117
|
"""Restore database from a backup file. DESTRUCTIVE — replaces current DB.
|
|
58
118
|
|
|
119
|
+
Rate-limited to one call every BACKUP_RESTORE_MIN_INTERVAL_SECS (default
|
|
120
|
+
60 s). A client hammering restore in a loop is the exact shape of the
|
|
121
|
+
v5.5.4 incident.
|
|
122
|
+
|
|
59
123
|
Args:
|
|
60
124
|
filename: Backup filename (e.g., 'nexo-2026-03-11-1200.db')
|
|
61
125
|
"""
|
|
126
|
+
err = _check_rate_limit("backup_restore", BACKUP_RESTORE_MIN_INTERVAL_SECS)
|
|
127
|
+
if err is not None:
|
|
128
|
+
return err
|
|
129
|
+
|
|
62
130
|
src = os.path.join(BACKUP_DIR, filename)
|
|
63
131
|
if not os.path.isfile(src):
|
|
64
132
|
return f"Backup not found: {filename}"
|
|
65
133
|
|
|
66
134
|
# Create safety backup first
|
|
67
135
|
safety = os.path.join(BACKUP_DIR, f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db")
|
|
68
|
-
import sqlite3
|
|
69
136
|
src_conn = sqlite3.connect(DB_PATH)
|
|
70
137
|
try:
|
|
71
138
|
dst_conn = sqlite3.connect(safety)
|
|
@@ -8,6 +8,8 @@ import shutil
|
|
|
8
8
|
import sqlite3
|
|
9
9
|
import tarfile
|
|
10
10
|
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
11
13
|
from datetime import datetime, timezone
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
|
|
@@ -23,6 +25,36 @@ IGNORED_FILENAMES = {".DS_Store"}
|
|
|
23
25
|
IGNORED_DIRS = {"__pycache__"}
|
|
24
26
|
IGNORED_SUFFIXES = {".pyc", ".pyo"}
|
|
25
27
|
|
|
28
|
+
# v5.5.6: rate-limit the whole-bundle export so a runaway MCP client cannot
|
|
29
|
+
# loop this tool. Each export snapshots the entire NEXO state through
|
|
30
|
+
# sqlite3.Connection.backup() plus a tree copy — in the v5.5.4 incident a
|
|
31
|
+
# similar loop wrote 8.5 GB in 37 minutes. Overridable for tests / deliberate
|
|
32
|
+
# batch exports via NEXO_EXPORT_MIN_INTERVAL_SECS.
|
|
33
|
+
EXPORT_MIN_INTERVAL_SECS = int(os.environ.get("NEXO_EXPORT_MIN_INTERVAL_SECS", "120"))
|
|
34
|
+
_export_rate_lock = threading.Lock()
|
|
35
|
+
_export_last_call_ts = [0.0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _check_export_rate_limit() -> str | None:
|
|
39
|
+
now = time.time()
|
|
40
|
+
with _export_rate_lock:
|
|
41
|
+
last = _export_last_call_ts[0]
|
|
42
|
+
elapsed = now - last
|
|
43
|
+
if last > 0 and elapsed < EXPORT_MIN_INTERVAL_SECS:
|
|
44
|
+
remaining = int(EXPORT_MIN_INTERVAL_SECS - elapsed)
|
|
45
|
+
return (
|
|
46
|
+
f"Rate-limited: export_user_bundle called {int(elapsed)}s ago "
|
|
47
|
+
f"(min {EXPORT_MIN_INTERVAL_SECS}s between calls). Wait {remaining}s. "
|
|
48
|
+
"If you see this repeatedly, a client may be stuck in a tool-use loop."
|
|
49
|
+
)
|
|
50
|
+
_export_last_call_ts[0] = now
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _reset_export_rate_limit_state_for_tests() -> None:
|
|
55
|
+
with _export_rate_lock:
|
|
56
|
+
_export_last_call_ts[0] = 0.0
|
|
57
|
+
|
|
26
58
|
|
|
27
59
|
def _now_stamp() -> str:
|
|
28
60
|
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
@@ -137,6 +169,9 @@ def _load_personal_scripts() -> tuple[list[dict], list[dict]]:
|
|
|
137
169
|
|
|
138
170
|
|
|
139
171
|
def export_user_bundle(output_path: str = "") -> dict:
|
|
172
|
+
err = _check_export_rate_limit()
|
|
173
|
+
if err is not None:
|
|
174
|
+
return {"ok": False, "error": err, "rate_limited": True}
|
|
140
175
|
output = Path(output_path).expanduser() if output_path.strip() else (EXPORTS_DIR / f"nexo-user-data-{_now_stamp()}.tar.gz")
|
|
141
176
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
142
177
|
STAGING_DIR.mkdir(parents=True, exist_ok=True)
|