nexo-brain 7.30.7 → 7.30.9

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.30.7",
3
+ "version": "7.30.9",
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.30.7` is the current packaged-runtime line. Patch release over v7.30.6 - the Deep Sleep retention update is republished with the required release smoke contract so final closeout, npm, GitHub, and runtime verification stay aligned.
21
+ Version `7.30.9` is the current packaged-runtime line. Patch release over v7.30.8 - post-update self-heal now stamps a verified repair baseline, and doctor release gates distinguish current installation failures from historical operator/session drift.
22
+
23
+ Previously in `7.30.8`: patch release over v7.30.7 - Deep Sleep now folds parallel Codex sub-agents into their parent thread and Local Context stops the `entity_facts` cartesian blow-up that created runaway sidecar databases.
24
+
25
+ Previously in `7.30.7`: patch release over v7.30.6 - the Deep Sleep retention update is republished with the required release smoke contract so final closeout, npm, GitHub, and runtime verification stay aligned.
22
26
 
23
27
  Previously in `7.30.6`: patch release over v7.30.5 - Deep Sleep now rotates its operational artifacts and logs automatically, keeping historical installs bounded without touching local-context memory.
24
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.7",
3
+ "version": "7.30.9",
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",
@@ -70,6 +70,7 @@ DATA_DIR.mkdir(parents=True, exist_ok=True)
70
70
  SRC_DIR = Path(__file__).resolve().parent
71
71
  NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(SRC_DIR)))
72
72
  REPO_DIR = SRC_DIR.parent
73
+ REPAIR_BASELINE_FILE = "last-repair-baseline.json"
73
74
 
74
75
 
75
76
  def _resolve_repo_dir() -> Path:
@@ -90,6 +91,22 @@ def _resolve_repo_dir() -> Path:
90
91
 
91
92
  _RESOLVED_REPO_DIR = _resolve_repo_dir()
92
93
 
94
+
95
+ def _stamp_runtime_repair_baseline(dest: Path) -> str:
96
+ operations_dir = dest / "operations"
97
+ operations_dir.mkdir(parents=True, exist_ok=True)
98
+ now = time.time()
99
+ payload = {
100
+ "last_repair_epoch": now,
101
+ "last_repair_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)),
102
+ "source": "auto_update._run_runtime_post_sync",
103
+ "reason": "verified runtime repair baseline after update/post-sync",
104
+ }
105
+ (operations_dir / REPAIR_BASELINE_FILE).write_text(
106
+ json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
107
+ )
108
+ return "runtime-repair-baseline"
109
+
93
110
  LAST_CHECK_FILE = DATA_DIR / "auto_update_last_check.json"
94
111
  MIGRATION_VERSION_FILE = DATA_DIR / "migration_version"
95
112
  CLAUDE_MD_VERSION_FILE = DATA_DIR / "claude_md_version.txt"
@@ -5210,6 +5227,10 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
5210
5227
  if verify.returncode != 0:
5211
5228
  return False, [verify.stderr.strip() or verify.stdout.strip() or "import verify failed"]
5212
5229
  actions.append("verify")
5230
+ try:
5231
+ actions.append(_stamp_runtime_repair_baseline(dest))
5232
+ except Exception as exc:
5233
+ actions.append(f"runtime-repair-baseline-warning:{exc.__class__.__name__}")
5213
5234
  return True, actions
5214
5235
 
5215
5236
 
@@ -17,6 +17,7 @@ class DoctorCheck:
17
17
  repair_plan: list[str] = field(default_factory=list)
18
18
  escalation_prompt: str = ""
19
19
  fixed: bool = False
20
+ category: str = "installed_product" # installed_product, operator_history
20
21
 
21
22
 
22
23
  @dataclass
@@ -46,7 +46,7 @@ def run_doctor(tier: str = "boot", fix: bool = False, plane: str = "") -> Doctor
46
46
  report.duration_ms = int((time.monotonic() - start) * 1000)
47
47
  return report
48
48
 
49
- _, preflight = diagnostic_plane_preflight(plane)
49
+ normalized_plane, preflight = diagnostic_plane_preflight(plane)
50
50
  if preflight is not None:
51
51
  report.add(preflight)
52
52
  report.compute_status()
@@ -60,7 +60,7 @@ def run_doctor(tier: str = "boot", fix: bool = False, plane: str = "") -> Doctor
60
60
  if not runner:
61
61
  continue
62
62
  try:
63
- checks = runner(fix=fix)
63
+ checks = runner(fix=fix, plane=normalized_plane)
64
64
  for check in checks:
65
65
  report.add(check)
66
66
  except Exception as exc:
@@ -899,7 +899,7 @@ def check_f06_migration_consistency() -> DoctorCheck:
899
899
  )
900
900
 
901
901
 
902
- def run_boot_checks(fix: bool = False) -> list[DoctorCheck]:
902
+ def run_boot_checks(fix: bool = False, plane: str = "") -> list[DoctorCheck]:
903
903
  """Run all boot-tier checks."""
904
904
  checks = [
905
905
  safe_check(check_db_exists),
@@ -357,7 +357,7 @@ def check_learning_count() -> DoctorCheck:
357
357
  )
358
358
 
359
359
 
360
- def run_deep_checks(fix: bool = False) -> list[DoctorCheck]:
360
+ def run_deep_checks(fix: bool = False, plane: str = "") -> list[DoctorCheck]:
361
361
  """Run all deep-tier checks. Read-only."""
362
362
  return [
363
363
  safe_check(check_self_audit_summary),
@@ -64,6 +64,47 @@ CORE_AUTOMATION_CALLERS_BY_CRON = {
64
64
  "morning-agent": ("morning_agent",),
65
65
  "sleep": ("sleep/nightly",),
66
66
  }
67
+ REPAIR_BASELINE_FILE = "last-repair-baseline.json"
68
+
69
+
70
+ def _post_update_repair_baseline_epoch() -> float:
71
+ path = paths.operations_dir() / REPAIR_BASELINE_FILE
72
+ if not path.is_file():
73
+ return 0.0
74
+ try:
75
+ payload = json.loads(path.read_text())
76
+ except Exception:
77
+ return 0.0
78
+ if not isinstance(payload, dict):
79
+ return 0.0
80
+ for key in ("last_repair_epoch", "timestamp_epoch"):
81
+ try:
82
+ value = float(payload.get(key) or 0)
83
+ except Exception:
84
+ value = 0.0
85
+ if value > 0:
86
+ return value
87
+ raw_iso = str(payload.get("last_repair_at") or payload.get("timestamp") or "").strip()
88
+ if not raw_iso:
89
+ return 0.0
90
+ try:
91
+ parsed = dt.datetime.fromisoformat(raw_iso.replace("Z", "+00:00"))
92
+ except Exception:
93
+ return 0.0
94
+ if parsed.tzinfo is None:
95
+ parsed = parsed.replace(tzinfo=dt.timezone.utc)
96
+ return parsed.timestamp()
97
+
98
+
99
+ def _history_cutoff_epoch(*, days: int) -> float:
100
+ return max(time.time() - (days * 86400), _post_update_repair_baseline_epoch())
101
+
102
+
103
+ def _history_baseline_sqlite() -> str:
104
+ epoch = _post_update_repair_baseline_epoch()
105
+ if epoch <= 0:
106
+ return ""
107
+ return dt.datetime.fromtimestamp(epoch, tz=dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
67
108
 
68
109
 
69
110
  def _evolution_objective_payload() -> dict:
@@ -271,7 +312,7 @@ def _recent_codex_session_parity_status(*, days: int = 7, max_files: int = 24) -
271
312
  Path.home() / ".codex" / "sessions",
272
313
  Path.home() / ".codex" / "archived_sessions",
273
314
  ]
274
- cutoff = time.time() - (days * 86400)
315
+ cutoff = _history_cutoff_epoch(days=days)
275
316
  candidates: list[tuple[float, Path]] = []
276
317
  for root in roots:
277
318
  if not root.exists():
@@ -288,6 +329,7 @@ def _recent_codex_session_parity_status(*, days: int = 7, max_files: int = 24) -
288
329
 
289
330
  status = {
290
331
  "files": len(files),
332
+ "history_baseline_epoch": _post_update_repair_baseline_epoch(),
291
333
  "bootstrap_sessions": 0,
292
334
  "startup_sessions": 0,
293
335
  "heartbeat_sessions": 0,
@@ -578,7 +620,7 @@ def _recent_codex_conditioned_file_discipline_status(*, days: int = 7, max_files
578
620
  Path.home() / ".codex" / "sessions",
579
621
  Path.home() / ".codex" / "archived_sessions",
580
622
  ]
581
- cutoff = time.time() - (days * 86400)
623
+ cutoff = _history_cutoff_epoch(days=days)
582
624
  candidates: list[tuple[float, Path]] = []
583
625
  for root in roots:
584
626
  if not root.exists():
@@ -593,6 +635,7 @@ def _recent_codex_conditioned_file_discipline_status(*, days: int = 7, max_files
593
635
  candidates.sort(key=lambda item: item[0], reverse=True)
594
636
  files = candidates[:max_files]
595
637
  status["files"] = len(files)
638
+ status["history_baseline_epoch"] = _post_update_repair_baseline_epoch()
596
639
 
597
640
  for file_mtime, path in files:
598
641
  cwd = ""
@@ -2312,6 +2355,7 @@ def check_codex_session_parity() -> DoctorCheck:
2312
2355
  status="healthy",
2313
2356
  severity="info",
2314
2357
  summary="Codex session parity check skipped (Codex not selected)",
2358
+ category="operator_history",
2315
2359
  )
2316
2360
 
2317
2361
  audit = _recent_codex_session_parity_status()
@@ -2329,6 +2373,7 @@ def check_codex_session_parity() -> DoctorCheck:
2329
2373
  "Codex is selected, but there are no recent durable Codex sessions to inspect. "
2330
2374
  "NEXO cannot prove that manual Codex sessions are entering the shared-brain startup flow."
2331
2375
  ),
2376
+ category="operator_history",
2332
2377
  )
2333
2378
 
2334
2379
  evidence = [
@@ -2378,6 +2423,7 @@ def check_codex_session_parity() -> DoctorCheck:
2378
2423
  "Codex is selected, but recent durable Codex sessions are not consistently showing NEXO bootstrap markers or `nexo_startup`. "
2379
2424
  "Manual Codex sessions may still be starting too plain."
2380
2425
  ) if status != "healthy" else "",
2426
+ category="operator_history",
2381
2427
  )
2382
2428
 
2383
2429
 
@@ -2400,6 +2446,7 @@ def check_bootstrap_reached_startup() -> DoctorCheck:
2400
2446
  status="healthy",
2401
2447
  severity="info",
2402
2448
  summary="Startup reachability skipped (Codex not selected)",
2449
+ category="operator_history",
2403
2450
  )
2404
2451
 
2405
2452
  audit = _recent_codex_session_parity_status(days=1, max_files=48)
@@ -2425,6 +2472,7 @@ def check_bootstrap_reached_startup() -> DoctorCheck:
2425
2472
  evidence=evidence,
2426
2473
  repair_plan=["Start Codex through the managed NEXO launcher and re-run doctor"],
2427
2474
  escalation_prompt="NEXO cannot prove recent Codex sessions reached startup.",
2475
+ category="operator_history",
2428
2476
  )
2429
2477
 
2430
2478
  status = "healthy" if missing == 0 else "critical"
@@ -2447,6 +2495,7 @@ def check_bootstrap_reached_startup() -> DoctorCheck:
2447
2495
  escalation_prompt=(
2448
2496
  "Codex sessions are starting without the shared-brain startup step, so memory/guard continuity is not guaranteed."
2449
2497
  ) if status != "healthy" else "",
2498
+ category="operator_history",
2450
2499
  )
2451
2500
 
2452
2501
 
@@ -2468,6 +2517,7 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
2468
2517
  status="healthy",
2469
2518
  severity="info",
2470
2519
  summary="Codex conditioned-file discipline check skipped (Codex not selected)",
2520
+ category="operator_history",
2471
2521
  )
2472
2522
 
2473
2523
  audit = _recent_codex_conditioned_file_discipline_status()
@@ -2491,6 +2541,7 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
2491
2541
  severity="info",
2492
2542
  summary="No active conditioned-file learnings defined for Codex session audits",
2493
2543
  evidence=evidence,
2544
+ category="operator_history",
2494
2545
  )
2495
2546
 
2496
2547
  if audit["files"] == 0 or audit["conditioned_sessions"] == 0:
@@ -2501,6 +2552,7 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
2501
2552
  severity="info",
2502
2553
  summary="No conditioned-file touches seen in recent Codex sessions",
2503
2554
  evidence=evidence + [f"conditioned touches: {audit['conditioned_touches']}"],
2555
+ category="operator_history",
2504
2556
  )
2505
2557
 
2506
2558
  evidence.extend([
@@ -2595,10 +2647,11 @@ def check_codex_conditioned_file_discipline() -> DoctorCheck:
2595
2647
  "Codex sessions are touching conditioned files without the expected protocol/guard sequence. "
2596
2648
  "Until this is clean, parity with Claude hooks is still incomplete."
2597
2649
  ) if status != "healthy" else "",
2650
+ category="operator_history",
2598
2651
  )
2599
2652
 
2600
2653
 
2601
- def check_codex_protocol_compliance() -> DoctorCheck:
2654
+ def check_codex_protocol_compliance(include_history: bool = True) -> DoctorCheck:
2602
2655
  try:
2603
2656
  schedule = _load_json(SCHEDULE_FILE) if SCHEDULE_FILE.is_file() else {}
2604
2657
  except Exception:
@@ -2646,6 +2699,16 @@ def check_codex_protocol_compliance() -> DoctorCheck:
2646
2699
  ),
2647
2700
  )
2648
2701
 
2702
+ if not include_history:
2703
+ return DoctorCheck(
2704
+ id="installation_live.codex_protocol_compliance",
2705
+ tier="runtime",
2706
+ status="healthy",
2707
+ severity="info",
2708
+ summary="Codex live protocol enforcement is installed",
2709
+ evidence=[f"codex PreToolUse hook: managed ({hooks.get('pretool_matcher') or '*'})"],
2710
+ )
2711
+
2649
2712
  startup = _recent_codex_session_parity_status(days=1)
2650
2713
  conditioned = _recent_codex_conditioned_file_discipline_status(days=1)
2651
2714
  sessions = int(startup.get("files") or conditioned.get("files") or 0)
@@ -2659,6 +2722,7 @@ def check_codex_protocol_compliance() -> DoctorCheck:
2659
2722
  repair_plan=[
2660
2723
  "Run Codex through the managed NEXO bootstrap so doctor can verify live protocol compliance",
2661
2724
  ],
2725
+ category="operator_history",
2662
2726
  )
2663
2727
 
2664
2728
  startup_violation_sessions = 0
@@ -2709,6 +2773,7 @@ def check_codex_protocol_compliance() -> DoctorCheck:
2709
2773
  escalation_prompt=(
2710
2774
  "Codex CLI parity is not clean: recent sessions miss startup/heartbeat or bypass conditioned-file guard discipline."
2711
2775
  ) if status != "healthy" else "",
2776
+ category="operator_history",
2712
2777
  )
2713
2778
 
2714
2779
 
@@ -2898,11 +2963,14 @@ def check_protocol_compliance() -> DoctorCheck:
2898
2963
  debt_rows = None
2899
2964
  if {"protocol_tasks", "protocol_debt"}.issubset(tables):
2900
2965
  window = "-7 days"
2966
+ history_floor = _history_baseline_sqlite()
2967
+ task_floor_clause = " AND opened_at >= ?" if history_floor else ""
2968
+ task_params = (window, history_floor) if history_floor else (window,)
2901
2969
  tasks = conn.execute(
2902
- """SELECT * FROM protocol_tasks
2903
- WHERE opened_at >= datetime('now', ?)
2970
+ f"""SELECT * FROM protocol_tasks
2971
+ WHERE opened_at >= datetime('now', ?){task_floor_clause}
2904
2972
  ORDER BY opened_at DESC""",
2905
- (window,),
2973
+ task_params,
2906
2974
  ).fetchall()
2907
2975
  protocol_debt_cols = {
2908
2976
  row["name"] for row in conn.execute("PRAGMA table_info(protocol_debt)").fetchall()
@@ -2946,6 +3014,8 @@ def check_protocol_compliance() -> DoctorCheck:
2946
3014
  task_status_expr = "'' AS task_status"
2947
3015
  if "status" in protocol_task_cols:
2948
3016
  task_status_expr = "pt.status AS task_status"
3017
+ debt_floor_clause = " AND pd.created_at >= ?" if history_floor else ""
3018
+ debt_params = (window, history_floor) if history_floor else (window,)
2949
3019
  open_debts = conn.execute(
2950
3020
  f"""SELECT
2951
3021
  pd.severity,
@@ -2956,9 +3026,9 @@ def check_protocol_compliance() -> DoctorCheck:
2956
3026
  {task_status_expr}
2957
3027
  FROM protocol_debt pd
2958
3028
  LEFT JOIN protocol_tasks pt ON pt.task_id = pd.task_id
2959
- WHERE pd.status = 'open' AND pd.created_at >= datetime('now', ?)
3029
+ WHERE pd.status = 'open' AND pd.created_at >= datetime('now', ?){debt_floor_clause}
2960
3030
  ORDER BY pd.created_at DESC""",
2961
- (window,),
3031
+ debt_params,
2962
3032
  ).fetchall()
2963
3033
  debt_counter: dict[tuple[str, str], int] = {}
2964
3034
  for row in open_debts:
@@ -2987,12 +3057,13 @@ def check_protocol_compliance() -> DoctorCheck:
2987
3057
  else:
2988
3058
  debt_rows = [
2989
3059
  dict(row) for row in conn.execute(
2990
- """SELECT severity, debt_type, COUNT(*) AS total
3060
+ f"""SELECT severity, debt_type, COUNT(*) AS total
2991
3061
  FROM protocol_debt
2992
3062
  WHERE status = 'open' AND created_at >= datetime('now', ?)
3063
+ {"AND created_at >= ?" if history_floor else ""}
2993
3064
  GROUP BY severity, debt_type
2994
3065
  ORDER BY total DESC, debt_type ASC""",
2995
- (window,),
3066
+ (window, history_floor) if history_floor else (window,),
2996
3067
  ).fetchall()
2997
3068
  ]
2998
3069
  has_cortex_evaluations = bool(
@@ -3010,16 +3081,33 @@ def check_protocol_compliance() -> DoctorCheck:
3010
3081
  ).fetchall()
3011
3082
  }
3012
3083
  first_eval_row = conn.execute(
3013
- """SELECT MIN(created_at) AS first_eval
3084
+ f"""SELECT MIN(created_at) AS first_eval
3014
3085
  FROM cortex_evaluations
3015
- WHERE created_at >= datetime('now', ?)""",
3016
- (window,),
3086
+ WHERE created_at >= datetime('now', ?)
3087
+ {"AND created_at >= ?" if history_floor else ""}""",
3088
+ (window, history_floor) if history_floor else (window,),
3017
3089
  ).fetchone()
3018
3090
  if first_eval_row and first_eval_row["first_eval"]:
3019
3091
  first_cortex_eval_at = str(first_eval_row["first_eval"])
3020
3092
  finally:
3021
3093
  conn.close()
3022
3094
 
3095
+ history_floor = _history_baseline_sqlite()
3096
+ if tasks is not None and debt_rows is not None and not tasks and not debt_rows and history_floor:
3097
+ return DoctorCheck(
3098
+ id="runtime.protocol_compliance",
3099
+ tier="runtime",
3100
+ status="healthy",
3101
+ severity="info",
3102
+ summary="No protocol drift after the last verified runtime repair",
3103
+ evidence=[
3104
+ "live protocol window: 7d",
3105
+ f"post-update repair baseline: {history_floor}",
3106
+ "no protocol tasks or open debt after repair baseline",
3107
+ ],
3108
+ category="operator_history",
3109
+ )
3110
+
3023
3111
  if tasks is not None and debt_rows is not None and (tasks or debt_rows):
3024
3112
  closed_tasks = [row for row in tasks if row["status"] != "open"]
3025
3113
  verify_required = [row for row in closed_tasks if row["must_verify"] and row["status"] == "done"]
@@ -3122,6 +3210,7 @@ def check_protocol_compliance() -> DoctorCheck:
3122
3210
  escalation_prompt=(
3123
3211
  "Task discipline is drifting in live runtime data. NEXO is still skipping verification, change logging, or correction capture."
3124
3212
  ) if status != "healthy" else "",
3213
+ category="operator_history",
3125
3214
  )
3126
3215
  except Exception:
3127
3216
  pass
@@ -3140,6 +3229,7 @@ def check_protocol_compliance() -> DoctorCheck:
3140
3229
  escalation_prompt=(
3141
3230
  "NEXO cannot verify heartbeat / guard_check / change_log compliance because the latest weekly Deep Sleep summary is missing."
3142
3231
  ),
3232
+ category="operator_history",
3143
3233
  )
3144
3234
 
3145
3235
  protocol = summary.get("protocol_summary") or {}
@@ -3197,6 +3287,7 @@ def check_protocol_compliance() -> DoctorCheck:
3197
3287
  escalation_prompt=(
3198
3288
  "Heartbeat / guard_check / change_log discipline is drifting. NEXO is at risk of repeating known errors and hiding change history."
3199
3289
  ) if status != "healthy" else "",
3290
+ category="operator_history",
3200
3291
  )
3201
3292
 
3202
3293
 
@@ -3979,9 +4070,15 @@ def check_memory_fabric_health(fix: bool = False) -> DoctorCheck:
3979
4070
  )
3980
4071
 
3981
4072
 
3982
- def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
4073
+ def _filter_runtime_checks_for_plane(checks: list[DoctorCheck], plane: str = "") -> list[DoctorCheck]:
4074
+ if plane == "installation_live":
4075
+ return [check for check in checks if check.category != "operator_history"]
4076
+ return checks
4077
+
4078
+
4079
+ def run_runtime_checks(fix: bool = False, plane: str = "") -> list[DoctorCheck]:
3983
4080
  """Run all runtime-tier checks. Read-only by default."""
3984
- return [
4081
+ checks = [
3985
4082
  safe_check(check_immune_status),
3986
4083
  safe_check(check_watchdog_status),
3987
4084
  safe_check(check_runner_health_status),
@@ -3992,7 +4089,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
3992
4089
  safe_check(check_codex_session_parity),
3993
4090
  safe_check(check_bootstrap_reached_startup),
3994
4091
  safe_check(check_codex_conditioned_file_discipline),
3995
- safe_check(check_codex_protocol_compliance),
4092
+ safe_check(check_codex_protocol_compliance, include_history=plane != "installation_live"),
3996
4093
  safe_check(check_claude_desktop_shared_brain),
3997
4094
  safe_check(check_transcript_source_parity),
3998
4095
  safe_check(check_client_assumption_regressions),
@@ -4009,3 +4106,4 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
4009
4106
  safe_check(check_personal_script_registry, fix=fix),
4010
4107
  safe_check(check_skill_health, fix=fix),
4011
4108
  ]
4109
+ return _filter_runtime_checks_for_plane(checks, plane=plane)
@@ -55,6 +55,10 @@ ENTITY_DOSSIER_MAX_ASSETS = int(os.environ.get("NEXO_ENTITY_DOSSIER_MAX_ASSETS",
55
55
  ENTITY_DOSSIER_MAX_CHUNKS = int(os.environ.get("NEXO_ENTITY_DOSSIER_MAX_CHUNKS", "1200") or "1200")
56
56
  ENTITY_DOSSIER_MAX_FACTS = int(os.environ.get("NEXO_ENTITY_DOSSIER_MAX_FACTS", "3000") or "3000")
57
57
  ENTITY_FACT_MIN_CONFIDENCE = float(os.environ.get("NEXO_ENTITY_FACT_MIN_CONFIDENCE", "0.45") or "0.45")
58
+ # Hard ceilings to stop the entity_facts cartesian blow-up (chunks × entities × candidates).
59
+ # Without these a single document could emit thousands of facts; 258k assets produced 337M rows / 255 GB.
60
+ ENTITY_FACTS_MAX_PER_ASSET = int(os.environ.get("NEXO_ENTITY_FACTS_MAX_PER_ASSET", "200") or "200")
61
+ ENTITY_FACT_MAX_VALUE_LEN = int(os.environ.get("NEXO_ENTITY_FACT_MAX_VALUE_LEN", "240") or "240")
58
62
  ENTITY_FACTS_LLM_ENABLED = os.environ.get("NEXO_ENTITY_FACTS_LLM_ENABLED", "1").strip().lower() not in {"0", "false", "no", "off"}
59
63
  LOCAL_PRESENCE_MODEL_SPEC = "qwen3-0.6b-q4-local-presence"
60
64
  FOREGROUND_GOVERNOR_ENABLED = os.environ.get("NEXO_LOCAL_INDEX_FOREGROUND_GOVERNOR", "1").strip().lower() not in {"0", "false", "no", "off"}
@@ -3133,28 +3137,42 @@ def _replace_entity_facts(conn, asset_id: str) -> int:
3133
3137
  ).fetchall()
3134
3138
  inserted = 0
3135
3139
  for chunk in chunks:
3140
+ if inserted >= ENTITY_FACTS_MAX_PER_ASSET:
3141
+ break
3136
3142
  text = str(chunk["text"] or "")
3137
3143
  if not text or contains_secret(text):
3138
3144
  continue
3139
3145
  candidates = _fact_candidate_lines(text)
3140
3146
  if not candidates:
3141
3147
  candidates = [("mencion", sentence.strip(), 0.48) for sentence in re.split(r"(?<=[.!?])\s+", text) if sentence.strip()][:4]
3148
+ chunk_id = str(chunk["chunk_id"] or "")
3142
3149
  for entity in entities_by_id.values():
3150
+ if inserted >= ENTITY_FACTS_MAX_PER_ASSET:
3151
+ break
3143
3152
  aliases = sorted(alias for alias in entity["aliases"] if alias)
3144
- direct = _chunk_mentions_entity(text, aliases)
3153
+ # Only attribute a chunk's facts to entities actually mentioned in THAT chunk.
3154
+ # Previously every candidate was attached to every entity in the asset (a
3155
+ # chunks × entities × candidates cartesian product) which produced 337M junk
3156
+ # rows / 255 GB. Gating on mention is both the size fix and the correctness fix.
3157
+ if not _chunk_mentions_entity(text, aliases):
3158
+ continue
3145
3159
  for predicate, value, base_confidence in candidates:
3146
- predicate = _strip_entity_aliases_from_predicate(predicate, aliases)
3147
- confidence = base_confidence if direct else min(base_confidence, 0.56)
3148
- if confidence < ENTITY_FACT_MIN_CONFIDENCE:
3160
+ if inserted >= ENTITY_FACTS_MAX_PER_ASSET:
3161
+ break
3162
+ # Drop paragraph-as-fact noise: real facts carry short values.
3163
+ if len(value) > ENTITY_FACT_MAX_VALUE_LEN:
3149
3164
  continue
3165
+ if base_confidence < ENTITY_FACT_MIN_CONFIDENCE:
3166
+ continue
3167
+ predicate = _strip_entity_aliases_from_predicate(predicate, aliases)
3150
3168
  if _insert_entity_fact(
3151
3169
  conn,
3152
3170
  entity_id=entity["entity_id"],
3153
3171
  predicate=predicate,
3154
3172
  value=value,
3155
3173
  source_asset_id=asset_id,
3156
- source_chunk_id=str(chunk["chunk_id"] or ""),
3157
- confidence=confidence,
3174
+ source_chunk_id=chunk_id,
3175
+ confidence=base_confidence,
3158
3176
  ):
3159
3177
  inserted += 1
3160
3178
  return inserted
@@ -155,6 +155,117 @@ def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]
155
155
  return _transcripts.collect_transcripts_since(since_iso, until_iso)
156
156
 
157
157
 
158
+ # ── Fold parallel sub-agent threads into their parent ──────────────────────
159
+
160
+
161
+ def _is_subagent(session: dict) -> bool:
162
+ """True when a session was spawned as a sub-agent thread of another session."""
163
+ if str(session.get("thread_source", "")).strip().lower() == "subagent":
164
+ return True
165
+ if str(session.get("parent_thread_id", "") or "").strip():
166
+ return True
167
+ source = session.get("source")
168
+ return isinstance(source, dict) and "subagent" in source
169
+
170
+
171
+ def _root_thread_key(session: dict, by_uid: dict[str, dict]) -> str:
172
+ """Resolve the top-of-tree thread for a session, following parent links.
173
+
174
+ Sub-agent rollouts carry ``parent_thread_id``; we walk up until we reach a
175
+ session with no parent (the real top-level thread). When the parent is not
176
+ part of this batch we still group siblings under the parent id so several
177
+ explorers spawned by the same (absent) parent collapse together. The walk is
178
+ bounded so a malformed/cyclic chain can never loop forever.
179
+ """
180
+ cur = session
181
+ for _ in range(16):
182
+ parent = str(cur.get("parent_thread_id", "") or "").strip()
183
+ if not parent:
184
+ break
185
+ nxt = by_uid.get(parent)
186
+ if nxt is None or nxt is cur:
187
+ return parent
188
+ cur = nxt
189
+ return str(cur.get("session_uid", "") or cur.get("session_file", ""))
190
+
191
+
192
+ def dedupe_sessions(sessions: list[dict]) -> tuple[list[dict], list[dict]]:
193
+ """Fold parallel sub-agent threads into their parent so each real thread is
194
+ analyzed and counted once instead of once per spawned explorer.
195
+
196
+ Sessions are grouped by their root thread (see :func:`_root_thread_key`).
197
+ Within a group the actual parent session is kept as the canonical thread
198
+ (falling back to a non-sub-agent member, then the earliest one); the folded
199
+ sub-agent transcripts are appended to the canonical session — so no content
200
+ is lost — and their ids/nicknames are recorded on the kept session
201
+ (``folded_subagents``) and in the returned report.
202
+
203
+ Returns ``(kept_sessions, dedupe_report)``. Distinct top-level threads are
204
+ never merged.
205
+ """
206
+ by_uid: dict[str, dict] = {}
207
+ for session in sessions:
208
+ uid = str(session.get("session_uid", "") or "").strip()
209
+ if uid:
210
+ by_uid.setdefault(uid, session)
211
+
212
+ groups: dict[str, list[dict]] = {}
213
+ order: list[str] = []
214
+ for session in sessions:
215
+ key = _root_thread_key(session, by_uid)
216
+ if key not in groups:
217
+ groups[key] = []
218
+ order.append(key)
219
+ groups[key].append(session)
220
+
221
+ kept: list[dict] = []
222
+ report: list[dict] = []
223
+ for key in order:
224
+ members = groups[key]
225
+ if len(members) == 1:
226
+ kept.append(members[0])
227
+ continue
228
+ representative = next(
229
+ (m for m in members if str(m.get("session_uid", "") or "") == key), None
230
+ )
231
+ if representative is None:
232
+ representative = next((m for m in members if not _is_subagent(m)), None)
233
+ if representative is None:
234
+ representative = min(members, key=lambda m: str(m.get("modified", "")))
235
+
236
+ folded = [m for m in members if m is not representative]
237
+ rep_messages = representative.setdefault("messages", [])
238
+ rep_tools = representative.setdefault("tool_uses", [])
239
+ for child in folded:
240
+ label = child.get("agent_nickname") or child["session_file"]
241
+ role = child.get("agent_role") or "subagent"
242
+ rep_messages.append({
243
+ "role": "user",
244
+ "index": 0,
245
+ "text": f"──── folded sub-agent thread: {label} ({role}) — {child['session_file']} ────",
246
+ })
247
+ rep_messages.extend(child.get("messages") or [])
248
+ rep_tools.extend(child.get("tool_uses") or [])
249
+ representative["message_count"] = len(rep_messages)
250
+ representative["tool_use_count"] = len(rep_tools)
251
+ representative["folded_subagents"] = [
252
+ {
253
+ "session_file": m["session_file"],
254
+ "agent_nickname": m.get("agent_nickname", ""),
255
+ "agent_role": m.get("agent_role", ""),
256
+ }
257
+ for m in folded
258
+ ]
259
+ kept.append(representative)
260
+ report.append({
261
+ "root_thread": key,
262
+ "kept": representative["session_file"],
263
+ "folded": [m["session_file"] for m in folded],
264
+ "count": len(members),
265
+ })
266
+ return kept, report
267
+
268
+
158
269
  # ── Database queries ──────────────────────────────────────────────────────
159
270
 
160
271
 
@@ -818,6 +929,17 @@ def main():
818
929
  sessions = collect_transcripts_since(fallback_since)
819
930
  print(f" Found {len(sessions)} sessions")
820
931
 
932
+ # Fold parallel sub-agent rollouts into their parent thread so a single
933
+ # logical thread is not analyzed (and counted) once per spawned explorer,
934
+ # which otherwise inflates the finding count.
935
+ sessions, dedupe_report = dedupe_sessions(sessions)
936
+ folded_total = sum(len(item["folded"]) for item in dedupe_report)
937
+ if folded_total:
938
+ print(
939
+ f" Folded {folded_total} sub-agent session(s) into "
940
+ f"{len(dedupe_report)} parent thread(s); {len(sessions)} unique threads remain"
941
+ )
942
+
821
943
  if not sessions:
822
944
  print(f"[collect] No new sessions found. Writing minimal context file.")
823
945
  output_file = DEEP_SLEEP_DIR / f"{run_id}-context.txt"
@@ -959,9 +1081,12 @@ def main():
959
1081
  "source": s.get("source", ""),
960
1082
  "session_path": s.get("session_path", ""),
961
1083
  "session_txt_file": session_txt_map.get(s["session_file"], ""),
1084
+ "folded_subagents": s.get("folded_subagents", []),
962
1085
  }
963
1086
  for s in sessions
964
1087
  ],
1088
+ "sessions_folded": folded_total,
1089
+ "dedupe_report": dedupe_report,
965
1090
  "total_messages": sum(s["message_count"] for s in sessions),
966
1091
  "total_tool_uses": sum(s["tool_use_count"] for s in sessions),
967
1092
  "followups_active": len(followups),
@@ -208,6 +208,9 @@ def extract_claude_session(jsonl_path: Path, *, min_user_messages: int = MIN_USE
208
208
  "messages": messages,
209
209
  "tool_uses": tool_uses,
210
210
  "source": "claude_projects",
211
+ "session_uid": jsonl_path.stem,
212
+ "thread_source": "user",
213
+ "parent_thread_id": "",
211
214
  }
212
215
 
213
216
 
@@ -216,6 +219,7 @@ def extract_codex_session(jsonl_path: Path, *, min_user_messages: int = MIN_USER
216
219
  tool_uses = []
217
220
  user_msg_count = 0
218
221
  session_meta: dict = {}
222
+ spawn_meta: dict = {}
219
223
 
220
224
  try:
221
225
  with open(jsonl_path, "r") as f:
@@ -232,7 +236,16 @@ def extract_codex_session(jsonl_path: Path, *, min_user_messages: int = MIN_USER
232
236
  data = payload.get("payload", {})
233
237
 
234
238
  if item_type == "session_meta" and isinstance(data, dict):
235
- session_meta = data
239
+ # A sub-agent rollout embeds two session_meta records: its
240
+ # own first, then the parent it forked from. Keep the FIRST
241
+ # as this thread's identity (last-wins would mislabel the
242
+ # sub-agent as its parent) and remember whichever record
243
+ # carries the sub-agent spawn linkage.
244
+ if not session_meta:
245
+ session_meta = data
246
+ src = data.get("source")
247
+ if not spawn_meta and isinstance(src, dict) and isinstance(src.get("subagent"), dict):
248
+ spawn_meta = data
236
249
  continue
237
250
 
238
251
  if item_type == "event_msg" and isinstance(data, dict) and data.get("type") == "user_message":
@@ -280,6 +293,17 @@ def extract_codex_session(jsonl_path: Path, *, min_user_messages: int = MIN_USER
280
293
  if user_msg_count < _min_user_messages(min_user_messages):
281
294
  return None
282
295
 
296
+ spawn_source = (spawn_meta or session_meta).get("source")
297
+ thread_spawn: dict = {}
298
+ if isinstance(spawn_source, dict) and isinstance(spawn_source.get("subagent"), dict):
299
+ thread_spawn = spawn_source["subagent"].get("thread_spawn") or {}
300
+ parent_thread_id = str(
301
+ thread_spawn.get("parent_thread_id", "")
302
+ or session_meta.get("forked_from_id", "")
303
+ or ""
304
+ )
305
+ is_subagent = bool(thread_spawn) or str(session_meta.get("thread_source", "")).lower() == "subagent"
306
+
283
307
  return {
284
308
  "client": "codex",
285
309
  "session_file": _session_identifier("codex", jsonl_path.name),
@@ -294,6 +318,10 @@ def extract_codex_session(jsonl_path: Path, *, min_user_messages: int = MIN_USER
294
318
  "cwd": session_meta.get("cwd", ""),
295
319
  "originator": session_meta.get("originator", ""),
296
320
  "session_uid": session_meta.get("id", ""),
321
+ "thread_source": "subagent" if is_subagent else (session_meta.get("thread_source", "") or "user"),
322
+ "parent_thread_id": parent_thread_id,
323
+ "agent_nickname": str(session_meta.get("agent_nickname", "") or thread_spawn.get("agent_nickname", "") or ""),
324
+ "agent_role": str(session_meta.get("agent_role", "") or thread_spawn.get("agent_role", "") or ""),
297
325
  }
298
326
 
299
327