nexo-brain 7.23.4 → 7.23.6

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.23.4",
3
+ "version": "7.23.6",
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.23.4` is the current packaged-runtime line. Patch over v7.23.3 - release tags now fail closed when npm publication fails and OpenClaw lockfile metadata stays synchronized with the release version.
21
+ Version `7.23.6` is the current packaged-runtime line. Patch over v7.23.5 - `nexo update` clears safe legacy `cognitive.db` shadows and keeps superseded archives under runtime backup retention.
22
+
23
+ Previously in `7.23.5`: patch over v7.23.4 - `nexo update` keeps external CLI maintenance summary copy in English.
22
24
 
23
25
  Previously in `7.23.3`: patch over v7.23.2 - Followup runner skips DONE terminal statuses so already-finished followups do not re-enter executable batches.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.23.4",
3
+ "version": "7.23.6",
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",
@@ -32,6 +32,7 @@ except ModuleNotFoundError as exc:
32
32
  sys.path.insert(0, core_path)
33
33
  from product_mode import enforce_desktop_product_contract
34
34
  from runtime_home import export_resolved_nexo_home, managed_nexo_home
35
+ from cognitive_paths import cleanup_legacy_cognitive_db_artifacts
35
36
 
36
37
  try:
37
38
  from tree_hygiene import is_duplicate_artifact_name
@@ -3505,7 +3506,7 @@ def _format_external_clis_results(results: dict) -> list[str]:
3505
3506
  any_updated = True
3506
3507
  lines.append(
3507
3508
  f" CLI updated: {pkg} {entry.get('old')} -> {entry.get('new')} "
3508
- f"— reinicia terminal para activar"
3509
+ f"— restart the terminal to activate"
3509
3510
  )
3510
3511
  elif status == "already_latest":
3511
3512
  any_checked_latest = True
@@ -3518,7 +3519,7 @@ def _format_external_clis_results(results: dict) -> list[str]:
3518
3519
  # CLIs that the operator never installed shouldn't spam the summary.
3519
3520
 
3520
3521
  if not any_updated and not any_failed and any_checked_latest:
3521
- lines.append(" CLIs externos: ya en última versión")
3522
+ lines.append(" External CLIs: already on latest versions")
3522
3523
 
3523
3524
  return lines
3524
3525
 
@@ -4747,6 +4748,17 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
4747
4748
  except Exception as exc:
4748
4749
  actions.append(f"legacy-personal-brain-db-stubs-warning:{exc.__class__.__name__}")
4749
4750
 
4751
+ try:
4752
+ cognitive_cleanup = cleanup_legacy_cognitive_db_artifacts(dry_run=False)
4753
+ removed = len(cognitive_cleanup.get("removed", []) or [])
4754
+ archived = len(cognitive_cleanup.get("archived", []) or [])
4755
+ if removed:
4756
+ actions.append(f"legacy-cognitive-db-duplicates-removed:{removed}")
4757
+ if archived:
4758
+ actions.append(f"legacy-cognitive-db-archived:{archived}")
4759
+ except Exception as exc:
4760
+ actions.append(f"legacy-cognitive-db-warning:{exc.__class__.__name__}")
4761
+
4750
4762
  try:
4751
4763
  _emit_progress(progress_fn, "Applying Desktop product contract...")
4752
4764
  contract = enforce_desktop_product_contract(source="runtime_post_sync")
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import sqlite3
10
+ import tarfile
10
11
  from datetime import datetime, timezone
11
12
  from pathlib import Path
12
13
  from typing import Any
@@ -68,10 +69,12 @@ def _sha256(path: Path) -> str:
68
69
  def _sqlite_signature(path: Path) -> dict[str, Any]:
69
70
  if not path.exists():
70
71
  return {"exists": False}
72
+ stat = path.stat()
71
73
  signature: dict[str, Any] = {
72
74
  "exists": True,
73
75
  "path": str(path),
74
- "size_bytes": path.stat().st_size,
76
+ "size_bytes": stat.st_size,
77
+ "mtime_epoch": stat.st_mtime,
75
78
  "sha256": _sha256(path),
76
79
  }
77
80
  try:
@@ -101,6 +104,10 @@ def _migration_marker_path() -> Path:
101
104
  return paths.runtime_state_dir() / "cognitive-db-migration.json"
102
105
 
103
106
 
107
+ def _cleanup_marker_path() -> Path:
108
+ return paths.runtime_state_dir() / "cognitive-db-cleanup.jsonl"
109
+
110
+
104
111
  def _write_migration_marker(source: Path, target: Path) -> None:
105
112
  marker = {
106
113
  "at": datetime.now(timezone.utc).isoformat(),
@@ -115,6 +122,190 @@ def _write_migration_marker(source: Path, target: Path) -> None:
115
122
  marker_path.write_text(json.dumps(marker, indent=2, sort_keys=True) + "\n", encoding="utf-8")
116
123
 
117
124
 
125
+ def _append_cleanup_marker(event: dict[str, Any]) -> None:
126
+ marker_path = _cleanup_marker_path()
127
+ marker_path.parent.mkdir(parents=True, exist_ok=True)
128
+ payload = {
129
+ "at": datetime.now(timezone.utc).isoformat(),
130
+ **event,
131
+ }
132
+ with marker_path.open("a", encoding="utf-8") as handle:
133
+ handle.write(json.dumps(payload, sort_keys=True) + "\n")
134
+
135
+
136
+ def _sidecar_paths(db_path: Path) -> list[Path]:
137
+ return [db_path, Path(f"{db_path}-wal"), Path(f"{db_path}-shm")]
138
+
139
+
140
+ def _existing_sidecars(db_path: Path) -> list[Path]:
141
+ return [path for path in _sidecar_paths(db_path) if path.exists()]
142
+
143
+
144
+ def _wal_has_uncheckpointed_data(db_path: Path) -> bool:
145
+ wal_path = Path(f"{db_path}-wal")
146
+ try:
147
+ return wal_path.is_file() and wal_path.stat().st_size > 0
148
+ except OSError:
149
+ return True
150
+
151
+
152
+ def _canonical_supersedes_legacy(canonical_sig: dict[str, Any], legacy_sig: dict[str, Any]) -> bool:
153
+ if not canonical_sig.get("exists") or not legacy_sig.get("exists"):
154
+ return False
155
+ if not canonical_sig.get("sqlite_ok") or not legacy_sig.get("sqlite_ok"):
156
+ return False
157
+ if float(canonical_sig.get("mtime_epoch") or 0) < float(legacy_sig.get("mtime_epoch") or 0):
158
+ return False
159
+ canonical_tables = set(canonical_sig.get("tables") or [])
160
+ legacy_tables = set(legacy_sig.get("tables") or [])
161
+ if canonical_tables and legacy_tables and not legacy_tables.issubset(canonical_tables):
162
+ return False
163
+ return True
164
+
165
+
166
+ def _remove_paths(paths_to_remove: list[Path]) -> list[str]:
167
+ removed: list[str] = []
168
+ for path in paths_to_remove:
169
+ try:
170
+ if path.exists():
171
+ path.unlink()
172
+ removed.append(str(path))
173
+ except FileNotFoundError:
174
+ continue
175
+ return removed
176
+
177
+
178
+ def _archive_and_remove_legacy_db(
179
+ legacy_db: Path,
180
+ *,
181
+ canonical_sig: dict[str, Any],
182
+ legacy_sig: dict[str, Any],
183
+ reason: str,
184
+ ) -> dict[str, Any]:
185
+ files = _existing_sidecars(legacy_db)
186
+ backup_root = paths.create_backup_dir("legacy-cognitive-db")
187
+ backup_dir = Path(backup_root)
188
+ archive_path = backup_dir / "cognitive-legacy.tar.gz"
189
+ manifest_path = backup_dir / "manifest.json"
190
+ with tarfile.open(archive_path, "w:gz") as archive:
191
+ for file_path in files:
192
+ archive.add(file_path, arcname=file_path.name)
193
+ with tarfile.open(archive_path, "r:gz") as archive:
194
+ archived_names = sorted(archive.getnames())
195
+ archive_sha = _sha256(archive_path)
196
+ manifest = {
197
+ "reason": reason,
198
+ "source": str(legacy_db),
199
+ "archived_files": [str(path) for path in files],
200
+ "archive": str(archive_path),
201
+ "archive_sha256": archive_sha,
202
+ "archive_members": archived_names,
203
+ "canonical": canonical_sig,
204
+ "legacy": legacy_sig,
205
+ }
206
+ manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
207
+ removed = _remove_paths(files)
208
+ paths.finalize_backup_snapshot(backup_dir)
209
+ _append_cleanup_marker({
210
+ "action": "archive_superseded_legacy",
211
+ "source": str(legacy_db),
212
+ "archive": str(archive_path),
213
+ "archive_sha256": archive_sha,
214
+ "removed": removed,
215
+ "reason": reason,
216
+ })
217
+ return {
218
+ "path": str(legacy_db),
219
+ "action": "archived",
220
+ "archive_path": str(archive_path),
221
+ "manifest_path": str(manifest_path),
222
+ "removed": removed,
223
+ "reason": reason,
224
+ }
225
+
226
+
227
+ def cleanup_legacy_cognitive_db_artifacts(*, dry_run: bool = False) -> dict[str, Any]:
228
+ """Remove or archive safe legacy cognitive DB shadows.
229
+
230
+ Identical legacy duplicates are deleted directly. Divergent legacy DBs are
231
+ only archived when the canonical DB is valid, newer, and has a compatible
232
+ schema. Ambiguous cases are left in place so write callers still block.
233
+ """
234
+ override = _configured_override()
235
+ report: dict[str, Any] = {
236
+ "ok": True,
237
+ "dry_run": dry_run,
238
+ "removed": [],
239
+ "archived": [],
240
+ "skipped": [],
241
+ "errors": [],
242
+ }
243
+ if override is not None:
244
+ report["skipped"].append({"reason": "env_override", "path": str(override)})
245
+ return report
246
+
247
+ canonical = canonical_cognitive_db_path()
248
+ canonical_sig = _sqlite_signature(canonical)
249
+ if not canonical_sig.get("exists"):
250
+ report["skipped"].append({"reason": "canonical_missing", "path": str(canonical)})
251
+ return report
252
+ if not canonical_sig.get("sqlite_ok"):
253
+ report["ok"] = False
254
+ report["skipped"].append({"reason": "canonical_not_sqlite_ok", "path": str(canonical)})
255
+ return report
256
+
257
+ for legacy_db in legacy_cognitive_db_paths():
258
+ legacy_sig = _sqlite_signature(legacy_db)
259
+ if not legacy_sig.get("exists"):
260
+ continue
261
+ files = _existing_sidecars(legacy_db)
262
+ if _wal_has_uncheckpointed_data(legacy_db):
263
+ report["skipped"].append({"path": str(legacy_db), "reason": "legacy_wal_has_data"})
264
+ continue
265
+ if legacy_sig.get("sha256") == canonical_sig.get("sha256"):
266
+ item = {
267
+ "path": str(legacy_db),
268
+ "action": "removed-identical-duplicate",
269
+ "files": [str(path) for path in files],
270
+ }
271
+ if not dry_run:
272
+ item["removed"] = _remove_paths(files)
273
+ _append_cleanup_marker({
274
+ "action": "remove_identical_duplicate",
275
+ "source": str(legacy_db),
276
+ "removed": item["removed"],
277
+ "legacy_sha256": legacy_sig.get("sha256"),
278
+ })
279
+ report["removed"].append(item)
280
+ continue
281
+ if _canonical_supersedes_legacy(canonical_sig, legacy_sig):
282
+ if dry_run:
283
+ report["archived"].append({
284
+ "path": str(legacy_db),
285
+ "action": "would-archive-superseded-legacy",
286
+ "reason": "canonical_newer_schema_compatible",
287
+ })
288
+ continue
289
+ try:
290
+ report["archived"].append(_archive_and_remove_legacy_db(
291
+ legacy_db,
292
+ canonical_sig=canonical_sig,
293
+ legacy_sig=legacy_sig,
294
+ reason="canonical_newer_schema_compatible",
295
+ ))
296
+ except Exception as exc:
297
+ report["ok"] = False
298
+ report["errors"].append({"path": str(legacy_db), "error": str(exc)})
299
+ continue
300
+ report["skipped"].append({
301
+ "path": str(legacy_db),
302
+ "reason": "divergent_requires_manual_review",
303
+ "canonical_mtime_epoch": canonical_sig.get("mtime_epoch"),
304
+ "legacy_mtime_epoch": legacy_sig.get("mtime_epoch"),
305
+ })
306
+ return report
307
+
308
+
118
309
  def audit_cognitive_db_paths() -> dict[str, Any]:
119
310
  canonical = canonical_cognitive_db_path()
120
311
  canonical_sig = _sqlite_signature(canonical)
@@ -173,7 +364,14 @@ def migrate_legacy_cognitive_db_if_needed() -> dict[str, Any]:
173
364
  canonical.parent.mkdir(parents=True, exist_ok=True)
174
365
  shutil.copy2(source, canonical)
175
366
  _write_migration_marker(source, canonical)
176
- return {"migrated": True, "reason": "legacy_copied", "source": str(source), "path": str(canonical)}
367
+ cleanup = cleanup_legacy_cognitive_db_artifacts()
368
+ return {
369
+ "migrated": True,
370
+ "reason": "legacy_copied",
371
+ "source": str(source),
372
+ "path": str(canonical),
373
+ "cleanup": cleanup,
374
+ }
177
375
 
178
376
 
179
377
  def resolve_cognitive_db(*, for_write: bool = True, migrate: bool = True, create_parent: bool = True) -> Path:
@@ -183,6 +381,7 @@ def resolve_cognitive_db(*, for_write: bool = True, migrate: bool = True, create
183
381
  target.parent.mkdir(parents=True, exist_ok=True)
184
382
  if migrate:
185
383
  migrate_legacy_cognitive_db_if_needed()
384
+ cleanup_legacy_cognitive_db_artifacts()
186
385
  audit = audit_cognitive_db_paths()
187
386
  if for_write and audit["status"] == "error":
188
387
  raise CognitiveDbPathConflict(
@@ -191,4 +390,3 @@ def resolve_cognitive_db(*, for_write: bool = True, migrate: bool = True, create
191
390
  + ", ".join(entry["path"] for entry in audit["legacy"] if entry["signature"].get("exists"))
192
391
  )
193
392
  return target
194
-
@@ -92,6 +92,7 @@ TECHNICAL_PREFIXES = (
92
92
  "desktop-local-install-",
93
93
  "packaged-code-f06-conflicts-",
94
94
  "legacy-shim-conflicts-",
95
+ "legacy-cognitive-db-",
95
96
  "legacy-personal-brain-db-stubs-",
96
97
  "legacy-root-db-stubs-",
97
98
  "codex-live-sync-",