fix(update): make the ZIP replace atomic across all entries + dedupe venv layout
Closes #76104, closes #76105. #76104 — `_atomic_replace_dir` (#49145) made each individual directory swap safe, but `_update_via_zip` replaced ~70 top-level entries in a loop with no atomicity across iterations. `agent/` lands at os.listdir index 13 and `tools/` at 66, so an interruption between them left the new `agent/context_compressor.py` (module-level `from tools.todo_tool import TODO_INJECTION_HEADER`) beside a stale `tools/todo_tool.py` — every file valid Python, the tree unbootable. That is the mechanism behind the ImportError fixed in #76091, and the "partial update" field report in #63717. Split into stage-all-then-swap-all: - `_stage_replacement` copies each dir to a sibling staging path, touching nothing live, so a failure during the long copy phase is a no-op. - `_commit_staged_replacements` performs the renames and, if any fails, restores every entry already swapped — the tree lands wholly new or wholly old, never mixed. This shrinks the failure window from a full tree copy to N renames and makes what remains recoverable. Added an up-front free-space check, since staging needs a second copy of the tree; a clear error beats running out mid-swap. #76105 — venv interpreter resolution was open-coded in 7 places across 4 files using 3 different Windows predicates. #76091 added the seventh because the correct behaviour lived 2400 lines away. Hoisted `venv_bin_dir()` / `venv_python_path()` into hermes_constants (import-safe, no new imports) and routed every site through them; `managed_uv._venv_python` now delegates so its 6 callers are untouched. `_atomic_replace_dir` is retained — it is re-exported from main.py and has its own #49145 regression test; removing it is out of scope here. Tests: 10 new (rollback-on-mid-swap-failure is mutation-verified — it fails when the rollback loop is removed), plus a guard that fails if a new call site hand-rolls Scripts/bin again. E2E-verified against the real staging + commit helpers with a live tree.
This commit is contained in:
@@ -384,9 +384,9 @@ def update_managed_uv(
|
||||
|
||||
|
||||
def _venv_python(venv_dir: Path) -> Path:
|
||||
if platform.system() == "Windows":
|
||||
return venv_dir / "Scripts" / "python.exe"
|
||||
return venv_dir / "bin" / "python"
|
||||
from hermes_constants import venv_python_path
|
||||
|
||||
return venv_python_path(venv_dir)
|
||||
|
||||
|
||||
def _remove_tree(path: Path, *, boundary: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user