nexo-brain 5.0.0 → 5.0.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.0.0",
3
+ "version": "5.0.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
@@ -87,6 +87,17 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
87
87
  - when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
88
88
  - NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
89
89
 
90
+ Version `5.0.2` closes the small post-5.0.1 doctor drift:
91
+
92
+ - deep doctor now reads the live `learnings` schema correctly whether the install uses `status` or the older `archived` flag
93
+ - a real upgraded runtime was revalidated with `nexo update`, `nexo doctor --tier deep`, `nexo doctor --tier all`, and a fresh Claude Code startup smoke
94
+
95
+ Version `5.0.1` hardens the live 5.0 upgrade path:
96
+
97
+ - managed Claude Code hooks are now cleaned up when an older release left obsolete core-managed entries behind
98
+ - upgrades no longer preserve the stale `heartbeat-guard.sh` path that could create warning storms and fake "hung" symptoms after `nexo update`
99
+ - the corrected path has been revalidated on a real install with `nexo clients sync`, Codex/Claude Code headless runtime access, email-monitor recovery, and a full `nexo update`
100
+
90
101
  Version `5.0.0` closes the loop between memory, decisions, outcomes, and reusable behavior:
91
102
 
92
103
  - goal profiles are now explicit and auditable instead of living as hidden heuristics
@@ -702,7 +713,7 @@ The installer handles everything and syncs the same `nexo` MCP brain into Claude
702
713
  - Node.js project detected
703
714
  Configuring MCP server...
704
715
  Setting up nervous system...
705
- 13 core recovery-aware jobs configured.
716
+ 15 core recovery-aware jobs configured.
706
717
  Dashboard configured at localhost:6174.
707
718
  Caffeinate enabled.
708
719
  Generating operator instructions...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
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",
@@ -386,6 +386,32 @@ CORE_HOOK_SPECS = [
386
386
  },
387
387
  ]
388
388
 
389
+ LEGACY_CORE_HOOK_IDENTITIES_BY_EVENT = {
390
+ "PostToolUse": {
391
+ "heartbeat-guard.sh",
392
+ },
393
+ }
394
+
395
+
396
+ def _current_core_hook_identities_by_event() -> dict[str, set[str]]:
397
+ identities: dict[str, set[str]] = {}
398
+ for spec in CORE_HOOK_SPECS:
399
+ identities.setdefault(spec["event"], set()).add(spec["identity"])
400
+ return identities
401
+
402
+
403
+ CURRENT_CORE_HOOK_IDENTITIES_BY_EVENT = _current_core_hook_identities_by_event()
404
+
405
+
406
+ def _managed_core_hook_identities_by_event() -> dict[str, set[str]]:
407
+ managed = {event: set(identities) for event, identities in CURRENT_CORE_HOOK_IDENTITIES_BY_EVENT.items()}
408
+ for event, identities in LEGACY_CORE_HOOK_IDENTITIES_BY_EVENT.items():
409
+ managed.setdefault(event, set()).update(identities)
410
+ return managed
411
+
412
+
413
+ MANAGED_CORE_HOOK_IDENTITIES_BY_EVENT = _managed_core_hook_identities_by_event()
414
+
389
415
 
390
416
  def _resolve_hook_source_dir(runtime_root: Path) -> Path:
391
417
  direct = runtime_root / "hooks"
@@ -450,6 +476,25 @@ def _merge_core_hooks(existing_hooks, *, runtime_root: Path, nexo_home: Path) ->
450
476
  hooks_dir = _resolve_hook_source_dir(runtime_root)
451
477
  managed_count = 0
452
478
 
479
+ for event, managed_identities in MANAGED_CORE_HOOK_IDENTITIES_BY_EVENT.items():
480
+ sections = _normalize_hook_sections(hooks_payload.get(event))
481
+ desired_identities = CURRENT_CORE_HOOK_IDENTITIES_BY_EVENT.get(event, set())
482
+ cleaned_sections: list[dict] = []
483
+ for section in sections:
484
+ cleaned_hooks = []
485
+ for hook in section["hooks"]:
486
+ identity = _hook_identity(hook.get("command", ""))
487
+ if identity in managed_identities and identity not in desired_identities:
488
+ continue
489
+ cleaned_hooks.append(hook)
490
+ cleaned_sections.append(
491
+ {
492
+ "matcher": section.get("matcher", "*") or "*",
493
+ "hooks": cleaned_hooks,
494
+ }
495
+ )
496
+ hooks_payload[event] = cleaned_sections
497
+
453
498
  for spec in CORE_HOOK_SPECS:
454
499
  event = spec["event"]
455
500
  sections = _normalize_hook_sections(hooks_payload.get(event))
@@ -264,7 +264,20 @@ def check_learning_count() -> DoctorCheck:
264
264
  severity="info",
265
265
  summary="No learnings table yet",
266
266
  )
267
- count = conn.execute("SELECT COUNT(*) FROM learnings WHERE archived=0").fetchone()[0]
267
+ columns = {
268
+ row[1]
269
+ for row in conn.execute("PRAGMA table_info(learnings)").fetchall()
270
+ }
271
+ if "status" in columns:
272
+ count = conn.execute(
273
+ "SELECT COUNT(*) FROM learnings WHERE COALESCE(status, 'active') != 'archived'"
274
+ ).fetchone()[0]
275
+ elif "archived" in columns:
276
+ count = conn.execute(
277
+ "SELECT COUNT(*) FROM learnings WHERE archived=0"
278
+ ).fetchone()[0]
279
+ else:
280
+ count = conn.execute("SELECT COUNT(*) FROM learnings").fetchone()[0]
268
281
  finally:
269
282
  conn.close()
270
283
  return DoctorCheck(
@@ -272,15 +285,16 @@ def check_learning_count() -> DoctorCheck:
272
285
  tier="deep",
273
286
  status="healthy",
274
287
  severity="info",
275
- summary=f"{count} active learnings in memory",
288
+ summary=f"{count} non-archived learnings in memory",
276
289
  )
277
290
  except Exception as e:
278
291
  return DoctorCheck(
279
292
  id="deep.learning_count",
280
293
  tier="deep",
281
- status="healthy",
282
- severity="info",
283
- summary=f"Learning check skipped: {e}",
294
+ status="degraded",
295
+ severity="warn",
296
+ summary=f"Learning check unreadable: {e}",
297
+ evidence=[str(e)],
284
298
  )
285
299
 
286
300