NEXO 7.11.0 — Doc-only releases stop forcing MCP restarts
Published 2026-04-27. Minor release. Replaces a version-string-only restart gate with a content-aware one.
Why
Before 7.11.0 every nexo update that bumped version.json wrote mcp-restart-required.json, and every connected MCP client — Claude Code, Codex, Claude Desktop — had to drop its session and reconnect. That cost was unconditional, even when the release changed nothing the running server actually executes. v7.10.1 was a README-only patch and still triggered a forced restart for everyone. So did v7.10.0 in the gh-pages content patch that followed it. Operators were paying a session-restart tax for byte-identical Python.
What changed
The runtime now computes a SHA-256 digest over every .py file under src/ that the live MCP server can import — excluding subprocess-only subtrees (scripts/, tests/, migrations/, crons/) plus the obvious noise (__pycache__/, node_modules/, .git/). Anything outside that set — markdown, JSON, templates, presets, CHANGELOG.md, marketing files, gh-pages assets — never shifts the digest.
nexo update captures the pre-update digest, applies the new code, recomputes the digest, and only writes the restart marker when the digest actually changed (or the release explicitly opted in via "force_restart": true in version.json). Doc-only releases now end with the line MCP source unchanged (no .py byte changed) — no restart needed. instead of forcing every operator to reconnect.
The running server caches its own fingerprint at startup (prime_process_fingerprint() next to the existing prime_process_version()), so the in-process check is content-aware too: resolve_restart_required() compares installed_fingerprint against process_fingerprint and only falls back to the legacy version-string check when the fingerprint cannot be computed.
Conservative fallback
Learning #186 says: never silently leave a process running stale code after nexo update. The new gate honors it. If compute_mcp_runtime_fingerprint returns the empty string on either side — missing source tree, unreadable file, fresh install — the gate writes the marker as it always did. The new logic is allowed to skip a restart that wasn't going to change behavior; it never misses one that would.
Escape hatch
For releases that change behavior in ways the fingerprint can't see — e.g. the wire format of a config file the server reads via json.load rather than import — the releaser can set "force_restart": true in version.json for that specific release. The marker is then written even when the fingerprint matches. Reason becomes brain_update_force and shows up in nexo mcp-status.
Behavior matrix
- Real fix in
src/plugins/foo.py→ fingerprint changes → marker written → restart required. Same as before. - New MCP tool added → new
.pyfile undersrc/→ fingerprint changes → restart required. - README / blog / launch-plan /
CHANGELOG.mdonly → fingerprint unchanged → no restart. - New file under
src/scripts/orsrc/migrations/→ excluded subtree → no restart. - Migration runs in its own subprocess;
nexo updatestill applies it. Only the live MCP server avoids the cycle. force_restart: trueinversion.json→ marker written regardless.- Fingerprint cannot be computed → treated as changed → marker written. Conservative.
Inspection
nexo mcp-status --json now exposes installed_fingerprint, process_fingerprint, fingerprint_match. The reason field on a forced restart is one of marker_required, marker_corrupt, fingerprint_mismatch, or the legacy version_mismatch fallback. Marker schema bumped to v2 with optional from_fingerprint / to_fingerprint; the v1 marker reader stays backwards-compatible.
Tests
14 new tests in tests/test_runtime_fingerprint.py covering determinism, doc-only no-shift, every excluded subtree, missing-dir empty-string, fingerprint-match no-restart, fingerprint-mismatch restart, fallback to version mismatch, marker-always-wins, and the force_restart opt-in. 4 new tests in tests/test_packaged_update_runtime.py covering the npm-install path.