nexo-brain 7.31.2 → 7.31.4

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.31.2",
3
+ "version": "7.31.4",
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,9 +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 `7.31.2` is the current packaged-runtime line. Patch release over v7.31.1 - the session ID becomes a durable identity (sessions survive quiet work periods; physical cleanup at 24h), runtime residents are isolated per generation so two installs can never kill each other's resident, and obsolete residents retire themselves once clients disconnect.
21
+ Version `7.31.4` is the current packaged-runtime line. Patch release over v7.31.3 - memory recall honours absolute time ranges (ISO dates, start..end ranges, datetimes, epochs) and enforces the window in SQL, so asking about a specific past day returns that day.
22
22
 
23
- Previously in `7.31.1`: patch release over v7.31.0 - headless automations pause and queue when the selected engine is unavailable (credits, rate limits, expired auth) and resume automatically with one operator notice in their language; protocol nudge shaping ships in shadow mode; and the client config push stops writing an invalid `mcp__*` permission rule to Claude Code settings.
23
+ Previously in `7.31.3`: patch release over v7.31.2 - the recommended Claude Code model returns to Opus 4.8 with max reasoning (installs riding NEXO defaults migrate back automatically; customized models untouched), and the dead heartbeat-enforcement hook trio is removed from the source tree.
24
24
 
25
25
  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.
26
26
 
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-fable-5", reasoning_effort: "max", display_name: "Fable 5 with max reasoning" },
118
+ claude_code: { model: "claude-opus-4-8", reasoning_effort: "max", display_name: "Opus 4.8 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.31.2",
3
+ "version": "7.31.4",
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", "claude-opus-4-8")
2061
+ old_prefixes = ("claude-opus-4-6", "claude-opus-4-7", "claude-fable-5")
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-fable-5")
2092
+ claude["model"] = str(source_claude.get("model") or "claude-opus-4-8")
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-fable-5", "reasoning_effort": "max"},
86
+ "claude_code": {"model": "claude-opus-4-8", "reasoning_effort": "max"},
87
87
  "codex": {"model": "gpt-5.5", "reasoning_effort": "xhigh"},
88
88
  }
89
89
  return dict(defaults.get(client, {}))
@@ -750,12 +750,23 @@ def list_memory_events(
750
750
  session_id: str = "",
751
751
  project_key: str = "",
752
752
  limit: int = 20,
753
+ start_ts: float | None = None,
754
+ end_ts: float | None = None,
753
755
  ) -> list[dict]:
754
756
  conn = _core().get_db()
755
757
  if not _table_exists(conn, "memory_events"):
756
758
  return []
757
759
  clauses = ["1=1"]
758
760
  params: list[Any] = []
761
+ # time_range push-down (ff78ff94): bounds must constrain the SQL fetch
762
+ # itself — Python-side filtering after a recency-truncated fetch made old
763
+ # windows return nothing.
764
+ if start_ts is not None:
765
+ clauses.append("created_at >= ?")
766
+ params.append(float(start_ts))
767
+ if end_ts is not None:
768
+ clauses.append("created_at < ?")
769
+ params.append(float(end_ts))
759
770
  if event_type.strip():
760
771
  clauses.append("event_type = ?")
761
772
  params.append(event_type.strip().lower())
@@ -798,12 +809,20 @@ def list_memory_observations(
798
809
  project_key: str = "",
799
810
  status: str = "",
800
811
  limit: int = 20,
812
+ start_ts: float | None = None,
813
+ end_ts: float | None = None,
801
814
  ) -> list[dict]:
802
815
  conn = _core().get_db()
803
816
  if not _table_exists(conn, "memory_observations"):
804
817
  return []
805
818
  clauses = ["1=1"]
806
819
  params: list[Any] = []
820
+ if start_ts is not None:
821
+ clauses.append("created_at >= ?")
822
+ params.append(float(start_ts))
823
+ if end_ts is not None:
824
+ clauses.append("created_at < ?")
825
+ params.append(float(end_ts))
807
826
  if observation_type.strip():
808
827
  clauses.append("observation_type = ?")
809
828
  params.append(observation_type.strip().lower())
@@ -845,6 +864,8 @@ def search_memory_observations_fts(
845
864
  *,
846
865
  project_key: str = "",
847
866
  limit: int = 20,
867
+ start_ts: float | None = None,
868
+ end_ts: float | None = None,
848
869
  ) -> list[dict]:
849
870
  conn = _core().get_db()
850
871
  if not _table_exists(conn, "memory_observations_fts"):
@@ -859,6 +880,12 @@ def search_memory_observations_fts(
859
880
  WHERE memory_observations_fts MATCH ?
860
881
  """
861
882
  params: list[Any] = [fts]
883
+ if start_ts is not None:
884
+ sql += " AND o.created_at >= ?"
885
+ params.append(float(start_ts))
886
+ if end_ts is not None:
887
+ sql += " AND o.created_at < ?"
888
+ params.append(float(end_ts))
862
889
  if project_key.strip():
863
890
  sql += " AND o.project_key = ?"
864
891
  params.append(project_key.strip())
@@ -39,9 +39,9 @@
39
39
  "open-computer-use": {
40
40
  "source_type": "npm",
41
41
  "package": "open-computer-use",
42
- "version": "0.1.52",
43
- "integrity": "sha512-KlOHmFvXHe2IEMGE/O+zMN5ASo+FQ42copj4j1xEOnyeLq4oxUxhtHqEdPUACCUcMZaHzKXfZboL1dk5a2GjLA==",
44
- "tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.52.tgz",
42
+ "version": "0.1.53",
43
+ "integrity": "sha512-5qwCPl7Gm4Wk2i/wFkq2dVLN2SzNRQSJTd95zXdGF+u5ZsUXkFx1IFVdNbYelWOpc4fgy8Z8/gYrbacj/2chig==",
44
+ "tarball": "https://registry.npmjs.org/open-computer-use/-/open-computer-use-0.1.53.tgz",
45
45
  "bin": "open-computer-use-mcp",
46
46
  "engines": {}
47
47
  },
@@ -82,6 +82,40 @@ def _parse_time_range(value: str = "") -> tuple[float | None, float | None, str]
82
82
  unit = match.group(2)
83
83
  delta = timedelta(hours=amount) if unit.startswith("h") else timedelta(days=amount)
84
84
  return (now - delta).timestamp(), now.timestamp(), clean
85
+
86
+ # Operator bug (session ff78ff94, 11-jun): absolute values silently fell
87
+ # through to (None, None, "") which DISABLED the filter — asking for a
88
+ # specific past day returned the most recent events instead. Support ISO
89
+ # dates, ISO ranges (date end is inclusive: bound = next midnight), ISO
90
+ # datetimes, and epoch seconds / epoch ranges.
91
+ def _point(text, *, end_of_day=False):
92
+ text = text.strip()
93
+ if re.fullmatch(r"\d{9,}(\.\d+)?", text):
94
+ return float(text), False
95
+ try:
96
+ if re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
97
+ day = datetime.fromisoformat(text)
98
+ if end_of_day:
99
+ return (day + timedelta(days=1)).timestamp(), True
100
+ return day.timestamp(), True
101
+ return datetime.fromisoformat(text).timestamp(), False
102
+ except ValueError:
103
+ return None, False
104
+
105
+ if ".." in clean:
106
+ left, _, right = clean.partition("..")
107
+ start_ts, _ = _point(left)
108
+ end_ts, _ = _point(right, end_of_day=True)
109
+ if start_ts is not None and end_ts is not None and end_ts > start_ts:
110
+ return start_ts, end_ts, f"range:{clean}"
111
+ return None, None, ""
112
+
113
+ point_ts, is_date = _point(clean)
114
+ if point_ts is not None:
115
+ if is_date:
116
+ return point_ts, point_ts + 86400, f"day:{clean}"
117
+ # Single datetime/epoch: a one-hour window centred forward.
118
+ return point_ts, point_ts + 3600, f"at:{clean}"
85
119
  return None, None, ""
86
120
 
87
121
 
@@ -177,6 +211,8 @@ def memory_search(
177
211
  clean_query,
178
212
  project_key="",
179
213
  limit=max_items * 3,
214
+ start_ts=start,
215
+ end_ts=end,
180
216
  ):
181
217
  uid = item.get("observation_uid") or f"id:{item.get('id')}"
182
218
  observations_by_uid[uid] = item
@@ -184,6 +220,8 @@ def memory_search(
184
220
  query=clean_query,
185
221
  project_key="",
186
222
  limit=max_items * 3,
223
+ start_ts=start,
224
+ end_ts=end,
187
225
  ):
188
226
  uid = item.get("observation_uid") or f"id:{item.get('id')}"
189
227
  observations_by_uid.setdefault(uid, item)
@@ -192,6 +230,8 @@ def memory_search(
192
230
  query=clean_query,
193
231
  project_key="",
194
232
  limit=max_items * 3,
233
+ start_ts=start,
234
+ end_ts=end,
195
235
  )
196
236
 
197
237
  candidates = [
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "claude_code": {
4
- "model": "claude-fable-5",
4
+ "model": "claude-opus-4-8",
5
5
  "reasoning_effort": "max",
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]"]
6
+ "display_name": "Opus 4.8 with max reasoning",
7
+ "recommendation_version": 5,
8
+ "previous_defaults": [
9
+ "claude-fable-5",
10
+ "claude-opus-4-7[1m]",
11
+ "claude-opus-4-7",
12
+ "claude-opus-4-6[1m]"
13
+ ]
9
14
  },
10
15
  "codex": {
11
16
  "model": "gpt-5.5",
12
17
  "reasoning_effort": "xhigh",
13
18
  "display_name": "GPT-5.5 with max reasoning",
14
19
  "recommendation_version": 2,
15
- "previous_defaults": ["gpt-5.4"]
20
+ "previous_defaults": [
21
+ "gpt-5.4"
22
+ ]
16
23
  }
17
24
  }
@@ -20,7 +20,7 @@ from typing import Any
20
20
  _FALLBACK: dict[str, Any] = {
21
21
  "schema_version": 1,
22
22
  "claude_code": {
23
- "model": "claude-fable-5",
23
+ "model": "claude-opus-4-8",
24
24
  "reasoning_effort": "max",
25
25
  "display_name": "Fable 5 with max reasoning",
26
26
  "recommendation_version": 4,
@@ -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", "claude-opus-4-8")
102
+ _CLAUDE_DEFAULT_PREFIXES = ("claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-fable-5")
103
103
 
104
104
 
105
105
  def heal_runtime_profiles(profiles: dict) -> tuple[dict, list[str]]:
@@ -51,7 +51,8 @@ DEFAULT_RETRY_AFTER_S = {
51
51
  _FAILURE_PATTERNS = (
52
52
  ("credits", re.compile(
53
53
  r"credit balance is too low|insufficient[_ ]quota|exceeded your current quota"
54
- r"|billing hard limit|out of credits|usage limit reached|plan limits",
54
+ r"|billing hard limit|out of credits|usage limit reached|hit your usage limit"
55
+ r"|purchase more credits|plan limits",
55
56
  re.I)),
56
57
  ("rate_limit", re.compile(
57
58
  r"rate[_ -]?limit|too many requests|\b429\b|overloaded[_ ]error|\b529\b"
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "tiers": {
3
3
  "maximo": {
4
- "claude_code": { "model": "claude-fable-5", "effort": "max" },
4
+ "claude_code": { "model": "claude-opus-4-8", "effort": "max" },
5
5
  "codex": { "model": "gpt-5.5", "effort": "xhigh" }
6
6
  },
7
7
  "alto": {
8
- "claude_code": { "model": "claude-fable-5", "effort": "xhigh" },
8
+ "claude_code": { "model": "claude-opus-4-8", "effort": "xhigh" },
9
9
  "codex": { "model": "gpt-5.5", "effort": "high" }
10
10
  },
11
11
  "medio": {
12
- "claude_code": { "model": "claude-fable-5", "effort": "high" },
12
+ "claude_code": { "model": "claude-opus-4-8", "effort": "high" },
13
13
  "codex": { "model": "gpt-5.5", "effort": "medium" }
14
14
  },
15
15
  "bajo": {
16
- "claude_code": { "model": "claude-fable-5", "effort": "medium" },
16
+ "claude_code": { "model": "claude-opus-4-8", "effort": "medium" },
17
17
  "codex": { "model": "gpt-5.5", "effort": "low" }
18
18
  },
19
19
  "muy_bajo": {
@@ -1,103 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Heartbeat enforcement for NEXO sessions.
3
-
4
- Tracks user messages vs heartbeat calls. Emits a warning when more than two
5
- user messages pass without a heartbeat call.
6
-
7
- Modes:
8
- - HEARTBEAT_MODE=user_msg: increment counter on UserPromptSubmit
9
- - HEARTBEAT_MODE=post_tool: inspect PostToolUse payload, reset on heartbeat,
10
- warn when other tools keep running without one
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import json
16
- import os
17
- import sys
18
- import time
19
- from pathlib import Path
20
-
21
- try:
22
- import paths
23
- except ModuleNotFoundError as exc:
24
- if getattr(exc, "name", "") != "paths":
25
- raise
26
-
27
- class _PathsFallback:
28
- @staticmethod
29
- def operations_dir():
30
- return Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo")) / "operations"
31
-
32
- paths = _PathsFallback()
33
-
34
- STATE_FILE = paths.operations_dir() / ".heartbeat-state.json"
35
- THRESHOLD = 2
36
- HEARTBEAT_TOOL = "nexo_heartbeat"
37
- SKIP_TOOLS = {"nexo_startup", "nexo_stop", "nexo_smart_startup"}
38
-
39
-
40
- def _read_state() -> dict:
41
- try:
42
- return json.loads(STATE_FILE.read_text())
43
- except Exception:
44
- return {"user_msgs": 0, "last_heartbeat_ts": 0.0, "last_user_msg_ts": 0.0}
45
-
46
-
47
- def _write_state(state: dict) -> None:
48
- try:
49
- STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
50
- STATE_FILE.write_text(json.dumps(state))
51
- except Exception:
52
- pass
53
-
54
-
55
- def handle_user_message() -> int:
56
- state = _read_state()
57
- state["user_msgs"] = state.get("user_msgs", 0) + 1
58
- state["last_user_msg_ts"] = time.time()
59
- _write_state(state)
60
- return 0
61
-
62
-
63
- def handle_post_tool(payload: dict) -> int:
64
- tool_name = str(payload.get("tool_name", "")).strip()
65
- short_name = tool_name.rsplit("__", 1)[-1] if "__" in tool_name else tool_name
66
- state = _read_state()
67
-
68
- if short_name == HEARTBEAT_TOOL:
69
- state["user_msgs"] = 0
70
- state["last_heartbeat_ts"] = time.time()
71
- _write_state(state)
72
- return 0
73
-
74
- if short_name in SKIP_TOOLS:
75
- return 0
76
-
77
- user_msgs = state.get("user_msgs", 0)
78
- if user_msgs > THRESHOLD:
79
- print(
80
- f"\nWARNING: HEARTBEAT OVERDUE ({user_msgs} user messages without nexo_heartbeat). "
81
- "Call nexo_heartbeat(sid=SID, task='...') before continuing."
82
- )
83
- return 0
84
-
85
-
86
- def main() -> int:
87
- mode = os.environ.get("HEARTBEAT_MODE", "").strip()
88
- if mode == "user_msg":
89
- return handle_user_message()
90
- if mode == "post_tool":
91
- raw = sys.stdin.read()
92
- if not raw.strip():
93
- return 0
94
- try:
95
- payload = json.loads(raw)
96
- except Exception:
97
- return 0
98
- return handle_post_tool(payload)
99
- return 0
100
-
101
-
102
- if __name__ == "__main__":
103
- raise SystemExit(main())
@@ -1,20 +0,0 @@
1
- #!/bin/bash
2
- # NEXO PostToolUse hook — heartbeat enforcement checker
3
- set -uo pipefail
4
-
5
- INPUT=$(cat || true)
6
- [ -z "$INPUT" ] && exit 0
7
-
8
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
9
- HELPER=""
10
- if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
11
- HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
12
- elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
13
- HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
14
- elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
15
- HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
16
- fi
17
-
18
- [ -z "$HELPER" ] && exit 0
19
- HEARTBEAT_MODE=post_tool python3 "$HELPER" <<< "$INPUT" 2>/dev/null || true
20
- exit 0
@@ -1,17 +0,0 @@
1
- #!/bin/bash
2
- # NEXO UserPromptSubmit hook — track user messages for heartbeat enforcement
3
- set -uo pipefail
4
-
5
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
6
- HELPER=""
7
- if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
8
- HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
9
- elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
10
- HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
11
- elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
12
- HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
13
- fi
14
-
15
- [ -z "$HELPER" ] && exit 0
16
- HEARTBEAT_MODE=user_msg python3 "$HELPER" 2>/dev/null || true
17
- exit 0