nexo-brain 7.30.26 → 7.30.28
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 +150 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.28",
|
|
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.28` is the current packaged-runtime line. Patch release over v7.30.27 - F0.6 runtime repairs now run through an existing post-install hook, so older updaters execute script-conflict recovery and `core/current` refresh on the first upgrade.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.27`: patch release over v7.30.26 - post-update repair now recovers core scripts archived by older F0.6 shim reconciliation and refreshes `core/current` from `core`, so same-version snapshots cannot keep stale watchdog code.
|
|
24
|
+
|
|
25
|
+
Previously in `7.30.26`: patch release over v7.30.25 - `nexo update` now copies packaged core scripts through the F0.6 `scripts -> core/scripts` shim, so watchdog fixes reach the active LaunchAgent path.
|
|
22
26
|
|
|
23
27
|
Previously in `7.30.25`: patch release over v7.30.24 - Desktop-facing maintenance diagnostics now self-heal or stay informational when the runtime is healthy, and watchdog uses the canonical cognitive DB layout.
|
|
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.28",
|
|
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
|
@@ -5444,10 +5444,20 @@ def _persist_guardian_hard_defaults(dest: Path) -> tuple[bool, str | None]:
|
|
|
5444
5444
|
non-default value (``shadow`` / ``off`` / anything else). Only
|
|
5445
5445
|
fills missing keys or upgrades explicit defaults.
|
|
5446
5446
|
"""
|
|
5447
|
+
def _join_messages(*messages: str | None) -> str | None:
|
|
5448
|
+
parts = [str(message) for message in messages if str(message or "").strip()]
|
|
5449
|
+
return ";".join(parts) if parts else None
|
|
5450
|
+
|
|
5451
|
+
repair_message = None
|
|
5452
|
+
try:
|
|
5453
|
+
_repair_changed, repair_message = _run_f06_post_update_repairs(dest)
|
|
5454
|
+
except Exception as exc:
|
|
5455
|
+
repair_message = f"f06-post-update-repair-warning:{exc.__class__.__name__}"
|
|
5456
|
+
|
|
5447
5457
|
if _is_ephemeral_runtime_install(dest):
|
|
5448
|
-
return False, "guardian-hard-persist-skipped:ephemeral"
|
|
5458
|
+
return False, _join_messages("guardian-hard-persist-skipped:ephemeral", repair_message)
|
|
5449
5459
|
if os.environ.get("NEXO_GUARDIAN_PERSIST_HARD", "").strip().lower() == "off":
|
|
5450
|
-
return False, "guardian-hard-persist-skipped:operator-opt-out"
|
|
5460
|
+
return False, _join_messages("guardian-hard-persist-skipped:operator-opt-out", repair_message)
|
|
5451
5461
|
|
|
5452
5462
|
config_path = dest / "personal" / "config" / "guardian-runtime-overrides.json"
|
|
5453
5463
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -5477,16 +5487,16 @@ def _persist_guardian_hard_defaults(dest: Path) -> tuple[bool, str | None]:
|
|
|
5477
5487
|
current[key] = default
|
|
5478
5488
|
|
|
5479
5489
|
if current == original and config_path.is_file():
|
|
5480
|
-
return False,
|
|
5490
|
+
return False, repair_message
|
|
5481
5491
|
|
|
5482
5492
|
try:
|
|
5483
5493
|
tmp_path = config_path.with_suffix(config_path.suffix + ".tmp")
|
|
5484
5494
|
tmp_path.write_text(json.dumps(current, indent=2, ensure_ascii=False) + "\n")
|
|
5485
5495
|
tmp_path.replace(config_path)
|
|
5486
5496
|
except Exception as exc: # pragma: no cover - filesystem failures
|
|
5487
|
-
return False, f"guardian-hard-persist-error:{exc.__class__.__name__}"
|
|
5497
|
+
return False, _join_messages(f"guardian-hard-persist-error:{exc.__class__.__name__}", repair_message)
|
|
5488
5498
|
|
|
5489
|
-
return True,
|
|
5499
|
+
return True, repair_message
|
|
5490
5500
|
|
|
5491
5501
|
|
|
5492
5502
|
from datetime import datetime as _adaptive_datetime # isolated alias; other modules use ``time.time()``
|
|
@@ -5590,12 +5600,141 @@ def _maybe_promote_adaptive_weights_empirically(dest: Path) -> tuple[bool, str |
|
|
|
5590
5600
|
return True, None
|
|
5591
5601
|
|
|
5592
5602
|
|
|
5603
|
+
def _promote_f06_core_scripts_from_latest_legacy_conflict(dest: Path) -> tuple[bool, str | None]:
|
|
5604
|
+
"""Recover core script updates archived by the pre-fix F0.6 shim path.
|
|
5605
|
+
|
|
5606
|
+
Older updaters copied packaged scripts into ``NEXO_HOME/scripts`` after
|
|
5607
|
+
breaking the F0.6 compatibility symlink. The subsequent shim reconciliation
|
|
5608
|
+
saw those fresh scripts as legacy conflicts and archived them under
|
|
5609
|
+
``runtime/backups/legacy-shim-conflicts-*`` instead of replacing
|
|
5610
|
+
``core/scripts``. This hook runs from the freshly-copied release tree, so it
|
|
5611
|
+
can repair the first upgrade that introduces the updater fix.
|
|
5612
|
+
"""
|
|
5613
|
+
try:
|
|
5614
|
+
marker = (dest / ".structure-version").read_text(encoding="utf-8").strip()
|
|
5615
|
+
except Exception:
|
|
5616
|
+
marker = ""
|
|
5617
|
+
if marker != "F0.6":
|
|
5618
|
+
return False, None
|
|
5619
|
+
|
|
5620
|
+
core_scripts = dest / "core" / "scripts"
|
|
5621
|
+
backups_dir = dest / "runtime" / "backups"
|
|
5622
|
+
if not core_scripts.is_dir() or not backups_dir.is_dir():
|
|
5623
|
+
return False, None
|
|
5624
|
+
|
|
5625
|
+
try:
|
|
5626
|
+
candidates = sorted(
|
|
5627
|
+
[p for p in backups_dir.glob("legacy-shim-conflicts-*") if (p / "scripts").is_dir()],
|
|
5628
|
+
key=lambda p: p.stat().st_mtime,
|
|
5629
|
+
reverse=True,
|
|
5630
|
+
)
|
|
5631
|
+
except Exception:
|
|
5632
|
+
return False, "f06-core-scripts-promote-warning:scan"
|
|
5633
|
+
|
|
5634
|
+
if not candidates:
|
|
5635
|
+
return False, None
|
|
5636
|
+
|
|
5637
|
+
def _should_skip(path: Path) -> bool:
|
|
5638
|
+
return (
|
|
5639
|
+
path.name == "__pycache__"
|
|
5640
|
+
or path.name.startswith(".")
|
|
5641
|
+
or is_duplicate_artifact_name(path)
|
|
5642
|
+
)
|
|
5643
|
+
|
|
5644
|
+
def _source_is_not_older(src: Path, dst: Path) -> bool:
|
|
5645
|
+
if not dst.exists() and not dst.is_symlink():
|
|
5646
|
+
return True
|
|
5647
|
+
try:
|
|
5648
|
+
return src.stat().st_mtime >= dst.stat().st_mtime
|
|
5649
|
+
except Exception:
|
|
5650
|
+
return False
|
|
5651
|
+
|
|
5652
|
+
copied = 0
|
|
5653
|
+
|
|
5654
|
+
def _copy_entry(src: Path, dst: Path) -> None:
|
|
5655
|
+
nonlocal copied
|
|
5656
|
+
if _should_skip(src):
|
|
5657
|
+
return
|
|
5658
|
+
if src.is_dir():
|
|
5659
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
5660
|
+
for child in src.iterdir():
|
|
5661
|
+
_copy_entry(child, dst / child.name)
|
|
5662
|
+
return
|
|
5663
|
+
if not src.is_file() or not _source_is_not_older(src, dst):
|
|
5664
|
+
return
|
|
5665
|
+
try:
|
|
5666
|
+
if dst.exists() and dst.is_file() and src.read_bytes() == dst.read_bytes():
|
|
5667
|
+
return
|
|
5668
|
+
except Exception:
|
|
5669
|
+
pass
|
|
5670
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
5671
|
+
shutil.copy2(str(src), str(dst))
|
|
5672
|
+
if src.suffix == ".sh":
|
|
5673
|
+
dst.chmod(0o755)
|
|
5674
|
+
copied += 1
|
|
5675
|
+
|
|
5676
|
+
for backup_root in candidates[:3]:
|
|
5677
|
+
scripts_backup = backup_root / "scripts"
|
|
5678
|
+
try:
|
|
5679
|
+
for item in scripts_backup.iterdir():
|
|
5680
|
+
_copy_entry(item, core_scripts / item.name)
|
|
5681
|
+
except Exception as exc:
|
|
5682
|
+
return False, f"f06-core-scripts-promote-warning:{exc.__class__.__name__}"
|
|
5683
|
+
if copied:
|
|
5684
|
+
return True, f"f06-core-scripts-promoted:{copied}"
|
|
5685
|
+
|
|
5686
|
+
return False, None
|
|
5687
|
+
|
|
5688
|
+
|
|
5689
|
+
def _refresh_active_versioned_runtime_snapshot(dest: Path) -> tuple[bool, str | None]:
|
|
5690
|
+
"""Rebuild ``core/current`` from ``core`` after an update.
|
|
5691
|
+
|
|
5692
|
+
Version equality is not enough evidence that the active snapshot contains
|
|
5693
|
+
the same bytes as ``core``: a same-version repair can update ``core/scripts``
|
|
5694
|
+
after ``core/current`` was already activated. Refreshing the snapshot keeps
|
|
5695
|
+
Desktop/CLI clients that resolve through ``core/current`` on the repaired
|
|
5696
|
+
runtime tree.
|
|
5697
|
+
"""
|
|
5698
|
+
core_root = dest / "core"
|
|
5699
|
+
if not core_root.is_dir() or not (core_root / "cli.py").is_file():
|
|
5700
|
+
return False, None
|
|
5701
|
+
try:
|
|
5702
|
+
from runtime_versioning import activate_versioned_runtime_snapshot, read_version_for_path
|
|
5703
|
+
|
|
5704
|
+
version = str(read_version_for_path(core_root) or "").strip()
|
|
5705
|
+
if not version:
|
|
5706
|
+
return False, "runtime-snapshot-refresh-skipped:missing-version"
|
|
5707
|
+
result = activate_versioned_runtime_snapshot(source_root=core_root, version=version)
|
|
5708
|
+
except Exception as exc:
|
|
5709
|
+
return False, f"runtime-snapshot-refresh-warning:{exc.__class__.__name__}"
|
|
5710
|
+
if not result.get("ok"):
|
|
5711
|
+
return False, f"runtime-snapshot-refresh-warning:{result.get('error', 'unknown')}"
|
|
5712
|
+
return True, f"runtime-snapshot-refreshed:{version}"
|
|
5713
|
+
|
|
5714
|
+
|
|
5715
|
+
def _run_f06_post_update_repairs(dest: Path) -> tuple[bool, str | None]:
|
|
5716
|
+
changed = False
|
|
5717
|
+
messages: list[str] = []
|
|
5718
|
+
for repair in (
|
|
5719
|
+
_promote_f06_core_scripts_from_latest_legacy_conflict,
|
|
5720
|
+
_refresh_active_versioned_runtime_snapshot,
|
|
5721
|
+
):
|
|
5722
|
+
repair_changed, repair_message = repair(dest)
|
|
5723
|
+
changed = changed or repair_changed
|
|
5724
|
+
if repair_message:
|
|
5725
|
+
messages.append(repair_message)
|
|
5726
|
+
return changed, ";".join(messages) if messages else None
|
|
5727
|
+
|
|
5728
|
+
|
|
5593
5729
|
# Whitelist of post-install hooks to invoke from the fresh tree. Each entry
|
|
5594
5730
|
# is the function name inside ``auto_update.py`` of the freshly-copied
|
|
5595
5731
|
# code. The subprocess resolves them on the NEW module and calls
|
|
5596
5732
|
# ``fn(dest)`` returning ``(bool, str | None)``. New hooks added in
|
|
5597
5733
|
# future releases only need an entry here — no extra wiring.
|
|
5598
5734
|
_POST_INSTALL_FRESH_HOOKS = (
|
|
5735
|
+
("f06-post-update-repairs", "_run_f06_post_update_repairs"),
|
|
5736
|
+
("f06-core-scripts-promoted", "_promote_f06_core_scripts_from_latest_legacy_conflict"),
|
|
5737
|
+
("runtime-snapshot-refreshed", "_refresh_active_versioned_runtime_snapshot"),
|
|
5599
5738
|
("guardian-hard-persisted", "_persist_guardian_hard_defaults"),
|
|
5600
5739
|
("adaptive-weights-promoted", "_maybe_promote_adaptive_weights_empirically"),
|
|
5601
5740
|
)
|
|
@@ -5631,7 +5770,7 @@ def _run_post_install_hooks_fresh(dest: Path, *, env: dict | None = None) -> lis
|
|
|
5631
5770
|
"import json, sys\n"
|
|
5632
5771
|
"from pathlib import Path\n"
|
|
5633
5772
|
f"sys.path.insert(0, {repr(str(code_root))})\n"
|
|
5634
|
-
"
|
|
5773
|
+
"fallback_hooks = json.loads(" + repr(hook_specs_json) + ")\n"
|
|
5635
5774
|
f"dest = Path({repr(dest_str)})\n"
|
|
5636
5775
|
"results = []\n"
|
|
5637
5776
|
"try:\n"
|
|
@@ -5639,6 +5778,11 @@ def _run_post_install_hooks_fresh(dest: Path, *, env: dict | None = None) -> lis
|
|
|
5639
5778
|
"except Exception as exc:\n"
|
|
5640
5779
|
" print(json.dumps({'error': 'import_auto_update_failed', 'detail': repr(exc)}))\n"
|
|
5641
5780
|
" sys.exit(0)\n"
|
|
5781
|
+
"fresh_hooks = getattr(fresh, '_POST_INSTALL_FRESH_HOOKS', None)\n"
|
|
5782
|
+
"if isinstance(fresh_hooks, (list, tuple)) and fresh_hooks:\n"
|
|
5783
|
+
" hooks = [(str(tag), str(fn)) for tag, fn in fresh_hooks]\n"
|
|
5784
|
+
"else:\n"
|
|
5785
|
+
" hooks = fallback_hooks\n"
|
|
5642
5786
|
"for tag, fn_name in hooks:\n"
|
|
5643
5787
|
" fn = getattr(fresh, fn_name, None)\n"
|
|
5644
5788
|
" if fn is None:\n"
|