nexo-brain 3.1.1 → 3.1.2
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/package.json +1 -1
- package/src/auto_update.py +2 -0
- package/src/cron_recovery.py +52 -1
- package/src/crons/manifest.json +1 -0
- package/src/crons/sync.py +3 -3
- package/src/doctor/providers/runtime.py +2 -2
- package/src/tools_learnings.py +13 -1
- package/templates/launchagents/README.md +1 -1
- package/templates/launchagents/com.nexo.evolution.plist +5 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
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/auto_update.py
CHANGED
|
@@ -1198,6 +1198,7 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1198
1198
|
"evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
|
|
1199
1199
|
"client_sync.py",
|
|
1200
1200
|
"client_preferences.py", "agent_runner.py", "bootstrap_docs.py",
|
|
1201
|
+
"hook_guardrails.py", "protocol_settings.py", "public_evolution_queue.py",
|
|
1201
1202
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1202
1203
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1203
1204
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
@@ -1248,6 +1249,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1248
1249
|
"evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
|
|
1249
1250
|
"client_sync.py",
|
|
1250
1251
|
"client_preferences.py", "agent_runner.py", "bootstrap_docs.py",
|
|
1252
|
+
"hook_guardrails.py", "protocol_settings.py", "public_evolution_queue.py",
|
|
1251
1253
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1252
1254
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1253
1255
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
package/src/cron_recovery.py
CHANGED
|
@@ -6,6 +6,8 @@ import os
|
|
|
6
6
|
import plistlib
|
|
7
7
|
import sqlite3
|
|
8
8
|
import contextlib
|
|
9
|
+
import hashlib
|
|
10
|
+
import socket
|
|
9
11
|
from datetime import datetime, timedelta, timezone
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
|
|
@@ -31,6 +33,55 @@ def _load_json(path: Path, default):
|
|
|
31
33
|
return default
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
def _schedule_machine_id() -> str:
|
|
37
|
+
schedule = _load_json(SCHEDULE_FILE, {})
|
|
38
|
+
if isinstance(schedule, dict):
|
|
39
|
+
public = schedule.get("public_contribution")
|
|
40
|
+
if isinstance(public, dict):
|
|
41
|
+
candidate = str(public.get("machine_id") or "").strip().lower()
|
|
42
|
+
if candidate:
|
|
43
|
+
return candidate
|
|
44
|
+
candidate = socket.gethostname().strip().lower()
|
|
45
|
+
return candidate or "nexo-machine"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _stable_schedule_bucket(key: str, modulo: int) -> int:
|
|
49
|
+
if modulo <= 0:
|
|
50
|
+
return 0
|
|
51
|
+
digest = hashlib.sha256(f"{_schedule_machine_id()}::{key}".encode("utf-8")).digest()
|
|
52
|
+
return int.from_bytes(digest[:8], "big") % modulo
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_declared_schedule(cron: dict) -> dict:
|
|
56
|
+
schedule = cron.get("schedule")
|
|
57
|
+
if not isinstance(schedule, dict):
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
resolved = dict(schedule)
|
|
61
|
+
strategy = str(cron.get("schedule_strategy") or resolved.pop("strategy", "")).strip().lower()
|
|
62
|
+
if strategy != "machine_weekly_spread":
|
|
63
|
+
return resolved
|
|
64
|
+
|
|
65
|
+
if not {"hour", "minute", "weekday"} <= resolved.keys():
|
|
66
|
+
return resolved
|
|
67
|
+
|
|
68
|
+
total_week_minutes = 7 * 24 * 60
|
|
69
|
+
base_total = (
|
|
70
|
+
(int(resolved.get("weekday", 0)) % 7) * 1440
|
|
71
|
+
+ (int(resolved.get("hour", 0)) % 24) * 60
|
|
72
|
+
+ (int(resolved.get("minute", 0)) % 60)
|
|
73
|
+
)
|
|
74
|
+
offset = _stable_schedule_bucket(str(cron.get("id") or "cron"), total_week_minutes)
|
|
75
|
+
slot_total = (base_total + offset) % total_week_minutes
|
|
76
|
+
weekday, minute_of_day = divmod(slot_total, 1440)
|
|
77
|
+
hour, minute = divmod(minute_of_day, 60)
|
|
78
|
+
return {
|
|
79
|
+
"weekday": weekday,
|
|
80
|
+
"hour": hour,
|
|
81
|
+
"minute": minute,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
34
85
|
def load_enabled_crons() -> list[dict]:
|
|
35
86
|
manifest_candidates = [
|
|
36
87
|
NEXO_HOME / "crons" / "manifest.json",
|
|
@@ -215,7 +266,7 @@ def effective_schedule(cron: dict) -> dict:
|
|
|
215
266
|
return {
|
|
216
267
|
"source": "manifest",
|
|
217
268
|
"schedule_type": "calendar",
|
|
218
|
-
"calendar": cron
|
|
269
|
+
"calendar": resolve_declared_schedule(cron),
|
|
219
270
|
"run_at_load": should_run_at_load(cron),
|
|
220
271
|
}
|
|
221
272
|
return {
|
package/src/crons/manifest.json
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
{
|
|
108
108
|
"id": "evolution",
|
|
109
109
|
"script": "scripts/nexo-evolution-run.py",
|
|
110
|
+
"schedule_strategy": "machine_weekly_spread",
|
|
110
111
|
"schedule": {"hour": 5, "minute": 0, "weekday": 0},
|
|
111
112
|
"description": "Weekly self-improvement cycle — propose and evaluate changes",
|
|
112
113
|
"core": true,
|
package/src/crons/sync.py
CHANGED
|
@@ -31,7 +31,7 @@ _runtime_root = Path(os.environ.get("NEXO_CODE", str(_DEFAULT_RUNTIME_ROOT)))
|
|
|
31
31
|
if str(_runtime_root) not in sys.path:
|
|
32
32
|
sys.path.insert(0, str(_runtime_root))
|
|
33
33
|
|
|
34
|
-
from cron_recovery import should_run_at_load
|
|
34
|
+
from cron_recovery import resolve_declared_schedule, should_run_at_load
|
|
35
35
|
|
|
36
36
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
37
37
|
SOURCE_ROOT = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
|
|
@@ -242,7 +242,7 @@ def build_plist(cron: dict) -> dict:
|
|
|
242
242
|
plist["StartInterval"] = cron["interval_seconds"]
|
|
243
243
|
elif "schedule" in cron and not cron.get("keep_alive"):
|
|
244
244
|
cal = {}
|
|
245
|
-
s = cron
|
|
245
|
+
s = resolve_declared_schedule(cron)
|
|
246
246
|
if "hour" in s:
|
|
247
247
|
cal["Hour"] = s["hour"]
|
|
248
248
|
if "minute" in s:
|
|
@@ -443,7 +443,7 @@ StandardError=append:{stderr_log}
|
|
|
443
443
|
elif "interval_seconds" in cron:
|
|
444
444
|
timer_spec = f"OnUnitActiveSec={cron['interval_seconds']}s\nOnBootSec=60s"
|
|
445
445
|
elif "schedule" in cron:
|
|
446
|
-
s = cron
|
|
446
|
+
s = resolve_declared_schedule(cron)
|
|
447
447
|
h, m = s.get("hour", 0), s.get("minute", 0)
|
|
448
448
|
if "weekday" in s:
|
|
449
449
|
# Manifest weekday uses launchd convention: 0=Sunday … 6=Saturday (7=Sunday alias)
|
|
@@ -24,7 +24,7 @@ from client_preferences import (
|
|
|
24
24
|
normalize_client_preferences,
|
|
25
25
|
resolve_client_runtime_profile,
|
|
26
26
|
)
|
|
27
|
-
from cron_recovery import should_run_at_load
|
|
27
|
+
from cron_recovery import resolve_declared_schedule, should_run_at_load
|
|
28
28
|
from doctor.models import DoctorCheck, safe_check
|
|
29
29
|
|
|
30
30
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
@@ -865,7 +865,7 @@ def _launchagent_schedule_expectations() -> dict[str, dict]:
|
|
|
865
865
|
expected["RunAtLoad"] = True if should_run_at_load(cron) else None
|
|
866
866
|
expected["schedule_configured"] = True
|
|
867
867
|
elif "schedule" in cron:
|
|
868
|
-
schedule = cron
|
|
868
|
+
schedule = resolve_declared_schedule(cron)
|
|
869
869
|
cal = {}
|
|
870
870
|
if "hour" in schedule:
|
|
871
871
|
cal["Hour"] = schedule["hour"]
|
package/src/tools_learnings.py
CHANGED
|
@@ -376,6 +376,18 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
|
|
|
376
376
|
if "error" in superseded:
|
|
377
377
|
return f"ERROR: Learning #{new_id} created but supersede failed: {superseded['error']}"
|
|
378
378
|
|
|
379
|
+
# Post-insert verification: confirm the learning actually persisted
|
|
380
|
+
verify_conn = get_db()
|
|
381
|
+
verified = verify_conn.execute(
|
|
382
|
+
"SELECT id, title, category FROM learnings WHERE id = ? AND status = 'active'",
|
|
383
|
+
(result["id"],)
|
|
384
|
+
).fetchone()
|
|
385
|
+
if not verified:
|
|
386
|
+
return (
|
|
387
|
+
f"⚠ PERSISTENCE FAILURE: Learning #{result['id']} was inserted but NOT found on verification read. "
|
|
388
|
+
f"Retry nexo_learning_add or investigate DB integrity."
|
|
389
|
+
)
|
|
390
|
+
|
|
379
391
|
meta = []
|
|
380
392
|
if prevention:
|
|
381
393
|
meta.append("with prevention")
|
|
@@ -384,7 +396,7 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
|
|
|
384
396
|
if supersedes_id:
|
|
385
397
|
meta.append(f"supersedes={int(supersedes_id)}")
|
|
386
398
|
meta_str = f" ({', '.join(meta)})" if meta else ""
|
|
387
|
-
return f"Learning #{result['id']} added in {category}: {title}{meta_str}{repetition_msg}"
|
|
399
|
+
return f"Learning #{result['id']} added in {category}: {title}{meta_str} ✓verified{repetition_msg}"
|
|
388
400
|
|
|
389
401
|
|
|
390
402
|
def handle_learning_search(query: str, category: str = '') -> str:
|
|
@@ -104,7 +104,7 @@ These agents power NEXO's learning and memory systems. Strongly recommended.
|
|
|
104
104
|
|
|
105
105
|
| File | Schedule | What it does |
|
|
106
106
|
|------|----------|-------------|
|
|
107
|
-
| `com.nexo.evolution.plist` |
|
|
107
|
+
| `com.nexo.evolution.plist` | Machine-staggered weekly (managed installs) | Reviews the week's patterns and proposes improvements to NEXO's own configuration. Managed installs spread each machine across the week to avoid PR spikes; the static plist template is only a manual fallback. |
|
|
108
108
|
| `com.nexo.followup-hygiene.plist` | Sundays 05:00 | Cleans up stale followups and reminders. Archives long-pending items and deduplicates entries. Keeps the operational database noise-free. |
|
|
109
109
|
|
|
110
110
|
### Optional
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<!-- com.nexo.evolution
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
only propose them.
|
|
3
|
+
Static fallback template for the weekly evolution cycle.
|
|
4
|
+
Managed installs now derive a machine-staggered weekly slot from the
|
|
5
|
+
cron manifest to avoid bunching public evolution PRs on Sunday.
|
|
6
|
+
If you install this plist manually, customize Weekday/Hour/Minute
|
|
7
|
+
yourself instead of assuming the managed stagger applies here.
|
|
9
8
|
-->
|
|
10
9
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
11
10
|
<plist version="1.0">
|