nexo-brain 5.5.1 → 5.5.2

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": "5.5.1",
3
+ "version": "5.5.2",
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,7 @@
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 `5.5.1` is the current packaged-runtime line: headless Protocol Enforcer all crons get enforcement rules automatically.
21
+ Version `5.5.2` is the current packaged-runtime line: auto-repair unloaded LaunchAgents on startup and ensure_schedules, plus headless model fallback cleanup so automation scripts defer to the configured runtime profile.
22
22
 
23
23
  Previously in `5.4.6`: runtime dependency management in `nexo update` + daily auto-update cron.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain \u2014 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",
@@ -1206,11 +1206,31 @@ def ensure_personal_schedules(*, dry_run: bool = False) -> dict:
1206
1206
  existing = schedules_by_path.get(script["path"], [])
1207
1207
  matching = next((item for item in existing if item.get("schedule_managed") and _schedule_matches(item, declared)), None)
1208
1208
  if matching:
1209
- report["already_present"].append({
1209
+ entry = {
1210
1210
  "name": script["name"],
1211
1211
  "cron_id": matching["cron_id"],
1212
1212
  "schedule_label": matching.get("schedule_label", ""),
1213
- })
1213
+ }
1214
+ plist_path = matching.get("plist_path", "")
1215
+ if plist_path and platform.system() == "Darwin" and Path(plist_path).exists():
1216
+ label = matching.get("launchd_label") or f"com.nexo.{matching['cron_id']}"
1217
+ svc = _launchctl_service_state(label)
1218
+ if not svc.get("loaded"):
1219
+ if not dry_run:
1220
+ result = subprocess.run(
1221
+ ["launchctl", "bootstrap", f"gui/{os.getuid()}", plist_path],
1222
+ capture_output=True, timeout=5,
1223
+ )
1224
+ if result.returncode == 0:
1225
+ entry["reloaded"] = True
1226
+ entry["reason"] = "plist on disk but not loaded in launchd"
1227
+ else:
1228
+ entry["reload_failed"] = True
1229
+ entry["reason"] = result.stderr.decode(errors="replace").strip() or "bootstrap failed"
1230
+ else:
1231
+ entry["reloaded"] = True
1232
+ entry["reason"] = "plist on disk but not loaded in launchd (dry_run)"
1233
+ report["already_present"].append(entry)
1214
1234
  continue
1215
1235
 
1216
1236
  repair_reasons = [item.get("schedule_state", item.get("schedule_origin", "unknown")) for item in existing]
@@ -198,7 +198,7 @@ Rules:
198
198
  try:
199
199
  result = run_automation_prompt(
200
200
  prompt,
201
- model=_USER_MODEL or "opus",
201
+ model=_USER_MODEL,
202
202
  timeout=300,
203
203
  output_format="text",
204
204
  append_system_prompt="Return exactly one valid JSON object.",
@@ -134,7 +134,7 @@ def analyze_session(
134
134
 
135
135
  result = run_automation_prompt(
136
136
  prompt,
137
- model=_USER_MODEL or "opus",
137
+ model=_USER_MODEL,
138
138
  timeout=CLAUDE_TIMEOUT,
139
139
  output_format="text",
140
140
  append_system_prompt=JSON_SYSTEM_PROMPT,
@@ -164,7 +164,7 @@ def analyze_session(
164
164
  )
165
165
  convert_result = run_automation_prompt(
166
166
  convert_prompt,
167
- model=_USER_MODEL or "sonnet",
167
+ model=_USER_MODEL,
168
168
  timeout=120,
169
169
  output_format="text",
170
170
  append_system_prompt=JSON_SYSTEM_PROMPT,
@@ -240,7 +240,7 @@ def main():
240
240
  try:
241
241
  result = run_automation_prompt(
242
242
  prompt,
243
- model=_USER_MODEL or "opus",
243
+ model=_USER_MODEL,
244
244
  timeout=CLAUDE_TIMEOUT,
245
245
  output_format="text",
246
246
  allowed_tools="Read,Grep,Bash",
@@ -279,7 +279,7 @@ Format:
279
279
  try:
280
280
  result = run_automation_prompt(
281
281
  prompt,
282
- model=_USER_MODEL or "opus",
282
+ model=_USER_MODEL,
283
283
  timeout=21600,
284
284
  output_format="text",
285
285
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -2049,7 +2049,7 @@ Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
2049
2049
  try:
2050
2050
  result = run_automation_prompt(
2051
2051
  prompt,
2052
- model=_USER_MODEL or "opus",
2052
+ model=_USER_MODEL,
2053
2053
  timeout=21600,
2054
2054
  output_format="text",
2055
2055
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -226,7 +226,7 @@ def call_claude_cli(prompt: str) -> str:
226
226
  """Call the configured automation backend for the managed evolution prompt."""
227
227
  result = run_automation_prompt(
228
228
  prompt,
229
- model=_USER_MODEL or "opus",
229
+ model=_USER_MODEL,
230
230
  timeout=CLI_TIMEOUT,
231
231
  output_format="text",
232
232
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -242,7 +242,7 @@ def call_public_claude_cli(prompt: str, *, cwd: Path) -> str:
242
242
  prompt,
243
243
  cwd=cwd,
244
244
  env={"NEXO_PUBLIC_CONTRIBUTION": "1"},
245
- model=_USER_MODEL or "opus",
245
+ model=_USER_MODEL,
246
246
  timeout=CLI_TIMEOUT,
247
247
  output_format="text",
248
248
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash",
@@ -915,7 +915,7 @@ Write the report. Be concise — max 40 lines."""
915
915
  try:
916
916
  result = run_automation_prompt(
917
917
  prompt,
918
- model=_USER_MODEL or "opus",
918
+ model=_USER_MODEL,
919
919
  timeout=21600,
920
920
  output_format="text",
921
921
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -158,7 +158,7 @@ Rules:
158
158
  try:
159
159
  result = run_automation_prompt(
160
160
  prompt,
161
- model=_USER_MODEL or "sonnet",
161
+ model=_USER_MODEL,
162
162
  timeout=60,
163
163
  output_format="text",
164
164
  append_system_prompt=JSON_ONLY_SYSTEM_PROMPT,
@@ -254,7 +254,7 @@ Execute without asking."""
254
254
  try:
255
255
  result = run_automation_prompt(
256
256
  prompt,
257
- model=_USER_MODEL or "opus",
257
+ model=_USER_MODEL,
258
258
  timeout=21600,
259
259
  output_format="text",
260
260
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -445,7 +445,7 @@ Execute without asking."""
445
445
  try:
446
446
  result = run_automation_prompt(
447
447
  prompt,
448
- model=_USER_MODEL or "opus",
448
+ model=_USER_MODEL,
449
449
  timeout=21600,
450
450
  output_format="text",
451
451
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -347,7 +347,7 @@ Execute without asking."""
347
347
  try:
348
348
  result = run_automation_prompt(
349
349
  prompt,
350
- model=_USER_MODEL or "opus",
350
+ model=_USER_MODEL,
351
351
  timeout=21600,
352
352
  output_format="text",
353
353
  allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
@@ -332,10 +332,9 @@ def handle_startup(
332
332
  la_warnings = _check_launchagents()
333
333
  if la_warnings:
334
334
  lines.append("")
335
- lines.append("⚠ LAUNCHAGENT MISMATCH (plist on disk ≠ loaded in memory):")
335
+ lines.append("⚠ LAUNCHAGENT HEALTH:")
336
336
  for w in la_warnings:
337
337
  lines.append(f" {w}")
338
- lines.append(" Fix: launchctl unload + load the affected plists, or restart.")
339
338
 
340
339
  return "\n".join(lines)
341
340
 
@@ -351,6 +350,12 @@ def _check_launchagents() -> list[str]:
351
350
  plist_dir = os.path.expanduser("~/Library/LaunchAgents")
352
351
  warnings = []
353
352
 
353
+ def _stderr_text(result, fallback: str) -> str:
354
+ stderr = getattr(result, "stderr", "")
355
+ if isinstance(stderr, bytes):
356
+ stderr = stderr.decode(errors="replace")
357
+ return stderr.strip() or fallback
358
+
354
359
  for plist_path in glob.glob(os.path.join(plist_dir, "com.nexo.*.plist")):
355
360
  label = os.path.basename(plist_path).replace(".plist", "")
356
361
  try:
@@ -363,7 +368,17 @@ def _check_launchagents() -> list[str]:
363
368
  capture_output=True, text=True, timeout=5
364
369
  )
365
370
  if result.returncode != 0:
366
- warnings.append(f"{label}: not loaded (plist exists on disk)")
371
+ repair = subprocess.run(
372
+ ["launchctl", "bootstrap", f"gui/{os.getuid()}", plist_path],
373
+ capture_output=True, text=True, timeout=5,
374
+ )
375
+ if repair.returncode == 0:
376
+ warnings.append(f"{label}: AUTO-REPAIRED (was not loaded, reloaded from disk)")
377
+ else:
378
+ warnings.append(
379
+ f"{label}: REPAIR FAILED — "
380
+ f"{_stderr_text(repair, 'not loaded (plist exists on disk)')}"
381
+ )
367
382
  continue
368
383
 
369
384
  # Parse loaded ProgramArguments from launchctl output
@@ -384,10 +399,31 @@ def _check_launchagents() -> list[str]:
384
399
  # Check if loaded path points to /tmp or nonexistent path
385
400
  stale = any("/tmp/" in a or not os.path.exists(a) for a in loaded_args if "/" in a)
386
401
  if stale:
387
- # Auto-repair: reload the plist
388
- subprocess.run(["launchctl", "unload", plist_path], capture_output=True, timeout=5)
389
- subprocess.run(["launchctl", "load", plist_path], capture_output=True, timeout=5)
390
- warnings.append(f"{label}: AUTO-REPAIRED (was pointing to stale/tmp path, reloaded from disk)")
402
+ bootout = subprocess.run(
403
+ ["launchctl", "bootout", f"gui/{os.getuid()}/{label}"],
404
+ capture_output=True,
405
+ text=True,
406
+ timeout=5,
407
+ )
408
+ if bootout.returncode != 0:
409
+ warnings.append(
410
+ f"{label}: REPAIR FAILED — "
411
+ f"{_stderr_text(bootout, 'could not unload stale launchd entry')}"
412
+ )
413
+ continue
414
+ repair = subprocess.run(
415
+ ["launchctl", "bootstrap", f"gui/{os.getuid()}", plist_path],
416
+ capture_output=True,
417
+ text=True,
418
+ timeout=5,
419
+ )
420
+ if repair.returncode == 0:
421
+ warnings.append(f"{label}: AUTO-REPAIRED (was pointing to stale/tmp path, reloaded from disk)")
422
+ else:
423
+ warnings.append(
424
+ f"{label}: REPAIR FAILED — "
425
+ f"{_stderr_text(repair, 'could not reload stale plist from disk')}"
426
+ )
391
427
  else:
392
428
  warnings.append(f"{label}: loaded args differ from disk plist")
393
429
  except Exception: