nexo-brain 7.23.11 → 7.23.12

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.11",
3
+ "version": "7.23.12",
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,11 +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.23.11` is the current packaged-runtime line. Patch over v7.23.10 - older installed runtimes can update safely even when `cognitive_paths.py` has not been synced yet.
21
+ Version `7.23.12` is the current packaged-runtime line. Patch over v7.23.11 - protected database recovery now repairs degraded Brain tables from backup without rolling back newer rows.
22
22
 
23
- Previously in `7.23.6`: patch over v7.23.5 - `nexo update` clears safe legacy `cognitive.db` shadows and keeps superseded archives under runtime backup retention.
23
+ Previously in `7.23.11`: patch over v7.23.10 - older installed runtimes can update safely even when `cognitive_paths.py` has not been synced yet.
24
24
 
25
- Previously in `7.23.5`: patch over v7.23.4 - `nexo update` keeps external CLI maintenance summary copy in English.
25
+ Previously in `7.23.6`: patch over v7.23.5 - `nexo update` clears safe legacy `cognitive.db` shadows and keeps superseded archives under runtime backup retention.
26
26
 
27
27
  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.
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.23.11",
3
+ "version": "7.23.12",
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/db_guard.py CHANGED
@@ -29,7 +29,7 @@ auto_update.py):
29
29
  diff_row_counts(current, reference, tables) -> WipeReport
30
30
  safe_sqlite_backup(source, dest) -> tuple[bool, str | None]
31
31
  validate_backup_matches_source(source, dest, tables) -> tuple[bool, str | None]
32
- restore_tables_from_backup(source, target, tables) -> dict
32
+ restore_tables_from_backup(source, target, tables, mode) -> dict
33
33
  kill_nexo_mcp_servers(dry_run) -> dict
34
34
  quiesce_nexo_db_writers(dry_run) -> dict
35
35
  resume_nexo_launchagents(labels, dry_run) -> dict
@@ -501,23 +501,42 @@ def _quote_identifier(identifier: str) -> str:
501
501
  return '"' + identifier.replace('"', '""') + '"'
502
502
 
503
503
 
504
+ def _quote_sql_name(identifier: str) -> str:
505
+ return '"' + identifier.replace('"', '""') + '"'
506
+
507
+
508
+ def _table_columns(conn: sqlite3.Connection, schema: str, table: str) -> list[str]:
509
+ if schema not in {"main", "backup_db"}:
510
+ raise ValueError(f"refusing unsafe schema identifier: {schema!r}")
511
+ quoted = _quote_identifier(table)
512
+ rows = conn.execute(f"PRAGMA {schema}.table_info({quoted})").fetchall()
513
+ return [str(row[1]) for row in rows]
514
+
515
+
504
516
  def restore_tables_from_backup(
505
517
  source: str | Path,
506
518
  target: str | Path,
507
519
  tables: tuple[str, ...] = LOCAL_CONTEXT_TABLES,
520
+ *,
521
+ mode: str = "replace",
508
522
  ) -> dict:
509
- """Replace selected tables in ``target`` with the copy from ``source``.
523
+ """Restore selected tables in ``target`` from ``source``.
510
524
 
511
- This is intentionally table-scoped. It lets Doctor/repair recover days of
512
- local indexing from a backup without rolling back newer conversations,
513
- credentials, followups, or other Brain state created after that backup.
525
+ ``mode="replace"`` keeps the historical behavior: target rows are deleted
526
+ and replaced by the backup table. ``mode="merge_missing"`` preserves target
527
+ rows and inserts missing rows from the backup with ``INSERT OR IGNORE``.
528
+ This is intentionally table-scoped so repair can recover data without
529
+ rolling back unrelated Brain state created after the backup.
514
530
  """
531
+ if mode not in {"replace", "merge_missing"}:
532
+ raise ValueError(f"unsupported restore mode: {mode!r}")
515
533
  src = Path(source)
516
534
  dst = Path(target)
517
535
  result: dict = {
518
536
  "ok": False,
519
537
  "source": str(src),
520
538
  "target": str(dst),
539
+ "mode": mode,
521
540
  "tables": {},
522
541
  "errors": [],
523
542
  }
@@ -553,13 +572,29 @@ def restore_tables_from_backup(
553
572
  continue
554
573
  conn.execute(create_sql)
555
574
  before = _table_count(conn, table) or 0
556
- conn.execute(f"DELETE FROM main.{quoted}")
557
- conn.execute(f"INSERT INTO main.{quoted} SELECT * FROM backup_db.{quoted}")
575
+ if mode == "replace":
576
+ conn.execute(f"DELETE FROM main.{quoted}")
577
+ conn.execute(f"INSERT INTO main.{quoted} SELECT * FROM backup_db.{quoted}")
578
+ status = "restored"
579
+ else:
580
+ target_columns = _table_columns(conn, "main", table)
581
+ source_columns = set(_table_columns(conn, "backup_db", table))
582
+ common_columns = [column for column in target_columns if column in source_columns]
583
+ if not common_columns:
584
+ result["tables"][table] = {"status": "no_common_columns", "before": int(before)}
585
+ continue
586
+ column_sql = ", ".join(_quote_sql_name(column) for column in common_columns)
587
+ conn.execute(
588
+ f"INSERT OR IGNORE INTO main.{quoted} ({column_sql}) "
589
+ f"SELECT {column_sql} FROM backup_db.{quoted}"
590
+ )
591
+ status = "merged"
558
592
  after = _table_count(conn, table) or 0
559
593
  result["tables"][table] = {
560
- "status": "restored",
594
+ "status": status,
561
595
  "before": int(before),
562
596
  "after": int(after),
597
+ "restored": max(int(after) - int(before), 0),
563
598
  }
564
599
  conn.commit()
565
600
  result["ok"] = not result["errors"]
@@ -202,21 +202,26 @@ def check_db_integrity(fix: bool = False) -> DoctorCheck:
202
202
  evidence.extend(["Local memory regression:", *local_regression.summary_lines()])
203
203
 
204
204
  if fix and recoverable_regression:
205
- report = restore_tables_from_backup(reference, db_path)
205
+ report = restore_tables_from_backup(
206
+ reference,
207
+ db_path,
208
+ tables=PROTECTED_TABLES,
209
+ mode="merge_missing",
210
+ )
206
211
  if report.get("ok"):
207
212
  restored = {
208
213
  table: payload
209
214
  for table, payload in (report.get("tables") or {}).items()
210
- if isinstance(payload, dict) and payload.get("status") == "restored"
215
+ if isinstance(payload, dict) and payload.get("status") in {"restored", "merged"}
211
216
  }
212
- restored_rows = sum(int(payload.get("after") or 0) for payload in restored.values())
217
+ restored_rows = sum(int(payload.get("restored") or 0) for payload in restored.values())
213
218
  return DoctorCheck(
214
219
  id="boot.db_integrity",
215
220
  tier="boot",
216
221
  status="healthy",
217
222
  severity="info",
218
- summary=f"Local memory restored from backup ({restored_rows} protected rows)",
219
- evidence=evidence + [f"Restored local-memory tables: {len(restored)}"],
223
+ summary=f"Database protected tables restored from backup ({restored_rows} rows recovered)",
224
+ evidence=evidence + [f"Restored protected tables: {len(restored)}"],
220
225
  fixed=True,
221
226
  )
222
227
  return DoctorCheck(
@@ -224,7 +229,7 @@ def check_db_integrity(fix: bool = False) -> DoctorCheck:
224
229
  tier="boot",
225
230
  status="critical",
226
231
  severity="error",
227
- summary="Local memory repair failed",
232
+ summary="Database protected-table repair failed",
228
233
  evidence=evidence + [f"Restore errors: {report.get('errors') or []}"],
229
234
  repair_plan=["Close NEXO Desktop and run nexo doctor --tier boot --plane database_real --fix"],
230
235
  )