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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/auto_update.py +21 -0
- package/src/doctor/models.py +1 -0
- package/src/doctor/orchestrator.py +2 -2
- package/src/doctor/providers/boot.py +1 -1
- package/src/doctor/providers/deep.py +1 -1
- package/src/doctor/providers/runtime.py +114 -16
- package/src/local_context/api.py +24 -6
- package/src/scripts/deep-sleep/collect.py +125 -0
- package/src/transcript_utils.py +29 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
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.
|
|
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.
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -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
|
|
package/src/doctor/models.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
package/src/local_context/api.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
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=
|
|
3157
|
-
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),
|
package/src/transcript_utils.py
CHANGED
|
@@ -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
|
|
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
|
|