19cff89300
Reviewer egilewski found the original defer was circular (#83590 comment): the self-lock preflight wrote .update-incomplete and exited, but the next launch only ran the full recovery AFTER main.py's third-party imports — so a healthy venv's probes made the early pass a no-op, main.py imported cryptography eagerly, the .pyd got mapped again, and the deferred install re-hit the exact self-lock it was meant to escape. Close the loop by making the marker guarantee the install runs BEFORE any native extension can be imported: - hermes_cli/_install_repair.py (new, stdlib-only): single source of truth for the core .[all] reinstall — ensurepip bootstrap, uv-pip/pip resolution with VIRTUAL_ENV, Termux env stripping, Windows hermes*.exe quarantine, per-extra fallback ladder, and fd1→fd2 routing for acp safety. Deliberately free of managed_uv/hermes_constants imports so it stays importable in the corrupted-venv state it exists to repair. - hermes_cli/_early_recovery.py: recover_if_needed now completes a pending .update-incomplete install BEFORE the import probes, on every launch that sees the marker (unless argv is update). Success clears the marker; failure bumps an attempts counter inside the marker body and keeps it. A 3-attempt ceiling stops a persistently-failing install from reinstall-hammering every launch (hermes acp included) — past the ceiling the late post-import recovery takes over with its manual recovery instructions. Single-flight lock shared with the late path. - hermes_cli/main.py: _recover_core_update_marker_locked delegates the install to the shared executor (no duplicated logic); ensure_uv stays in the late path so a venv whose uv vanished mid-update still bootstraps it. - tests: 7 new regressions — the reviewer's exact case (marker + healthy venv → install runs while sys.modules has no cryptography), failure keeps marker + increments attempts, retry ceiling, lazy marker does not trigger core install (#58004 invariant), argv-update skip, and corrupt/missing marker bodies. The key test was sabotage-verified: removing the pre-import branch makes it fail with zero install calls, while a lone-lazy-marker test still passes; restoring the branch makes it pass again. Refs #83569