nexo-brain 5.9.0 → 5.9.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 +3 -1
- package/package.json +1 -1
- package/src/cli.py +50 -7
- package/src/desktop_bridge.py +28 -0
- package/src/resonance_map.py +49 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.9.
|
|
3
|
+
"version": "5.9.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.9.
|
|
21
|
+
Version `5.9.1` is the current packaged-runtime line: adds `default_resonance` to `brain/calibration.json` via the Desktop-facing schema (`nexo schema --json`), so NEXO Desktop's Preferences dialog renders a select with `Máximo` / `Alto (recomendado)` / `Medio` / `Bajo` automatically — no Desktop release needed. `resolve_tier_for_caller` reads calibration first and falls back to the legacy `schedule.json` location. `nexo preferences --resonance` writes both. The UI control only affects interactive sessions (`nexo chat`, Desktop new conversation, interactive `nexo update`); crons and background processes stay pinned per caller in `resonance_map.py`.
|
|
22
|
+
|
|
23
|
+
Previously in `5.9.0`: every Claude/Codex invocation now flows through a central **resonance map** and a **unified session log**. Four tiers (`MAXIMO` / `ALTO` / `MEDIO` / `BAJO`) each resolve to a concrete `(model, reasoning_effort)` pair per backend. User-facing callers (`nexo chat`, Desktop new conversation, interactive `nexo update`) honour the user's `default_resonance` preference; system-owned callers (deep-sleep, evolution, catchup, GBP posts, …) run at a fixed tier chosen per caller in `src/resonance_map.py` — the user's preference never downgrades a cron we decided needs `MAXIMO`. Unknown callers raise `UnregisteredCallerError`. Migration #41 adds `caller`, `session_type`, `started_at`, `ended_at`, `pid`, `resonance_tier` to `automation_runs`; interactive sessions record a row at spawn (with `ended_at=NULL`) and update it on close, so the Brain now has a single source of truth for every Claude/Codex call regardless of origin. New `nexo preferences --resonance` CLI. New MCP tools `nexo_session_log_create` / `nexo_session_log_close` let NEXO Desktop (which spawns `claude` directly from its TypeScript process) feed the same log.
|
|
22
24
|
|
|
23
25
|
Previously in `5.8.2`: the Brain core no longer auto-classifies `followups` and `reminders` on behalf of agents. v5.8.0's `classify_task()` heuristic (NEXO-specific ID prefixes `NF-PROTOCOL-*` / `NF-DS-*` / `NF-AUDIT-*`, Spanish user-verbs `debes` / `revisar` / `firmar`, agent keywords `monitor` / `auditoría diaria` / `checkpoint`) was fine for NEXO's own DB but bled convention into every third-party agent plugged into the shared Brain. The core now persists `internal=0` and `owner=NULL` when the caller omits them, and clients that want automatic classification (NEXO Desktop does, via its `_legacyClassifyOwner` helpers) compute it themselves and pass the result. Migration #40 keeps the columns + indexes; rows already backfilled by v5.8.0 keep their values. `normalise_owner` still explicitly rejects the string `"nexo"` so legacy hardcoding cannot sneak back in.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.9.
|
|
3
|
+
"version": "5.9.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/cli.py
CHANGED
|
@@ -1099,6 +1099,34 @@ def _clients_sync(args):
|
|
|
1099
1099
|
return 0 if result.get("ok") else 1
|
|
1100
1100
|
|
|
1101
1101
|
|
|
1102
|
+
def _write_calibration_default_resonance(tier: str) -> None:
|
|
1103
|
+
"""Persist ``preferences.default_resonance`` in ``brain/calibration.json``.
|
|
1104
|
+
|
|
1105
|
+
NEXO Desktop's preferences UI reads from calibration.json (matches the
|
|
1106
|
+
rest of the user-facing knobs — autonomy, communication, assistant_name,
|
|
1107
|
+
…). This helper keeps the CLI path writing to both calibration.json
|
|
1108
|
+
AND schedule.json so the two surfaces never disagree.
|
|
1109
|
+
"""
|
|
1110
|
+
cal_path = NEXO_HOME / "brain" / "calibration.json"
|
|
1111
|
+
try:
|
|
1112
|
+
cal_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1113
|
+
if cal_path.exists():
|
|
1114
|
+
data = json.loads(cal_path.read_text())
|
|
1115
|
+
if not isinstance(data, dict):
|
|
1116
|
+
data = {}
|
|
1117
|
+
else:
|
|
1118
|
+
data = {}
|
|
1119
|
+
prefs = data.get("preferences")
|
|
1120
|
+
if not isinstance(prefs, dict):
|
|
1121
|
+
prefs = {}
|
|
1122
|
+
prefs["default_resonance"] = tier
|
|
1123
|
+
data["preferences"] = prefs
|
|
1124
|
+
cal_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
1125
|
+
except Exception as exc: # best-effort; schedule.json still has the value
|
|
1126
|
+
print(f"[NEXO] Warning: could not update calibration.json: {exc}",
|
|
1127
|
+
file=sys.stderr)
|
|
1128
|
+
|
|
1129
|
+
|
|
1102
1130
|
def _preferences(args):
|
|
1103
1131
|
"""Read or change user preferences stored in schedule.json.
|
|
1104
1132
|
|
|
@@ -1110,7 +1138,11 @@ def _preferences(args):
|
|
|
1110
1138
|
load_client_preferences,
|
|
1111
1139
|
save_client_preferences,
|
|
1112
1140
|
)
|
|
1113
|
-
from resonance_map import
|
|
1141
|
+
from resonance_map import (
|
|
1142
|
+
DEFAULT_RESONANCE,
|
|
1143
|
+
TIERS,
|
|
1144
|
+
_load_user_default_resonance,
|
|
1145
|
+
)
|
|
1114
1146
|
|
|
1115
1147
|
prefs = load_client_preferences()
|
|
1116
1148
|
if not isinstance(prefs, dict):
|
|
@@ -1125,26 +1157,37 @@ def _preferences(args):
|
|
|
1125
1157
|
file=sys.stderr,
|
|
1126
1158
|
)
|
|
1127
1159
|
return 2
|
|
1160
|
+
# Write to schedule.json (legacy CLI location)…
|
|
1128
1161
|
save_client_preferences(default_resonance=tier)
|
|
1162
|
+
# …and to calibration.json (where NEXO Desktop's preferences UI
|
|
1163
|
+
# reads/writes). Keeping both in sync means the two surfaces agree.
|
|
1164
|
+
_write_calibration_default_resonance(tier)
|
|
1129
1165
|
prefs = load_client_preferences()
|
|
1130
1166
|
|
|
1131
|
-
|
|
1167
|
+
calibration_value = _load_user_default_resonance()
|
|
1168
|
+
schedule_value = str(
|
|
1132
1169
|
(prefs.get("default_resonance") if isinstance(prefs, dict) else "")
|
|
1133
|
-
or
|
|
1134
|
-
)
|
|
1170
|
+
or ""
|
|
1171
|
+
).strip().lower()
|
|
1172
|
+
current_resonance = calibration_value or schedule_value or DEFAULT_RESONANCE
|
|
1135
1173
|
|
|
1136
1174
|
if args.show or args.resonance:
|
|
1175
|
+
is_explicit = bool(calibration_value or schedule_value)
|
|
1137
1176
|
payload = {
|
|
1138
1177
|
"default_resonance": current_resonance,
|
|
1139
|
-
"default_resonance_is_explicit":
|
|
1140
|
-
|
|
1178
|
+
"default_resonance_is_explicit": is_explicit,
|
|
1179
|
+
"default_resonance_source": (
|
|
1180
|
+
"calibration.json" if calibration_value
|
|
1181
|
+
else ("schedule.json" if schedule_value else "default")
|
|
1182
|
+
),
|
|
1141
1183
|
"available_tiers": list(TIERS),
|
|
1142
1184
|
}
|
|
1143
1185
|
if args.json:
|
|
1144
1186
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
1145
1187
|
else:
|
|
1146
1188
|
print(f"default_resonance = {current_resonance}")
|
|
1147
|
-
|
|
1189
|
+
print(f" source: {payload['default_resonance_source']}")
|
|
1190
|
+
if not is_explicit:
|
|
1148
1191
|
print(f" (inherited from DEFAULT_RESONANCE; run "
|
|
1149
1192
|
f"`nexo preferences --resonance alto` to set explicitly)")
|
|
1150
1193
|
return 0
|
package/src/desktop_bridge.py
CHANGED
|
@@ -191,6 +191,34 @@ def _schema_fields() -> list[dict]:
|
|
|
191
191
|
{"value": "verbose", "label": {"es": "Detallado", "en": "Verbose"}},
|
|
192
192
|
],
|
|
193
193
|
},
|
|
194
|
+
{
|
|
195
|
+
"path": "preferences.default_resonance",
|
|
196
|
+
"file": "calibration.json",
|
|
197
|
+
"label": {"es": "Resonancia por defecto", "en": "Default resonance"},
|
|
198
|
+
"type": "select",
|
|
199
|
+
"group": "preferences",
|
|
200
|
+
"default": "alto",
|
|
201
|
+
"hint": {
|
|
202
|
+
"es": (
|
|
203
|
+
"Potencia del modelo para sesiones interactivas (nexo chat y "
|
|
204
|
+
"nueva conversación en Desktop). Los crons y procesos de fondo "
|
|
205
|
+
"(deep sleep, evolution, etc.) ignoran esta preferencia — los "
|
|
206
|
+
"definimos nosotros en resonance_map.py por calidad."
|
|
207
|
+
),
|
|
208
|
+
"en": (
|
|
209
|
+
"Model power for interactive sessions (nexo chat and Desktop "
|
|
210
|
+
"new conversation). Crons and background processes (deep sleep, "
|
|
211
|
+
"evolution, etc.) ignore this preference — we pin them per "
|
|
212
|
+
"caller in resonance_map.py based on quality needs."
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
"options": [
|
|
216
|
+
{"value": "maximo", "label": {"es": "Máximo", "en": "Maximum"}},
|
|
217
|
+
{"value": "alto", "label": {"es": "Alto (recomendado)", "en": "High (recommended)"}},
|
|
218
|
+
{"value": "medio", "label": {"es": "Medio", "en": "Medium"}},
|
|
219
|
+
{"value": "bajo", "label": {"es": "Bajo", "en": "Low"}},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
194
222
|
{
|
|
195
223
|
"path": "meta.role",
|
|
196
224
|
"file": "calibration.json",
|
package/src/resonance_map.py
CHANGED
|
@@ -174,6 +174,48 @@ class UnregisteredCallerError(ValueError):
|
|
|
174
174
|
# Resolution
|
|
175
175
|
# ---------------------------------------------------------------------------
|
|
176
176
|
|
|
177
|
+
def _load_user_default_resonance() -> str:
|
|
178
|
+
"""Resolve the user's ``default_resonance`` preference.
|
|
179
|
+
|
|
180
|
+
Reads ``calibration.json`` first (``preferences.default_resonance``, the
|
|
181
|
+
location NEXO Desktop's preferences UI writes to) and falls back to
|
|
182
|
+
``schedule.json`` (``default_resonance``, the location the CLI used to
|
|
183
|
+
write to in v5.9.0). Returns an empty string if neither source has a
|
|
184
|
+
valid tier — callers should treat empty as "no preference".
|
|
185
|
+
"""
|
|
186
|
+
import json as _json
|
|
187
|
+
import os as _os
|
|
188
|
+
from pathlib import Path as _Path
|
|
189
|
+
|
|
190
|
+
home = _Path(_os.environ.get("NEXO_HOME", str(_Path.home() / ".nexo")))
|
|
191
|
+
|
|
192
|
+
# calibration.json (Desktop UI writes here)
|
|
193
|
+
cal_path = home / "brain" / "calibration.json"
|
|
194
|
+
try:
|
|
195
|
+
if cal_path.exists():
|
|
196
|
+
cal = _json.loads(cal_path.read_text())
|
|
197
|
+
prefs = cal.get("preferences") if isinstance(cal, dict) else None
|
|
198
|
+
if isinstance(prefs, dict):
|
|
199
|
+
tier = str(prefs.get("default_resonance") or "").strip().lower()
|
|
200
|
+
if tier in TIERS:
|
|
201
|
+
return tier
|
|
202
|
+
except (OSError, _json.JSONDecodeError):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# schedule.json (CLI legacy)
|
|
206
|
+
sched_path = home / "config" / "schedule.json"
|
|
207
|
+
try:
|
|
208
|
+
if sched_path.exists():
|
|
209
|
+
sched = _json.loads(sched_path.read_text())
|
|
210
|
+
tier = str((sched or {}).get("default_resonance") or "").strip().lower()
|
|
211
|
+
if tier in TIERS:
|
|
212
|
+
return tier
|
|
213
|
+
except (OSError, _json.JSONDecodeError):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
|
|
177
219
|
def resolve_tier_for_caller(caller: str, user_default: str | None = None) -> str:
|
|
178
220
|
"""Return the resonance tier that should apply to ``caller``.
|
|
179
221
|
|
|
@@ -181,6 +223,9 @@ def resolve_tier_for_caller(caller: str, user_default: str | None = None) -> str
|
|
|
181
223
|
if the user has no preference recorded).
|
|
182
224
|
- System-owned callers resolve to their fixed tier.
|
|
183
225
|
- Unknown callers raise ``UnregisteredCallerError``.
|
|
226
|
+
|
|
227
|
+
When ``user_default`` is not passed, the function looks it up from the
|
|
228
|
+
calibration.json preferences first and schedule.json second.
|
|
184
229
|
"""
|
|
185
230
|
if not caller:
|
|
186
231
|
raise UnregisteredCallerError(
|
|
@@ -188,7 +233,10 @@ def resolve_tier_for_caller(caller: str, user_default: str | None = None) -> str
|
|
|
188
233
|
"in src/resonance_map.py so its reasoning budget is deliberate."
|
|
189
234
|
)
|
|
190
235
|
if caller in USER_FACING_CALLERS:
|
|
191
|
-
|
|
236
|
+
resolved_default = user_default
|
|
237
|
+
if resolved_default is None:
|
|
238
|
+
resolved_default = _load_user_default_resonance()
|
|
239
|
+
tier = (resolved_default or DEFAULT_RESONANCE).strip().lower()
|
|
192
240
|
if tier not in TIERS:
|
|
193
241
|
tier = DEFAULT_RESONANCE
|
|
194
242
|
return tier
|