nexo-brain 7.30.32 → 7.31.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.32",
3
+ "version": "7.31.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,11 @@
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 `7.30.32` is the current packaged-runtime line. Patch release over v7.30.31 - packaged update/doctor now repair npm/npx wrapper drift, archive stale personal script backups, validate observable automation health contracts, and block legacy Claude/Codex project memory writes.
21
+ Version `7.31.0` is the current packaged-runtime line. Minor release over v7.30.33 - the recommended Claude Code model moves from Opus 4.8 to Fable 5 with max reasoning (`claude-fable-5`) across all four main resonance tiers (the `muy_bajo` tier keeps Haiku for cheap internal classifiers and Codex stays on GPT-5.5), existing installs riding NEXO defaults auto-migrate on update while customized models are respected, and learning housekeeping no longer aborts when the embedding backend is missing.
22
+
23
+ Previously in `7.30.33`: patch release over v7.30.32 - personal agent/script status now keeps the newest real run between manual executions and cron history, so a successful manual agent run cannot be hidden behind an older scheduled failure.
24
+
25
+ Previously in `7.30.32`: patch release over v7.30.31 - packaged update/doctor now repair npm/npx wrapper drift, archive stale personal script backups, validate observable automation health contracts, and block legacy Claude/Codex project memory writes.
22
26
 
23
27
  Previously in `7.30.31`: patch release over v7.30.30 - Core Rules now reach agents both through a compact managed bootstrap summary and task-specific `cortex/task_open` injection from the protected `core_rules` registry.
24
28
 
package/bin/nexo-brain.js CHANGED
@@ -115,7 +115,7 @@ const PUBLIC_CONTRIBUTION_UPSTREAM = "wazionapps/nexo";
115
115
  const MODEL_DEFAULTS_PATH = path.join(__dirname, "..", "src", "model_defaults.json");
116
116
  function _loadModelDefaults() {
117
117
  const fallback = {
118
- claude_code: { model: "claude-opus-4-7[1m]", reasoning_effort: "max", display_name: "Opus 4.7 with 1M context" },
118
+ claude_code: { model: "claude-fable-5", reasoning_effort: "max", display_name: "Fable 5 with max reasoning" },
119
119
  codex: { model: "gpt-5.5", reasoning_effort: "xhigh", display_name: "GPT-5.5 with max reasoning" },
120
120
  };
121
121
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.32",
3
+ "version": "7.31.0",
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",
@@ -2058,7 +2058,7 @@ def _refresh_resonance_tiers_model_defaults(dest: Path = NEXO_HOME) -> list[str]
2058
2058
  dest / "personal" / "brain" / "resonance_tiers.json",
2059
2059
  dest / "brain" / "resonance_tiers.json",
2060
2060
  ]
2061
- old_prefixes = ("claude-opus-4-6", "claude-opus-4-7")
2061
+ old_prefixes = ("claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8")
2062
2062
 
2063
2063
  for target_path in target_paths:
2064
2064
  try:
@@ -2089,7 +2089,7 @@ def _refresh_resonance_tiers_model_defaults(dest: Path = NEXO_HOME) -> list[str]
2089
2089
  model = str(claude.get("model") or "").strip()
2090
2090
  tier_changed = False
2091
2091
  if model and model.startswith(old_prefixes):
2092
- claude["model"] = str(source_claude.get("model") or "claude-opus-4-8")
2092
+ claude["model"] = str(source_claude.get("model") or "claude-fable-5")
2093
2093
  tier_changed = True
2094
2094
  if tier_changed and not str(claude.get("effort") or "").strip() and source_claude.get("effort"):
2095
2095
  claude["effort"] = str(source_claude.get("effort"))
@@ -83,7 +83,7 @@ except Exception:
83
83
 
84
84
  def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
85
85
  defaults = {
86
- "claude_code": {"model": "claude-opus-4-7[1m]", "reasoning_effort": "max"},
86
+ "claude_code": {"model": "claude-fable-5", "reasoning_effort": "max"},
87
87
  "codex": {"model": "gpt-5.5", "reasoning_effort": "xhigh"},
88
88
  }
89
89
  return dict(defaults.get(client, {}))
@@ -23,6 +23,30 @@ def _now_text() -> str:
23
23
  return datetime.datetime.now().isoformat(timespec="seconds")
24
24
 
25
25
 
26
+ def _run_timestamp_sort_key(value: str | None) -> tuple[int, str]:
27
+ text = str(value or "").strip()
28
+ if not text:
29
+ return (0, "")
30
+ normalized = text.replace("Z", "+00:00")
31
+ try:
32
+ parsed = datetime.datetime.fromisoformat(normalized)
33
+ except ValueError:
34
+ return (1, text)
35
+ if parsed.tzinfo is not None:
36
+ parsed = parsed.astimezone(datetime.timezone.utc).replace(tzinfo=None)
37
+ return (2, parsed.isoformat(timespec="seconds"))
38
+
39
+
40
+ def _newer_run(candidate: dict | None, current: dict | None) -> dict | None:
41
+ if not candidate or not candidate.get("started_at"):
42
+ return current
43
+ if not current or not current.get("started_at"):
44
+ return candidate
45
+ if _run_timestamp_sort_key(candidate.get("started_at")) > _run_timestamp_sort_key(current.get("started_at")):
46
+ return candidate
47
+ return current
48
+
49
+
26
50
  def _get_db():
27
51
  """Resolve db._core lazily so reload-heavy tests use the live connection module."""
28
52
  return importlib.import_module("db._core").get_db()
@@ -406,11 +430,16 @@ def list_personal_scripts(include_disabled: bool = True, *, include_core: bool =
406
430
  schedule["last_exit_code"] = latest["exit_code"]
407
431
  script["schedules"] = script_schedules
408
432
  script["has_schedule"] = bool(script_schedules)
409
- latest = None
433
+ latest = _newer_run(
434
+ {"started_at": script.get("last_run_at"), "exit_code": script.get("last_exit_code")},
435
+ None,
436
+ )
410
437
  for schedule in script_schedules:
411
438
  started_at = schedule.get("last_run_at")
412
- if started_at and (latest is None or started_at > latest.get("started_at", "")):
413
- latest = {"started_at": started_at, "exit_code": schedule.get("last_exit_code")}
439
+ latest = _newer_run(
440
+ {"started_at": started_at, "exit_code": schedule.get("last_exit_code")},
441
+ latest,
442
+ )
414
443
  if latest:
415
444
  script["last_run_at"] = latest["started_at"]
416
445
  script["last_exit_code"] = latest["exit_code"]
@@ -6,11 +6,13 @@
6
6
  "chrome-devtools-mcp": {
7
7
  "source_type": "npm",
8
8
  "package": "chrome-devtools-mcp",
9
- "version": "1.1.1",
10
- "integrity": "sha512-Fs/ASXAkQqvYCbJjHIx/pnShjyIoZoPxdg4J3wjaA9FLkRb2ngGnisu2AGcBIXdw5qrPkOuV/cOlGOonpsE1qw==",
11
- "tarball": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.1.1.tgz",
9
+ "version": "1.2.0",
10
+ "integrity": "sha512-xHd8hoLZQArDsYhu8OUHvKBIiihx1Co9DgAPHWaM4kzRf41TpZ0IuxKioIWTEGzFKpRqQzIxpFqydY4AKqP5sQ==",
11
+ "tarball": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.2.0.tgz",
12
12
  "bin": "chrome-devtools-mcp",
13
- "engines": {"node": "^20.19.0 || ^22.12.0 || >=23"}
13
+ "engines": {
14
+ "node": "^20.19.0 || ^22.12.0 || >=23"
15
+ }
14
16
  },
15
17
  "mac-use-mcp": {
16
18
  "source_type": "npm",
@@ -19,7 +21,9 @@
19
21
  "integrity": "sha512-UcVkzvHuw+f21nEZwb3MwdqWGxLK/nYlhN55SRD6FZ2yn2t3ji0zKLD2XvQLp0ugeYyS92TqVgnw6E4P5VB+bg==",
20
22
  "tarball": "https://registry.npmjs.org/mac-use-mcp/-/mac-use-mcp-1.1.1.tgz",
21
23
  "bin": "mac-use-mcp",
22
- "engines": {"node": ">=22"}
24
+ "engines": {
25
+ "node": ">=22"
26
+ }
23
27
  },
24
28
  "native-devtools-mcp": {
25
29
  "source_type": "npm",
@@ -28,7 +32,9 @@
28
32
  "integrity": "sha512-TIR8QCKzYCaHY+N1IWB7OM6pZH49HJxRj1dZjT4RNkviA1QpgINm8H95ohMCbf4ZC5jdFssMlb9KmWNZnCeCSw==",
29
33
  "tarball": "https://registry.npmjs.org/native-devtools-mcp/-/native-devtools-mcp-0.10.1.tgz",
30
34
  "bin": "native-devtools-mcp",
31
- "engines": {"node": ">=18"}
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
32
38
  },
33
39
  "open-computer-use": {
34
40
  "source_type": "npm",
@@ -37,7 +43,7 @@
37
43
  "integrity": "sha512-KlOHmFvXHe2IEMGE/O+zMN5ASo+FQ42copj4j1xEOnyeLq4oxUxhtHqEdPUACCUcMZaHzKXfZboL1dk5a2GjLA==",
38
44
  "tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.52.tgz",
39
45
  "bin": "open-computer-use-mcp",
40
- "engines": {"node": "^20.19.0 || ^22.12.0 || >=23"}
46
+ "engines": {}
41
47
  },
42
48
  "desktop-commander": {
43
49
  "source_type": "npm",
@@ -46,7 +52,9 @@
46
52
  "integrity": "sha512-ZgdBDihpaLfrzQQQGQCPmElYMx91oUXeVEWbxbygeUfq2aOZvHrcVMeuTGy9oMDp9vxjq6d/+ZGE0mQLJnAWkw==",
47
53
  "tarball": "https://registry.npmjs.org/@wonderwhy-er/desktop-commander/-/desktop-commander-0.2.42.tgz",
48
54
  "bin": "desktop-commander",
49
- "engines": {"node": ">=18.0.0"}
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ }
50
58
  }
51
59
  }
52
60
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "claude_code": {
4
- "model": "claude-opus-4-8",
4
+ "model": "claude-fable-5",
5
5
  "reasoning_effort": "max",
6
- "display_name": "Opus 4.8 with max reasoning",
7
- "recommendation_version": 3,
8
- "previous_defaults": ["claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]"]
6
+ "display_name": "Fable 5 with max reasoning",
7
+ "recommendation_version": 4,
8
+ "previous_defaults": ["claude-opus-4-8", "claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]"]
9
9
  },
10
10
  "codex": {
11
11
  "model": "gpt-5.5",
@@ -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-8",
23
+ "model": "claude-fable-5",
24
24
  "reasoning_effort": "max",
25
- "display_name": "Opus 4.8 with max reasoning",
26
- "recommendation_version": 3,
27
- "previous_defaults": ["claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]"],
25
+ "display_name": "Fable 5 with max reasoning",
26
+ "recommendation_version": 4,
27
+ "previous_defaults": ["claude-opus-4-8", "claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]"],
28
28
  },
29
29
  "codex": {
30
30
  "model": "gpt-5.5",
@@ -99,7 +99,7 @@ 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
- _CLAUDE_DEFAULT_PREFIXES = ("claude-opus-4-6", "claude-opus-4-7")
102
+ _CLAUDE_DEFAULT_PREFIXES = ("claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8")
103
103
 
104
104
 
105
105
  def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "tiers": {
3
3
  "maximo": {
4
- "claude_code": { "model": "claude-opus-4-8", "effort": "max" },
4
+ "claude_code": { "model": "claude-fable-5", "effort": "max" },
5
5
  "codex": { "model": "gpt-5.5", "effort": "xhigh" }
6
6
  },
7
7
  "alto": {
8
- "claude_code": { "model": "claude-opus-4-8", "effort": "xhigh" },
8
+ "claude_code": { "model": "claude-fable-5", "effort": "xhigh" },
9
9
  "codex": { "model": "gpt-5.5", "effort": "high" }
10
10
  },
11
11
  "medio": {
12
- "claude_code": { "model": "claude-opus-4-8", "effort": "high" },
12
+ "claude_code": { "model": "claude-fable-5", "effort": "high" },
13
13
  "codex": { "model": "gpt-5.5", "effort": "medium" }
14
14
  },
15
15
  "bajo": {
16
- "claude_code": { "model": "claude-opus-4-8", "effort": "medium" },
16
+ "claude_code": { "model": "claude-fable-5", "effort": "medium" },
17
17
  "codex": { "model": "gpt-5.5", "effort": "low" }
18
18
  },
19
19
  "muy_bajo": {
@@ -158,10 +158,21 @@ def detect_duplicates(conn):
158
158
  if len(learnings) < 2:
159
159
  return []
160
160
 
161
- model = build_fastembed_embedding("bge-base-embeddings")
162
- texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
163
- embeddings = list(model.embed(texts))
164
- embeddings = np.array(embeddings)
161
+ # build_fastembed_embedding() lazily imports fastembed inside its body, so a
162
+ # missing backend raises ModuleNotFoundError HERE, not at the import guard
163
+ # above (which only resolves the helper symbol). Without this guard the whole
164
+ # housekeeping run crashes with exit 1 whenever there are >=2 learnings to
165
+ # compare and fastembed is absent. Degrade exactly like the "not available"
166
+ # branch instead of aborting decay/prioritization/archival.
167
+ try:
168
+ model = build_fastembed_embedding("bge-base-embeddings")
169
+ texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
170
+ embeddings = list(model.embed(texts))
171
+ embeddings = np.array(embeddings)
172
+ except Exception as exc:
173
+ print(f"[{ts}] Dedup skipped: embedding backend unavailable "
174
+ f"({type(exc).__name__}: {exc})")
175
+ return []
165
176
 
166
177
  # Normalize
167
178
  norms = np.linalg.norm(embeddings, axis=1, keepdims=True)