nexo-brain 7.11.0 → 7.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.11.0",
3
+ "version": "7.11.1",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,7 @@
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.11.0` is the current packaged-runtime line. Minor release — introduces a runtime fingerprint that gates `mcp-restart-required.json`. `nexo update` now only forces connected MCP clients (Claude Code, Codex, Claude Desktop) to restart when at least one `.py` file the running server actually imports has changed. README-only, blog-only, changelog-only releases skip the restart entirely. Conservative fallback (#186): if the fingerprint can't be computed, the gate behaves like the legacy version-string check and writes the marker. Explicit opt-in escape hatch via `"force_restart": true` in `version.json`. Marker schema bumped to v2 with optional `from_fingerprint` / `to_fingerprint`. Full write-up in [`docs/runtime-fingerprint.md`](docs/runtime-fingerprint.md).
21
+ Version `7.11.1` is the current packaged-runtime line. Patch release — caches the runtime fingerprint by `(file_count, size_total, max_mtime)` signature so MCP startup and the per-tool-call `resolve_restart_required` skip the 263-file rehash when nothing on disk changed. ~11× speedup warm path (~40ms ~3.7ms locally), ~10-20s/day saved across Claude Code / Codex / headless / deep-sleep / cron startups. Cache miss is always safe (falls through to full hash and self-repairs). Default `use_cache=False` keeps `plugins/update.py` on the ground-truth path around `git pull` / `npm update`. Builds on the v7.11.0 runtime fingerprint that gates `mcp-restart-required.json`. Full write-up in [`docs/runtime-fingerprint.md`](docs/runtime-fingerprint.md).
22
22
 
23
23
  Previously in `7.10.0`: minor release — **removes the LLM proxy override path that 7.9.28 → 7.9.34 introduced**. Background: 7.9.28 added two opt-in files at `~/.nexo/config/llm_endpoint.json` and `~/.nexo/config/auth_provider.json` that let a third-party orchestrator (NEXO Desktop) redirect every Anthropic SDK call from Brain to a custom proxy and resolve the bearer via a local helper, with concrete model names translated to wire aliases (`nexo-max`, `nexo-high`, `nexo-medium`, `nexo-low`, `nexo-mini`) and an `Idempotency-Key` per request. NEXO Desktop's commercial model has changed: Desktop is now a wrapper over the user's own Claude Code subscription (Max / Pro), with a separate Desktop licence. Brain calls go directly to `api.anthropic.com` using the user's existing OAuth (the one stored under `~/.claude/` and consumed by Claude Code spawns) or a plain `ANTHROPIC_API_KEY`. There is no NEXO bearer, no NEXO proxy, no NEXO credit accounting in this codebase. Every proxy symbol is gone from `call_model_raw.py` and `agent_runner.py`; the proxy-specific tests and `docs/api/override-files.md` are removed; any pre-existing override files on disk are simply ignored from this release forward.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.11.0",
3
+ "version": "7.11.1",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -160,6 +160,98 @@ def restart_required_marker_path() -> Path:
160
160
  return paths.operations_dir() / "mcp-restart-required.json"
161
161
 
162
162
 
163
+ def fingerprint_cache_path() -> Path:
164
+ """Where the runtime fingerprint cache lives.
165
+
166
+ The cache lets `prime_process_fingerprint()` and `installed_runtime_fingerprint()`
167
+ skip hashing 200+ source files on every MCP startup / tool call when the
168
+ runtime tree on disk hasn't changed (same file count, same total size, same
169
+ max mtime). Invalidates automatically when any source byte changes.
170
+ """
171
+ return paths.operations_dir() / "fingerprint-cache.json"
172
+
173
+
174
+ def _runtime_tree_signature(src_dir: Path) -> tuple[int, int, float] | None:
175
+ """Cheap stat-only walk over the fingerprint-tracked tree.
176
+
177
+ Returns ``(file_count, size_total, max_mtime)`` or ``None`` when the source
178
+ tree cannot be traversed. This is the cache key — if it matches, the bytes
179
+ haven't changed in any way the fingerprint would care about.
180
+ """
181
+ try:
182
+ files = _iter_runtime_source_files(src_dir)
183
+ except Exception:
184
+ return None
185
+ if not files:
186
+ return None
187
+ count = 0
188
+ size_total = 0
189
+ max_mtime = 0.0
190
+ for path in files:
191
+ try:
192
+ st = path.stat()
193
+ except Exception:
194
+ return None
195
+ count += 1
196
+ size_total += int(st.st_size)
197
+ if st.st_mtime > max_mtime:
198
+ max_mtime = float(st.st_mtime)
199
+ return (count, size_total, max_mtime)
200
+
201
+
202
+ def _read_fingerprint_cache(src_dir: Path) -> str:
203
+ """Return cached fingerprint when the on-disk signature still matches.
204
+
205
+ Empty string means cache miss (corrupt, missing, or signature drifted).
206
+ Cache miss is always safe — caller falls through to a full hash.
207
+ """
208
+ cache_path = fingerprint_cache_path()
209
+ if not cache_path.is_file():
210
+ return ""
211
+ try:
212
+ payload = json.loads(cache_path.read_text(encoding="utf-8"))
213
+ except Exception:
214
+ return ""
215
+ if not isinstance(payload, dict):
216
+ return ""
217
+ if str(payload.get("src_dir") or "") != str(src_dir):
218
+ return ""
219
+ sig = _runtime_tree_signature(src_dir)
220
+ if sig is None:
221
+ return ""
222
+ try:
223
+ cached_count = int(payload.get("file_count"))
224
+ cached_size = int(payload.get("size_total"))
225
+ cached_mtime = float(payload.get("max_mtime"))
226
+ except (TypeError, ValueError):
227
+ return ""
228
+ if cached_count != sig[0] or cached_size != sig[1] or cached_mtime != sig[2]:
229
+ return ""
230
+ fingerprint = str(payload.get("fingerprint") or "").strip()
231
+ return fingerprint
232
+
233
+
234
+ def _write_fingerprint_cache(src_dir: Path, fingerprint: str) -> None:
235
+ """Persist the fingerprint+signature pair. Best-effort; failures don't propagate."""
236
+ if not fingerprint:
237
+ return
238
+ sig = _runtime_tree_signature(src_dir)
239
+ if sig is None:
240
+ return
241
+ payload = {
242
+ "fingerprint": fingerprint,
243
+ "src_dir": str(src_dir),
244
+ "file_count": sig[0],
245
+ "size_total": sig[1],
246
+ "max_mtime": sig[2],
247
+ "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
248
+ }
249
+ try:
250
+ _write_json_atomic(fingerprint_cache_path(), payload)
251
+ except Exception:
252
+ pass
253
+
254
+
163
255
  def _candidate_version_files(base: Path) -> list[Path]:
164
256
  return [
165
257
  base / "version.json",
@@ -225,7 +317,9 @@ def _iter_runtime_source_files(src_dir: Path) -> list[Path]:
225
317
  return out
226
318
 
227
319
 
228
- def compute_mcp_runtime_fingerprint(src_dir: Path | None = None) -> str:
320
+ def compute_mcp_runtime_fingerprint(
321
+ src_dir: Path | None = None, *, use_cache: bool = False
322
+ ) -> str:
229
323
  """Hash of every Python source file the running MCP can import.
230
324
 
231
325
  Returns a sha256 hex digest, or "" when the source tree cannot be located
@@ -240,6 +334,14 @@ def compute_mcp_runtime_fingerprint(src_dir: Path | None = None) -> str:
240
334
  * non-`.py` assets (docs, blogs, READMEs, JSON/YAML configs, templates,
241
335
  CHANGELOG, marketing files) — these never affect what the live MCP
242
336
  process executes
337
+
338
+ When ``use_cache=True`` (hot paths: server startup, every tool call) the
339
+ function consults ``fingerprint-cache.json``: if the on-disk tree
340
+ signature (file count + total size + max mtime) still matches the cached
341
+ one, the cached digest is returned without re-reading any byte. Cache miss
342
+ falls through to the normal full-hash path and writes a fresh entry. The
343
+ update flow keeps ``use_cache=False`` (default) so it always sees ground
344
+ truth around the pull/npm step.
243
345
  """
244
346
  if src_dir is None:
245
347
  candidates: list[Path] = []
@@ -267,6 +369,11 @@ def compute_mcp_runtime_fingerprint(src_dir: Path | None = None) -> str:
267
369
  if src_dir is None:
268
370
  return ""
269
371
 
372
+ if use_cache:
373
+ cached = _read_fingerprint_cache(src_dir)
374
+ if cached:
375
+ return cached
376
+
270
377
  files = _iter_runtime_source_files(src_dir)
271
378
  if not files:
272
379
  return ""
@@ -283,11 +390,19 @@ def compute_mcp_runtime_fingerprint(src_dir: Path | None = None) -> str:
283
390
  except Exception:
284
391
  return ""
285
392
  h.update(b"\n")
286
- return h.hexdigest()
393
+ digest = h.hexdigest()
394
+ if use_cache and digest:
395
+ _write_fingerprint_cache(src_dir, digest)
396
+ return digest
287
397
 
288
398
 
289
399
  def installed_runtime_fingerprint() -> str:
290
- """Fingerprint of whatever runtime source tree is on disk right now."""
400
+ """Fingerprint of whatever runtime source tree is on disk right now.
401
+
402
+ Hot path — runs on every MCP tool call via ``resolve_restart_required``.
403
+ Uses the disk-signature cache so a repeated call without any source
404
+ change is a few stat() syscalls instead of 200+ file reads.
405
+ """
291
406
  candidates: list[Path] = []
292
407
  try:
293
408
  root = active_runtime_root()
@@ -308,7 +423,7 @@ def installed_runtime_fingerprint() -> str:
308
423
  except Exception:
309
424
  pass
310
425
  for cand in candidates:
311
- fp = compute_mcp_runtime_fingerprint(cand)
426
+ fp = compute_mcp_runtime_fingerprint(cand, use_cache=True)
312
427
  if fp:
313
428
  return fp
314
429
  return ""
@@ -616,7 +731,7 @@ def prime_process_fingerprint() -> str:
616
731
  except Exception:
617
732
  pass
618
733
  for cand in candidates:
619
- fp = compute_mcp_runtime_fingerprint(cand)
734
+ fp = compute_mcp_runtime_fingerprint(cand, use_cache=True)
620
735
  if fp:
621
736
  PROCESS_FINGERPRINT = fp
622
737
  return PROCESS_FINGERPRINT