nexo-brain 7.30.2 → 7.30.3

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": "7.30.2",
3
+ "version": "7.30.3",
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.30.2` is the current packaged-runtime line. 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.
21
+ Version `7.30.3` is the current packaged-runtime line. 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.
22
+
23
+ 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
24
 
23
25
  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
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.2",
3
+ "version": "7.30.3",
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/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
- return run_runtime_backup_prune(backups_root=root)
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 item["class"] in {"TECHNICAL", "TEMPORARY"} and id(item) not in delete_ids:
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)