nexo-brain 5.1.0 → 5.1.1

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.1.0",
3
+ "version": "5.1.1",
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": "5.1.0",
3
+ "version": "5.1.1",
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",
@@ -2710,6 +2710,137 @@ def check_release_artifact_sync() -> DoctorCheck:
2710
2710
  )
2711
2711
 
2712
2712
 
2713
+ def check_release_trace_hygiene() -> DoctorCheck:
2714
+ db_path = NEXO_HOME / "data" / "nexo.db"
2715
+ if not db_path.is_file():
2716
+ return DoctorCheck(
2717
+ id="runtime.release_trace_hygiene",
2718
+ tier="runtime",
2719
+ status="healthy",
2720
+ severity="info",
2721
+ summary="Release trace hygiene unavailable (no DB)",
2722
+ evidence=[],
2723
+ repair_plan=[],
2724
+ escalation_prompt="",
2725
+ )
2726
+
2727
+ try:
2728
+ conn = sqlite3.connect(str(db_path), timeout=2)
2729
+ conn.row_factory = sqlite3.Row
2730
+ try:
2731
+ tables = {
2732
+ row[0]
2733
+ for row in conn.execute(
2734
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('workflow_goals', 'workflow_runs')"
2735
+ ).fetchall()
2736
+ }
2737
+ if "workflow_goals" not in tables or "workflow_runs" not in tables:
2738
+ return DoctorCheck(
2739
+ id="runtime.release_trace_hygiene",
2740
+ tier="runtime",
2741
+ status="healthy",
2742
+ severity="info",
2743
+ summary="Release trace hygiene unavailable (workflow tables absent)",
2744
+ evidence=[],
2745
+ repair_plan=[],
2746
+ escalation_prompt="",
2747
+ )
2748
+
2749
+ stale_run_samples: list[str] = []
2750
+ stale_goal_samples: list[str] = []
2751
+ now = dt.datetime.now(dt.timezone.utc)
2752
+ stale_after_hours = 6
2753
+
2754
+ run_rows = conn.execute(
2755
+ """SELECT run_id, goal, updated_at
2756
+ FROM workflow_runs
2757
+ WHERE workflow_kind = 'audit-phase'
2758
+ AND status NOT IN ('completed', 'failed', 'cancelled')
2759
+ ORDER BY updated_at DESC"""
2760
+ ).fetchall()
2761
+ for row in run_rows:
2762
+ updated_at = _parse_timestamp(row["updated_at"] or "")
2763
+ if updated_at is None:
2764
+ stale_run_samples.append(f"{row['run_id']}: unreadable updated_at")
2765
+ continue
2766
+ if updated_at.tzinfo is None:
2767
+ updated_at = updated_at.replace(tzinfo=dt.timezone.utc)
2768
+ age_hours = (now - updated_at).total_seconds() / 3600
2769
+ if age_hours >= stale_after_hours:
2770
+ stale_run_samples.append(
2771
+ f"{row['run_id']}: {age_hours:.1f}h stale ({str(row['goal'] or '')[:72]})"
2772
+ )
2773
+
2774
+ goal_rows = conn.execute(
2775
+ """SELECT g.goal_id, g.title, g.updated_at,
2776
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id), 0) AS run_count,
2777
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id
2778
+ AND r.status NOT IN ('completed', 'failed', 'cancelled')), 0) AS open_run_count
2779
+ FROM workflow_goals g
2780
+ WHERE g.status = 'active'
2781
+ AND (g.goal_id LIKE 'WG-AUDIT-%' OR g.title LIKE 'NEXO-AUDIT-%')
2782
+ ORDER BY g.updated_at DESC"""
2783
+ ).fetchall()
2784
+ for row in goal_rows:
2785
+ if int(row["open_run_count"] or 0) > 0:
2786
+ continue
2787
+ updated_at = _parse_timestamp(row["updated_at"] or "")
2788
+ if updated_at is None:
2789
+ stale_goal_samples.append(f"{row['goal_id']}: unreadable updated_at")
2790
+ continue
2791
+ if updated_at.tzinfo is None:
2792
+ updated_at = updated_at.replace(tzinfo=dt.timezone.utc)
2793
+ age_hours = (now - updated_at).total_seconds() / 3600
2794
+ if age_hours >= stale_after_hours:
2795
+ stale_goal_samples.append(
2796
+ f"{row['goal_id']}: {age_hours:.1f}h stale ({str(row['title'] or '')[:72]})"
2797
+ )
2798
+ finally:
2799
+ conn.close()
2800
+ except Exception as exc:
2801
+ return DoctorCheck(
2802
+ id="runtime.release_trace_hygiene",
2803
+ tier="runtime",
2804
+ status="degraded",
2805
+ severity="warn",
2806
+ summary="Release trace hygiene check failed",
2807
+ evidence=[str(exc)],
2808
+ repair_plan=["Inspect workflow_goals/workflow_runs state manually"],
2809
+ escalation_prompt="Release traces could not be audited, so stale audit artifacts may be hiding in the runtime.",
2810
+ )
2811
+
2812
+ evidence = [
2813
+ f"stale audit workflows: {len(stale_run_samples)}",
2814
+ f"stale audit goals: {len(stale_goal_samples)}",
2815
+ ]
2816
+ evidence.extend(stale_run_samples[:3])
2817
+ evidence.extend(stale_goal_samples[:3])
2818
+ if stale_run_samples or stale_goal_samples:
2819
+ return DoctorCheck(
2820
+ id="runtime.release_trace_hygiene",
2821
+ tier="runtime",
2822
+ status="degraded",
2823
+ severity="warn",
2824
+ summary="Release trace hygiene needs cleanup",
2825
+ evidence=evidence,
2826
+ repair_plan=[
2827
+ "Close or complete stale audit-phase workflows and active audit goals",
2828
+ "Keep workflow/goal state aligned with the real shipped state after releases",
2829
+ ],
2830
+ escalation_prompt="Audit/release traces drifted away from reality, which makes shipping state look ambiguous.",
2831
+ )
2832
+ return DoctorCheck(
2833
+ id="runtime.release_trace_hygiene",
2834
+ tier="runtime",
2835
+ status="healthy",
2836
+ severity="info",
2837
+ summary="Release trace hygiene OK",
2838
+ evidence=evidence,
2839
+ repair_plan=[],
2840
+ escalation_prompt="",
2841
+ )
2842
+
2843
+
2713
2844
  def check_state_watchers() -> DoctorCheck:
2714
2845
  db_path = NEXO_HOME / "data" / "nexo.db"
2715
2846
  summary_path = NEXO_HOME / "operations" / "state-watchers-status.json"
@@ -2988,6 +3119,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
2988
3119
  safe_check(check_automation_telemetry),
2989
3120
  safe_check(check_state_watchers),
2990
3121
  safe_check(check_release_artifact_sync),
3122
+ safe_check(check_release_trace_hygiene),
2991
3123
  safe_check(check_launchagent_inventory),
2992
3124
  safe_check(check_launchagent_integrity, fix=fix),
2993
3125
  safe_check(check_personal_script_registry, fix=fix),
@@ -229,8 +229,20 @@ def handle_session_diary_write(decisions: str, summary: str,
229
229
  orphan_changes = conn.execute(
230
230
  "SELECT COUNT(*) FROM change_log WHERE (commit_ref IS NULL OR commit_ref = '')"
231
231
  ).fetchone()[0]
232
+ recent_orphan_changes = conn.execute(
233
+ """SELECT COUNT(*) FROM change_log
234
+ WHERE (commit_ref IS NULL OR commit_ref = '')
235
+ AND created_at >= datetime('now', '-7 days')"""
236
+ ).fetchone()[0]
232
237
  if orphan_changes > 0:
233
- warnings.append(f"{orphan_changes} changes sin commit_ref")
238
+ if recent_orphan_changes > 0 and recent_orphan_changes != orphan_changes:
239
+ warnings.append(
240
+ f"{recent_orphan_changes} changes recientes sin commit_ref ({orphan_changes} históricas total)"
241
+ )
242
+ elif recent_orphan_changes > 0:
243
+ warnings.append(f"{recent_orphan_changes} changes recientes sin commit_ref")
244
+ else:
245
+ warnings.append(f"{orphan_changes} changes históricas sin commit_ref")
234
246
  orphan_decisions = conn.execute(
235
247
  "SELECT COUNT(*) FROM decisions WHERE (outcome IS NULL OR outcome = '') AND created_at < datetime('now', '-7 days')"
236
248
  ).fetchone()[0]
@@ -78,6 +78,10 @@ CLAUDE_CLI = _resolve_claude_cli()
78
78
 
79
79
  findings = []
80
80
 
81
+ AUDIT_GOAL_NEXT_ACTION = "Convert the recurring theme into an explicit workflow or close it as intentional noise."
82
+ AUDIT_GOAL_OWNER = "system:self-audit"
83
+ AUDIT_GOAL_STALE_HOURS = 36
84
+
81
85
 
82
86
  def log(msg):
83
87
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -492,7 +496,7 @@ def _upsert_workflow_goal_inline(conn: sqlite3.Connection, *, area: str, sample_
492
496
  f"Recurring {area} theme detected by daily self-audit. "
493
497
  f"The theme '{sample_goal}' appeared {count} times without a durable goal, learning, or resolved workflow."
494
498
  )
495
- next_action = "Convert the recurring theme into an explicit workflow or close it as intentional noise."
499
+ next_action = AUDIT_GOAL_NEXT_ACTION
496
500
  success_signal = "The theme stops resurfacing in unresolved protocol tasks."
497
501
  now_iso = datetime.now().isoformat(timespec="seconds")
498
502
  if existing:
@@ -504,7 +508,7 @@ def _upsert_workflow_goal_inline(conn: sqlite3.Connection, *, area: str, sample_
504
508
  if "priority" in columns:
505
509
  updates["priority"] = "high"
506
510
  if "owner" in columns:
507
- updates["owner"] = "system:self-audit"
511
+ updates["owner"] = AUDIT_GOAL_OWNER
508
512
  if "next_action" in columns:
509
513
  updates["next_action"] = next_action
510
514
  if "success_signal" in columns:
@@ -534,7 +538,7 @@ def _upsert_workflow_goal_inline(conn: sqlite3.Connection, *, area: str, sample_
534
538
  if "priority" in columns:
535
539
  values["priority"] = "high"
536
540
  if "owner" in columns:
537
- values["owner"] = "system:self-audit"
541
+ values["owner"] = AUDIT_GOAL_OWNER
538
542
  if "next_action" in columns:
539
543
  values["next_action"] = next_action
540
544
  if "success_signal" in columns:
@@ -553,6 +557,75 @@ def _upsert_workflow_goal_inline(conn: sqlite3.Connection, *, area: str, sample_
553
557
  return {"ok": True, "action": "created", "goal_id": goal_id}
554
558
 
555
559
 
560
+ def _retire_stale_audit_goals_inline(
561
+ conn: sqlite3.Connection, *, max_age_hours: int = AUDIT_GOAL_STALE_HOURS
562
+ ) -> dict:
563
+ if not _table_exists(conn, "workflow_goals"):
564
+ return {"ok": False, "reason": "workflow_goals_missing"}
565
+
566
+ has_runs = _table_exists(conn, "workflow_runs")
567
+ if has_runs:
568
+ rows = conn.execute(
569
+ """SELECT g.goal_id, g.title, g.status, g.owner, g.next_action, g.opened_at, g.updated_at,
570
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id), 0) AS run_count,
571
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id
572
+ AND r.status NOT IN ('completed', 'failed', 'cancelled')), 0) AS open_run_count
573
+ FROM workflow_goals g
574
+ WHERE g.status = 'active'
575
+ AND g.goal_id LIKE 'WG-AUDIT-%'
576
+ ORDER BY g.updated_at DESC, g.opened_at DESC"""
577
+ ).fetchall()
578
+ else:
579
+ rows = conn.execute(
580
+ """SELECT g.goal_id, g.title, g.status, g.owner, g.next_action, g.opened_at, g.updated_at,
581
+ 0 AS run_count,
582
+ 0 AS open_run_count
583
+ FROM workflow_goals g
584
+ WHERE g.status = 'active'
585
+ AND g.goal_id LIKE 'WG-AUDIT-%'
586
+ ORDER BY g.updated_at DESC, g.opened_at DESC"""
587
+ ).fetchall()
588
+
589
+ if not rows:
590
+ return {"ok": True, "retired": 0}
591
+
592
+ now = datetime.now()
593
+ now_iso = now.isoformat(timespec="seconds")
594
+ retired = 0
595
+ for row in rows:
596
+ if str(row["next_action"] or "").strip() != AUDIT_GOAL_NEXT_ACTION:
597
+ continue
598
+ owner = str(row["owner"] or "").strip()
599
+ if owner and owner != AUDIT_GOAL_OWNER:
600
+ continue
601
+ if int(row["open_run_count"] or 0) > 0:
602
+ continue
603
+ updated_at = _parse_mixed_datetime(row["updated_at"]) or _parse_mixed_datetime(row["opened_at"])
604
+ if not updated_at:
605
+ continue
606
+ age_hours = (now - updated_at).total_seconds() / 3600
607
+ if age_hours < max_age_hours:
608
+ continue
609
+ conn.execute(
610
+ """UPDATE workflow_goals
611
+ SET status = 'abandoned',
612
+ next_action = ?,
613
+ blocker_reason = ?,
614
+ updated_at = ?,
615
+ closed_at = ?
616
+ WHERE goal_id = ?""",
617
+ (
618
+ "Ninguna. Placeholder stale retirado automáticamente; el self-audit lo recreará si el patrón reaparece.",
619
+ f"Self-audit placeholder stale >{max_age_hours}h sin workflow runs abiertos.",
620
+ now_iso,
621
+ now_iso,
622
+ row["goal_id"],
623
+ ),
624
+ )
625
+ retired += 1
626
+ return {"ok": True, "retired": retired}
627
+
628
+
556
629
  def _queue_public_core_handoff(
557
630
  conn: sqlite3.Connection,
558
631
  *,
@@ -1174,6 +1247,11 @@ def check_unformalized_mentions():
1174
1247
  conn.close()
1175
1248
  return
1176
1249
 
1250
+ retired_result = _retire_stale_audit_goals_inline(conn)
1251
+ retired_count = int(retired_result.get("retired") or 0)
1252
+ if retired_count:
1253
+ finding("INFO", "formalization", f"retired {retired_count} stale self-audit workflow goals")
1254
+
1177
1255
  rows = conn.execute(
1178
1256
  """SELECT goal, area, learning_id, followup_id
1179
1257
  FROM protocol_tasks