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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/db_guard.py +43 -8
- package/src/doctor/providers/boot.py +11 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.23.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"""
|
|
523
|
+
"""Restore selected tables in ``target`` from ``source``.
|
|
510
524
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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":
|
|
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(
|
|
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")
|
|
215
|
+
if isinstance(payload, dict) and payload.get("status") in {"restored", "merged"}
|
|
211
216
|
}
|
|
212
|
-
restored_rows = sum(int(payload.get("
|
|
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"
|
|
219
|
-
evidence=evidence + [f"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="
|
|
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
|
)
|