nexo-brain 7.20.12 → 7.20.14

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.20.12",
3
+ "version": "7.20.14",
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.20.12` is the current packaged-runtime line. Patch release over v7.20.11Local Context now keeps the first index pass separate from live change tracking, persists the current indexing start time, caps compact context payloads for agents, and installs the Windows host scheduler needed to keep WSL indexing alive after reboots.
21
+ Version `7.20.14` is the current packaged-runtime line. Patch release over v7.20.13Brain protects Local Memory during update/recovery paths, rotates runtime backup families to the latest 5 entries, keeps first-indexing status stable, and exposes bounded indexing speed profiles for Desktop.
22
+
23
+ Previously in `7.20.13`: patch release over v7.20.12 — Brain recovery now pauses all known DB writers before restoring `nexo.db`, and Doctor can repair the zero-byte/locked database state that made Desktop Local Memory show zero files.
24
+
25
+ Previously in `7.20.12`: patch release over v7.20.11 — Local Context now keeps the first index pass separate from live change tracking, persists the current indexing start time, caps compact context payloads for agents, and installs the Windows host scheduler needed to keep WSL indexing alive after reboots.
22
26
 
23
27
  Previously in `7.20.11`: patch release over v7.20.10 — Local Context now starts from real system volume roots plus mounted/removable/network volumes, filters system/cache/app/product artifacts, and injects relevant local evidence automatically into heartbeat, task-open and pre-action context.
24
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.20.12",
3
+ "version": "7.20.14",
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",
@@ -1336,7 +1336,7 @@ def _reload_launch_agents_after_bump() -> dict:
1336
1336
  return result
1337
1337
 
1338
1338
 
1339
- AUTO_UPDATE_BACKUP_KEEP = 10
1339
+ AUTO_UPDATE_BACKUP_KEEP = 5
1340
1340
  """Maximum number of auto-update backups to keep per prefix.
1341
1341
 
1342
1342
  Both `pre-autoupdate-*/` (DB snapshots) and `runtime-tree-*/` (code mirrors)
@@ -1409,10 +1409,13 @@ def _self_heal_if_wiped() -> dict | None:
1409
1409
  CRITICAL_TABLES,
1410
1410
  HOURLY_BACKUP_MAX_AGE,
1411
1411
  MIN_REFERENCE_ROWS,
1412
+ PROTECTED_TABLES,
1412
1413
  db_looks_wiped,
1413
1414
  db_row_counts,
1415
+ find_best_hourly_backup,
1414
1416
  find_latest_hourly_backup,
1415
- kill_nexo_mcp_servers,
1417
+ quiesce_nexo_db_writers,
1418
+ resume_nexo_launchagents,
1416
1419
  safe_sqlite_backup,
1417
1420
  validate_backup_matches_source,
1418
1421
  )
@@ -1423,11 +1426,16 @@ def _self_heal_if_wiped() -> dict | None:
1423
1426
  primary = DATA_DIR / "nexo.db"
1424
1427
  if not primary.is_file():
1425
1428
  return None
1426
- if not db_looks_wiped(primary, CRITICAL_TABLES):
1429
+ if not db_looks_wiped(primary, PROTECTED_TABLES):
1427
1430
  return None
1428
- reference = find_latest_hourly_backup(
1431
+ reference = find_best_hourly_backup(
1429
1432
  paths.backups_dir(),
1430
1433
  max_age_seconds=HOURLY_BACKUP_MAX_AGE,
1434
+ tables=PROTECTED_TABLES,
1435
+ ) or find_latest_hourly_backup(
1436
+ paths.backups_dir(),
1437
+ max_age_seconds=HOURLY_BACKUP_MAX_AGE,
1438
+ tables=PROTECTED_TABLES,
1431
1439
  )
1432
1440
  if reference is None:
1433
1441
  _log("self-heal: nexo.db looks wiped but no usable hourly backup found — skipping.")
@@ -1436,7 +1444,7 @@ def _self_heal_if_wiped() -> dict | None:
1436
1444
  "reason": "no_usable_hourly_backup",
1437
1445
  "primary_db": str(primary),
1438
1446
  }
1439
- ref_counts = db_row_counts(reference, CRITICAL_TABLES)
1447
+ ref_counts = db_row_counts(reference, PROTECTED_TABLES)
1440
1448
  ref_total = sum(v for v in ref_counts.values() if isinstance(v, int))
1441
1449
  if ref_total < MIN_REFERENCE_ROWS:
1442
1450
  _log(f"self-heal: reference backup {reference.name} has {ref_total} rows, below floor {MIN_REFERENCE_ROWS}")
@@ -1467,11 +1475,29 @@ def _self_heal_if_wiped() -> dict | None:
1467
1475
  f"(reference={reference.name}, {ref_total} critical rows). Restoring..."
1468
1476
  )
1469
1477
 
1470
- # Kill any live MCP servers so they cannot overwrite the restored DB.
1471
- kill_report = kill_nexo_mcp_servers(dry_run=False)
1472
- if kill_report.get("terminated"):
1473
- _log(f"self-heal: terminated {kill_report['terminated']} live MCP server(s).")
1474
- time.sleep(0.5)
1478
+ # Pause any live DB writers so they cannot overwrite the restored DB or
1479
+ # keep stale handles open. Desktop installs have more writers than the MCP
1480
+ # server: local-index, email-monitor, followup-runner, watchdog and catchup.
1481
+ quiesce_report = quiesce_nexo_db_writers(dry_run=False)
1482
+ stopped_launchagents = list((quiesce_report.get("launchagents") or {}).get("stopped") or [])
1483
+ if quiesce_report.get("terminated") or stopped_launchagents:
1484
+ _log(
1485
+ "self-heal: quiesced DB writers "
1486
+ f"(terminated={quiesce_report.get('terminated', 0)}, "
1487
+ f"launchagents={len(stopped_launchagents)})."
1488
+ )
1489
+ if quiesce_report.get("errors"):
1490
+ _log(f"self-heal: DB writer quiesce warnings: {quiesce_report.get('errors')}")
1491
+
1492
+ def _resume_quiesced() -> dict | None:
1493
+ if not stopped_launchagents:
1494
+ return None
1495
+ report = resume_nexo_launchagents(stopped_launchagents)
1496
+ if report.get("started"):
1497
+ _log(f"self-heal: resumed {len(report['started'])} launchagent(s).")
1498
+ if report.get("errors"):
1499
+ _log(f"self-heal: launchagent resume warnings: {report.get('errors')}")
1500
+ return report
1475
1501
 
1476
1502
  # Snapshot the current (wiped) state so the heal is reversible.
1477
1503
  pre_heal_dir = paths.backups_dir() / f"pre-heal-{time.strftime('%Y-%m-%d-%H%M%S')}"
@@ -1497,27 +1523,34 @@ def _self_heal_if_wiped() -> dict | None:
1497
1523
  ok, err = safe_sqlite_backup(reference, primary)
1498
1524
  if not ok:
1499
1525
  _log(f"self-heal: restore copy failed: {err}")
1526
+ resume_report = _resume_quiesced()
1500
1527
  return {
1501
1528
  "action": "failed",
1502
1529
  "reason": "restore_copy_failed",
1503
1530
  "error": err,
1504
1531
  "reference": str(reference),
1505
1532
  "pre_heal_dir": str(pre_heal_dir),
1533
+ "quiesce": quiesce_report,
1534
+ "resume": resume_report,
1506
1535
  }
1507
- valid, valid_err = validate_backup_matches_source(reference, primary, CRITICAL_TABLES)
1536
+ valid, valid_err = validate_backup_matches_source(reference, primary, PROTECTED_TABLES)
1508
1537
  if not valid:
1509
1538
  _log(f"self-heal: post-restore validation failed: {valid_err}")
1539
+ resume_report = _resume_quiesced()
1510
1540
  return {
1511
1541
  "action": "failed",
1512
1542
  "reason": "validation_failed",
1513
1543
  "error": valid_err,
1514
1544
  "reference": str(reference),
1515
1545
  "pre_heal_dir": str(pre_heal_dir),
1546
+ "quiesce": quiesce_report,
1547
+ "resume": resume_report,
1516
1548
  }
1517
1549
 
1518
- final_counts = db_row_counts(primary, CRITICAL_TABLES)
1550
+ final_counts = db_row_counts(primary, PROTECTED_TABLES)
1519
1551
  final_total = sum(v for v in final_counts.values() if isinstance(v, int))
1520
1552
  _log(f"self-heal: restored {final_total} critical rows from {reference.name}.")
1553
+ resume_report = _resume_quiesced()
1521
1554
  try:
1522
1555
  SELF_HEAL_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
1523
1556
  SELF_HEAL_STATE_FILE.write_text(json.dumps({
@@ -1525,6 +1558,7 @@ def _self_heal_if_wiped() -> dict | None:
1525
1558
  "reference": str(reference),
1526
1559
  "critical_rows_restored": final_total,
1527
1560
  "pre_heal_dir": str(pre_heal_dir),
1561
+ "quiesced_launchagents": stopped_launchagents,
1528
1562
  }))
1529
1563
  except Exception as e:
1530
1564
  _log(f"self-heal: state write warning: {e}")
@@ -1535,7 +1569,9 @@ def _self_heal_if_wiped() -> dict | None:
1535
1569
  "reference_rows": ref_total,
1536
1570
  "restored_rows": final_total,
1537
1571
  "pre_heal_dir": str(pre_heal_dir),
1538
- "terminated_servers": kill_report.get("terminated", 0),
1572
+ "terminated_servers": int((quiesce_report.get("mcp") or {}).get("terminated") or 0),
1573
+ "quiesce": quiesce_report,
1574
+ "resume": resume_report,
1539
1575
  }
1540
1576
 
1541
1577