NEXO 7.11.1 — Fingerprint cache by mtime/size signature

Published 2026-04-27. Patch release over v7.11.0 — pure performance, no behavior change.

Why

v7.11.0 introduced the runtime fingerprint that gates mcp-restart-required.json by hashing every .py file under src/ the live MCP can import. The hash is checked twice on every tool call (once at server startup via prime_process_fingerprint(), once on every resolve_restart_required() through installed_runtime_fingerprint()). On a 263-file tree that costs ~40ms cold and is paid by every MCP startup — Claude Code, Codex, headless followup-runner, deep-sleep, every cron job. Daily cumulative: 10-20s of pure rehashing of bytes that didn't change.

What changed

The runtime now caches the fingerprint at ~/.nexo/runtime/operations/fingerprint-cache.json with a cheap signature key — (src_dir, file_count, size_total, max_mtime) over the same set of .py files the v7.11.0 fingerprint covers. When the on-disk signature still matches the cached one, the cached digest is returned without re-reading any byte. The check is a stat-only walk over the tree (a few milliseconds) instead of a 13MB SHA-256 grind.

Local benchmark on the live src/: cold ~39.9ms → warm ~3.7ms (≈11× speedup). That's per call — the realistic daily savings across all MCP startups is ~10-20 seconds of latency that simply disappears.

Safety

Cache miss is always safe. Corrupt JSON, signature drift, missing file — any of these falls through to the full hash and rewrites the cache atomically. The new keyword-only parameter compute_mcp_runtime_fingerprint(src_dir, *, use_cache=False) defaults to False, so plugins/update.py always sees ground truth around git pull / npm update; the hot callers (installed_runtime_fingerprint, prime_process_fingerprint) opt in.

The signature key catches every realistic kind of source change: any byte edit shifts size_total; any save touches mtime; any added/removed file shifts file_count. The pathological case (someone restoring a file via touch -r with the original mtime AND preserving exact byte length) doesn't occur in normal release flow — git pull always touches mtimes, npm update writes fresh files. If it ever did matter, the operator can delete the cache file or set "force_restart": true in version.json for that release.

What didn't change

Behavior of the restart gate itself is identical to v7.11.0. Doc-only releases still skip the marker; real .py changes still write it. The middleware contract, the nexo mcp-status JSON shape, the force_restart escape hatch, the conservative fallback (#186) when the fingerprint can't be computed — all preserved exactly.

Tests

6 new tests in tests/test_runtime_fingerprint.py: cache hit skips all .py reads (verified by spying on Path.read_bytes), cache miss when a .py file changes, cache miss when src_dir changes, corrupt cache falls back to full hash and self-repairs, default use_cache=False keeps update.py on the ground-truth path, prime_process_fingerprint warms the on-disk cache. Total fingerprint test count: 20. Targeted regression run: 53 passed.

Full changelog entry → · docs/runtime-fingerprint.md