nexo-brain 7.23.5 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/auto_update.py +12 -0
- package/src/cognitive_paths.py +201 -3
- package/src/scripts/prune_runtime_backups.py +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.23.
|
|
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,9 +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.
|
|
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
22
|
|
|
23
|
-
Previously in `7.23.
|
|
23
|
+
Previously in `7.23.5`: patch over v7.23.4 - `nexo update` keeps external CLI maintenance summary copy in English.
|
|
24
24
|
|
|
25
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.
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.23.
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -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
|
|
@@ -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")
|
package/src/cognitive_paths.py
CHANGED
|
@@ -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":
|
|
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
|
-
|
|
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
|
-
|