nexo-brain 7.9.18 → 7.9.19
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.9.
|
|
3
|
+
"version": "7.9.19",
|
|
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,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.9.
|
|
21
|
+
Version `7.9.19` is the current packaged-runtime line. Patch release over `7.9.18`: runtime doctor now distinguishes real install breakage from tracked in-progress work, interactive Desktop sessions no longer poison automation telemetry scoring, stale filesystem skill rows are pruned during sync, stale protocol debt draining marks rows resolved, and watchdog treats LaunchAgent SIGTERM reloads as supervisor interruptions instead of failures. It includes the v7.9.18 client-sync bootstrap fix.
|
|
22
|
+
|
|
23
|
+
Previously in `7.9.18`: packaged client-sync imports now work when `NEXO_HOME` is unset, so `nexo clients sync`, `nexo update`, and runtime doctor bootstrap checks no longer hit the `_user_home` import-order crash.
|
|
22
24
|
|
|
23
25
|
Previously in `7.9.17`: continuity snapshot idempotency marks its SHA-1 digest as non-security usage, keeping the high-severity Bandit gate green while preserving stable idempotency keys.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.19",
|
|
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/_skills.py
CHANGED
|
@@ -90,8 +90,16 @@ def _normalize_level(value: str | None) -> str:
|
|
|
90
90
|
|
|
91
91
|
def _normalize_mode(value: str | None, *, has_script: bool = False, has_content: bool = False) -> str:
|
|
92
92
|
mode = (value or "").strip().lower()
|
|
93
|
-
if mode
|
|
93
|
+
if mode == "guide":
|
|
94
94
|
return mode
|
|
95
|
+
if mode == "execute":
|
|
96
|
+
return "execute" if has_script else "guide"
|
|
97
|
+
if mode == "hybrid":
|
|
98
|
+
if has_script and has_content:
|
|
99
|
+
return "hybrid"
|
|
100
|
+
if has_script:
|
|
101
|
+
return "execute"
|
|
102
|
+
return "guide"
|
|
95
103
|
if has_script and has_content:
|
|
96
104
|
return "hybrid"
|
|
97
105
|
if has_script:
|
|
@@ -161,6 +169,14 @@ def _sync_dirs() -> list[tuple[str, Path]]:
|
|
|
161
169
|
]
|
|
162
170
|
|
|
163
171
|
|
|
172
|
+
def _is_relative_to(path: Path, parent: Path) -> bool:
|
|
173
|
+
try:
|
|
174
|
+
path.resolve(strict=False).relative_to(parent.resolve(strict=False))
|
|
175
|
+
return True
|
|
176
|
+
except (OSError, ValueError):
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
164
180
|
def _ensure_skill_dirs():
|
|
165
181
|
PERSONAL_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
166
182
|
RUNTIME_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -1379,7 +1395,50 @@ def sync_skill_directories() -> dict:
|
|
|
1379
1395
|
for skill in discovered.values():
|
|
1380
1396
|
synced.append(_upsert_filesystem_skill(skill)["id"])
|
|
1381
1397
|
|
|
1382
|
-
|
|
1398
|
+
pruned: list[str] = []
|
|
1399
|
+
discovered_ids = set(discovered)
|
|
1400
|
+
conn = get_db()
|
|
1401
|
+
rows = conn.execute(
|
|
1402
|
+
"""SELECT id, source_kind, definition_path, file_path
|
|
1403
|
+
FROM skills
|
|
1404
|
+
WHERE source_kind IN ('core', 'community', 'personal')
|
|
1405
|
+
AND definition_path != ''"""
|
|
1406
|
+
).fetchall()
|
|
1407
|
+
sync_roots = [root.resolve(strict=False) for _source_kind, root in _sync_dirs()]
|
|
1408
|
+
sync_roots.extend(
|
|
1409
|
+
path.resolve(strict=False)
|
|
1410
|
+
for path in (
|
|
1411
|
+
NEXO_HOME / "core" / "skills",
|
|
1412
|
+
NEXO_HOME / "skills-core",
|
|
1413
|
+
NEXO_HOME / "personal" / "skills",
|
|
1414
|
+
NEXO_HOME / "skills",
|
|
1415
|
+
NEXO_HOME / "community" / "skills",
|
|
1416
|
+
)
|
|
1417
|
+
)
|
|
1418
|
+
for row in rows:
|
|
1419
|
+
skill_id = str(row["id"] or "")
|
|
1420
|
+
if not skill_id or skill_id in discovered_ids:
|
|
1421
|
+
continue
|
|
1422
|
+
definition_path = Path(str(row["definition_path"] or "")).expanduser()
|
|
1423
|
+
try:
|
|
1424
|
+
definition_resolved = definition_path.resolve(strict=False)
|
|
1425
|
+
except OSError:
|
|
1426
|
+
definition_resolved = definition_path
|
|
1427
|
+
if not any(_is_relative_to(definition_resolved, root) for root in sync_roots):
|
|
1428
|
+
continue
|
|
1429
|
+
if definition_path.is_file():
|
|
1430
|
+
continue
|
|
1431
|
+
runtime_file = Path(str(row["file_path"] or "")).expanduser()
|
|
1432
|
+
conn.execute("DELETE FROM skill_usage WHERE skill_id = ?", (skill_id,))
|
|
1433
|
+
conn.execute("DELETE FROM skills WHERE id = ?", (skill_id,))
|
|
1434
|
+
conn.execute("DELETE FROM unified_search WHERE source = 'skill' AND source_id = ?", (skill_id,))
|
|
1435
|
+
if runtime_file.is_file() and _is_relative_to(runtime_file, RUNTIME_SKILLS_DIR):
|
|
1436
|
+
runtime_file.unlink(missing_ok=True)
|
|
1437
|
+
pruned.append(skill_id)
|
|
1438
|
+
if pruned:
|
|
1439
|
+
conn.commit()
|
|
1440
|
+
|
|
1441
|
+
return {"synced": len(synced), "ids": sorted(synced), "pruned": sorted(pruned), "issues": issues}
|
|
1383
1442
|
|
|
1384
1443
|
|
|
1385
1444
|
def import_skill_from_directory(path: str, source_kind: str = "personal") -> dict:
|
|
@@ -2122,14 +2122,10 @@ def check_codex_session_parity() -> DoctorCheck:
|
|
|
2122
2122
|
"Run `nexo update` or `nexo clients sync` so every Codex session inherits the managed bootstrap, not just a subset"
|
|
2123
2123
|
)
|
|
2124
2124
|
if missing_startup:
|
|
2125
|
-
status = "degraded"
|
|
2126
|
-
severity = "warn"
|
|
2127
2125
|
repair_plan.append(
|
|
2128
2126
|
"Use `nexo chat` or keep the global Codex bootstrap intact so every Codex session actually calls `nexo_startup`"
|
|
2129
2127
|
)
|
|
2130
2128
|
if missing_heartbeat:
|
|
2131
|
-
status = "degraded"
|
|
2132
|
-
severity = "warn"
|
|
2133
2129
|
repair_plan.append("Keep `nexo_heartbeat` on every user turn so restored/plain Codex sessions do not drift off-protocol")
|
|
2134
2130
|
if missing_bootstrap or missing_startup or missing_heartbeat:
|
|
2135
2131
|
evidence.append(
|
|
@@ -2247,16 +2243,15 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
|
|
|
2247
2243
|
and audit["delete_without_protocol"] == 0
|
|
2248
2244
|
and audit["delete_without_guard_ack"] == 0
|
|
2249
2245
|
)
|
|
2250
|
-
|
|
2246
|
+
tracked_mutation_without_open_debt = (
|
|
2251
2247
|
no_open_conditioned_debt
|
|
2252
2248
|
and audit["write_without_protocol"] > 0
|
|
2253
2249
|
and audit["write_without_guard_ack"] == 0
|
|
2254
|
-
and audit["delete_without_protocol"] == 0
|
|
2255
2250
|
and audit["delete_without_guard_ack"] == 0
|
|
2256
2251
|
)
|
|
2257
2252
|
|
|
2258
2253
|
if audit["write_without_protocol"] or audit["write_without_guard_ack"]:
|
|
2259
|
-
if
|
|
2254
|
+
if tracked_mutation_without_open_debt:
|
|
2260
2255
|
status = "healthy"
|
|
2261
2256
|
severity = "info"
|
|
2262
2257
|
else:
|
|
@@ -2280,8 +2275,8 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
|
|
|
2280
2275
|
summary=(
|
|
2281
2276
|
"Historical Codex conditioned-file drift has no open protocol debt"
|
|
2282
2277
|
if historical_read_only
|
|
2283
|
-
else "Tracked Codex conditioned-file drift has no open protocol debt"
|
|
2284
|
-
if
|
|
2278
|
+
else "Tracked Codex conditioned-file mutation drift has no open protocol debt"
|
|
2279
|
+
if tracked_mutation_without_open_debt
|
|
2285
2280
|
else "Recent Codex sessions respect conditioned-file discipline"
|
|
2286
2281
|
if status == "healthy"
|
|
2287
2282
|
else "Recent Codex sessions are bypassing conditioned-file discipline"
|
|
@@ -2524,6 +2519,7 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2524
2519
|
continue
|
|
2525
2520
|
live_guard_task_ids.add(str(row["task_id"] or ""))
|
|
2526
2521
|
live_guard_debt = 0
|
|
2522
|
+
open_task_debt = 0
|
|
2527
2523
|
if {"task_id", "session_id"}.issubset(protocol_debt_cols):
|
|
2528
2524
|
task_status_expr = "'' AS task_status"
|
|
2529
2525
|
if "status" in protocol_task_cols:
|
|
@@ -2549,12 +2545,13 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2549
2545
|
session_id = str(row["session_id"] or "")
|
|
2550
2546
|
task_id = str(row["task_id"] or "")
|
|
2551
2547
|
task_status = str(row["task_status"] or "")
|
|
2552
|
-
if
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2548
|
+
if task_status == "open":
|
|
2549
|
+
open_task_debt += 1
|
|
2550
|
+
if (
|
|
2551
|
+
debt_type == "unacknowledged_guard_blocking"
|
|
2552
|
+
and session_id in active_session_ids
|
|
2553
|
+
):
|
|
2554
|
+
live_guard_task_ids.add(task_id or f"debt:{session_id}:{row['created_at']}")
|
|
2558
2555
|
continue
|
|
2559
2556
|
debt_counter[(severity, debt_type)] = debt_counter.get((severity, debt_type), 0) + 1
|
|
2560
2557
|
live_guard_debt = len(live_guard_task_ids)
|
|
@@ -2662,6 +2659,8 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2662
2659
|
evidence.append("high-stakes decision-eval rollout not yet seeded in the live window")
|
|
2663
2660
|
for row in debt_rows[:5]:
|
|
2664
2661
|
evidence.append(f"open {row['severity']} debt — {row['debt_type']}: {row['total']}")
|
|
2662
|
+
if open_task_debt:
|
|
2663
|
+
evidence.append(f"in-progress task protocol debt pending: {open_task_debt}")
|
|
2665
2664
|
if live_guard_debt:
|
|
2666
2665
|
evidence.append(f"live in-progress guard acknowledgements pending: {live_guard_debt}")
|
|
2667
2666
|
|
|
@@ -3131,15 +3130,20 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3131
3130
|
"created_at",
|
|
3132
3131
|
}.issubset(columns)
|
|
3133
3132
|
if schema_has_status:
|
|
3133
|
+
interactive_expr = "0"
|
|
3134
|
+
if "session_type" in columns:
|
|
3135
|
+
interactive_expr = "COALESCE(session_type, '') LIKE 'interactive%'"
|
|
3134
3136
|
row = conn.execute(
|
|
3135
|
-
"""
|
|
3137
|
+
f"""
|
|
3136
3138
|
SELECT
|
|
3137
3139
|
COUNT(*) AS runs,
|
|
3138
3140
|
SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END) AS successful_runs,
|
|
3139
3141
|
SUM(CASE WHEN status != 'ok' THEN 1 ELSE 0 END) AS failed_runs,
|
|
3140
|
-
SUM(CASE WHEN status = 'ok' AND (
|
|
3141
|
-
SUM(CASE WHEN status = 'ok' AND
|
|
3142
|
-
SUM(CASE WHEN status = 'ok' AND
|
|
3142
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) THEN 1 ELSE 0 END) AS scored_successful_runs,
|
|
3143
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) AND (input_tokens + cached_input_tokens + output_tokens) > 0 THEN 1 ELSE 0 END) AS usage_runs,
|
|
3144
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) AND total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
3145
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) AND cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
3146
|
+
SUM(CASE WHEN status = 'ok' AND ({interactive_expr}) AND ((input_tokens + cached_input_tokens + output_tokens) = 0 OR total_cost_usd IS NULL) THEN 1 ELSE 0 END) AS interactive_unmetered_runs,
|
|
3143
3147
|
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
3144
3148
|
FROM automation_runs
|
|
3145
3149
|
WHERE created_at >= datetime('now', ?)
|
|
@@ -3153,9 +3157,11 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3153
3157
|
COUNT(*) AS runs,
|
|
3154
3158
|
COUNT(*) AS successful_runs,
|
|
3155
3159
|
0 AS failed_runs,
|
|
3160
|
+
COUNT(*) AS scored_successful_runs,
|
|
3156
3161
|
SUM(CASE WHEN (input_tokens + cached_input_tokens + output_tokens) > 0 THEN 1 ELSE 0 END) AS usage_runs,
|
|
3157
3162
|
SUM(CASE WHEN total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
3158
3163
|
SUM(CASE WHEN cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
3164
|
+
0 AS interactive_unmetered_runs,
|
|
3159
3165
|
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
3160
3166
|
FROM automation_runs
|
|
3161
3167
|
WHERE created_at >= datetime('now', ?)
|
|
@@ -3191,11 +3197,13 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3191
3197
|
|
|
3192
3198
|
successful_runs = int((row["successful_runs"] if row else 0) or 0)
|
|
3193
3199
|
failed_runs = int((row["failed_runs"] if row else 0) or 0)
|
|
3200
|
+
scored_successful_runs = int((row["scored_successful_runs"] if row and "scored_successful_runs" in row.keys() else successful_runs) or 0)
|
|
3194
3201
|
usage_runs = int((row["usage_runs"] if row else 0) or 0)
|
|
3195
3202
|
cost_runs = int((row["cost_runs"] if row else 0) or 0)
|
|
3196
3203
|
pricing_gaps = int((row["pricing_gaps"] if row else 0) or 0)
|
|
3197
|
-
|
|
3198
|
-
|
|
3204
|
+
interactive_unmetered_runs = int((row["interactive_unmetered_runs"] if row and "interactive_unmetered_runs" in row.keys() else 0) or 0)
|
|
3205
|
+
usage_denominator = scored_successful_runs or (successful_runs if not interactive_unmetered_runs else 0)
|
|
3206
|
+
cost_denominator = scored_successful_runs or (successful_runs if not interactive_unmetered_runs else 0)
|
|
3199
3207
|
missing_usage_runs = max(0, usage_denominator - usage_runs) if usage_denominator else 0
|
|
3200
3208
|
usage_coverage = round((usage_runs / usage_denominator) * 100, 1) if usage_denominator else 100.0
|
|
3201
3209
|
cost_coverage = round((cost_runs / cost_denominator) * 100, 1) if cost_denominator else 100.0
|
|
@@ -3204,12 +3212,15 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3204
3212
|
f"runs={total_runs}",
|
|
3205
3213
|
f"successful_runs={successful_runs}",
|
|
3206
3214
|
f"failed_runs={failed_runs}",
|
|
3215
|
+
f"scored_successful_runs={scored_successful_runs}",
|
|
3207
3216
|
f"usage_coverage={usage_coverage}%",
|
|
3208
3217
|
f"cost_coverage={cost_coverage}%",
|
|
3209
3218
|
f"pricing_gaps={pricing_gaps}",
|
|
3210
3219
|
]
|
|
3211
3220
|
if missing_usage_runs:
|
|
3212
3221
|
evidence.append(f"missing_usage_runs={missing_usage_runs}")
|
|
3222
|
+
if interactive_unmetered_runs:
|
|
3223
|
+
evidence.append(f"interactive_unmetered_runs_excluded={interactive_unmetered_runs}")
|
|
3213
3224
|
backends = str((row["backends"] if row else "") or "").strip()
|
|
3214
3225
|
if backends:
|
|
3215
3226
|
evidence.append(f"backends={backends}")
|
|
@@ -207,7 +207,7 @@ def run(
|
|
|
207
207
|
report["drained_ids"].append(int(row["id"]))
|
|
208
208
|
if not dry_run:
|
|
209
209
|
conn.execute(
|
|
210
|
-
"UPDATE protocol_debt SET resolved_at = ?, resolution = ? "
|
|
210
|
+
"UPDATE protocol_debt SET status = 'resolved', resolved_at = ?, resolution = ? "
|
|
211
211
|
"WHERE id = ? AND resolved_at IS NULL",
|
|
212
212
|
(
|
|
213
213
|
now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
@@ -652,10 +652,14 @@ for monitor in "${MONITORS[@]}"; do
|
|
|
652
652
|
fi
|
|
653
653
|
else
|
|
654
654
|
if [ -n "$last_exit" ] && [ "$last_exit" != "0" ]; then
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
655
|
+
if [ "$last_exit" = "143" ] && echo "$last_error" | grep -qi "SIGTERM"; then
|
|
656
|
+
details="${details}Last run ended by SIGTERM; treated as supervisor reload/interruption, not cron failure. "
|
|
657
|
+
else
|
|
658
|
+
latest_run_failed=true
|
|
659
|
+
status="FAIL"
|
|
660
|
+
details="${details}Last run exited ${last_exit}. "
|
|
661
|
+
[ -n "$last_error" ] && details="${details}Error: ${last_error}. "
|
|
662
|
+
fi
|
|
659
663
|
fi
|
|
660
664
|
if [ "$age" -gt $(( max_stale * 3 )) ]; then
|
|
661
665
|
if [ "$recovery_policy" = "catchup" ]; then
|