nexo-brain 7.30.2 → 7.30.4
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 +5 -1
- package/package.json +1 -1
- package/src/auto_update.py +2 -1
- package/src/enforcement_engine.py +11 -0
- package/src/paths.py +8 -1
- package/src/scripts/prune_runtime_backups.py +24 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.4",
|
|
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,11 @@
|
|
|
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.30.
|
|
21
|
+
Version `7.30.4` is the current packaged-runtime line. Patch release over v7.30.3 - local runtime update post-sync now gives bounded Memory Fabric repair enough time to finish, and headless automations now treat `nexo_stop` as a terminal close so followup/deep-sleep runners do not reopen no-op protocol loops.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.3`: patch release over v7.30.2 - release closeout now protects the freshly written runtime backup from technical pruning and validates protocol evidence against the canonical `runtime/data/nexo.db` layout.
|
|
24
|
+
|
|
25
|
+
Previously in `7.30.2`: patch release over v7.30.1 - Deep Sleep now fails closed on partial nightly runs, carries complete learning context into synthesis, writes an explicit agent start packet, and creates governed followups with verification, priority, owner, and date fields.
|
|
22
26
|
|
|
23
27
|
Previously in `7.30.1`: patch release over v7.30.0 - morning briefings now behave more like a start-of-day assistant, explain news/weather as verified optional sources, and keep user type inference inside NEXO instead of asking the user to choose a role manually.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.4",
|
|
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
|
@@ -98,6 +98,7 @@ TEMPLATE_FILE = _RESOLVED_REPO_DIR / "templates" / "CLAUDE.md.template"
|
|
|
98
98
|
|
|
99
99
|
CHECK_COOLDOWN_SECONDS = 3600 # 1 hour
|
|
100
100
|
GIT_TIMEOUT_SECONDS = 4 # stay well under the 5s total budget
|
|
101
|
+
RUNTIME_POST_SYNC_TIMEOUT_SECONDS = 180
|
|
101
102
|
CRITICAL_BACKUP_TABLES = ("learnings", "session_diary", "guard_checks", "protocol_debt")
|
|
102
103
|
LOCAL_CONTEXT_BACKUP_TABLES = (
|
|
103
104
|
"local_index_roots",
|
|
@@ -4883,7 +4884,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4883
4884
|
cwd=str(dest),
|
|
4884
4885
|
capture_output=True,
|
|
4885
4886
|
text=True,
|
|
4886
|
-
timeout=
|
|
4887
|
+
timeout=RUNTIME_POST_SYNC_TIMEOUT_SECONDS,
|
|
4887
4888
|
env=env,
|
|
4888
4889
|
)
|
|
4889
4890
|
if init_result.returncode != 0:
|
|
@@ -475,6 +475,10 @@ class HeadlessEnforcer:
|
|
|
475
475
|
# per task cycle. Cleared on skill_match OR task_close.
|
|
476
476
|
self._multi_step_event_fired: bool = False
|
|
477
477
|
self._post_close_cooldown_until: float = 0.0
|
|
478
|
+
# A headless nexo_stop is terminal for the automation cycle. Once
|
|
479
|
+
# seen, periodic/conditional reminders stay suppressed so cron
|
|
480
|
+
# runners can reach TURN_END instead of reopening the task loop.
|
|
481
|
+
self._session_stopped: bool = False
|
|
478
482
|
try:
|
|
479
483
|
self._post_close_cooldown_seconds = max(
|
|
480
484
|
0,
|
|
@@ -2287,6 +2291,10 @@ class HeadlessEnforcer:
|
|
|
2287
2291
|
self._start_post_close_cooldown()
|
|
2288
2292
|
self._resolve_r17_commitments_from_task_close(tool_input)
|
|
2289
2293
|
|
|
2294
|
+
if name == "nexo_stop":
|
|
2295
|
+
self._session_stopped = True
|
|
2296
|
+
self._start_post_close_cooldown()
|
|
2297
|
+
|
|
2290
2298
|
# v7.7 Gap 1 — autonomous detector for multi_step_task_detected.
|
|
2291
2299
|
# The event was dispatched by the map but nothing ever raised it.
|
|
2292
2300
|
# Heuristic: three or more edit/execute/delegate calls within the
|
|
@@ -2602,6 +2610,9 @@ class HeadlessEnforcer:
|
|
|
2602
2610
|
_logger.info("POST_CLOSE_COOLDOWN: cleared %d queued protocol injection(s)", removed)
|
|
2603
2611
|
|
|
2604
2612
|
def check_periodic(self):
|
|
2613
|
+
if getattr(self, "_session_stopped", False):
|
|
2614
|
+
_logger.info("SESSION_STOPPED: periodic checks suppressed (nexo_stop seen)")
|
|
2615
|
+
return
|
|
2605
2616
|
if self._post_close_cooldown_active():
|
|
2606
2617
|
_logger.info("POST_CLOSE_COOLDOWN: periodic checks suppressed")
|
|
2607
2618
|
return
|
package/src/paths.py
CHANGED
|
@@ -60,6 +60,7 @@ import shutil
|
|
|
60
60
|
import subprocess
|
|
61
61
|
import sys
|
|
62
62
|
import time
|
|
63
|
+
from collections.abc import Iterable
|
|
63
64
|
from pathlib import Path
|
|
64
65
|
|
|
65
66
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
@@ -465,6 +466,7 @@ def run_runtime_backup_prune(
|
|
|
465
466
|
max_bytes: str | int | None = None,
|
|
466
467
|
backups_root: Path | None = None,
|
|
467
468
|
delete_all_technical: bool = False,
|
|
469
|
+
protect_paths: Iterable[str | Path] | None = None,
|
|
468
470
|
timeout: int = 120,
|
|
469
471
|
) -> dict:
|
|
470
472
|
"""Run the technical-backup pruner. Safe no-op when the script is absent."""
|
|
@@ -487,6 +489,8 @@ def run_runtime_backup_prune(
|
|
|
487
489
|
]
|
|
488
490
|
if delete_all_technical:
|
|
489
491
|
args.append("--delete-all-technical")
|
|
492
|
+
for protected in protect_paths or ():
|
|
493
|
+
args.extend(["--protect", str(protected)])
|
|
490
494
|
try:
|
|
491
495
|
proc = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
|
492
496
|
report = json.loads(proc.stdout or "{}") if proc.stdout.strip().startswith("{") else {}
|
|
@@ -634,10 +638,13 @@ def create_backup_path(prefix: str, suffix: str = "", *, backups_root: Path | No
|
|
|
634
638
|
def finalize_backup_snapshot(_path: Path | str | None = None, *, backups_root: Path | None = None) -> dict:
|
|
635
639
|
"""Post-snapshot cleanup; callers invoke after writing large artifacts."""
|
|
636
640
|
root = Path(backups_root) if backups_root is not None else None
|
|
641
|
+
protect_paths = []
|
|
637
642
|
if root is None and _path is not None:
|
|
638
643
|
snapshot = Path(_path)
|
|
639
644
|
root = snapshot.parent
|
|
640
|
-
|
|
645
|
+
if _path is not None:
|
|
646
|
+
protect_paths.append(Path(_path))
|
|
647
|
+
return run_runtime_backup_prune(backups_root=root, protect_paths=protect_paths)
|
|
641
648
|
|
|
642
649
|
|
|
643
650
|
def memory_dir() -> Path:
|
|
@@ -261,6 +261,14 @@ def gather_entries(backups_root: Path) -> list[dict]:
|
|
|
261
261
|
return items
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
def path_key(path: str | Path) -> str:
|
|
265
|
+
candidate = Path(path)
|
|
266
|
+
try:
|
|
267
|
+
return str(candidate.resolve())
|
|
268
|
+
except OSError:
|
|
269
|
+
return str(candidate.absolute())
|
|
270
|
+
|
|
271
|
+
|
|
264
272
|
def plan_prunes(
|
|
265
273
|
items: list[dict],
|
|
266
274
|
*,
|
|
@@ -420,11 +428,24 @@ def run(args: argparse.Namespace) -> int:
|
|
|
420
428
|
local_context_keep=max(0, args.local_context_keep),
|
|
421
429
|
hourly_keep=max(0, args.hourly_keep),
|
|
422
430
|
)
|
|
431
|
+
protected_paths = {path_key(path) for path in (args.protect or [])}
|
|
432
|
+
protected_ids: set[int] = set()
|
|
433
|
+
if protected_paths:
|
|
434
|
+
protected_items = [item for item in items if path_key(item["path"]) in protected_paths]
|
|
435
|
+
protected_ids = {id(item) for item in protected_items}
|
|
436
|
+
if protected_ids:
|
|
437
|
+
to_delete = [item for item in to_delete if id(item) not in protected_ids]
|
|
438
|
+
keep_ids = {id(item) for item in to_keep}
|
|
439
|
+
to_keep.extend(item for item in protected_items if id(item) not in keep_ids)
|
|
423
440
|
|
|
424
441
|
if args.delete_all_technical:
|
|
425
442
|
delete_ids = {id(item) for item in to_delete}
|
|
426
443
|
for item in items:
|
|
427
|
-
if
|
|
444
|
+
if (
|
|
445
|
+
item["class"] in {"TECHNICAL", "TEMPORARY"}
|
|
446
|
+
and id(item) not in delete_ids
|
|
447
|
+
and id(item) not in protected_ids
|
|
448
|
+
):
|
|
428
449
|
to_delete.append(item)
|
|
429
450
|
delete_ids.add(id(item))
|
|
430
451
|
to_keep = [item for item in items if id(item) not in delete_ids]
|
|
@@ -447,6 +468,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
447
468
|
"tmp_ttl_minutes": args.tmp_ttl_minutes,
|
|
448
469
|
"local_context_keep": args.local_context_keep,
|
|
449
470
|
"hourly_keep": args.hourly_keep,
|
|
471
|
+
"protected_paths": sorted(protected_paths),
|
|
450
472
|
"restore_point_guard": restore_guard,
|
|
451
473
|
},
|
|
452
474
|
"totals": {
|
|
@@ -560,6 +582,7 @@ def main() -> int:
|
|
|
560
582
|
ap.add_argument("--tmp-ttl-minutes", type=int, default=int(os.environ.get("NEXO_BACKUP_TMP_TTL_MINUTES", "30")), help="delete orphan temporary backup files older than this (default: 30)")
|
|
561
583
|
ap.add_argument("--local-context-keep", type=int, default=int(os.environ.get("NEXO_LOCAL_CONTEXT_BACKUP_KEEP_LAST", "1")), help="local-context backup files to keep under the global cap (default: 1)")
|
|
562
584
|
ap.add_argument("--hourly-keep", type=int, default=int(os.environ.get("NEXO_BACKUP_KEEP_LAST", "3")), help="hourly nexo DB backups to keep under the global cap (default: 3)")
|
|
585
|
+
ap.add_argument("--protect", action="append", default=[], help="backup path to keep even if it is otherwise prunable; repeat for multiple paths")
|
|
563
586
|
args = ap.parse_args()
|
|
564
587
|
try:
|
|
565
588
|
return run(args)
|