nexo-brain 2.6.16 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.6.16",
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,17 @@ 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, and `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.
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.
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`.
46
52
 
47
53
  ### Client Capability Matrix
48
54
 
@@ -50,11 +56,12 @@ Version `2.6.14` closes those parity gaps in practice, `2.6.15` hardens the inst
50
56
  |------------|-------------|-------|----------------|
51
57
  | Shared brain / MCP runtime | Yes | Yes | Yes |
52
58
  | Managed bootstrap document | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | Not applicable |
53
- | Global startup bootstrap sync | Native via hooks + bootstrap | Managed via bootstrap + Codex config `initial_messages` | MCP only |
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 |
54
60
  | `nexo chat` terminal client | Yes | Yes | No |
55
61
  | Background automation backend | Recommended | Supported | No |
56
62
  | Raw transcript source for Deep Sleep | Yes | Yes | No |
57
63
  | Native hook depth | Deepest | Partial, compensated | None |
64
+ | Runtime doctor parity audit | Yes | Yes | Shared-brain only |
58
65
  | Recommended today | Yes | Supported | Shared-brain companion |
59
66
 
60
67
  ## The Problem
@@ -191,6 +198,9 @@ Deep Sleep now also mixes **recent context with older context across a 60-day ho
191
198
  - recurring multi-week themes
192
199
  - cross-domain links between older learnings and current failures
193
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.
194
204
 
195
205
  ## Cognitive Cortex
196
206
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.6.16",
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",
@@ -1434,10 +1434,19 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
1434
1434
 
1435
1435
  schedule_path = dest / "config" / "schedule.json"
1436
1436
  schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
1437
+ normalized_preferences = normalize_client_preferences(schedule_payload)
1438
+ if normalized_preferences != {
1439
+ key: schedule_payload.get(key)
1440
+ for key in normalized_preferences
1441
+ }:
1442
+ merged_schedule = dict(schedule_payload)
1443
+ merged_schedule.update(normalized_preferences)
1444
+ schedule_path.parent.mkdir(parents=True, exist_ok=True)
1445
+ schedule_path.write_text(json.dumps(merged_schedule, indent=2, ensure_ascii=False) + "\n")
1437
1446
  client_sync_result = sync_all_clients(
1438
1447
  nexo_home=dest,
1439
1448
  runtime_root=dest,
1440
- preferences=normalize_client_preferences(schedule_payload),
1449
+ preferences=normalized_preferences,
1441
1450
  )
1442
1451
  if client_sync_result.get("ok"):
1443
1452
  actions.append("client-sync")
@@ -73,7 +73,9 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
73
73
  version_file = nexo_home / "version.json"
74
74
  if version_file.is_file():
75
75
  try:
76
- return str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
76
+ candidate = str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
77
+ if candidate:
78
+ return candidate
77
79
  except Exception:
78
80
  pass
79
81
  return "NEXO"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import os
6
6
  import shutil
7
7
  import sys
8
+ import tomllib
8
9
  from pathlib import Path
9
10
 
10
11
  from runtime_power import load_schedule_config, save_schedule_config
@@ -51,6 +52,14 @@ def _user_home() -> Path:
51
52
  return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
52
53
 
53
54
 
55
+ def _codex_config_path(home: Path) -> Path:
56
+ return home / ".codex" / "config.toml"
57
+
58
+
59
+ def _codex_bootstrap_path(home: Path) -> Path:
60
+ return home / ".codex" / "AGENTS.md"
61
+
62
+
54
63
  def _coerce_bool(value, default: bool) -> bool:
55
64
  if isinstance(value, bool):
56
65
  return value
@@ -127,6 +136,56 @@ def normalize_interactive_clients(value) -> dict[str, bool]:
127
136
  return normalized
128
137
 
129
138
 
139
+ def _codex_artifacts_suggest_nexo_management(home: Path) -> bool:
140
+ bootstrap_path = _codex_bootstrap_path(home)
141
+ if bootstrap_path.is_file():
142
+ try:
143
+ bootstrap_text = bootstrap_path.read_text()
144
+ except Exception:
145
+ bootstrap_text = ""
146
+ if (
147
+ "nexo-codex-agents-version:" in bootstrap_text
148
+ or "NEXO Shared Brain for Codex" in bootstrap_text
149
+ or "<!-- nexo:core:start -->" in bootstrap_text
150
+ ):
151
+ return True
152
+
153
+ config_path = _codex_config_path(home)
154
+ if not config_path.is_file():
155
+ return False
156
+
157
+ try:
158
+ payload = tomllib.loads(config_path.read_text())
159
+ except Exception:
160
+ try:
161
+ raw_text = config_path.read_text()
162
+ except Exception:
163
+ return False
164
+ return "[mcp_servers.nexo]" in raw_text or "[nexo.codex]" in raw_text
165
+
166
+ if not isinstance(payload, dict):
167
+ return False
168
+ mcp_servers = payload.get("mcp_servers")
169
+ if isinstance(mcp_servers, dict) and "nexo" in mcp_servers:
170
+ return True
171
+ nexo_table = payload.get("nexo")
172
+ if isinstance(nexo_table, dict) and "codex" in nexo_table:
173
+ return True
174
+ return False
175
+
176
+
177
+ def _backfill_interactive_clients(
178
+ interactive_clients: dict[str, bool],
179
+ *,
180
+ user_home: str | os.PathLike[str] | None = None,
181
+ ) -> dict[str, bool]:
182
+ normalized = dict(interactive_clients)
183
+ home = Path(user_home).expanduser() if user_home else _user_home()
184
+ if not normalized.get(CLIENT_CODEX, False) and _codex_artifacts_suggest_nexo_management(home):
185
+ normalized[CLIENT_CODEX] = True
186
+ return normalized
187
+
188
+
130
189
  def normalize_default_terminal_client(value, interactive_clients: dict[str, bool] | None = None) -> str:
131
190
  interactive_clients = normalize_interactive_clients(interactive_clients or {})
132
191
  candidate = normalize_client_key(value)
@@ -210,9 +269,16 @@ def normalize_client_runtime_profiles(value) -> dict[str, dict[str, str]]:
210
269
  return normalized
211
270
 
212
271
 
213
- def normalize_client_preferences(schedule: dict | None = None) -> dict:
272
+ def normalize_client_preferences(
273
+ schedule: dict | None = None,
274
+ *,
275
+ user_home: str | os.PathLike[str] | None = None,
276
+ ) -> dict:
214
277
  schedule = dict(schedule or {})
215
- interactive_clients = normalize_interactive_clients(schedule.get("interactive_clients"))
278
+ interactive_clients = _backfill_interactive_clients(
279
+ normalize_interactive_clients(schedule.get("interactive_clients")),
280
+ user_home=user_home,
281
+ )
216
282
  automation_enabled = normalize_automation_enabled(schedule.get("automation_enabled"))
217
283
  default_terminal_client = normalize_default_terminal_client(
218
284
  schedule.get("default_terminal_client"),
@@ -80,10 +80,12 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
80
80
  version_file = nexo_home / "version.json"
81
81
  if version_file.is_file():
82
82
  try:
83
- return str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
83
+ candidate = str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
84
+ if candidate:
85
+ return candidate
84
86
  except Exception:
85
87
  pass
86
- return ""
88
+ return "NEXO"
87
89
 
88
90
 
89
91
  def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str] | None = None) -> Path:
@@ -241,10 +243,12 @@ def _sync_codex_managed_config(
241
243
  *,
242
244
  bootstrap_prompt: str,
243
245
  runtime_profile: dict | None,
246
+ server_config: dict | None,
244
247
  ) -> dict:
245
248
  payload = _load_toml_object(path)
246
249
  action = "updated" if payload else "created"
247
250
  runtime_profile = dict(runtime_profile or {})
251
+ server_config = dict(server_config or {})
248
252
 
249
253
  if runtime_profile.get("model"):
250
254
  payload["model"] = runtime_profile["model"]
@@ -261,10 +265,19 @@ def _sync_codex_managed_config(
261
265
  nexo_table = payload.setdefault("nexo", {})
262
266
  codex_table = nexo_table.setdefault("codex", {})
263
267
  codex_table["bootstrap_managed"] = True
268
+ codex_table["mcp_managed"] = True
264
269
  codex_table["bootstrap_bytes"] = len(bootstrap_prompt.encode("utf-8")) if bootstrap_prompt else 0
265
270
  if runtime_profile.get("model"):
266
271
  codex_table["managed_model"] = runtime_profile["model"]
267
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", "")
268
281
 
269
282
  _write_toml_object(path, payload)
270
283
  return {
@@ -272,6 +285,7 @@ def _sync_codex_managed_config(
272
285
  "action": action,
273
286
  "path": str(path),
274
287
  "bootstrap_managed": True,
288
+ "mcp_managed": True,
275
289
  "model": runtime_profile.get("model", ""),
276
290
  "reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
277
291
  }
@@ -294,7 +308,7 @@ def _write_json_object(path: Path, payload: dict) -> None:
294
308
  path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
295
309
 
296
310
 
297
- 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:
298
312
  payload = _load_json_object(path)
299
313
  mcp_servers = payload.setdefault("mcpServers", {})
300
314
  if not isinstance(mcp_servers, dict):
@@ -302,6 +316,12 @@ def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
302
316
  payload["mcpServers"] = mcp_servers
303
317
  action = "updated" if "nexo" in mcp_servers else "created"
304
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)
305
325
  _write_json_object(path, payload)
306
326
  return {
307
327
  "ok": True,
@@ -311,6 +331,18 @@ def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
311
331
  }
312
332
 
313
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
+
314
346
  def sync_claude_code(
315
347
  *,
316
348
  nexo_home: str | os.PathLike[str] | None = None,
@@ -359,10 +391,15 @@ def sync_claude_desktop(
359
391
  python_path=python_path,
360
392
  operator_name=operator_name,
361
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
+ )
362
398
  return _sync_json_client(
363
399
  _claude_desktop_config_path(Path(user_home).expanduser() if user_home else None),
364
400
  server_config,
365
401
  "claude_desktop",
402
+ managed_metadata=_claude_desktop_managed_metadata(server_config, operator_name=resolved_name),
366
403
  )
367
404
 
368
405
 
@@ -408,6 +445,7 @@ def sync_codex(
408
445
  config_path,
409
446
  bootstrap_prompt=prompt_text,
410
447
  runtime_profile=runtime_profile,
448
+ server_config=server_config,
411
449
  )
412
450
  return result
413
451
 
@@ -423,29 +461,12 @@ def sync_codex(
423
461
  timeout=30,
424
462
  env=env,
425
463
  )
426
- if result.returncode != 0:
427
- result = {
428
- "ok": False,
429
- "client": "codex",
430
- "path": str(config_path),
431
- "error": (result.stderr or result.stdout or "codex mcp add failed").strip(),
432
- }
433
- bootstrap_result = sync_client_bootstrap(
434
- "codex",
435
- nexo_home=nexo_home,
436
- operator_name=operator_name,
437
- user_home=user_home,
438
- )
439
- result["bootstrap"] = bootstrap_result
440
- if not bootstrap_result.get("ok"):
441
- result["error"] = f"{result['error']}; bootstrap: {bootstrap_result.get('error', 'unknown error')}"
442
- return result
443
464
  sync_result = {
444
465
  "ok": True,
445
466
  "client": "codex",
446
467
  "action": "updated",
447
468
  "path": str(config_path),
448
- "mode": "cli",
469
+ "mode": "cli" if result.returncode == 0 else "config_only",
449
470
  }
450
471
  bootstrap_result = sync_client_bootstrap(
451
472
  "codex",
@@ -462,7 +483,10 @@ def sync_codex(
462
483
  config_path,
463
484
  bootstrap_prompt=bootstrap_result.get("content") or "",
464
485
  runtime_profile=runtime_profile,
486
+ server_config=server_config,
465
487
  )
488
+ if result.returncode != 0:
489
+ sync_result["warning"] = (result.stderr or result.stdout or "codex mcp add failed").strip()
466
490
  return sync_result
467
491
 
468
492
 
@@ -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 = set(neighbor_boosts.keys()) - existing_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 new_neighbor_hashes:
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
- new_neighbor_hashes.discard(nh)
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: