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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/bootstrap_docs.py +5 -4
- package/src/db/_continuity.py +1 -1
- package/src/db/_schema.py +6 -0
- package/src/runtime_versioning.py +89 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
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.
|
|
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.
|
|
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",
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -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
|
|
package/src/db/_continuity.py
CHANGED
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
|
|