nexo-brain 7.20.2 → 7.20.3
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.
|
|
3
|
+
"version": "7.20.3",
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.20.
|
|
3
|
+
"version": "7.20.3",
|
|
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",
|
|
@@ -3833,6 +3833,66 @@ def check_automation_caller_coverage(days: int = 7) -> DoctorCheck:
|
|
|
3833
3833
|
)
|
|
3834
3834
|
|
|
3835
3835
|
|
|
3836
|
+
def check_local_index_hygiene(fix: bool = False) -> DoctorCheck:
|
|
3837
|
+
try:
|
|
3838
|
+
from local_context import api as local_context_api
|
|
3839
|
+
|
|
3840
|
+
result = local_context_api.local_index_hygiene(fix=fix)
|
|
3841
|
+
residue = result.get("residue") or {}
|
|
3842
|
+
cleanup = result.get("cleanup") or {}
|
|
3843
|
+
suspect_roots = [str(path) for path in result.get("removed_roots") or []]
|
|
3844
|
+
residue_total = sum(int(residue.get(key, 0) or 0) for key in ("assets", "jobs", "errors", "dirs", "checkpoints"))
|
|
3845
|
+
cleanup_total = sum(int(cleanup.get(key, 0) or 0) for key in ("assets", "jobs", "errors", "dirs", "checkpoints"))
|
|
3846
|
+
evidence = [
|
|
3847
|
+
"suspect_installer_roots=" + str(len(suspect_roots)),
|
|
3848
|
+
"residue=" + json.dumps(residue, sort_keys=True),
|
|
3849
|
+
"cleanup=" + json.dumps(cleanup, sort_keys=True),
|
|
3850
|
+
]
|
|
3851
|
+
evidence.extend(f"root={path}" for path in suspect_roots[:5])
|
|
3852
|
+
if residue_total == 0 and not suspect_roots:
|
|
3853
|
+
return DoctorCheck(
|
|
3854
|
+
id="runtime.local_index_hygiene",
|
|
3855
|
+
tier="runtime",
|
|
3856
|
+
status="healthy",
|
|
3857
|
+
severity="info",
|
|
3858
|
+
summary="Local memory index hygiene is clean",
|
|
3859
|
+
evidence=evidence,
|
|
3860
|
+
repair_plan=[],
|
|
3861
|
+
)
|
|
3862
|
+
if fix:
|
|
3863
|
+
return DoctorCheck(
|
|
3864
|
+
id="runtime.local_index_hygiene",
|
|
3865
|
+
tier="runtime",
|
|
3866
|
+
status="healthy",
|
|
3867
|
+
severity="info",
|
|
3868
|
+
summary="Local memory index hygiene repaired",
|
|
3869
|
+
evidence=evidence,
|
|
3870
|
+
repair_plan=[],
|
|
3871
|
+
fixed=cleanup_total > 0 or bool(suspect_roots),
|
|
3872
|
+
)
|
|
3873
|
+
return DoctorCheck(
|
|
3874
|
+
id="runtime.local_index_hygiene",
|
|
3875
|
+
tier="runtime",
|
|
3876
|
+
status="degraded",
|
|
3877
|
+
severity="warn",
|
|
3878
|
+
summary="Local memory index has stale removed-root residue",
|
|
3879
|
+
evidence=evidence,
|
|
3880
|
+
repair_plan=["Run `nexo doctor --tier runtime --fix` to purge stale local memory roots and installer-volume residue"],
|
|
3881
|
+
escalation_prompt="Local memory status may show stale pending or failed jobs from removed roots.",
|
|
3882
|
+
)
|
|
3883
|
+
except Exception as exc:
|
|
3884
|
+
return DoctorCheck(
|
|
3885
|
+
id="runtime.local_index_hygiene",
|
|
3886
|
+
tier="runtime",
|
|
3887
|
+
status="degraded",
|
|
3888
|
+
severity="warn",
|
|
3889
|
+
summary="Local memory index hygiene could not be checked",
|
|
3890
|
+
evidence=[str(exc)],
|
|
3891
|
+
repair_plan=["Inspect local_context.api.local_index_hygiene and runtime DB tables"],
|
|
3892
|
+
escalation_prompt="Support cannot verify local memory index residue.",
|
|
3893
|
+
)
|
|
3894
|
+
|
|
3895
|
+
|
|
3836
3896
|
def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
3837
3897
|
"""Run all runtime-tier checks. Read-only by default."""
|
|
3838
3898
|
return [
|
|
@@ -3854,6 +3914,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
|
3854
3914
|
safe_check(check_automation_telemetry),
|
|
3855
3915
|
safe_check(check_automation_caller_coverage),
|
|
3856
3916
|
safe_check(check_state_watchers),
|
|
3917
|
+
safe_check(check_local_index_hygiene, fix=fix),
|
|
3857
3918
|
safe_check(check_release_artifact_sync),
|
|
3858
3919
|
safe_check(check_release_trace_hygiene),
|
|
3859
3920
|
safe_check(check_launchagent_inventory),
|
|
@@ -16,6 +16,7 @@ from .api import (
|
|
|
16
16
|
get_neighbors,
|
|
17
17
|
list_exclusions,
|
|
18
18
|
list_roots,
|
|
19
|
+
local_index_hygiene,
|
|
19
20
|
model_status,
|
|
20
21
|
pause,
|
|
21
22
|
purge_asset,
|
|
@@ -39,6 +40,7 @@ __all__ = [
|
|
|
39
40
|
"get_neighbors",
|
|
40
41
|
"list_exclusions",
|
|
41
42
|
"list_roots",
|
|
43
|
+
"local_index_hygiene",
|
|
42
44
|
"model_status",
|
|
43
45
|
"pause",
|
|
44
46
|
"purge_asset",
|
package/src/local_context/api.py
CHANGED
|
@@ -64,9 +64,10 @@ def remove_root(path: str) -> dict:
|
|
|
64
64
|
conn = _conn()
|
|
65
65
|
root_path = norm_path(path)
|
|
66
66
|
conn.execute("UPDATE local_index_roots SET status='removed', updated_at=? WHERE root_path=?", (now(), root_path))
|
|
67
|
+
cleanup = _purge_removed_root_payloads(conn, root_paths=[root_path])
|
|
67
68
|
conn.commit()
|
|
68
|
-
log_event("info", "root_removed", "Root removed", path=redact_path(root_path))
|
|
69
|
-
return {"ok": True, "root_path": root_path}
|
|
69
|
+
log_event("info", "root_removed", "Root removed", path=redact_path(root_path), cleanup=cleanup)
|
|
70
|
+
return {"ok": True, "root_path": root_path, "cleanup": cleanup}
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
def list_roots() -> list[dict]:
|
|
@@ -108,6 +109,8 @@ def _mounted_volume_roots() -> list[str]:
|
|
|
108
109
|
try:
|
|
109
110
|
if candidate.name.startswith(".") or not candidate.is_dir():
|
|
110
111
|
continue
|
|
112
|
+
if _should_skip_mounted_root(candidate):
|
|
113
|
+
continue
|
|
111
114
|
resolved = candidate.resolve()
|
|
112
115
|
if resolved == root_resolved:
|
|
113
116
|
continue
|
|
@@ -137,6 +140,124 @@ def ensure_default_roots() -> dict:
|
|
|
137
140
|
return {"ok": True, "created": len(created), "roots": list_roots()}
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _should_skip_mounted_root(candidate: Path) -> bool:
|
|
144
|
+
name = candidate.name.strip().lower()
|
|
145
|
+
if name in {"nexo desktop", "nexo desktop beta"} or name.startswith("nexo desktop "):
|
|
146
|
+
return True
|
|
147
|
+
try:
|
|
148
|
+
app_bundles = [child.name.lower() for child in candidate.iterdir() if child.suffix.lower() == ".app"]
|
|
149
|
+
except Exception:
|
|
150
|
+
app_bundles = []
|
|
151
|
+
if any(name.startswith("nexo desktop") for name in app_bundles):
|
|
152
|
+
installer_markers = (
|
|
153
|
+
candidate / ".background",
|
|
154
|
+
candidate / "Applications",
|
|
155
|
+
candidate / ".DS_Store",
|
|
156
|
+
)
|
|
157
|
+
if any(marker.exists() for marker in installer_markers):
|
|
158
|
+
return True
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _removed_root_filters(conn, *, root_paths: list[str] | None = None) -> tuple[list[int], list[str]]:
|
|
163
|
+
if root_paths:
|
|
164
|
+
placeholders = ",".join("?" for _ in root_paths)
|
|
165
|
+
rows = conn.execute(
|
|
166
|
+
f"SELECT id, root_path FROM local_index_roots WHERE root_path IN ({placeholders}) AND status='removed'",
|
|
167
|
+
tuple(root_paths),
|
|
168
|
+
).fetchall()
|
|
169
|
+
else:
|
|
170
|
+
rows = conn.execute("SELECT id, root_path FROM local_index_roots WHERE status='removed'").fetchall()
|
|
171
|
+
return [int(row["id"]) for row in rows], [str(row["root_path"]) for row in rows]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _removed_root_payload_counts(conn, *, root_paths: list[str] | None = None) -> dict:
|
|
175
|
+
root_ids, removed_paths = _removed_root_filters(conn, root_paths=root_paths)
|
|
176
|
+
if not root_ids and not removed_paths:
|
|
177
|
+
return {"assets": 0, "jobs": 0, "errors": 0, "dirs": 0, "checkpoints": 0}
|
|
178
|
+
asset_filter, params = _removed_root_asset_filter(root_ids, removed_paths)
|
|
179
|
+
if not asset_filter:
|
|
180
|
+
return {"assets": 0, "jobs": 0, "errors": 0, "dirs": 0, "checkpoints": 0}
|
|
181
|
+
asset_subquery = f"SELECT asset_id FROM local_assets WHERE {asset_filter}"
|
|
182
|
+
assets = int(conn.execute(f"SELECT COUNT(*) AS total FROM local_assets WHERE {asset_filter}", tuple(params)).fetchone()["total"] or 0)
|
|
183
|
+
jobs = int(conn.execute(f"SELECT COUNT(*) AS total FROM local_index_jobs WHERE asset_id IN ({asset_subquery})", tuple(params)).fetchone()["total"] or 0)
|
|
184
|
+
errors = int(conn.execute(f"SELECT COUNT(*) AS total FROM local_index_errors WHERE asset_id IN ({asset_subquery})", tuple(params)).fetchone()["total"] or 0)
|
|
185
|
+
for path in removed_paths:
|
|
186
|
+
errors += int(conn.execute("SELECT COUNT(*) AS total FROM local_index_errors WHERE asset_id='' AND (path = ? OR path LIKE ?)", (path, f"{path}/%")).fetchone()["total"] or 0)
|
|
187
|
+
dirs = 0
|
|
188
|
+
checkpoints = 0
|
|
189
|
+
if root_ids:
|
|
190
|
+
root_placeholders = ",".join("?" for _ in root_ids)
|
|
191
|
+
dirs = int(conn.execute(f"SELECT COUNT(*) AS total FROM local_index_dirs WHERE root_id IN ({root_placeholders})", tuple(root_ids)).fetchone()["total"] or 0)
|
|
192
|
+
checkpoints = int(conn.execute(f"SELECT COUNT(*) AS total FROM local_index_checkpoints WHERE root_id IN ({root_placeholders})", tuple(root_ids)).fetchone()["total"] or 0)
|
|
193
|
+
return {"assets": assets, "jobs": jobs, "errors": errors, "dirs": dirs, "checkpoints": checkpoints}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _removed_root_asset_filter(root_ids: list[int], removed_paths: list[str]) -> tuple[str, list[Any]]:
|
|
197
|
+
filters: list[str] = []
|
|
198
|
+
params: list[Any] = []
|
|
199
|
+
if root_ids:
|
|
200
|
+
root_placeholders = ",".join("?" for _ in root_ids)
|
|
201
|
+
filters.append(f"root_id IN ({root_placeholders})")
|
|
202
|
+
params.extend(root_ids)
|
|
203
|
+
for path in removed_paths:
|
|
204
|
+
filters.append("(path = ? OR path LIKE ?)")
|
|
205
|
+
params.extend([path, f"{path}/%"])
|
|
206
|
+
return " OR ".join(filters), params
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _purge_removed_root_payloads(conn, *, root_paths: list[str] | None = None) -> dict:
|
|
210
|
+
root_ids, removed_paths = _removed_root_filters(conn, root_paths=root_paths)
|
|
211
|
+
if not root_ids and not removed_paths:
|
|
212
|
+
return {"assets": 0, "jobs": 0, "errors": 0, "dirs": 0, "checkpoints": 0}
|
|
213
|
+
|
|
214
|
+
asset_filter, params = _removed_root_asset_filter(root_ids, removed_paths)
|
|
215
|
+
if not asset_filter:
|
|
216
|
+
return {"assets": 0, "jobs": 0, "errors": 0, "dirs": 0, "checkpoints": 0}
|
|
217
|
+
asset_subquery = f"SELECT asset_id FROM local_assets WHERE {asset_filter}"
|
|
218
|
+
counts = _removed_root_payload_counts(conn, root_paths=root_paths)
|
|
219
|
+
|
|
220
|
+
for table in ("local_embeddings", "local_chunks", "local_entities", "local_asset_versions"):
|
|
221
|
+
conn.execute(f"DELETE FROM {table} WHERE asset_id IN ({asset_subquery})", tuple(params))
|
|
222
|
+
conn.execute(f"DELETE FROM local_relations WHERE source_asset_id IN ({asset_subquery})", tuple(params))
|
|
223
|
+
conn.execute(f"DELETE FROM local_relations WHERE target_ref IN ({asset_subquery})", tuple(params))
|
|
224
|
+
conn.execute(f"DELETE FROM local_index_jobs WHERE asset_id IN ({asset_subquery})", tuple(params))
|
|
225
|
+
conn.execute(f"DELETE FROM local_index_errors WHERE asset_id IN ({asset_subquery})", tuple(params))
|
|
226
|
+
|
|
227
|
+
for path in removed_paths:
|
|
228
|
+
conn.execute("DELETE FROM local_index_errors WHERE path = ? OR path LIKE ?", (path, f"{path}/%"))
|
|
229
|
+
|
|
230
|
+
if root_ids:
|
|
231
|
+
root_placeholders = ",".join("?" for _ in root_ids)
|
|
232
|
+
conn.execute(f"DELETE FROM local_index_dirs WHERE root_id IN ({root_placeholders})", tuple(root_ids))
|
|
233
|
+
conn.execute(f"DELETE FROM local_index_checkpoints WHERE root_id IN ({root_placeholders})", tuple(root_ids))
|
|
234
|
+
conn.execute(f"DELETE FROM local_assets WHERE {asset_filter}", tuple(params))
|
|
235
|
+
return counts
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def local_index_hygiene(*, fix: bool = False) -> dict:
|
|
239
|
+
conn = _conn()
|
|
240
|
+
removed_paths: list[str] = []
|
|
241
|
+
for row in conn.execute("SELECT id, root_path FROM local_index_roots").fetchall():
|
|
242
|
+
path = str(row["root_path"] or "")
|
|
243
|
+
if _should_skip_mounted_root(Path(path)):
|
|
244
|
+
removed_paths.append(path)
|
|
245
|
+
if fix:
|
|
246
|
+
conn.execute("UPDATE local_index_roots SET status='removed', updated_at=? WHERE id=?", (now(), row["id"]))
|
|
247
|
+
before = _removed_root_payload_counts(conn)
|
|
248
|
+
cleanup = {"assets": 0, "jobs": 0, "errors": 0, "dirs": 0, "checkpoints": 0}
|
|
249
|
+
if fix:
|
|
250
|
+
cleanup = _purge_removed_root_payloads(conn)
|
|
251
|
+
conn.commit()
|
|
252
|
+
if fix and (removed_paths or any(int(cleanup.get(key, 0) or 0) for key in ("assets", "jobs", "errors", "dirs", "checkpoints"))):
|
|
253
|
+
log_event("info", "index_hygiene_repaired", "Local memory index hygiene repaired", roots=[redact_path(path) for path in removed_paths], cleanup=cleanup)
|
|
254
|
+
return {"ok": True, "fix": fix, "removed_roots": removed_paths, "residue": before, "cleanup": cleanup}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def repair_index_hygiene() -> dict:
|
|
258
|
+
return local_index_hygiene(fix=True)
|
|
259
|
+
|
|
260
|
+
|
|
140
261
|
def add_exclusion(path: str, *, reason: str = "user") -> dict:
|
|
141
262
|
conn = _conn()
|
|
142
263
|
excluded_path = norm_path(path)
|
|
@@ -398,7 +519,7 @@ def _mark_asset_deleted(conn, asset_id: str, deleted_at: float | None = None) ->
|
|
|
398
519
|
"""
|
|
399
520
|
UPDATE local_index_jobs
|
|
400
521
|
SET status='done', last_error_code='asset_deleted', updated_at=?
|
|
401
|
-
WHERE asset_id=? AND status IN ('pending', 'running')
|
|
522
|
+
WHERE asset_id=? AND status IN ('pending', 'running', 'failed')
|
|
402
523
|
""",
|
|
403
524
|
(deleted_at, asset_id),
|
|
404
525
|
)
|
|
@@ -1102,9 +1223,19 @@ def run_once(
|
|
|
1102
1223
|
def _problem_rows(conn) -> list[dict]:
|
|
1103
1224
|
rows = conn.execute(
|
|
1104
1225
|
"""
|
|
1105
|
-
SELECT path, phase, error_code, user_message, technical_detail, retryable, created_at
|
|
1106
|
-
FROM local_index_errors
|
|
1107
|
-
|
|
1226
|
+
SELECT e.path, e.phase, e.error_code, e.user_message, e.technical_detail, e.retryable, e.created_at
|
|
1227
|
+
FROM local_index_errors e
|
|
1228
|
+
LEFT JOIN local_assets a ON a.asset_id=e.asset_id
|
|
1229
|
+
LEFT JOIN local_index_roots r ON r.id=a.root_id
|
|
1230
|
+
WHERE COALESCE(r.status, 'active') != 'removed'
|
|
1231
|
+
AND NOT EXISTS (
|
|
1232
|
+
SELECT 1
|
|
1233
|
+
FROM local_index_roots rr
|
|
1234
|
+
WHERE rr.status='removed'
|
|
1235
|
+
AND e.path != ''
|
|
1236
|
+
AND (e.path = rr.root_path OR e.path LIKE rr.root_path || '/%')
|
|
1237
|
+
)
|
|
1238
|
+
ORDER BY e.id DESC
|
|
1108
1239
|
LIMIT 20
|
|
1109
1240
|
"""
|
|
1110
1241
|
).fetchall()
|
|
@@ -1387,9 +1518,24 @@ def status() -> dict:
|
|
|
1387
1518
|
conn = _conn()
|
|
1388
1519
|
paused = _is_paused()
|
|
1389
1520
|
assets = conn.execute(
|
|
1390
|
-
"
|
|
1521
|
+
"""
|
|
1522
|
+
SELECT COUNT(*) AS total, SUM(CASE WHEN a.status='active' THEN 1 ELSE 0 END) AS active
|
|
1523
|
+
FROM local_assets a
|
|
1524
|
+
LEFT JOIN local_index_roots r ON r.id=a.root_id
|
|
1525
|
+
WHERE COALESCE(r.status, 'active') != 'removed'
|
|
1526
|
+
"""
|
|
1391
1527
|
).fetchone()
|
|
1392
|
-
job_rows = conn.execute(
|
|
1528
|
+
job_rows = conn.execute(
|
|
1529
|
+
"""
|
|
1530
|
+
SELECT j.status, COUNT(*) AS total
|
|
1531
|
+
FROM local_index_jobs j
|
|
1532
|
+
JOIN local_assets a ON a.asset_id=j.asset_id
|
|
1533
|
+
LEFT JOIN local_index_roots r ON r.id=a.root_id
|
|
1534
|
+
WHERE a.status='active'
|
|
1535
|
+
AND COALESCE(r.status, 'active') != 'removed'
|
|
1536
|
+
GROUP BY j.status
|
|
1537
|
+
"""
|
|
1538
|
+
).fetchall()
|
|
1393
1539
|
job_counts = {row["status"]: int(row["total"] or 0) for row in job_rows}
|
|
1394
1540
|
pending = int(job_counts.get("pending", 0) or 0)
|
|
1395
1541
|
running_jobs = int(job_counts.get("running", 0) or 0)
|
|
@@ -1401,7 +1547,15 @@ def status() -> dict:
|
|
|
1401
1547
|
roots = list_roots()
|
|
1402
1548
|
volumes = []
|
|
1403
1549
|
by_volume = conn.execute(
|
|
1404
|
-
"
|
|
1550
|
+
"""
|
|
1551
|
+
SELECT a.volume_id, COUNT(*) AS files
|
|
1552
|
+
FROM local_assets a
|
|
1553
|
+
LEFT JOIN local_index_roots r ON r.id=a.root_id
|
|
1554
|
+
WHERE a.status='active'
|
|
1555
|
+
AND COALESCE(r.status, 'active') != 'removed'
|
|
1556
|
+
GROUP BY a.volume_id
|
|
1557
|
+
ORDER BY a.volume_id
|
|
1558
|
+
"""
|
|
1405
1559
|
).fetchall()
|
|
1406
1560
|
for row in by_volume:
|
|
1407
1561
|
volumes.append({"id": row["volume_id"], "label": row["volume_id"] or "Disk", "files": row["files"], "status": "active"})
|