nexo-brain 3.0.0 → 3.0.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 +1 -1
- package/package.json +1 -1
- package/src/agent_runner.py +5 -1
- package/src/client_preferences.py +5 -1
- package/src/client_sync.py +5 -1
- package/src/dashboard/app.py +1 -1
- package/src/db/_workflow.py +2 -2
- package/src/doctor/providers/boot.py +45 -19
- package/src/doctor/providers/runtime.py +7 -3
- package/src/requirements.txt +1 -0
- package/src/state_watchers_runtime.py +6 -6
- package/src/tools_sessions.py +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.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
|
@@ -50,7 +50,7 @@ That means NEXO now manages not only the shared runtime and MCP wiring, but also
|
|
|
50
50
|
|
|
51
51
|
Versions `2.6.14` through `2.7.0` established the practical shared-brain baseline: managed Claude/Codex bootstrap, Codex config sync, transcript-aware Deep Sleep, 60-day long-horizon analysis, weekly/monthly summary artifacts, retrieval auto-mode, and the first measured engineering loop.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Versions `3.0.0` and `3.0.1` close the next execution gap:
|
|
54
54
|
|
|
55
55
|
- protocol discipline is now a runtime contract, not just instructions:
|
|
56
56
|
- `nexo_task_open`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
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/agent_runner.py
CHANGED
|
@@ -9,9 +9,13 @@ import shutil
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import tempfile
|
|
11
11
|
import time
|
|
12
|
-
import tomllib
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
|
|
14
|
+
try:
|
|
15
|
+
import tomllib
|
|
16
|
+
except ModuleNotFoundError: # Python < 3.11
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
|
|
15
19
|
from client_preferences import (
|
|
16
20
|
BACKEND_NONE,
|
|
17
21
|
CLIENT_CLAUDE_CODE,
|
|
@@ -5,9 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
|
-
import tomllib
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
|
|
10
|
+
try:
|
|
11
|
+
import tomllib
|
|
12
|
+
except ModuleNotFoundError: # Python < 3.11
|
|
13
|
+
import tomli as tomllib
|
|
14
|
+
|
|
11
15
|
from runtime_power import load_schedule_config, save_schedule_config
|
|
12
16
|
|
|
13
17
|
|
package/src/client_sync.py
CHANGED
|
@@ -10,9 +10,13 @@ import shlex
|
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
-
import tomllib
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
|
|
15
|
+
try:
|
|
16
|
+
import tomllib
|
|
17
|
+
except ModuleNotFoundError: # Python < 3.11
|
|
18
|
+
import tomli as tomllib
|
|
19
|
+
|
|
16
20
|
from bootstrap_docs import sync_client_bootstrap
|
|
17
21
|
|
|
18
22
|
try:
|
package/src/dashboard/app.py
CHANGED
|
@@ -30,7 +30,7 @@ if _PARENT not in sys.path:
|
|
|
30
30
|
|
|
31
31
|
from agent_runner import AgentRunnerError, build_followup_terminal_shell_command
|
|
32
32
|
|
|
33
|
-
app = FastAPI(title="NEXO Brain Dashboard", version="3.0.
|
|
33
|
+
app = FastAPI(title="NEXO Brain Dashboard", version="3.0.1")
|
|
34
34
|
|
|
35
35
|
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
|
36
36
|
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
package/src/db/_workflow.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import secrets
|
|
6
6
|
import time
|
|
7
|
-
from datetime import
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
|
|
9
9
|
from db._core import get_db
|
|
10
10
|
|
|
@@ -48,7 +48,7 @@ def _workflow_goal_id() -> str:
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _now_sql() -> str:
|
|
51
|
-
return datetime.now(
|
|
51
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _as_json(value, default):
|
|
@@ -148,37 +148,63 @@ def check_python_runtime() -> DoctorCheck:
|
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
CRITICAL_CONFIG_FILES = (
|
|
152
|
+
("schedule.json", ("config", "schedule.json")),
|
|
153
|
+
("optionals.json", ("config", "optionals.json")),
|
|
154
|
+
("crons/manifest.json", ("crons", "manifest.json")),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
151
158
|
def check_config_parse() -> DoctorCheck:
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
"""Validate that critical JSON config files parse correctly."""
|
|
160
|
+
import json
|
|
161
|
+
|
|
162
|
+
errors: list[str] = []
|
|
163
|
+
checked: list[str] = []
|
|
164
|
+
|
|
165
|
+
for label, relative in CRITICAL_CONFIG_FILES:
|
|
166
|
+
path = NEXO_HOME.joinpath(*relative)
|
|
167
|
+
if not path.exists():
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
data = json.loads(path.read_text())
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
errors.append(f"{label}: {exc}")
|
|
173
|
+
continue
|
|
174
|
+
if not isinstance(data, dict):
|
|
175
|
+
errors.append(f"{label}: expected JSON object, got {type(data).__name__}")
|
|
176
|
+
continue
|
|
177
|
+
checked.append(label)
|
|
178
|
+
|
|
179
|
+
if errors:
|
|
155
180
|
return DoctorCheck(
|
|
156
181
|
id="boot.config_parse",
|
|
157
182
|
tier="boot",
|
|
158
|
-
status="
|
|
159
|
-
severity="
|
|
160
|
-
summary="
|
|
183
|
+
status="degraded",
|
|
184
|
+
severity="warn",
|
|
185
|
+
summary=f"{len(errors)} config file parse error" + ("s" if len(errors) != 1 else ""),
|
|
186
|
+
evidence=errors,
|
|
187
|
+
repair_plan=["Fix JSON syntax in the listed config files, or delete them to fall back to defaults"],
|
|
161
188
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
json.loads(schedule_file.read_text())
|
|
189
|
+
|
|
190
|
+
if not checked:
|
|
165
191
|
return DoctorCheck(
|
|
166
192
|
id="boot.config_parse",
|
|
167
193
|
tier="boot",
|
|
168
194
|
status="healthy",
|
|
169
195
|
severity="info",
|
|
170
|
-
summary="
|
|
171
|
-
)
|
|
172
|
-
except Exception as e:
|
|
173
|
-
return DoctorCheck(
|
|
174
|
-
id="boot.config_parse",
|
|
175
|
-
tier="boot",
|
|
176
|
-
status="degraded",
|
|
177
|
-
severity="warn",
|
|
178
|
-
summary=f"schedule.json parse error: {e}",
|
|
179
|
-
repair_plan=["Fix JSON syntax in schedule.json or delete to use defaults"],
|
|
196
|
+
summary="No config files present (using defaults)",
|
|
180
197
|
)
|
|
181
198
|
|
|
199
|
+
return DoctorCheck(
|
|
200
|
+
id="boot.config_parse",
|
|
201
|
+
tier="boot",
|
|
202
|
+
status="healthy",
|
|
203
|
+
severity="info",
|
|
204
|
+
summary=f"{len(checked)} config file" + ("s" if len(checked) != 1 else "") + " parse OK",
|
|
205
|
+
evidence=checked,
|
|
206
|
+
)
|
|
207
|
+
|
|
182
208
|
|
|
183
209
|
def run_boot_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
184
210
|
"""Run all boot-tier checks."""
|
|
@@ -12,9 +12,13 @@ import sqlite3
|
|
|
12
12
|
import subprocess
|
|
13
13
|
import sys
|
|
14
14
|
import time
|
|
15
|
-
import tomllib
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
|
|
17
|
+
try:
|
|
18
|
+
import tomllib
|
|
19
|
+
except ModuleNotFoundError: # Python < 3.11
|
|
20
|
+
import tomli as tomllib
|
|
21
|
+
|
|
18
22
|
from client_preferences import (
|
|
19
23
|
detect_installed_clients,
|
|
20
24
|
normalize_client_preferences,
|
|
@@ -738,7 +742,7 @@ def _parse_timestamp(value: str) -> dt.datetime | None:
|
|
|
738
742
|
except ValueError:
|
|
739
743
|
return None
|
|
740
744
|
if parsed.tzinfo is None:
|
|
741
|
-
parsed = parsed.replace(tzinfo=dt.
|
|
745
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
742
746
|
return parsed
|
|
743
747
|
|
|
744
748
|
|
|
@@ -2470,7 +2474,7 @@ def check_state_watchers() -> DoctorCheck:
|
|
|
2470
2474
|
generated_dt = dt.datetime.fromisoformat(str(generated_at).replace("Z", "+00:00"))
|
|
2471
2475
|
except Exception:
|
|
2472
2476
|
generated_dt = None
|
|
2473
|
-
if not generated_dt or (dt.datetime.now(dt.
|
|
2477
|
+
if not generated_dt or (dt.datetime.now(dt.timezone.utc) - generated_dt).total_seconds() > 36 * 3600:
|
|
2474
2478
|
status = "degraded"
|
|
2475
2479
|
severity = "warn"
|
|
2476
2480
|
repair_plan.append("Refresh state watchers daily so repo/API/expiry drift stays explicit")
|
package/src/requirements.txt
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
import sqlite3
|
|
7
7
|
import subprocess
|
|
8
|
-
from datetime import
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from urllib import error, request
|
|
11
11
|
|
|
@@ -30,7 +30,7 @@ def _manifest_file() -> Path:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def _now_iso() -> str:
|
|
33
|
-
return datetime.now(
|
|
33
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _parse_dt(value: str | None) -> datetime | None:
|
|
@@ -41,13 +41,13 @@ def _parse_dt(value: str | None) -> datetime | None:
|
|
|
41
41
|
for candidate in (normalized, normalized + "T00:00:00+00:00"):
|
|
42
42
|
try:
|
|
43
43
|
parsed = datetime.fromisoformat(candidate)
|
|
44
|
-
return parsed if parsed.tzinfo else parsed.replace(tzinfo=
|
|
44
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
45
45
|
except Exception:
|
|
46
46
|
continue
|
|
47
47
|
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
|
|
48
48
|
try:
|
|
49
49
|
parsed = datetime.strptime(text, fmt)
|
|
50
|
-
return parsed.replace(tzinfo=
|
|
50
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
51
51
|
except Exception:
|
|
52
52
|
continue
|
|
53
53
|
return None
|
|
@@ -150,7 +150,7 @@ def _evaluate_cron_drift(watcher: dict) -> dict:
|
|
|
150
150
|
last_run = _load_last_cron_run(cron_id)
|
|
151
151
|
if not last_run:
|
|
152
152
|
return {"health": "critical", "summary": f"cron {cron_id} has never run", "evidence": [f"threshold_seconds={threshold}"]}
|
|
153
|
-
age = (datetime.now(
|
|
153
|
+
age = (datetime.now(timezone.utc) - last_run).total_seconds()
|
|
154
154
|
if age > threshold * 2:
|
|
155
155
|
health = "critical"
|
|
156
156
|
elif age > threshold:
|
|
@@ -218,7 +218,7 @@ def _evaluate_expiry(watcher: dict) -> dict:
|
|
|
218
218
|
return {"health": "critical", "summary": f"expiry watcher missing due_at: {watcher.get('watcher_id')}", "evidence": []}
|
|
219
219
|
warn_days = int(config.get("warn_days") or 21)
|
|
220
220
|
critical_days = int(config.get("critical_days") or 7)
|
|
221
|
-
remaining = due_at - datetime.now(
|
|
221
|
+
remaining = due_at - datetime.now(timezone.utc)
|
|
222
222
|
days = remaining.total_seconds() / 86400
|
|
223
223
|
if days <= critical_days:
|
|
224
224
|
health = "critical"
|
package/src/tools_sessions.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
import secrets
|
|
8
8
|
import threading
|
|
9
|
-
from datetime import datetime,
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from db import (
|
|
12
12
|
register_session, update_session, complete_session,
|
|
@@ -133,7 +133,7 @@ def _session_portability_bundle(sid: str = "") -> dict:
|
|
|
133
133
|
]
|
|
134
134
|
return {
|
|
135
135
|
"ok": True,
|
|
136
|
-
"generated_at": datetime.now(
|
|
136
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
137
137
|
"session": {
|
|
138
138
|
"sid": session_id,
|
|
139
139
|
"task": session_row["task"],
|