nexo-brain 7.9.15 → 7.9.18

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.9.13",
3
+ "version": "7.9.18",
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 `7.9.14` is the current packaged-runtime line. Patch release over `7.9.13`: `task_close(done)` now hard-blocks missing verify/change-log/cortex evidence instead of silently degrading to debt-only closes, self-audit auto-drains stale `protocol_debt` every day, and Codex session parity now flags partial bootstrap/startup/heartbeat drift instead of passing as healthy when only one recent session behaved correctly. Coordinated Desktop release remains v0.28.14.
21
+ Version `7.9.18` is the current packaged-runtime line. Patch release over `7.9.17`: packaged client-sync imports now work when `NEXO_HOME` is unset, so `nexo clients sync`, `nexo update`, and runtime doctor bootstrap checks no longer hit the `_user_home` import-order crash. It includes the v7.9.17 Bandit gate fix and the v7.9.16 restart-marker deadlock fix.
22
+
23
+ Previously in `7.9.17`: continuity snapshot idempotency marks its SHA-1 digest as non-security usage, keeping the high-severity Bandit gate green while preserving stable idempotency keys.
22
24
 
23
25
  Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.6 shutdown/archive/delete/app-exit checks.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.15",
3
+ "version": "7.9.18",
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",
@@ -19,6 +19,11 @@ from client_preferences import (
19
19
  )
20
20
  from runtime_home import resolve_nexo_home
21
21
 
22
+
23
+ def _user_home() -> Path:
24
+ return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
25
+
26
+
22
27
  def _resolve_templates_dir(module_file: str | os.PathLike[str]) -> Path:
23
28
  module_dir = Path(module_file).resolve().parent
24
29
  direct = module_dir / "templates"
@@ -66,10 +71,6 @@ BOOTSTRAP_SPECS = {
66
71
  }
67
72
 
68
73
 
69
- def _user_home() -> Path:
70
- return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
71
-
72
-
73
74
  def _default_nexo_home() -> Path:
74
75
  return resolve_nexo_home(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo")))
75
76
 
@@ -46,7 +46,7 @@ def build_snapshot_idempotency_key(
46
46
  normalized,
47
47
  ]
48
48
  )
49
- return hashlib.sha1(seed.encode("utf-8")).hexdigest()
49
+ return hashlib.sha1(seed.encode("utf-8"), usedforsecurity=False).hexdigest()
50
50
 
51
51
 
52
52
  def write_continuity_snapshot(
package/src/db/_schema.py CHANGED
@@ -911,6 +911,12 @@ def _m38_evolution_log_proposal_payload(conn):
911
911
 
912
912
  def _m55_cortex_critique_trace(conn):
913
913
  """Persist heuristic-vs-LLM critique traces for Cortex decisions."""
914
+ # Some legacy/minimal runtimes have schema_migrations backfilled through
915
+ # v48 without the optional Cortex table present. Repair the dependency
916
+ # before adding v55 columns so update never bricks those installs.
917
+ _m34_cortex_evaluations(conn)
918
+ _m35_cortex_evaluation_outcome_link(conn)
919
+ _m37_cortex_goal_profile_trace(conn)
914
920
  _migrate_add_column(conn, "cortex_evaluations", "heuristic_choice", "TEXT DEFAULT ''")
915
921
  _migrate_add_column(conn, "cortex_evaluations", "heuristic_reasoning", "TEXT DEFAULT ''")
916
922
  _migrate_add_column(conn, "cortex_evaluations", "critique_payload", "TEXT DEFAULT '{}'")
@@ -16,7 +16,13 @@ import paths
16
16
  CONTINUITY_API_LEVEL = 1
17
17
  MCP_STATUS_SCHEMA_VERSION = 1
18
18
  PROCESS_VERSION = ""
19
+ RESTART_CLIENT_ACTIONS = {
20
+ "claude_desktop": "restart_client_required",
21
+ "claude_code": "restart_session_required",
22
+ "codex": "restart_session_required",
23
+ }
19
24
  RESTART_ALLOWLIST = {
25
+ "nexo_startup",
20
26
  "nexo_status",
21
27
  "nexo_system_catalog",
22
28
  "nexo_tool_explain",
@@ -48,6 +54,61 @@ def _write_json_atomic(path: Path, payload: dict) -> None:
48
54
  tmp.replace(path)
49
55
 
50
56
 
57
+ def _normalize_restart_client(value: str | None) -> str:
58
+ candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
59
+ aliases = {
60
+ "claude": "claude_code",
61
+ "claudecode": "claude_code",
62
+ "claude_code": "claude_code",
63
+ "claude_desktop": "claude_desktop",
64
+ "claude_desktop_app": "claude_desktop",
65
+ "desktop": "claude_desktop",
66
+ "codex": "codex",
67
+ }
68
+ resolved = aliases.get(candidate, candidate)
69
+ if resolved in RESTART_CLIENT_ACTIONS:
70
+ return resolved
71
+ return ""
72
+
73
+
74
+ def _enabled_flag(value) -> bool:
75
+ if isinstance(value, str):
76
+ return value.strip().lower() not in {"", "0", "false", "no", "off", "disabled", "none"}
77
+ return bool(value)
78
+
79
+
80
+ def _restart_clients_from_preferences() -> dict[str, str]:
81
+ try:
82
+ from runtime_power import load_schedule_config
83
+
84
+ prefs = load_schedule_config()
85
+ except Exception:
86
+ prefs = {}
87
+
88
+ raw_clients = prefs.get("interactive_clients") if isinstance(prefs, dict) else {}
89
+ clients: dict[str, str] = {}
90
+ if isinstance(raw_clients, dict):
91
+ for raw_key, raw_enabled in raw_clients.items():
92
+ key = _normalize_restart_client(str(raw_key or ""))
93
+ if key and _enabled_flag(raw_enabled):
94
+ clients[key] = RESTART_CLIENT_ACTIONS[key]
95
+ return clients
96
+
97
+
98
+ def _restart_clients_for_marker(*, client: str = "") -> dict[str, str]:
99
+ explicit_client = _normalize_restart_client(client or os.environ.get("NEXO_MCP_CLIENT", ""))
100
+ if explicit_client:
101
+ return {explicit_client: RESTART_CLIENT_ACTIONS[explicit_client]}
102
+
103
+ clients = _restart_clients_from_preferences()
104
+ if clients:
105
+ return clients
106
+
107
+ # Safe default for fresh/legacy installs: Claude Code is the primary
108
+ # terminal client, and avoiding absent clients prevents permanent markers.
109
+ return {"claude_code": RESTART_CLIENT_ACTIONS["claude_code"]}
110
+
111
+
51
112
  def core_container_dir() -> Path:
52
113
  return paths.home() / "core"
53
114
 
@@ -135,6 +196,7 @@ def write_restart_required_marker(
135
196
  from_version: str,
136
197
  to_version: str,
137
198
  reason: str = "brain_update",
199
+ client: str = "",
138
200
  ) -> dict:
139
201
  path = restart_required_marker_path()
140
202
  payload = {
@@ -144,11 +206,7 @@ def write_restart_required_marker(
144
206
  "to_version": str(to_version or "").strip(),
145
207
  "reason": str(reason or "brain_update"),
146
208
  "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
147
- "clients": {
148
- "claude_desktop": "restart_client_required",
149
- "claude_code": "restart_session_required",
150
- "codex": "restart_session_required",
151
- },
209
+ "clients": _restart_clients_for_marker(client=client),
152
210
  }
153
211
  _write_json_atomic(path, payload)
154
212
  payload["path"] = str(path)
@@ -206,6 +264,7 @@ def activate_versioned_runtime_snapshot(*, source_root: Path | None = None, vers
206
264
 
207
265
 
208
266
  def clear_restart_required_marker(*, client: str = "", installed_version: str = "", process_version: str = "") -> dict:
267
+ client = _normalize_restart_client(client)
209
268
  path = restart_required_marker_path()
210
269
  marker = read_restart_required_marker()
211
270
  if not marker.get("required"):
@@ -244,6 +303,7 @@ def clear_restart_required_marker(*, client: str = "", installed_version: str =
244
303
 
245
304
 
246
305
  def resolve_restart_required(*, client: str = "", installed_version: str = "", process_version: str = "") -> dict:
306
+ client = _normalize_restart_client(client)
247
307
  marker = read_restart_required_marker()
248
308
  installed = str(installed_version or installed_runtime_version() or "").strip()
249
309
  process = str(process_version or PROCESS_VERSION or installed).strip()
@@ -277,6 +337,7 @@ def resolve_restart_required(*, client: str = "", installed_version: str = "", p
277
337
 
278
338
 
279
339
  def build_mcp_status(*, client: str = "") -> dict:
340
+ client = _normalize_restart_client(client)
280
341
  state = resolve_restart_required(client=client)
281
342
  marker = state["marker"]
282
343
  return {
@@ -319,6 +380,28 @@ def prime_process_version() -> str:
319
380
  class RestartRequiredMiddleware(Middleware):
320
381
  client: str = ""
321
382
 
383
+ def __post_init__(self) -> None:
384
+ self.client = _normalize_restart_client(self.client)
385
+
386
+ def _ack_current_client_if_restarted(self, state: dict) -> dict:
387
+ if not self.client or not state.get("restart_required"):
388
+ return state
389
+ installed = str(state.get("installed_version") or "").strip()
390
+ process = str(state.get("process_version") or "").strip()
391
+ if not installed or not process or installed != process:
392
+ return state
393
+
394
+ clear_restart_required_marker(
395
+ client=self.client,
396
+ installed_version=installed,
397
+ process_version=process,
398
+ )
399
+ return resolve_restart_required(
400
+ client=self.client,
401
+ installed_version=installed,
402
+ process_version=process,
403
+ )
404
+
322
405
  async def _tool_result_for_restart_required(self, context, payload: dict) -> ToolResult:
323
406
  payload_text = json.dumps(payload, ensure_ascii=False)
324
407
  tool = None
@@ -344,6 +427,7 @@ class RestartRequiredMiddleware(Middleware):
344
427
  async def on_call_tool(self, context, call_next):
345
428
  tool_name = str(getattr(context.message, "name", "") or "").strip()
346
429
  state = resolve_restart_required(client=self.client)
430
+ state = self._ack_current_client_if_restarted(state)
347
431
  if not state["restart_required"] or tool_name in RESTART_ALLOWLIST:
348
432
  return await call_next(context)
349
433