nexo-brain 3.1.0 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "3.1.0",
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.0",
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",
@@ -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",
@@ -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["schedule"],
269
+ "calendar": resolve_declared_schedule(cron),
219
270
  "run_at_load": should_run_at_load(cron),
220
271
  }
221
272
  return {
@@ -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["schedule"]
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["schedule"]
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)
@@ -62,8 +62,8 @@ def _safe_slug(value: str) -> str:
62
62
 
63
63
  def _ensure_script_id(conn, name: str, path: str) -> str:
64
64
  existing = conn.execute(
65
- "SELECT id FROM personal_scripts WHERE path = ? OR name = ? ORDER BY path = ? DESC LIMIT 1",
66
- (path, name, path),
65
+ "SELECT id FROM personal_scripts WHERE path = ? LIMIT 1",
66
+ (path,),
67
67
  ).fetchone()
68
68
  if existing:
69
69
  return existing["id"]
@@ -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.get("schedule") or {}
868
+ schedule = resolve_declared_schedule(cron)
869
869
  cal = {}
870
870
  if "hour" in schedule:
871
871
  cal["Hour"] = schedule["hour"]
@@ -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` | Sundays 05:00 | Reviews the week's patterns and proposes improvements to NEXO's own configuration. Proposals are staged for user approval nothing is applied autonomously. |
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
- Runs nexo-evolution-run.py every Sunday at 05:00 to perform a weekly
4
- evolution cycle. Reviews patterns from the past week, proposes
5
- improvements to NEXO's own behavior and configuration, and stages
6
- them as pending proposals for the user to approve. This is NEXO's
7
- self-improvement mechanism it cannot apply changes autonomously,
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">