nexo-brain 7.9.18 → 7.9.20
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 +5 -1
- package/package.json +1 -1
- package/src/auto_update.py +1 -1
- package/src/crons/sync.py +9 -2
- package/src/db/_skills.py +61 -2
- package/src/doctor/providers/runtime.py +37 -23
- package/src/runtime_power.py +9 -2
- package/src/scripts/deep-sleep/phase_protocol_debt_drain.py +1 -1
- package/src/scripts/nexo-immune.py +2 -1
- package/src/scripts/nexo-watchdog.sh +8 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.20",
|
|
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,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.9.
|
|
21
|
+
Version `7.9.20` is the current packaged-runtime line. Patch release over `7.9.19`: packaged update/doctor repair now finds `runtime/crons/sync.py`, LaunchAgent PATH includes the managed Claude runtime installed under `~/.nexo/runtime/bootstrap/npm-global/bin`, root runtime backfill includes `claude_cli.py`, and Immune no longer treats the legacy optional `~/.claude-mem/claude-mem.db` as a required database.
|
|
22
|
+
|
|
23
|
+
Previously in `7.9.19`: 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.
|
|
24
|
+
|
|
25
|
+
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
26
|
|
|
23
27
|
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
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.20",
|
|
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
|
@@ -3816,7 +3816,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
3816
3816
|
|
|
3817
3817
|
# Backfill runtime CLI modules for existing installs
|
|
3818
3818
|
try:
|
|
3819
|
-
for fname in ("cli.py", "script_registry.py", "skills_runtime.py", "cron_recovery.py", "client_preferences.py", "agent_runner.py", "bootstrap_docs.py"):
|
|
3819
|
+
for fname in ("cli.py", "script_registry.py", "skills_runtime.py", "cron_recovery.py", "client_preferences.py", "claude_cli.py", "agent_runner.py", "bootstrap_docs.py"):
|
|
3820
3820
|
src_file = SRC_DIR / fname
|
|
3821
3821
|
dest_file = NEXO_HOME / fname
|
|
3822
3822
|
if src_file.is_file() and (not dest_file.exists() or src_file.stat().st_mtime > dest_file.stat().st_mtime):
|
package/src/crons/sync.py
CHANGED
|
@@ -39,8 +39,15 @@ except ImportError:
|
|
|
39
39
|
def resolve_launchagent_path() -> str:
|
|
40
40
|
"""Fallback when runtime_power is not importable."""
|
|
41
41
|
home = Path.home()
|
|
42
|
-
parts = [
|
|
43
|
-
|
|
42
|
+
parts = [
|
|
43
|
+
str(home / ".nexo/runtime/bootstrap/npm-global/bin"),
|
|
44
|
+
"/opt/homebrew/bin",
|
|
45
|
+
"/usr/local/bin",
|
|
46
|
+
"/usr/bin",
|
|
47
|
+
"/bin",
|
|
48
|
+
str(home / ".local/bin"),
|
|
49
|
+
str(home / ".nexo/bin"),
|
|
50
|
+
]
|
|
44
51
|
nvm_dir = home / ".nvm/versions/node"
|
|
45
52
|
if nvm_dir.is_dir():
|
|
46
53
|
versions = sorted(nvm_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)
|
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:
|
|
@@ -1131,17 +1131,20 @@ def _repair_special_launchagent_plists(items: list[tuple[str, Path]]) -> tuple[b
|
|
|
1131
1131
|
|
|
1132
1132
|
|
|
1133
1133
|
def _sync_launchagents_from_manifest() -> tuple[bool, list[str]]:
|
|
1134
|
-
sync_path =
|
|
1134
|
+
sync_path = paths.crons_dir() / "sync.py"
|
|
1135
|
+
if not sync_path.is_file():
|
|
1136
|
+
sync_path = NEXO_CODE / "crons" / "sync.py"
|
|
1135
1137
|
if not sync_path.is_file():
|
|
1136
1138
|
return False, [f"cron sync script not found at {sync_path}"]
|
|
1137
1139
|
|
|
1138
1140
|
try:
|
|
1141
|
+
runtime_code = paths.core_dir()
|
|
1139
1142
|
result = subprocess.run(
|
|
1140
1143
|
[sys.executable, str(sync_path)],
|
|
1141
1144
|
capture_output=True,
|
|
1142
1145
|
text=True,
|
|
1143
1146
|
timeout=30,
|
|
1144
|
-
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(
|
|
1147
|
+
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(runtime_code)},
|
|
1145
1148
|
)
|
|
1146
1149
|
except Exception as e:
|
|
1147
1150
|
return False, [f"cron sync failed: {e}"]
|
|
@@ -2122,14 +2125,10 @@ def check_codex_session_parity() -> DoctorCheck:
|
|
|
2122
2125
|
"Run `nexo update` or `nexo clients sync` so every Codex session inherits the managed bootstrap, not just a subset"
|
|
2123
2126
|
)
|
|
2124
2127
|
if missing_startup:
|
|
2125
|
-
status = "degraded"
|
|
2126
|
-
severity = "warn"
|
|
2127
2128
|
repair_plan.append(
|
|
2128
2129
|
"Use `nexo chat` or keep the global Codex bootstrap intact so every Codex session actually calls `nexo_startup`"
|
|
2129
2130
|
)
|
|
2130
2131
|
if missing_heartbeat:
|
|
2131
|
-
status = "degraded"
|
|
2132
|
-
severity = "warn"
|
|
2133
2132
|
repair_plan.append("Keep `nexo_heartbeat` on every user turn so restored/plain Codex sessions do not drift off-protocol")
|
|
2134
2133
|
if missing_bootstrap or missing_startup or missing_heartbeat:
|
|
2135
2134
|
evidence.append(
|
|
@@ -2247,16 +2246,15 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
|
|
|
2247
2246
|
and audit["delete_without_protocol"] == 0
|
|
2248
2247
|
and audit["delete_without_guard_ack"] == 0
|
|
2249
2248
|
)
|
|
2250
|
-
|
|
2249
|
+
tracked_mutation_without_open_debt = (
|
|
2251
2250
|
no_open_conditioned_debt
|
|
2252
2251
|
and audit["write_without_protocol"] > 0
|
|
2253
2252
|
and audit["write_without_guard_ack"] == 0
|
|
2254
|
-
and audit["delete_without_protocol"] == 0
|
|
2255
2253
|
and audit["delete_without_guard_ack"] == 0
|
|
2256
2254
|
)
|
|
2257
2255
|
|
|
2258
2256
|
if audit["write_without_protocol"] or audit["write_without_guard_ack"]:
|
|
2259
|
-
if
|
|
2257
|
+
if tracked_mutation_without_open_debt:
|
|
2260
2258
|
status = "healthy"
|
|
2261
2259
|
severity = "info"
|
|
2262
2260
|
else:
|
|
@@ -2280,8 +2278,8 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
|
|
|
2280
2278
|
summary=(
|
|
2281
2279
|
"Historical Codex conditioned-file drift has no open protocol debt"
|
|
2282
2280
|
if historical_read_only
|
|
2283
|
-
else "Tracked Codex conditioned-file drift has no open protocol debt"
|
|
2284
|
-
if
|
|
2281
|
+
else "Tracked Codex conditioned-file mutation drift has no open protocol debt"
|
|
2282
|
+
if tracked_mutation_without_open_debt
|
|
2285
2283
|
else "Recent Codex sessions respect conditioned-file discipline"
|
|
2286
2284
|
if status == "healthy"
|
|
2287
2285
|
else "Recent Codex sessions are bypassing conditioned-file discipline"
|
|
@@ -2524,6 +2522,7 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2524
2522
|
continue
|
|
2525
2523
|
live_guard_task_ids.add(str(row["task_id"] or ""))
|
|
2526
2524
|
live_guard_debt = 0
|
|
2525
|
+
open_task_debt = 0
|
|
2527
2526
|
if {"task_id", "session_id"}.issubset(protocol_debt_cols):
|
|
2528
2527
|
task_status_expr = "'' AS task_status"
|
|
2529
2528
|
if "status" in protocol_task_cols:
|
|
@@ -2549,12 +2548,13 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2549
2548
|
session_id = str(row["session_id"] or "")
|
|
2550
2549
|
task_id = str(row["task_id"] or "")
|
|
2551
2550
|
task_status = str(row["task_status"] or "")
|
|
2552
|
-
if
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2551
|
+
if task_status == "open":
|
|
2552
|
+
open_task_debt += 1
|
|
2553
|
+
if (
|
|
2554
|
+
debt_type == "unacknowledged_guard_blocking"
|
|
2555
|
+
and session_id in active_session_ids
|
|
2556
|
+
):
|
|
2557
|
+
live_guard_task_ids.add(task_id or f"debt:{session_id}:{row['created_at']}")
|
|
2558
2558
|
continue
|
|
2559
2559
|
debt_counter[(severity, debt_type)] = debt_counter.get((severity, debt_type), 0) + 1
|
|
2560
2560
|
live_guard_debt = len(live_guard_task_ids)
|
|
@@ -2662,6 +2662,8 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2662
2662
|
evidence.append("high-stakes decision-eval rollout not yet seeded in the live window")
|
|
2663
2663
|
for row in debt_rows[:5]:
|
|
2664
2664
|
evidence.append(f"open {row['severity']} debt — {row['debt_type']}: {row['total']}")
|
|
2665
|
+
if open_task_debt:
|
|
2666
|
+
evidence.append(f"in-progress task protocol debt pending: {open_task_debt}")
|
|
2665
2667
|
if live_guard_debt:
|
|
2666
2668
|
evidence.append(f"live in-progress guard acknowledgements pending: {live_guard_debt}")
|
|
2667
2669
|
|
|
@@ -3131,15 +3133,20 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3131
3133
|
"created_at",
|
|
3132
3134
|
}.issubset(columns)
|
|
3133
3135
|
if schema_has_status:
|
|
3136
|
+
interactive_expr = "0"
|
|
3137
|
+
if "session_type" in columns:
|
|
3138
|
+
interactive_expr = "COALESCE(session_type, '') LIKE 'interactive%'"
|
|
3134
3139
|
row = conn.execute(
|
|
3135
|
-
"""
|
|
3140
|
+
f"""
|
|
3136
3141
|
SELECT
|
|
3137
3142
|
COUNT(*) AS runs,
|
|
3138
3143
|
SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END) AS successful_runs,
|
|
3139
3144
|
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
|
|
3145
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) THEN 1 ELSE 0 END) AS scored_successful_runs,
|
|
3146
|
+
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,
|
|
3147
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) AND total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
3148
|
+
SUM(CASE WHEN status = 'ok' AND NOT ({interactive_expr}) AND cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
3149
|
+
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
3150
|
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
3144
3151
|
FROM automation_runs
|
|
3145
3152
|
WHERE created_at >= datetime('now', ?)
|
|
@@ -3153,9 +3160,11 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3153
3160
|
COUNT(*) AS runs,
|
|
3154
3161
|
COUNT(*) AS successful_runs,
|
|
3155
3162
|
0 AS failed_runs,
|
|
3163
|
+
COUNT(*) AS scored_successful_runs,
|
|
3156
3164
|
SUM(CASE WHEN (input_tokens + cached_input_tokens + output_tokens) > 0 THEN 1 ELSE 0 END) AS usage_runs,
|
|
3157
3165
|
SUM(CASE WHEN total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
3158
3166
|
SUM(CASE WHEN cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
3167
|
+
0 AS interactive_unmetered_runs,
|
|
3159
3168
|
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
3160
3169
|
FROM automation_runs
|
|
3161
3170
|
WHERE created_at >= datetime('now', ?)
|
|
@@ -3191,11 +3200,13 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3191
3200
|
|
|
3192
3201
|
successful_runs = int((row["successful_runs"] if row else 0) or 0)
|
|
3193
3202
|
failed_runs = int((row["failed_runs"] if row else 0) or 0)
|
|
3203
|
+
scored_successful_runs = int((row["scored_successful_runs"] if row and "scored_successful_runs" in row.keys() else successful_runs) or 0)
|
|
3194
3204
|
usage_runs = int((row["usage_runs"] if row else 0) or 0)
|
|
3195
3205
|
cost_runs = int((row["cost_runs"] if row else 0) or 0)
|
|
3196
3206
|
pricing_gaps = int((row["pricing_gaps"] if row else 0) or 0)
|
|
3197
|
-
|
|
3198
|
-
|
|
3207
|
+
interactive_unmetered_runs = int((row["interactive_unmetered_runs"] if row and "interactive_unmetered_runs" in row.keys() else 0) or 0)
|
|
3208
|
+
usage_denominator = scored_successful_runs or (successful_runs if not interactive_unmetered_runs else 0)
|
|
3209
|
+
cost_denominator = scored_successful_runs or (successful_runs if not interactive_unmetered_runs else 0)
|
|
3199
3210
|
missing_usage_runs = max(0, usage_denominator - usage_runs) if usage_denominator else 0
|
|
3200
3211
|
usage_coverage = round((usage_runs / usage_denominator) * 100, 1) if usage_denominator else 100.0
|
|
3201
3212
|
cost_coverage = round((cost_runs / cost_denominator) * 100, 1) if cost_denominator else 100.0
|
|
@@ -3204,12 +3215,15 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3204
3215
|
f"runs={total_runs}",
|
|
3205
3216
|
f"successful_runs={successful_runs}",
|
|
3206
3217
|
f"failed_runs={failed_runs}",
|
|
3218
|
+
f"scored_successful_runs={scored_successful_runs}",
|
|
3207
3219
|
f"usage_coverage={usage_coverage}%",
|
|
3208
3220
|
f"cost_coverage={cost_coverage}%",
|
|
3209
3221
|
f"pricing_gaps={pricing_gaps}",
|
|
3210
3222
|
]
|
|
3211
3223
|
if missing_usage_runs:
|
|
3212
3224
|
evidence.append(f"missing_usage_runs={missing_usage_runs}")
|
|
3225
|
+
if interactive_unmetered_runs:
|
|
3226
|
+
evidence.append(f"interactive_unmetered_runs_excluded={interactive_unmetered_runs}")
|
|
3213
3227
|
backends = str((row["backends"] if row else "") or "").strip()
|
|
3214
3228
|
if backends:
|
|
3215
3229
|
evidence.append(f"backends={backends}")
|
package/src/runtime_power.py
CHANGED
|
@@ -80,8 +80,15 @@ DEFAULT_CODEX_REASONING_EFFORT = _CODEX_DEFAULTS["reasoning_effort"]
|
|
|
80
80
|
def resolve_launchagent_path() -> str:
|
|
81
81
|
"""Build a PATH string for LaunchAgent plists that includes nvm node if present."""
|
|
82
82
|
home = Path.home()
|
|
83
|
-
parts = [
|
|
84
|
-
|
|
83
|
+
parts = [
|
|
84
|
+
str(home / ".nexo/runtime/bootstrap/npm-global/bin"),
|
|
85
|
+
"/opt/homebrew/bin",
|
|
86
|
+
"/usr/local/bin",
|
|
87
|
+
"/usr/bin",
|
|
88
|
+
"/bin",
|
|
89
|
+
str(home / ".local/bin"),
|
|
90
|
+
str(home / ".nexo/bin"),
|
|
91
|
+
]
|
|
85
92
|
# Detect nvm node
|
|
86
93
|
nvm_dir = home / ".nvm/versions/node"
|
|
87
94
|
if nvm_dir.is_dir():
|
|
@@ -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"),
|
|
@@ -367,8 +367,9 @@ def check_databases():
|
|
|
367
367
|
dbs = [
|
|
368
368
|
("nexo.db", paths.db_path()),
|
|
369
369
|
("cognitive.db", paths.data_dir() / "cognitive.db"),
|
|
370
|
-
("claude-mem.db", CLAUDE_MEM_DB),
|
|
371
370
|
]
|
|
371
|
+
if CLAUDE_MEM_DB.exists():
|
|
372
|
+
dbs.append(("claude-mem.db", CLAUDE_MEM_DB))
|
|
372
373
|
|
|
373
374
|
for name, path in dbs:
|
|
374
375
|
result = {"name": name, "status": "OK", "detail": ""}
|
|
@@ -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
|