nexo-brain 2.6.17 → 2.6.18
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 +10 -2
- package/package.json +1 -1
- package/src/client_sync.py +41 -19
- package/src/cognitive/_search.py +30 -3
- package/src/doctor/providers/runtime.py +325 -0
- package/src/plugins/cognitive_memory.py +4 -0
- package/src/scripts/deep-sleep/apply_findings.py +393 -0
- package/src/scripts/deep-sleep/collect.py +221 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +13 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.18",
|
|
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
|
@@ -38,11 +38,15 @@ That means NEXO now manages not only the shared runtime and MCP wiring, but also
|
|
|
38
38
|
- For Codex specifically, `nexo chat` and Codex headless automation inject the current bootstrap explicitly, so Codex starts as NEXO even when plain global Codex startup is inconsistent about global instructions.
|
|
39
39
|
- Deep Sleep now reads both Claude Code and Codex transcript stores, so overnight analysis still works even when the user spends the day in Codex.
|
|
40
40
|
|
|
41
|
-
Version `2.6.14` closes those parity gaps in practice, `2.6.15` hardens the installed-runtime migration path so existing users actually receive the managed bootstrap updates cleanly, `2.6.16` pushes the system further in three directions,
|
|
41
|
+
Version `2.6.14` closes those parity gaps in practice, `2.6.15` hardens the installed-runtime migration path so existing users actually receive the managed bootstrap updates cleanly, `2.6.16` pushes the system further in three directions, `2.6.17` finishes the annoying last-mile migration bugs for real existing installs, and `2.6.18` tightens the remaining practical gaps around manual Codex use, Deep Sleep horizon artifacts, and retrieval honesty:
|
|
42
42
|
|
|
43
43
|
- Codex now gets managed global bootstrap/model sync in `~/.codex/config.toml`, so sessions opened outside `nexo chat` are much less likely to start as plain Codex.
|
|
44
|
+
- Codex config now also persists a managed `mcp_servers.nexo` entry, so the shared brain survives even if ad-hoc Codex MCP state drifts.
|
|
45
|
+
- Runtime doctor now audits recent Codex sessions for real startup discipline and verifies Claude Desktop shared-brain metadata explicitly instead of treating both as invisible best-effort wiring.
|
|
44
46
|
- Retrieval is smarter by default: HyDE and spreading activation now auto-enable when the query shape benefits, while exact lookups remain conservative.
|
|
47
|
+
- Retrieval explanations now surface confidence and the auto-strategy that fired, while associative expansion trims itself back to `top_k` instead of leaking low-signal neighbors.
|
|
45
48
|
- Deep Sleep now blends recent context with older context over a 60-day horizon, and memory decay now tracks per-memory `stability` and `difficulty` instead of relying only on global decay constants.
|
|
49
|
+
- Deep Sleep now also carries project-priority weighting into its long-horizon context and writes reusable weekly/monthly summary artifacts instead of reasoning only day by day.
|
|
46
50
|
- Existing installs that already had NEXO connected to Codex now backfill that client state automatically during update/sync, so the managed Codex bootstrap actually lands without manual cleanup.
|
|
47
51
|
- Bootstrap docs now fall back to the operator name `NEXO` when local metadata is blank, avoiding broken headings in `CLAUDE.md` and `AGENTS.md`.
|
|
48
52
|
|
|
@@ -52,11 +56,12 @@ Version `2.6.14` closes those parity gaps in practice, `2.6.15` hardens the inst
|
|
|
52
56
|
|------------|-------------|-------|----------------|
|
|
53
57
|
| Shared brain / MCP runtime | Yes | Yes | Yes |
|
|
54
58
|
| Managed bootstrap document | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | Not applicable |
|
|
55
|
-
| Global startup bootstrap sync | Native via hooks + bootstrap | Managed via bootstrap + Codex config `initial_messages` | MCP
|
|
59
|
+
| Global startup bootstrap sync | Native via hooks + bootstrap | Managed via bootstrap + Codex config `initial_messages` + `mcp_servers.nexo` | Managed MCP-only shared-brain metadata |
|
|
56
60
|
| `nexo chat` terminal client | Yes | Yes | No |
|
|
57
61
|
| Background automation backend | Recommended | Supported | No |
|
|
58
62
|
| Raw transcript source for Deep Sleep | Yes | Yes | No |
|
|
59
63
|
| Native hook depth | Deepest | Partial, compensated | None |
|
|
64
|
+
| Runtime doctor parity audit | Yes | Yes | Shared-brain only |
|
|
60
65
|
| Recommended today | Yes | Supported | Shared-brain companion |
|
|
61
66
|
|
|
62
67
|
## The Problem
|
|
@@ -193,6 +198,9 @@ Deep Sleep now also mixes **recent context with older context across a 60-day ho
|
|
|
193
198
|
- recurring multi-week themes
|
|
194
199
|
- cross-domain links between older learnings and current failures
|
|
195
200
|
- stale followups and topics that keep being mentioned but never formalized
|
|
201
|
+
- weighted project pressure based on diary activity, followups, learnings, and decision outcomes
|
|
202
|
+
|
|
203
|
+
It now also writes **weekly and monthly Deep Sleep summaries** so the overnight system can reuse higher-horizon signals instead of rediscovering everything from scratch every day.
|
|
196
204
|
|
|
197
205
|
## Cognitive Cortex
|
|
198
206
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.18",
|
|
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/client_sync.py
CHANGED
|
@@ -243,10 +243,12 @@ def _sync_codex_managed_config(
|
|
|
243
243
|
*,
|
|
244
244
|
bootstrap_prompt: str,
|
|
245
245
|
runtime_profile: dict | None,
|
|
246
|
+
server_config: dict | None,
|
|
246
247
|
) -> dict:
|
|
247
248
|
payload = _load_toml_object(path)
|
|
248
249
|
action = "updated" if payload else "created"
|
|
249
250
|
runtime_profile = dict(runtime_profile or {})
|
|
251
|
+
server_config = dict(server_config or {})
|
|
250
252
|
|
|
251
253
|
if runtime_profile.get("model"):
|
|
252
254
|
payload["model"] = runtime_profile["model"]
|
|
@@ -263,10 +265,19 @@ def _sync_codex_managed_config(
|
|
|
263
265
|
nexo_table = payload.setdefault("nexo", {})
|
|
264
266
|
codex_table = nexo_table.setdefault("codex", {})
|
|
265
267
|
codex_table["bootstrap_managed"] = True
|
|
268
|
+
codex_table["mcp_managed"] = True
|
|
266
269
|
codex_table["bootstrap_bytes"] = len(bootstrap_prompt.encode("utf-8")) if bootstrap_prompt else 0
|
|
267
270
|
if runtime_profile.get("model"):
|
|
268
271
|
codex_table["managed_model"] = runtime_profile["model"]
|
|
269
272
|
codex_table["managed_reasoning_effort"] = runtime_profile.get("reasoning_effort", "") or ""
|
|
273
|
+
if server_config:
|
|
274
|
+
mcp_servers = payload.setdefault("mcp_servers", {})
|
|
275
|
+
mcp_servers["nexo"] = {
|
|
276
|
+
"command": server_config.get("command", ""),
|
|
277
|
+
"args": list(server_config.get("args", []) or []),
|
|
278
|
+
"env": dict(server_config.get("env", {}) or {}),
|
|
279
|
+
}
|
|
280
|
+
codex_table["managed_server_command"] = server_config.get("command", "")
|
|
270
281
|
|
|
271
282
|
_write_toml_object(path, payload)
|
|
272
283
|
return {
|
|
@@ -274,6 +285,7 @@ def _sync_codex_managed_config(
|
|
|
274
285
|
"action": action,
|
|
275
286
|
"path": str(path),
|
|
276
287
|
"bootstrap_managed": True,
|
|
288
|
+
"mcp_managed": True,
|
|
277
289
|
"model": runtime_profile.get("model", ""),
|
|
278
290
|
"reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
|
|
279
291
|
}
|
|
@@ -296,7 +308,7 @@ def _write_json_object(path: Path, payload: dict) -> None:
|
|
|
296
308
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
297
309
|
|
|
298
310
|
|
|
299
|
-
def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
|
|
311
|
+
def _sync_json_client(path: Path, server_config: dict, label: str, *, managed_metadata: dict | None = None) -> dict:
|
|
300
312
|
payload = _load_json_object(path)
|
|
301
313
|
mcp_servers = payload.setdefault("mcpServers", {})
|
|
302
314
|
if not isinstance(mcp_servers, dict):
|
|
@@ -304,6 +316,12 @@ def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
|
|
|
304
316
|
payload["mcpServers"] = mcp_servers
|
|
305
317
|
action = "updated" if "nexo" in mcp_servers else "created"
|
|
306
318
|
mcp_servers["nexo"] = server_config
|
|
319
|
+
if managed_metadata is not None:
|
|
320
|
+
nexo_meta = payload.setdefault("nexo", {})
|
|
321
|
+
if not isinstance(nexo_meta, dict):
|
|
322
|
+
nexo_meta = {}
|
|
323
|
+
payload["nexo"] = nexo_meta
|
|
324
|
+
nexo_meta.update(managed_metadata)
|
|
307
325
|
_write_json_object(path, payload)
|
|
308
326
|
return {
|
|
309
327
|
"ok": True,
|
|
@@ -313,6 +331,18 @@ def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
|
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
|
|
334
|
+
def _claude_desktop_managed_metadata(server_config: dict, *, operator_name: str) -> dict:
|
|
335
|
+
return {
|
|
336
|
+
"claude_desktop": {
|
|
337
|
+
"shared_brain_managed": True,
|
|
338
|
+
"shared_brain_mode": "mcp_only",
|
|
339
|
+
"managed_operator": operator_name or server_config.get("env", {}).get("NEXO_NAME", "") or "NEXO",
|
|
340
|
+
"managed_runtime_home": server_config.get("env", {}).get("NEXO_HOME", ""),
|
|
341
|
+
"managed_runtime_root": server_config.get("env", {}).get("NEXO_CODE", ""),
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
316
346
|
def sync_claude_code(
|
|
317
347
|
*,
|
|
318
348
|
nexo_home: str | os.PathLike[str] | None = None,
|
|
@@ -361,10 +391,15 @@ def sync_claude_desktop(
|
|
|
361
391
|
python_path=python_path,
|
|
362
392
|
operator_name=operator_name,
|
|
363
393
|
)
|
|
394
|
+
resolved_name = server_config.get("env", {}).get("NEXO_NAME", "") or _resolve_operator_name(
|
|
395
|
+
Path(nexo_home).expanduser() if nexo_home else _default_nexo_home(),
|
|
396
|
+
explicit=operator_name,
|
|
397
|
+
)
|
|
364
398
|
return _sync_json_client(
|
|
365
399
|
_claude_desktop_config_path(Path(user_home).expanduser() if user_home else None),
|
|
366
400
|
server_config,
|
|
367
401
|
"claude_desktop",
|
|
402
|
+
managed_metadata=_claude_desktop_managed_metadata(server_config, operator_name=resolved_name),
|
|
368
403
|
)
|
|
369
404
|
|
|
370
405
|
|
|
@@ -410,6 +445,7 @@ def sync_codex(
|
|
|
410
445
|
config_path,
|
|
411
446
|
bootstrap_prompt=prompt_text,
|
|
412
447
|
runtime_profile=runtime_profile,
|
|
448
|
+
server_config=server_config,
|
|
413
449
|
)
|
|
414
450
|
return result
|
|
415
451
|
|
|
@@ -425,29 +461,12 @@ def sync_codex(
|
|
|
425
461
|
timeout=30,
|
|
426
462
|
env=env,
|
|
427
463
|
)
|
|
428
|
-
if result.returncode != 0:
|
|
429
|
-
result = {
|
|
430
|
-
"ok": False,
|
|
431
|
-
"client": "codex",
|
|
432
|
-
"path": str(config_path),
|
|
433
|
-
"error": (result.stderr or result.stdout or "codex mcp add failed").strip(),
|
|
434
|
-
}
|
|
435
|
-
bootstrap_result = sync_client_bootstrap(
|
|
436
|
-
"codex",
|
|
437
|
-
nexo_home=nexo_home,
|
|
438
|
-
operator_name=operator_name,
|
|
439
|
-
user_home=user_home,
|
|
440
|
-
)
|
|
441
|
-
result["bootstrap"] = bootstrap_result
|
|
442
|
-
if not bootstrap_result.get("ok"):
|
|
443
|
-
result["error"] = f"{result['error']}; bootstrap: {bootstrap_result.get('error', 'unknown error')}"
|
|
444
|
-
return result
|
|
445
464
|
sync_result = {
|
|
446
465
|
"ok": True,
|
|
447
466
|
"client": "codex",
|
|
448
467
|
"action": "updated",
|
|
449
468
|
"path": str(config_path),
|
|
450
|
-
"mode": "cli",
|
|
469
|
+
"mode": "cli" if result.returncode == 0 else "config_only",
|
|
451
470
|
}
|
|
452
471
|
bootstrap_result = sync_client_bootstrap(
|
|
453
472
|
"codex",
|
|
@@ -464,7 +483,10 @@ def sync_codex(
|
|
|
464
483
|
config_path,
|
|
465
484
|
bootstrap_prompt=bootstrap_result.get("content") or "",
|
|
466
485
|
runtime_profile=runtime_profile,
|
|
486
|
+
server_config=server_config,
|
|
467
487
|
)
|
|
488
|
+
if result.returncode != 0:
|
|
489
|
+
sync_result["warning"] = (result.stderr or result.stdout or "codex mcp add failed").strip()
|
|
468
490
|
return sync_result
|
|
469
491
|
|
|
470
492
|
|
package/src/cognitive/_search.py
CHANGED
|
@@ -159,6 +159,7 @@ _HISTORICAL_CUES = frozenset({
|
|
|
159
159
|
_EXACT_LOOKUP_RE = re.compile(
|
|
160
160
|
r"(/|\\|::|\.[A-Za-z0-9]+|#L\d+|line \d+|error[: ]|exception|traceback|0x[0-9a-fA-F]+|[A-Z]{2,}-\d+)"
|
|
161
161
|
)
|
|
162
|
+
_MIN_NEIGHBOR_BOOST = 0.035
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def _apply_temporal_boost(results: list[dict], query_text: str) -> list[dict]:
|
|
@@ -254,6 +255,14 @@ def _auto_spreading_depth(query_text: str, source_type_filter: str = "") -> int:
|
|
|
254
255
|
return 0
|
|
255
256
|
|
|
256
257
|
|
|
258
|
+
def _result_confidence(score: float) -> str:
|
|
259
|
+
if score >= 0.82:
|
|
260
|
+
return "high"
|
|
261
|
+
if score >= 0.66:
|
|
262
|
+
return "medium"
|
|
263
|
+
return "low"
|
|
264
|
+
|
|
265
|
+
|
|
257
266
|
# ============================================================================
|
|
258
267
|
# FEATURE 0.5: Knowledge Graph Boost
|
|
259
268
|
# Memories connected to more KG nodes (files, areas, other learnings) are
|
|
@@ -928,13 +937,23 @@ def search(
|
|
|
928
937
|
r["co_activation_boost"] = boost
|
|
929
938
|
|
|
930
939
|
# Add neighbor memories not already in results
|
|
931
|
-
new_neighbor_hashes =
|
|
940
|
+
new_neighbor_hashes = {
|
|
941
|
+
nh
|
|
942
|
+
for nh, boost in neighbor_boosts.items()
|
|
943
|
+
if nh not in existing_hashes and boost >= _MIN_NEIGHBOR_BOOST
|
|
944
|
+
}
|
|
932
945
|
if new_neighbor_hashes:
|
|
946
|
+
ranked_new_neighbors = sorted(
|
|
947
|
+
new_neighbor_hashes,
|
|
948
|
+
key=lambda nh: neighbor_boosts.get(nh, 0.0),
|
|
949
|
+
reverse=True,
|
|
950
|
+
)[: max(1, min(3, top_k // 3 or 1))]
|
|
951
|
+
allowed_new_neighbors = set(ranked_new_neighbors)
|
|
933
952
|
for store_name, table in [("stm", "stm_memories"), ("ltm", "ltm_memories")]:
|
|
934
953
|
rows = db.execute(f"SELECT * FROM {table}").fetchall()
|
|
935
954
|
for row in rows:
|
|
936
955
|
nh = _canonical_co_id(store_name, row["id"])
|
|
937
|
-
if nh in
|
|
956
|
+
if nh in allowed_new_neighbors:
|
|
938
957
|
boost = neighbor_boosts[nh]
|
|
939
958
|
results.append({
|
|
940
959
|
"store": store_name,
|
|
@@ -951,10 +970,11 @@ def search(
|
|
|
951
970
|
"co_activation_boost": boost,
|
|
952
971
|
"lifecycle_state": row.get("lifecycle_state", "active"),
|
|
953
972
|
})
|
|
954
|
-
|
|
973
|
+
allowed_new_neighbors.discard(nh)
|
|
955
974
|
|
|
956
975
|
# Re-sort after applying boosts
|
|
957
976
|
results.sort(key=lambda x: x["score"], reverse=True)
|
|
977
|
+
results = results[:top_k]
|
|
958
978
|
|
|
959
979
|
# Add rank explanations
|
|
960
980
|
for rank, r in enumerate(results, 1):
|
|
@@ -970,6 +990,7 @@ def search(
|
|
|
970
990
|
if resolved_use_hyde:
|
|
971
991
|
ranking_desc = "hyde_centroid_similarity"
|
|
972
992
|
parts = [f"Ranked #{rank}: {ranking_desc}={score:.3f}"]
|
|
993
|
+
parts.append(f"confidence={_result_confidence(score)}")
|
|
973
994
|
parts.append(f"store={store}, strength={strength:.2f}, accesses={access_count}")
|
|
974
995
|
if r.get("kg_boost"):
|
|
975
996
|
parts.append(f"kg_boost=+{r['kg_boost']:.3f} ({r.get('kg_connections', 0)} edges)")
|
|
@@ -979,6 +1000,12 @@ def search(
|
|
|
979
1000
|
parts.append("hyde=auto")
|
|
980
1001
|
if spreading_depth is None and resolved_spreading_depth > 0:
|
|
981
1002
|
parts.append(f"spreading=auto:{resolved_spreading_depth}")
|
|
1003
|
+
if use_hyde is None and resolved_use_hyde and spreading_depth is None and resolved_spreading_depth > 0:
|
|
1004
|
+
parts.append("auto_strategy=semantic+associative recall")
|
|
1005
|
+
elif use_hyde is None and resolved_use_hyde:
|
|
1006
|
+
parts.append("auto_strategy=semantic expansion")
|
|
1007
|
+
elif spreading_depth is None and resolved_spreading_depth > 0:
|
|
1008
|
+
parts.append("auto_strategy=associative expansion")
|
|
982
1009
|
if created:
|
|
983
1010
|
parts.append(f"created={created[:10]}")
|
|
984
1011
|
if tags:
|
|
@@ -54,18 +54,162 @@ def _codex_bootstrap_config_status() -> dict:
|
|
|
54
54
|
"error": str(exc),
|
|
55
55
|
}
|
|
56
56
|
managed = bool(payload.get("nexo", {}).get("codex", {}).get("bootstrap_managed"))
|
|
57
|
+
mcp_managed = bool(payload.get("nexo", {}).get("codex", {}).get("mcp_managed"))
|
|
57
58
|
initial_messages = payload.get("initial_messages", [])
|
|
58
59
|
has_initial_messages = bool(initial_messages)
|
|
60
|
+
mcp_server = payload.get("mcp_servers", {}).get("nexo", {})
|
|
59
61
|
return {
|
|
60
62
|
"exists": True,
|
|
61
63
|
"path": str(path),
|
|
62
64
|
"bootstrap_managed": managed,
|
|
65
|
+
"mcp_managed": mcp_managed,
|
|
63
66
|
"has_initial_messages": has_initial_messages,
|
|
64
67
|
"model": str(payload.get("model", "") or ""),
|
|
65
68
|
"reasoning_effort": str(payload.get("model_reasoning_effort", "") or ""),
|
|
69
|
+
"has_mcp_server": isinstance(mcp_server, dict) and bool(mcp_server.get("command")) and bool(mcp_server.get("args")),
|
|
70
|
+
"mcp_runtime_home": str((mcp_server.get("env") or {}).get("NEXO_HOME", "") or ""),
|
|
71
|
+
"mcp_runtime_root": str((mcp_server.get("env") or {}).get("NEXO_CODE", "") or ""),
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
def _claude_desktop_shared_brain_status() -> dict:
|
|
76
|
+
path = Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
77
|
+
if not path.is_file():
|
|
78
|
+
return {"exists": False, "path": str(path), "shared_brain_managed": False}
|
|
79
|
+
try:
|
|
80
|
+
payload = json.loads(path.read_text())
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
return {
|
|
83
|
+
"exists": True,
|
|
84
|
+
"path": str(path),
|
|
85
|
+
"shared_brain_managed": False,
|
|
86
|
+
"error": str(exc),
|
|
87
|
+
}
|
|
88
|
+
mcp_server = (payload.get("mcpServers") or {}).get("nexo", {})
|
|
89
|
+
metadata = ((payload.get("nexo") or {}).get("claude_desktop") or {})
|
|
90
|
+
return {
|
|
91
|
+
"exists": True,
|
|
92
|
+
"path": str(path),
|
|
93
|
+
"has_mcp_server": isinstance(mcp_server, dict) and bool(mcp_server.get("command")) and bool(mcp_server.get("args")),
|
|
94
|
+
"shared_brain_managed": bool(metadata.get("shared_brain_managed")),
|
|
95
|
+
"shared_brain_mode": str(metadata.get("shared_brain_mode", "") or ""),
|
|
96
|
+
"managed_runtime_home": str(metadata.get("managed_runtime_home", "") or ""),
|
|
97
|
+
"managed_runtime_root": str(metadata.get("managed_runtime_root", "") or ""),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _recent_codex_session_parity_status(*, days: int = 7, max_files: int = 24) -> dict:
|
|
102
|
+
roots = [
|
|
103
|
+
Path.home() / ".codex" / "sessions",
|
|
104
|
+
Path.home() / ".codex" / "archived_sessions",
|
|
105
|
+
]
|
|
106
|
+
cutoff = time.time() - (days * 86400)
|
|
107
|
+
candidates: list[tuple[float, Path]] = []
|
|
108
|
+
for root in roots:
|
|
109
|
+
if not root.exists():
|
|
110
|
+
continue
|
|
111
|
+
for path in root.rglob("*.jsonl"):
|
|
112
|
+
try:
|
|
113
|
+
mtime = path.stat().st_mtime
|
|
114
|
+
except OSError:
|
|
115
|
+
continue
|
|
116
|
+
if mtime >= cutoff:
|
|
117
|
+
candidates.append((mtime, path))
|
|
118
|
+
candidates.sort(key=lambda item: item[0], reverse=True)
|
|
119
|
+
files = [path for _, path in candidates[:max_files]]
|
|
120
|
+
|
|
121
|
+
status = {
|
|
122
|
+
"files": len(files),
|
|
123
|
+
"bootstrap_sessions": 0,
|
|
124
|
+
"startup_sessions": 0,
|
|
125
|
+
"heartbeat_sessions": 0,
|
|
126
|
+
"origins": set(),
|
|
127
|
+
"samples": [],
|
|
128
|
+
}
|
|
129
|
+
for path in files:
|
|
130
|
+
saw_bootstrap = False
|
|
131
|
+
saw_startup = False
|
|
132
|
+
saw_heartbeat = False
|
|
133
|
+
origin = ""
|
|
134
|
+
try:
|
|
135
|
+
with path.open() as fh:
|
|
136
|
+
for raw in fh:
|
|
137
|
+
if (
|
|
138
|
+
not saw_bootstrap
|
|
139
|
+
and (
|
|
140
|
+
"NEXO Shared Brain for Codex" in raw
|
|
141
|
+
or "<!-- nexo-codex-agents-version:" in raw
|
|
142
|
+
or "You are NEXO" in raw
|
|
143
|
+
)
|
|
144
|
+
):
|
|
145
|
+
saw_bootstrap = True
|
|
146
|
+
try:
|
|
147
|
+
event = json.loads(raw)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
payload = event.get("payload", {})
|
|
151
|
+
if event.get("type") == "session_meta" and isinstance(payload, dict):
|
|
152
|
+
origin = str(payload.get("originator", "") or payload.get("source", "") or "")
|
|
153
|
+
if event.get("type") != "response_item" or not isinstance(payload, dict):
|
|
154
|
+
continue
|
|
155
|
+
if payload.get("type") != "function_call":
|
|
156
|
+
continue
|
|
157
|
+
name = str(payload.get("name", "") or "")
|
|
158
|
+
if name in {"mcp__nexo__nexo_startup", "nexo_startup"}:
|
|
159
|
+
saw_startup = True
|
|
160
|
+
elif name in {"mcp__nexo__nexo_heartbeat", "nexo_heartbeat"}:
|
|
161
|
+
saw_heartbeat = True
|
|
162
|
+
if saw_bootstrap and saw_startup and saw_heartbeat and origin:
|
|
163
|
+
break
|
|
164
|
+
except Exception:
|
|
165
|
+
continue
|
|
166
|
+
if origin:
|
|
167
|
+
status["origins"].add(origin)
|
|
168
|
+
if saw_bootstrap:
|
|
169
|
+
status["bootstrap_sessions"] += 1
|
|
170
|
+
if saw_startup:
|
|
171
|
+
status["startup_sessions"] += 1
|
|
172
|
+
if saw_heartbeat:
|
|
173
|
+
status["heartbeat_sessions"] += 1
|
|
174
|
+
status["samples"].append(
|
|
175
|
+
{
|
|
176
|
+
"file": str(path),
|
|
177
|
+
"bootstrap": saw_bootstrap,
|
|
178
|
+
"startup": saw_startup,
|
|
179
|
+
"heartbeat": saw_heartbeat,
|
|
180
|
+
"origin": origin,
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
status["origins"] = sorted(status["origins"])
|
|
184
|
+
return status
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _client_assumption_regressions() -> list[str]:
|
|
188
|
+
src_root = NEXO_CODE / "src"
|
|
189
|
+
if not src_root.is_dir():
|
|
190
|
+
return []
|
|
191
|
+
allowed_claude_projects = {
|
|
192
|
+
(src_root / "scripts" / "deep-sleep" / "collect.py").resolve(),
|
|
193
|
+
}
|
|
194
|
+
offenders: list[str] = []
|
|
195
|
+
for path in src_root.rglob("*.py"):
|
|
196
|
+
try:
|
|
197
|
+
text = path.read_text()
|
|
198
|
+
except Exception:
|
|
199
|
+
continue
|
|
200
|
+
resolved = path.resolve()
|
|
201
|
+
if ".claude/projects" in text and resolved not in allowed_claude_projects:
|
|
202
|
+
offenders.append(f"{path.relative_to(NEXO_CODE)} hardcodes ~/.claude/projects")
|
|
203
|
+
collect_path = src_root / "scripts" / "deep-sleep" / "collect.py"
|
|
204
|
+
try:
|
|
205
|
+
collect_text = collect_path.read_text()
|
|
206
|
+
except Exception:
|
|
207
|
+
collect_text = ""
|
|
208
|
+
if collect_text and (".claude/projects" in collect_text) and (".codex" not in collect_text or "find_codex_session_files" not in collect_text):
|
|
209
|
+
offenders.append("deep-sleep/collect.py references Claude transcripts without Codex transcript parity")
|
|
210
|
+
return offenders
|
|
211
|
+
|
|
212
|
+
|
|
69
213
|
def _file_age_seconds(path: Path) -> float | None:
|
|
70
214
|
"""Return file age in seconds, or None if not found."""
|
|
71
215
|
try:
|
|
@@ -1093,6 +1237,11 @@ def check_client_bootstrap_parity(fix: bool = False) -> DoctorCheck:
|
|
|
1093
1237
|
severity = "warn"
|
|
1094
1238
|
evidence.append(f"codex config missing managed bootstrap injection at {codex_config.get('path')}")
|
|
1095
1239
|
repair_plan.append("Run `nexo clients sync` or `nexo update` so plain Codex sessions inherit the NEXO bootstrap")
|
|
1240
|
+
elif codex_config.get("exists") and not codex_config.get("has_mcp_server"):
|
|
1241
|
+
status = "degraded"
|
|
1242
|
+
severity = "warn"
|
|
1243
|
+
evidence.append(f"codex config missing managed `mcp_servers.nexo` at {codex_config.get('path')}")
|
|
1244
|
+
repair_plan.append("Re-sync Codex so manual sessions keep the shared brain even if `codex mcp add` state drifts")
|
|
1096
1245
|
elif codex_config.get("exists"):
|
|
1097
1246
|
evidence.append(
|
|
1098
1247
|
"codex config bootstrap managed"
|
|
@@ -1140,6 +1289,151 @@ def check_client_bootstrap_parity(fix: bool = False) -> DoctorCheck:
|
|
|
1140
1289
|
)
|
|
1141
1290
|
|
|
1142
1291
|
|
|
1292
|
+
def check_codex_session_parity() -> DoctorCheck:
|
|
1293
|
+
try:
|
|
1294
|
+
schedule = _load_json(SCHEDULE_FILE) if SCHEDULE_FILE.is_file() else {}
|
|
1295
|
+
except Exception:
|
|
1296
|
+
schedule = {}
|
|
1297
|
+
prefs = normalize_client_preferences(schedule)
|
|
1298
|
+
wants_codex = bool(
|
|
1299
|
+
prefs.get("interactive_clients", {}).get("codex")
|
|
1300
|
+
or prefs.get("default_terminal_client") == "codex"
|
|
1301
|
+
or (prefs.get("automation_enabled", True) and prefs.get("automation_backend") == "codex")
|
|
1302
|
+
)
|
|
1303
|
+
if not wants_codex:
|
|
1304
|
+
return DoctorCheck(
|
|
1305
|
+
id="runtime.codex_sessions",
|
|
1306
|
+
tier="runtime",
|
|
1307
|
+
status="healthy",
|
|
1308
|
+
severity="info",
|
|
1309
|
+
summary="Codex session parity check skipped (Codex not selected)",
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
audit = _recent_codex_session_parity_status()
|
|
1313
|
+
if audit["files"] == 0:
|
|
1314
|
+
return DoctorCheck(
|
|
1315
|
+
id="runtime.codex_sessions",
|
|
1316
|
+
tier="runtime",
|
|
1317
|
+
status="degraded",
|
|
1318
|
+
severity="warn",
|
|
1319
|
+
summary="No recent Codex sessions found to verify startup discipline",
|
|
1320
|
+
repair_plan=[
|
|
1321
|
+
"Start Codex through `nexo chat` at least once so doctor can verify recent NEXO startup behavior",
|
|
1322
|
+
],
|
|
1323
|
+
escalation_prompt=(
|
|
1324
|
+
"Codex is selected, but there are no recent durable Codex sessions to inspect. "
|
|
1325
|
+
"NEXO cannot prove that manual Codex sessions are entering the shared-brain startup flow."
|
|
1326
|
+
),
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
evidence = [
|
|
1330
|
+
f"recent codex sessions inspected: {audit['files']}",
|
|
1331
|
+
f"bootstrap markers seen in {audit['bootstrap_sessions']}/{audit['files']}",
|
|
1332
|
+
f"nexo_startup seen in {audit['startup_sessions']}/{audit['files']}",
|
|
1333
|
+
f"nexo_heartbeat seen in {audit['heartbeat_sessions']}/{audit['files']}",
|
|
1334
|
+
]
|
|
1335
|
+
if audit["origins"]:
|
|
1336
|
+
evidence.append(f"origins: {', '.join(audit['origins'])}")
|
|
1337
|
+
|
|
1338
|
+
status = "healthy"
|
|
1339
|
+
severity = "info"
|
|
1340
|
+
repair_plan: list[str] = []
|
|
1341
|
+
if audit["bootstrap_sessions"] == 0:
|
|
1342
|
+
status = "degraded"
|
|
1343
|
+
severity = "warn"
|
|
1344
|
+
repair_plan.append("Run `nexo update` or `nexo clients sync` so plain Codex sessions inherit the managed bootstrap")
|
|
1345
|
+
if audit["startup_sessions"] == 0:
|
|
1346
|
+
status = "degraded"
|
|
1347
|
+
severity = "warn"
|
|
1348
|
+
repair_plan.append("Use `nexo chat` or keep the global Codex bootstrap intact so sessions actually call `nexo_startup`")
|
|
1349
|
+
|
|
1350
|
+
return DoctorCheck(
|
|
1351
|
+
id="runtime.codex_sessions",
|
|
1352
|
+
tier="runtime",
|
|
1353
|
+
status=status,
|
|
1354
|
+
severity=severity,
|
|
1355
|
+
summary="Recent Codex sessions show NEXO startup discipline" if status == "healthy" else "Recent Codex sessions need stronger NEXO startup discipline",
|
|
1356
|
+
evidence=evidence,
|
|
1357
|
+
repair_plan=repair_plan,
|
|
1358
|
+
escalation_prompt=(
|
|
1359
|
+
"Codex is selected, but recent durable Codex sessions are not consistently showing NEXO bootstrap markers or `nexo_startup`. "
|
|
1360
|
+
"Manual Codex sessions may still be starting too plain."
|
|
1361
|
+
) if status != "healthy" else "",
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def check_claude_desktop_shared_brain() -> DoctorCheck:
|
|
1366
|
+
try:
|
|
1367
|
+
schedule = _load_json(SCHEDULE_FILE) if SCHEDULE_FILE.is_file() else {}
|
|
1368
|
+
except Exception:
|
|
1369
|
+
schedule = {}
|
|
1370
|
+
prefs = normalize_client_preferences(schedule)
|
|
1371
|
+
wants_desktop = bool(prefs.get("interactive_clients", {}).get("claude_desktop"))
|
|
1372
|
+
installed = detect_installed_clients().get("claude_desktop", {}).get("installed", False)
|
|
1373
|
+
status_info = _claude_desktop_shared_brain_status()
|
|
1374
|
+
|
|
1375
|
+
if not wants_desktop and not installed:
|
|
1376
|
+
return DoctorCheck(
|
|
1377
|
+
id="runtime.claude_desktop",
|
|
1378
|
+
tier="runtime",
|
|
1379
|
+
status="healthy",
|
|
1380
|
+
severity="info",
|
|
1381
|
+
summary="Claude Desktop shared-brain check skipped (client not installed)",
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
evidence = [
|
|
1385
|
+
f"config: {status_info.get('path')}",
|
|
1386
|
+
f"shared brain mode: {status_info.get('shared_brain_mode') or 'mcp_only'}",
|
|
1387
|
+
]
|
|
1388
|
+
if status_info.get("managed_runtime_home"):
|
|
1389
|
+
evidence.append(f"runtime home: {status_info.get('managed_runtime_home')}")
|
|
1390
|
+
|
|
1391
|
+
if status_info.get("error"):
|
|
1392
|
+
return DoctorCheck(
|
|
1393
|
+
id="runtime.claude_desktop",
|
|
1394
|
+
tier="runtime",
|
|
1395
|
+
status="degraded",
|
|
1396
|
+
severity="warn",
|
|
1397
|
+
summary="Claude Desktop config is unreadable",
|
|
1398
|
+
evidence=evidence + [status_info["error"]],
|
|
1399
|
+
repair_plan=["Repair Claude Desktop config JSON and re-run `nexo clients sync`"],
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
if not status_info.get("exists") or not status_info.get("has_mcp_server"):
|
|
1403
|
+
return DoctorCheck(
|
|
1404
|
+
id="runtime.claude_desktop",
|
|
1405
|
+
tier="runtime",
|
|
1406
|
+
status="degraded",
|
|
1407
|
+
severity="warn",
|
|
1408
|
+
summary="Claude Desktop is not pointed at the shared NEXO brain",
|
|
1409
|
+
evidence=evidence,
|
|
1410
|
+
repair_plan=["Run `nexo clients sync` so Claude Desktop shares the same local brain"],
|
|
1411
|
+
escalation_prompt=(
|
|
1412
|
+
"Claude Desktop is installed or enabled, but its MCP config does not show the shared `nexo` runtime."
|
|
1413
|
+
),
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
if not status_info.get("shared_brain_managed"):
|
|
1417
|
+
return DoctorCheck(
|
|
1418
|
+
id="runtime.claude_desktop",
|
|
1419
|
+
tier="runtime",
|
|
1420
|
+
status="degraded",
|
|
1421
|
+
severity="warn",
|
|
1422
|
+
summary="Claude Desktop shares NEXO, but managed metadata is missing",
|
|
1423
|
+
evidence=evidence,
|
|
1424
|
+
repair_plan=["Re-sync Claude Desktop so doctor can verify the managed shared-brain contract"],
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
return DoctorCheck(
|
|
1428
|
+
id="runtime.claude_desktop",
|
|
1429
|
+
tier="runtime",
|
|
1430
|
+
status="healthy",
|
|
1431
|
+
severity="info",
|
|
1432
|
+
summary="Claude Desktop shared-brain parity OK (MCP-only mode)",
|
|
1433
|
+
evidence=evidence,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
|
|
1143
1437
|
def check_transcript_source_parity() -> DoctorCheck:
|
|
1144
1438
|
"""Check whether Deep Sleep can see transcript sources for the selected clients."""
|
|
1145
1439
|
try:
|
|
@@ -1207,6 +1501,34 @@ def check_transcript_source_parity() -> DoctorCheck:
|
|
|
1207
1501
|
)
|
|
1208
1502
|
|
|
1209
1503
|
|
|
1504
|
+
def check_client_assumption_regressions() -> DoctorCheck:
|
|
1505
|
+
offenders = _client_assumption_regressions()
|
|
1506
|
+
if not offenders:
|
|
1507
|
+
return DoctorCheck(
|
|
1508
|
+
id="runtime.client_assumptions",
|
|
1509
|
+
tier="runtime",
|
|
1510
|
+
status="healthy",
|
|
1511
|
+
severity="info",
|
|
1512
|
+
summary="No new Claude-only runtime path assumptions detected",
|
|
1513
|
+
)
|
|
1514
|
+
return DoctorCheck(
|
|
1515
|
+
id="runtime.client_assumptions",
|
|
1516
|
+
tier="runtime",
|
|
1517
|
+
status="critical",
|
|
1518
|
+
severity="error",
|
|
1519
|
+
summary=f"Detected {len(offenders)} client-parity regression(s) in runtime source",
|
|
1520
|
+
evidence=offenders[:10],
|
|
1521
|
+
repair_plan=[
|
|
1522
|
+
"Replace Claude-only transcript or hook assumptions with shared client abstractions",
|
|
1523
|
+
"Keep Deep Sleep and startup flows aware of both Claude Code and Codex surfaces",
|
|
1524
|
+
],
|
|
1525
|
+
escalation_prompt=(
|
|
1526
|
+
"A runtime source file drifted back to a Claude-only assumption. "
|
|
1527
|
+
"Audit the offending file and restore client-agnostic parity before shipping."
|
|
1528
|
+
),
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
|
|
1210
1532
|
def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
1211
1533
|
"""Run all runtime-tier checks. Read-only by default."""
|
|
1212
1534
|
return [
|
|
@@ -1216,7 +1538,10 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
|
1216
1538
|
check_cron_freshness(),
|
|
1217
1539
|
check_client_backend_preferences(),
|
|
1218
1540
|
check_client_bootstrap_parity(fix=fix),
|
|
1541
|
+
check_codex_session_parity(),
|
|
1542
|
+
check_claude_desktop_shared_brain(),
|
|
1219
1543
|
check_transcript_source_parity(),
|
|
1544
|
+
check_client_assumption_regressions(),
|
|
1220
1545
|
check_launchagent_integrity(fix=fix),
|
|
1221
1546
|
check_personal_script_registry(fix=fix),
|
|
1222
1547
|
check_skill_health(fix=fix),
|
|
@@ -65,6 +65,10 @@ def handle_cognitive_retrieve(
|
|
|
65
65
|
mode_parts.append(f"spreading={spreading_depth}")
|
|
66
66
|
elif spreading_depth is None:
|
|
67
67
|
mode_parts.append("spreading=AUTO")
|
|
68
|
+
if results:
|
|
69
|
+
top_score = float(results[0].get("score", 0.0) or 0.0)
|
|
70
|
+
confidence = "high" if top_score >= 0.82 else "medium" if top_score >= 0.66 else "low"
|
|
71
|
+
mode_parts.append(f"top_confidence={confidence}")
|
|
68
72
|
header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results ({', '.join(mode_parts)})\n\n"
|
|
69
73
|
return header + formatted
|
|
70
74
|
|