# NousResearch/hermes-agent · Windows update blocked by locked venv files and processes

## Open now

- 0 issues, 3 PRs open on GitHub.
- PR #37790: fix(installer): reap Windows update locker processes
- PR #57852: fix(update): ignore Windows system venv holder noise
- PR #64386: feat(cli): add --force-kill flag for Windows update (#63300)

Nearly all members concern Windows updates, installs, or venv recreation encountering locked files (such as hermes.exe, .pyd files, or pythonw.exe) held by running Hermes-related processes like the gateway. The group includes both bug reports and proposed changes that stop, pause, or work around these processes; one item concerns locking during a desktop packaging step.

## TL;DR

- 30 member(s): 0 open issue(s), 16 closed issue(s), 3 open PR(s), 3 merged PR(s), 8 PR(s) closed without merging.
- A fix was merged to main in PR #52044. Verify it addresses your case.
- **Recommended:** read PR #52044 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-26677,pr-42100,pr-52044`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.06`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #26670: fix(update): hermes update on Windows fails to quarantine hermes.exe when another instance is running
- #37731: Windows installer fails with "Access denied" — wipes venv while app + leaked child processes still hold files open
- #38789: \[Bug\]: Windows hermes update fails when Gateway/Dashboard lock native .pyd dependencies
- #39431: \[Bug\]: Auto-update fails on Windows — speedups.pyd access denied (venv bootstrap)
- #41178: \[Bug\]: Windows Desktop self-update rebuild fails because running Hermes.exe locks release/win-unpacked files
- #43268: \[Bug\] Windows Desktop auto-update fails every time: venv locked by running processes
- #47036: Bootstrap fails to recreate venv when gateway process is running — hermes.exe locked on Windows
- #47557: \[Bug\] Windows: scripts/install.ps1 venv recreation fails when pythonw.exe holds .pyd locks — only kills hermes.exe
- #47910: Windows installer fails to recreate venv: pre-delete kill only targets hermes.exe, not the pythonw.exe gateway holding the lock
- #48100: \[Bug\]: Windows installer fails with "Access Denied" on pythonw.exe during auto-update
- #49428: Windows self-replacement deadlock causes hermes update to fail (Error 5 / Access Denied) — suggest detached updater
- #51015: BUG: Windows pip upgrade fails with os error 32 (hermes.exe locked by self)
- #58387: Windows update fails: locked .pyd files by running python/pythonw processes
- #58730: Windows: hermes update fails persistently due to hermes.exe file lock (cannot overwrite running executable)
- #63300: hermes update fails on Windows when headroom proxy/tui_gateway processes are running
- #68760: hermes.exe locked during \`hermes update\` on Windows (WinError 32)

## Prior & competing PRs

- **#26677: fix(update): quarantine hermes.exe vs concurrent Windows instance (#26670)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/main.py`, `pyproject.toml`, `tests/hermes_cli/conftest.py`, `tests/hermes_cli/test_update_concurrent_quarantine.py`, `website/docs/getting-started/updating.md`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -6954,7 +6954,95 @@ def _hermes_exe_shims(scripts_dir: Path) -> list[Path]:
         ]
     
     
    -def _quarantine_running_hermes_exe(scripts_dir: Path) -> list[tuple[Path, Path]]:
    +def _detect_concurrent_hermes_instances(
    +    scripts_dir: Path, *, exclude_pid: int | None = None
    +) -> list[tuple[int, str]]:
    +    """Find other live processes whose .exe is one of our entry-point shims.
    +
    +    Windows blocks DELETE/REPLACE on a running .exe — and even RENAME on the
    +    same .exe when another process opened it without ``FILE_SHARE_DELETE``.
    +    The Hermes Desktop Electron app spawns ``hermes.EXE`` as a backend child,
    +    so during ``hermes update`` the user-invoked process and the desktop's
    +    child both hold the same file. The quarantine rename then fails with
    +    ``[WinError 32]`` and uv inherits the lock.
    +
    +    This helper enumerates processes whose ``exe`` matches one of the venv's
    +    shims (``hermes.exe`` / ``hermes-gateway.exe``) and returns ``(pid,
    +    process_name)`` pairs. The caller's own PID is excluded so the running
    +    ``hermes update`` invocation never reports itself.
    +
    +    Returns an empty list off-Windows, on missing psutil, or when no other
    +    instances exist. Never raises — process enumeration is best-effort.
    +    """
    +    if not _is_windows():
    +        return []
    +
    +    try:
    +        import psutil
    +    except Exception:
    +        return []
    +
    +    if exclude_pid is None:
    +        exclude_pid = os.getpid()
    +
    +    # Resolve every shim path to its canonical form once for cheap comparison.
    +    shim_paths: set[str] = set()
    +    for shim in _hermes_exe_shims(scripts_dir):
    +        try:
    +            shim_paths.add(str(shim.resolve()).lower())
    +        except OSError:
    +            shim_paths.add(str(shim).lower())
    +    if not shim_paths:
    +        return []
    +
    +    matches: list[tuple[int, str]] = []
    +    try:
    +        proc_iter = psutil.process_iter(["pid", "exe", "name"])
    +    except Exception:
    +        return []
    +
    +    for proc in proc_iter:
    +        try:
    +            info = proc.info
    +        except Exception:
    +            continue
    +        pid = info.get("pid")
    +        exe = info.get("exe")
    +        if not exe or pid is None or pid == exclude_pid:
    +            continue
    +        try:
    +            exe_norm = str(Path(exe).resolve()).lower()
    +        except (OSError, ValueError):
    +            exe_norm = str(exe).lower()
    +        if exe_norm in shim_paths:
    +            name = info.get("name") or Path(exe).name
    +            matches.append((int(pid), str(name)))
    +
    +    return matches
    +
    +
    +def _format_concurrent_instances_message(
    +    matches: list[tuple[int, str]], scripts_dir: Path
    +) -> str:
    +    """Build a human-readable explanation + remediation hint for the user."""
    +    shim = scripts_dir / "hermes.exe"
    +    lines = ["✗ Another hermes.exe is running:"]
    +    for pid, name in matches:
    +        lines.append(f"    PID {pid}  {name}")
    +    lines.append("")
    +    lines.append(f"  Updating now would fail to overwrite {shim} because")
    +    lines.append("  Windows blocks REPLACE on a running executable.")
    +    lines.append("")
    +    lines.append("  Close Hermes Desktop, exit any open `hermes` REPLs, and")
    +    lines.append("  stop the gateway (`hermes gateway stop`) before retrying.")
    +    lines.append("  Override with `hermes update --force` if you've already")
    +    lines.append("  confirmed those processes will not write to the venv.")
    +    return "\n".join(lines)
    +
    +
    +def _quarantine_running_hermes_exe(
    +    scripts_dir: Path, *, max_attempts: int = 4
    +) -> list[tuple[Path, Path]]:
         """Pre-empt Windows file lock on the running ``hermes.exe``.
     
         Windows allows RENAMING a mapped/running executable (the kernel tracks the
    ```

    ```diff
    @@ -6967,29 +7055,129 @@ def _quarantine_running_hermes_exe(scripts_dir: Path) -> list[tuple[Path, Path]]
         fresh shims at the original paths. The ``.old`` files are cleaned up on
         the next hermes invocation by ``_cleanup_quarantined_exes``.
     
    +    Rename can still fail when *another* process has opened the .exe without
    +    ``FILE_SHARE_DELETE`` — typically AV real-time scanners with transient
    +    handles (recovers in <1s), or the Hermes Desktop backend child process
    +    (won't recover until the user closes it). We mitigate:
    +
    +    1. Retry up to ``max_attempts`` times with exponential backoff
    +       (100/250/500/1000 ms). Handles the AV-scanner case.
    +    2. If all retries fail, schedule the .exe for replacement on next
    +       reboot via ``MoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT)``. This still
    +       lets uv create a fresh shim at the original path (Windows will keep
    +       the old file's content under a new name until the reboot), so the
    +       update can complete; the user just needs to reboot to fully unload
    +       the stale image.
    +    3. Print a clear warning naming the most likely culprit (running
    +       Hermes Desktop / gateway / REPL) and pointing to ``--force``.
    +
         Returns the list of (original, quarantined) pairs so the caller can roll
    -    back if the install itself fails before uv writes a replacement.
    +    back if the install itself fails before uv writes a replacement. Pairs
    +    where we used ``MOVEFILE_DELAY_UNTIL_REBOOT`` are NOT returned — they
    +    are already deferred and roll-back is meaningless.
         """
         moved: list[tuple[Path, Path]] = []
         if not _is_windows():
             return moved
     
         import time
    +
         stamp = int(time.time() * 1000)
    +    # Backoff schedule: first attempt is immediate, subsequent ones sleep.
    +    # 100ms / 250ms / 500ms covers the typical AV scanner re-scan window.
    +    backoff_ms = [0, 100, 250, 500, 1000]
    +    attempts = max(1, min(max_attempts, len(backoff_ms)))
    +
         for shim in _hermes_exe_shims(scripts_dir):
             if not shim.exists():
                 continue
             target = shim.with_suffix(shim.suffix + f".old.{stamp}")
    -        try:
    -            shim.rename(target)
    -            moved.append((shim, target))
    -        except OSError as e:
    -            # Best-effort: keep going. uv's failure later will surface the
    -            # real error; this is a heuristic, not a hard guarantee.
    -            print(f"  ⚠ Could not quarantine {shim.name}: {e}")
    +
    +        last_exc: OSError | None = None
    +        for attempt in range(attempts):
    +            delay = backoff_ms[attempt] / 1000.0
    +            if delay:
    +                time.sleep(delay)
    +            try:
    +                shim.rename(target)
    +                moved.append((shim, target))
    +                last_exc = None
    +                break
    +            except OSError as e:
    +                last_exc = e
    +                continue
    +
    +        if last_exc is None:
    +            continue
    +
    +        # All in-process renames failed. Try MoveFileEx with
    +        # MOVEFILE_DELAY_UNTIL_REBOOT as a last resort. This succeeds in the
    +        # exact case where the inline rename failed (another process holds
    +        # the handle without share-delete), at the cost of requiring a
    +        # reboot to fully reclaim the old .exe.
    +        scheduled = _schedule_replace_on_reboot(shim, target)
    +        if scheduled:
    +            print(
    +                f"  ⚠ {shim.name} is locked by another process; scheduled "
    +                f"replacement on next reboot."
    +            )
    +            print(
    +                "    The new shim was written at the same path, but a "
    +                "reboot is needed to fully unload the old one."
    +            )
    +            # Do NOT append to ``moved``: we don't want roll-back to undo a
    +            # reboot-deferred operation.
    +            continue
    +
    +        # Truly couldn't budge the .exe. Print an actionable warning and let
    +        # uv try its luck — sometimes uv's own retry handling pulls through.
    +        print(
    +            f"  ⚠ Could not quarantine {shim.name} ({last_exc.__class__.__name__}: "
    +            f"another process is holding it open)."
    +        )
    +        print(
    +            "    Close Hermes Desktop, exit other `hermes` REPLs, stop the "
    +            "gateway, or pause AV scanning, then re-run `hermes update`."
    +        )
    +
         return moved
     
     
    +def _schedule_replace_on_reboot(shim: Path, quarantine_target: Path) -> bool:
    +    """Schedule ``shim`` -> ``quarantine_target`` via PendingFileRenameOperations.
    +
    +    Uses Win32 ``MoveFileExW`` with ``MOVEFILE_REPLACE_EXISTING |
    +    MOVEFILE_DELAY_UNTIL_REBOOT``. The OS persists the rename in
    +    ``HKLM\\System\\CurrentControlSet\\Control\\Session Manager\\
    +    PendingFileRenameOperations`` and applies it before any user-mode code
    +    runs on next boot — at which point no process can hold the .exe.
    +
    +    Returns ``True`` if the schedule call succeeded, ``False`` otherwise
    +    (non-Windows, ctypes failure, lack of privilege, etc.). Never raises.
    +    """
    +    if not _is_windows():
    +        return False
    +    try:
    +        import ctypes
    +        from ctypes import wintypes
    +
    +        MOVEFILE_REPLACE_EXISTING = 0x1
    +        MOVEFILE_DELAY_UNTIL_REBOOT = 0x4
    +
    +        MoveFileExW = ctypes.windll.kernel32.MoveFileExW
    +        MoveFileExW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
    +        MoveFileExW.restype = wintypes.BOOL
    +
    +        ok = MoveFileExW(
    +            str(shim),
    +            str(quarantine_target),
    +            MOVEFILE_REPLACE_EXISTING | MOVEFILE_DELAY_UNTIL_REBOOT,
    +        )
    +        return bool(ok)
    +    except Exception:
    +        return False
    +
    +
     def _restore_quarantined_exes(moved: list[tuple[Path, Path]]) -> None:
         """Roll back ``_quarantine_running_hermes_exe`` if uv didn't write replacements."""
         for original, quarantined in moved:
    ```

    ```diff
    @@ -7775,6 +7963,18 @@ def _cmd_update_impl(args, gateway_mode: bool):
         print("⚕ Updating Hermes Agent...")
         print()
     
    +    # On Windows, abort early if another hermes.exe is holding the venv shim
    +    # open. Continuing would result in a string of WinError 32 warnings and
    +    # then either a deferred-rename leftover or a failed git-pull fast path
    +    # that silently falls back to the slower ZIP route. See issue #26670.
    +    if _is_windows() and not getattr(args, "force", False):
    +        scripts_dir = _venv_scripts_dir()
    +        if scripts_dir is not None:
    +            concurrent = _detect_concurrent_hermes_instances(scripts_dir)
    +            if concurrent:
    +                print(_format_concurrent_instances_message(concurrent, scripts_dir))
    +                sys.exit(2)
    +
         # Pre-update backup — runs before any git/file mutation so users can
         # always roll back to the exact state they had before this update.
         _run_pre_update_backup(args)
    ```

    ```diff
    @@ -11895,6 +12095,12 @@ def cmd_claw(args):
             default=False,
             help="Assume yes for interactive prompts (config migration, stash restore). API-key entry is skipped; run 'hermes config migrate' separately for those.",
         )
    +    update_parser.add_argument(
    +        "--force",
    +        action="store_true",
    +        default=False,
    +        help="Windows: proceed with the update even when another hermes.exe is detected. The concurrent process will likely cause WinError 32 warnings and may leave a reboot-deferred .exe replacement.",
    +    )
         update_parser.set_defaults(func=cmd_update)
     
         # =========================================================================
    ```
  - `pyproject.toml`

    ```diff
    @@ -220,6 +220,7 @@ include = ["agent", "agent.*", "tools", "tools.*", "hermes_cli", "gateway", "gat
     testpaths = ["tests"]
     markers = [
         "integration: marks tests requiring external services (API keys, Modal, etc.)",
    +    "real_concurrent_gate: opt out of the autouse stub that disables _detect_concurrent_hermes_instances",
     ]
     addopts = "-m 'not integration' -n auto"
     
    ```
  - `tests/hermes_cli/conftest.py`

    ```diff
    @@ -17,3 +17,30 @@ def all_assignees_spawnable(monkeypatch):
         """
         from hermes_cli import profiles
         monkeypatch.setattr(profiles, "profile_exists", lambda name: True)
    +
    +
    [redacted:email](autouse=True)
    +def _suppress_concurrent_hermes_gate(request, monkeypatch):
    +    """Default ``_detect_concurrent_hermes_instances`` to ``[]`` for every test.
    +
    +    The Windows update path now refuses to proceed when another
    +    ``hermes.exe`` is detected (issue #26670). On a developer's Windows
    +    machine running the test suite via ``hermes`` itself, this would
    +    flag the running agent as a concurrent instance and abort every
    +    ``cmd_update`` test. Tests that want to exercise the gate explicitly
    +    re-patch ``_detect_concurrent_hermes_instances`` with their own
    +    return value — autouse here gives a clean default without touching
    +    the rest of the suite.
    +
    +    Tests that need to call the REAL function (e.g. unit tests for the
    +    helper itself) opt out with ``@pytest.mark.real_concurrent_gate``.
    +    """
    +    if request.node.get_closest_marker("real_concurrent_gate"):
    +        return
    +    try:
    +        from hermes_cli import main as _cli_main
    +    except Exception:
    +        return
    +    monkeypatch.setattr(
    +        _cli_main, "_detect_concurrent_hermes_instances", lambda *_a, **_k: []
    +    )
    ```
  - `tests/hermes_cli/test_update_concurrent_quarantine.py`

    ```diff
    @@ -0,0 +1,328 @@
    +"""Tests for issue #26670 — concurrent hermes.exe detection and improved
    +quarantine retry / reboot-deferred fallback during `hermes update` on Windows.
    +
    +These tests force ``_is_windows`` to return ``True`` via patching so the
    +Windows-specific code paths can be exercised on any host.
    +"""
    +
    +from __future__ import annotations
    +
    +import os
    +import sys
    +import types
    +from pathlib import Path
    +from types import SimpleNamespace
    +from unittest.mock import MagicMock, patch
    +
    +import pytest
    +
    +from hermes_cli import main as cli_main
    +
    +
    +# Tests in this module either exercise the REAL _detect_concurrent_hermes_instances
    +# helper (and need the autouse stub in tests/hermes_cli/conftest.py disabled),
    +# or supply their own explicit return value via patch.object. Mark the whole
    +# module so the conftest fixture skips its default stub.
    +pytestmark = pytest.mark.real_concurrent_gate
    +
    +
    +# ---------------------------------------------------------------------------
    +# _detect_concurrent_hermes_instances
    +# ---------------------------------------------------------------------------
    +
    +
    +def _make_proc(pid: int, exe: str, name: str = "hermes.exe"):
    +    """Build a duck-typed psutil Process stand-in with the .info dict."""
    +    proc = MagicMock()
    +    proc.info = {"pid": pid, "exe": exe, "name": name}
    +    return proc
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_detect_concurrent_returns_empty_when_no_other_processes(_winp, tmp_path):
    +    scripts_dir = tmp_path
    +    (scripts_dir / "hermes.exe").write_bytes(b"")
    +    (scripts_dir / "hermes-gateway.exe").write_bytes(b"")
    +
    +    fake_psutil = types.SimpleNamespace(process_iter=lambda attrs: iter([]))
    +    with patch.dict(sys.modules, {"psutil": fake_psutil}):
    +        result = cli_main._detect_concurrent_hermes_instances(scripts_dir)
    +
    +    assert result == []
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_detect_concurrent_excludes_self_pid(_winp, tmp_path):
    +    scripts_dir = tmp_path
    +    shim = scripts_dir / "hermes.exe"
    +    shim.write_bytes(b"")
    +    my_pid = os.getpid()
    +
    +    procs = [_make_proc(my_pid, str(shim), "hermes.exe")]
    +    fake_psutil = types.SimpleNamespace(process_iter=lambda attrs: iter(procs))
    +    with patch.dict(sys.modules, {"psutil": fake_psutil}):
    +        result = cli_main._detect_concurrent_hermes_instances(scripts_dir)
    +
    +    assert result == []
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_detect_concurrent_finds_other_hermes_process(_winp, tmp_path):
    +    scripts_dir = tmp_path
    +    shim = scripts_dir / "hermes.exe"
    +    shim.write_bytes(b"")
    +
    +    other_pid = os.getpid() + 1
    +    procs = [
    +        _make_proc(other_pid, str(shim), "hermes.exe"),
    +        _make_proc(os.getpid() + 2, r"C:\\Windows\\System32\\notepad.exe", "notepad.exe"),
    +    ]
    +    fake_psutil = types.SimpleNamespace(process_iter=lambda attrs: iter(procs))
    +    with patch.dict(sys.modules, {"psutil": fake_psutil}):
    +        result = cli_main._detect_concurrent_hermes_instances(scripts_dir)
    +
    +    assert result == [(other_pid, "hermes.exe")]
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_detect_concurrent_matches_case_insensitively(_winp, tmp_path):
    +    scripts_dir = tmp_path
    +    shim = scripts_dir / "hermes.exe"
    +    shim.write_bytes(b"")
    +
    +    # Simulate the desktop spawning hermes.EXE (uppercase ext) from same path
    +    upper = str(shim).replace("hermes.exe", "HERMES.EXE")
    +    procs = [_make_proc(9999, upper, "HERMES.EXE")]
    +    fake_psutil = types.SimpleNamespace(process_iter=lambda attrs: iter(procs))
    +    with patch.dict(sys.modules, {"psutil": fake_psutil}):
    +        result = cli_main._detect_concurrent_hermes_instances(scripts_dir)
    +
    +    assert result == [(9999, "HERMES.EXE")]
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_detect_concurrent_no_psutil_returns_empty(_winp, tmp_path):
    +    scripts_dir = tmp_path
    +    (scripts_dir / "hermes.exe").write_bytes(b"")
    +
    +    # Block psutil import — simulate environment without it.
    +    with patch.dict(sys.modules, {"psutil": None}):
    +        result = cli_main._detect_concurrent_hermes_instances(scripts_dir)
    +
    +    assert result == []
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=False)
    +def test_detect_concurrent_is_noop_off_windows(_winp, tmp_path):
    +    """No process enumeration off-Windows; the file-lock issue is Windows-only."""
    +    assert cli_main._detect_concurrent_hermes_instances(tmp_path) == []
    +
    +
    +# ---------------------------------------------------------------------------
    +# _format_concurrent_instances_message
    +# ---------------------------------------------------------------------------
    +
    +
    +def test_format_message_mentions_pids_and_remediation(tmp_path):
    +    matches = [(1234, "hermes.exe"), (5678, "hermes.exe")]
    +    msg = cli_main._format_concurrent_instances_message(matches, tmp_path)
    +
    +    assert "1234" in msg
    +    assert "5678" in msg
    +    assert "hermes.exe" in msg
    +    assert "Hermes Desktop" in msg
    +    assert "--force" in msg
    +    # Mentions the file that would have been overwritten
    +    assert str(tmp_path / "hermes.exe") in msg
    +
    +
    +# ---------------------------------------------------------------------------
    +# _quarantine_running_hermes_exe — retry + reboot-deferred fallback
    +# ---------------------------------------------------------------------------
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_quarantine_succeeds_first_attempt(_winp, tmp_path):
    +    """When the rename works immediately, no warning, single rename pair returned."""
    +    shim = tmp_path / "hermes.exe"
    +    shim.write_bytes(b"old")
    +
    +    pairs = cli_main._quarantine_running_hermes_exe(tmp_path)
    +
    +    assert len(pairs) == 1
    +    orig, quarantine = pairs[0]
    +    assert orig == shim
    +    assert quarantine.name.startswith("hermes.exe.old.")
    +    assert quarantine.exists()
    +    assert not shim.exists()
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_quarantine_retries_then_succeeds(_winp, tmp_path, monkeypatch):
    +    """A transient OSError on the first attempt should not be fatal."""
    +    shim = tmp_path / "hermes.exe"
    +    shim.write_bytes(b"old")
    +
    +    original_rename = Path.rename
    +    call_count = {"n": 0}
    +
    +    def flaky_rename(self, target):
    +        call_count["n"] += 1
    +        if call_count["n"] == 1:
    +            raise OSError(32, "share violation (simulated AV scan)")
    +        return original_rename(self, target)
    +
    +    # Speed up the test: avoid actual sleeps in the backoff schedule.
    +    monkeypatch.setattr(cli_main, "_hermes_exe_shims", lambda d: [shim])
    +    with patch.object(Path, "rename", flaky_rename), patch(
    +        "time.sleep", lambda *_a, **_k: None
    +    ):
    +        pairs = cli_main._quarantine_running_hermes_exe(tmp_path)
    +
    +    assert call_count["n"] >= 2
    +    assert len(pairs) == 1
    +    assert not shim.exists()
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_quarantine_falls_back_to_reboot_schedule(_winp, tmp_path, capsys, monkeypatch):
    +    """When every retry fails, we schedule via MoveFileEx and warn helpfully."""
    +    shim = tmp_path / "hermes.exe"
    +    shim.write_bytes(b"locked")
    +
    +    def always_fails(self, target):
    +        raise OSError(32, "The process cannot access the file (simulated lock)")
    +
    +    scheduled_calls: list[tuple[Path, Path]] = []
    +
    +    def fake_schedule(s: Path, q: Path) -> bool:
    +        scheduled_calls.append((s, q))
    +        return True
    +
    +    monkeypatch.setattr(cli_main, "_hermes_exe_shims", lambda d: [shim])
    +    with patch.object(Path, "rename", always_fails), patch.object(
    +        cli_main, "_schedule_replace_on_reboot", fake_schedule
    +    ), patch("time.sleep", lambda *_a, **_k: None):
    +        pairs = cli_main._quarantine_running_hermes_exe(tmp_path)
    +
    +    captured = capsys.readouterr().out
    +
    +    # The reboot-deferred path was used.
    +    assert scheduled_calls and scheduled_calls[0][0] == shim
    +    # It is NOT added to the returned roll-back list (the issue calls this
    +    # out — don't undo a deferred operation).
    +    assert pairs == []
    +    # The user got a clear message, not raw [WinError 32].
    +    assert "scheduled" in captured.lower()
    +    assert "reboot" in captured.lower()
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_quarantine_actionable_warning_when_everything_fails(
    +    _winp, tmp_path, capsys, monkeypatch
    +):
    +    """When even MoveFileEx fails we should print remediation hints, not a bare error."""
    +    shim = tmp_path / "hermes.exe"
    +    shim.write_bytes(b"locked")
    +
    +    def always_fails(self, target):
    +        raise OSError(32, "share violation")
    +
    +    monkeypatch.setattr(cli_main, "_hermes_exe_shims", lambda d: [shim])
    +    with patch.object(Path, "rename", always_fails), patch.object(
    +        cli_main, "_schedule_replace_on_reboot", lambda *_a, **_k: False
    +    ), patch("time.sleep", lambda *_a, **_k: None):
    +        pairs = cli_main._quarantine_running_hermes_exe(tmp_path)
    +
    +    captured = capsys.readouterr().out
    +    assert pairs == []
    +    # New message format: no raw "[WinError 32]" dump; instead names the cause
    +    # and tells the user what to do.
    +    assert "another process" in captured.lower()
    +    assert "Hermes Desktop" in captured or "gateway" in captured.lower()
    +
    +
    +# ---------------------------------------------------------------------------
    +# cmd_update integration — concurrent-instance gate
    +# ---------------------------------------------------------------------------
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_cmd_update_aborts_on_concurrent_instance(_winp, tmp_path, capsys):
    +    """If another hermes.exe is running, the update bails out before
    +    touching the working tree (exit code 2)."""
    +    scripts_dir = tmp_path / "Scripts"
    +    scripts_dir.mkdir()
    +
    +    args = SimpleNamespace(
    +        check=False,
    +        gateway=False,
    +        yes=False,
    +        force=False,
    +        backup=False,
    +        no_backup=True,
    +    )
    +
    +    with patch.object(
    +        cli_main, "_venv_scripts_dir", return_value=scripts_dir
    +    ), patch.object(
    +        cli_main,
    +        "_detect_concurrent_hermes_instances",
    +        return_value=[(4242, "hermes.exe")],
    +    ), patch.object(
    +        cli_main, "_run_pre_update_backup"
    +    ) as mock_backup, patch.object(
    +        cli_main, "_install_hangup_protection", return_value={}
    +    ), patch.object(
    +        cli_main, "_finalize_update_output"
    +    ):
    +        with pytest.raises(SystemExit) as excinfo:
    +            cli_main.cmd_update(args)
    +
    +    assert excinfo.value.code == 2
    +    # The pre-update backup runs AFTER the concurrent check; should not have
    +    # been invoked.
    +    mock_backup.assert_not_called()
    +
    +    captured = capsys.readouterr().out
    +    assert "4242" in captured
    +    assert "--force" in captured
    +
    +
    [redacted:email](cli_main, "_is_windows", return_value=True)
    +def test_cmd_update_force_bypasses_concurrent_check(_winp, tmp_path):
    +    """--force lets the update proceed past the concurrent-instance gate
    +    (subsequent steps are mocked so we only verify the gate is skipped)."""
    +    scripts_dir = tmp_path / "Scripts"
    +    scripts_dir.mkdir()
    +
    +    args = SimpleNamespace(
    +        check=False,
    +        gateway=False,
    +        yes=False,
    +        force=True,  # ← the bypass
    +        backup=False,
    +        no_backup=True,
    +    )
    +
    +    detect = MagicMock(return_value=[(9, "hermes.exe")])
    +
    +    # Short-circuit out of _cmd_update_impl via a sentinel raise immediately
    +    # AFTER the gate. _run_pre_update_backup is the first call after the gate.
    +    sentinel = RuntimeError("reached post-gate body")
    +    with patch.object(
    +        cli_main, "_venv_scripts_dir", return_value=scripts_dir
    +    ), patch.object(
    +        cli_main, "_detect_concurrent_hermes_instances", detect
    +    ), patch.object(
    +        cli_main, "_run_pre_update_backup", side_effect=sentinel
    +    ), patch.object(
    +        cli_main, "_install_hangup_protection", return_value={}
    +    ), patch.object(
    +        cli_main, "_finalize_update_output"
    +    ):
    +        with pytest.raises(RuntimeError, match="reached post-gate body"):
    +            cli_main.cmd_update(args)
    +
    +    # When --force is set, we should not have even consulted psutil.
    +    detect.assert_not_called()
    ```
  - `uv.lock`

    ```diff
    @@ -40,7 +40,7 @@ wheels = [
     
     [[package]]
     name = "aiohttp"
    -version = "3.13.4"
    +version = "3.13.3"
     source = { registry = "https://pypi.org/simple" }
     dependencies = [
         { name = "aiohappyeyeballs" },
    ```

    ```diff
    @@ -51,93 +51,93 @@ dependencies = [
         { name = "propcache" },
         { name = "yarl" },
     ]
    -sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
    -wheels = [
    -    { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" },
    -    { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" },
    -    { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" },
    -    { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" },
    -    { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" },
    -    { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" },
    -    { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" },
    -    { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" },
    -    { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" },
    -    { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" },
    -    { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" },
    -    { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" },
    -    { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" },
    -    { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" },
    -    { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" },
    -    { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" },
    -    { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" },
    -    { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" },
    -    { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" },
    -    { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" },
    -    { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" },
    -    { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" },
    -    { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" },
    -    { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" },
    -    { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" },
    -    { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" },
    -    { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" },
    -    { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" },
    -    { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" },
    -    { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" },
    -    { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" },
    -    { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" },
    -    { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" },
    -    { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" },
    -    { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" },
    -    { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" },
    -    { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" },
    -    { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
    -    { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
    -    { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
    -    { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
    -    { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
    -    { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
    -    { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
    -    { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
    -    { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
    -    { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
    -    { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
    -    { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
    -    { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" },
    -    { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" },
    -    { url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" },
    -    { url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" },
    -    { url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" },
    -    { url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" },
    -    { url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" },
    -    { url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" },
    -    { url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" },
    -    { url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" },
    -    { url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" },
    -    { url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" },
    -    { url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" },
    -    { url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" },
    -    { url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" },
    -    { url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" },
    -    { url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" },
    -    { url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" },
    -    { url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" },
    -    { url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" },
    -    { url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" },
    -    { url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" },
    -    { url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" },
    -    { url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" },
    -    { url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" },
    -    { url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" },
    -    { url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" },
    -    { url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" },
    -    { url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" },
    -    { url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" },
    -    { url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" },
    -    { url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" },
    -    { url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" },
    -    { url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" },
    -    { url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" },
    -    { url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" },
    +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" },
    +    { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" },
    +    { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" },
    +    { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" },
    +    { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" },
    +    { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" },
    +    { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" },
    +    { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" },
    +    { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" },
    +    { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" },
    +    { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" },
    +    { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" },
    +    { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" },
    +    { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" },
    +    { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" },
    +    { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
    +    { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
    +    { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
    +    { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
    +    { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
    +    { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
    +    { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
    +    { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
    +    { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
    +    { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
    +    { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
    +    { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
    +    { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
    +    { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
    +    { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
    +    { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
    +    { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
    +    { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
    +    { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
    +    { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
    +    { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
    +    { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
    +    { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
    +    { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
    +    { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
    +    { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
    +    { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
    +    { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
    +    { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
    +    { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
    +    { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
    +    { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
    +    { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
    +    { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
    +    { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
    +    { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
    +    { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
    +    { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
    +    { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
    +    { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
    +    { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
    +    { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
    +    { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
    +    { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
    +    { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
    +    { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
    +    { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
    +    { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
    +    { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
    +    { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
    +    { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
    +    { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
    +    { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
    +    { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
    +    { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
    +    { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
    +    { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
    +    { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
    +    { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
    +    { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
    +    { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
    +    { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
    +    { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
     ]
     
     [[package]]
    ```

    ```diff
    @@ -321,7 +321,7 @@ wheels = [
     
     [[package]]
     name = "anthropic"
    -version = "0.87.0"
    +version = "0.86.0"
     source = { registry = "https://pypi.org/simple" }
     dependencies = [
         { name = "anyio" },
    ```

    ```diff
    @@ -333,9 +333,9 @@ dependencies = [
         { name = "sniffio" },
         { name = "typing-extensions" },
     ]
    -sdist = { url = "https://files.pythonhosted.org/packages/d6/8f/3281edf7c35cbac169810e5388eb9b38678c7ea9867c2d331237bd5dff08/anthropic-0.87.0.tar.gz", hash = "sha256:098fef3753cdd3c0daa86f95efb9c8d03a798d45c5170329525bb4653f6702d0", size = 588982, upload-time = "2026-03-31T17:52:41.697Z" }
    +sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" }
     wheels = [
    -    { url = "https://files.pythonhosted.org/packages/0d/02/99bf351933bdea0545a2b6e2d812ed878899e9a95f618351dfa3d0de0e69/anthropic-0.87.0-py3-none-any.whl", hash = "sha256:e2669b86d42c739d3df163f873c51719552e263a3d85179297180fb4fa00a236", size = 472126, upload-time = "2026-03-31T17:52:40.174Z" },
    +    { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" },
     ]
     
     [[package]]
    ```

    ```diff
    @@ -1569,11 +1569,10 @@ wheels = [
     
     [[package]]
     name = "hermes-agent"
    -version = "0.13.0"
    +version = "0.14.0"
     source = { editable = "." }
     dependencies = [
         { name = "croniter" },
    -    { name = "cryptography" },
         { name = "fire" },
         { name = "httpx", extra = ["socks"] },
         { name = "jinja2" },
    ```

    ```diff
    @@ -1759,19 +1758,18 @@ youtube = [
     [package.metadata]
     requires-dist = [
         { name = "agent-client-protocol", marker = "extra == 'acp'", specifier = "==0.9.0" },
    -    { name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.4" },
    -    { name = "aiohttp", marker = "extra == 'messaging'", specifier = "==3.13.4" },
    -    { name = "aiohttp", marker = "extra == 'slack'", specifier = "==3.13.4" },
    -    { name = "aiohttp", marker = "extra == 'sms'", specifier = "==3.13.4" },
    +    { name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.3" },
    +    { name = "aiohttp", marker = "extra == 'messaging'", specifier = "==3.13.3" },
    +    { name = "aiohttp", marker = "extra == 'slack'", specifier = "==3.13.3" },
    +    { name = "aiohttp", marker = "extra == 'sms'", specifier = "==3.13.3" },
         { name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" },
         { name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },
         { name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = "==2.2.42" },
    -    { name = "anthropic", marker = "extra == 'anthropic'", specifier = "==0.87.0" },
    +    { name = "anthropic", marker = "extra == 'anthropic'", specifier = "==0.86.0" },
         { name = "asyncpg", marker = "extra == 'matrix'", specifier = "==0.31.0" },
         { name = "boto3", marker = "extra == 'bedrock'", specifier = "==1.42.89" },
         { name = "brotlicffi", marker = "extra == 'messaging'", specifier = "==1.2.0.1" },
         { name = "croniter", specifier = "==6.0.0" },
    -    { name = "cryptography", specifier = "==46.0.7" },
         { name = "daytona", marker = "extra == 'daytona'", specifier = "==0.155.0" },
         { name = "debugpy", marker = "extra == 'dev'", specifier = "==1.8.20" },
         { name = "dingtalk-stream", marker = "extra == 'dingtalk'", specifier = "==0.24.3" },
    ```
  - `website/docs/getting-started/updating.md`

    ```diff
    @@ -69,6 +69,26 @@ updates:
     
     `--backup` was the always-on behavior in earlier builds, but it was adding minutes to every update on large homes, so it's now opt-in. The lightweight pairing-data snapshot above still runs unconditionally.
     
    +### Windows: another `hermes.exe` is running
    +
    +On Windows, `hermes update` will refuse to run if it detects another `hermes.exe` process holding the venv's entry-point executable open — most commonly the Hermes Desktop app's spawned backend, an open `hermes` REPL in another terminal, or a running gateway:
    +
    +```
    +$ hermes update
    +✗ Another hermes.exe is running:
    +    PID 12345  hermes.exe
    +
    +  Updating now would fail to overwrite ...\venv\Scripts\hermes.exe because
    +  Windows blocks REPLACE on a running executable.
    +
    +  Close Hermes Desktop, exit any open `hermes` REPLs, and
    +  stop the gateway (`hermes gateway stop`) before retrying.
    +  Override with `hermes update --force` if you've already
    +  confirmed those processes will not write to the venv.
    +```
    +
    +Close the listed processes and re-run. If you're sure the concurrent process won't interfere (rare — usually only useful when an antivirus shim is mis-attributed), pass `--force` to skip the check. In that case the updater will still retry the `.exe` rename with exponential backoff and, on stubborn locks, schedule the replacement for next reboot via `MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT)` so the update can complete.
    +
     Expected output looks like:
     
     ```
    ```
- **#39037: fix(cli): detect venv python workers before update to prevent .pyd lock failures (#38789)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b14d75f8afdb624be03cbefd19494ccc850bf367`, `87ae4ae94bc13e302be2a37b30226f2b19909227`, `#38789`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#42100: fix(desktop): stop running Hermes.exe locking win-unpacked before Windows pack**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/main.py`, `tests/hermes_cli/test_gui_command.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -7530,6 +7530,79 @@ def _purge_electron_build_cache(desktop_dir: Path) -> list[Path]:
         return removed
     
     
    +def _stop_desktop_processes_locking_build(desktop_dir: Path) -> list[int]:
    +    """Terminate any running desktop app executing from this build's ``release``
    +    dir so a rebuild can replace its (otherwise locked) executable.
    +
    +    On Windows a running ``Hermes.exe`` keeps an exclusive lock on
    +    ``release/win-unpacked/Hermes.exe``. electron-builder's pack then can't
    +    delete the stale binary and dies with ``remove …\\Hermes.exe: Access is
    +    denied`` / ``ERR_ELECTRON_BUILDER_CANNOT_EXECUTE`` (before-pack hits the same
    +    EPERM cleaning the dir). The retry path repeats the failure because the lock
    +    is still held. POSIX lets you unlink a running binary, so this is a no-op
    +    off-Windows.
    +
    +    Scope is deliberately narrow: only processes whose executable lives *inside*
    +    this desktop's ``release`` tree are stopped — a packaged install elsewhere or
    +    an unrelated "Hermes" process is never touched. Best-effort: never raises.
    +    Returns the PIDs we asked to stop.
    +    """
    +    if sys.platform != "win32":
    +        return []
    +    try:
    +        import psutil
    +    except Exception:
    +        return []
    +    try:
    +        release_dir = (desktop_dir / "release").resolve()
    +    except OSError:
    +        return []
    +    if not release_dir.is_dir():
    +        return []
    +
    +    me = os.getpid()
    +    victims = []
    +    try:
    +        proc_iter = psutil.process_iter(["pid", "exe"])
    +    except Exception:
    +        return []
    +    for proc in proc_iter:
    +        try:
    +            info = proc.info
    +        except Exception:
    +            continue
    +        pid = info.get("pid")
    +        exe = info.get("exe")
    +        if not exe or pid is None or pid == me:
    +            continue
    +        try:
    +            exe_path = Path(exe).resolve()
    +        except (OSError, ValueError):
    +            continue
    +        if release_dir in exe_path.parents:
    +            victims.append(proc)
    +
    +    stopped: list[int] = []
    +    for proc in victims:
    +        try:
    +            proc.terminate()
    +            stopped.append(int(proc.pid))
    +        except Exception:
    +            continue
    +    if stopped:
    +        # Wait for the handles (and thus the file locks) to actually release.
    +        try:
    +            _, alive = psutil.wait_procs(victims, timeout=5)
    +            for proc in alive:
    +                try:
    +                    proc.kill()
    +                except Exception:
    +                    continue
    +        except Exception:
    +            pass
    +    return stopped
    +
    +
     def _desktop_macos_relaunchable_fixup(desktop_dir: Path) -> None:
         """Make a locally-built (unsigned) macOS desktop app survive in-place self-update.
     
    ```

    ```diff
    @@ -7686,6 +7759,15 @@ def cmd_gui(args: argparse.Namespace):
                 build_label = "source build" if source_mode else "packaged app"
                 print(f"→ Building desktop {build_label}...")
                 build_script = "build" if source_mode else "pack"
    +            if not source_mode:
    +                # A running desktop instance launched from release/win-unpacked
    +                # holds Hermes.exe locked on Windows, so the pack can't replace
    +                # it ("Access is denied" / ERR_ELECTRON_BUILDER_CANNOT_EXECUTE).
    +                # Stop it first so the rebuild — including the installer's
    +                # headless --update rebuild — succeeds instead of failing cryptically.
    +                stopped = _stop_desktop_processes_locking_build(desktop_dir)
    +                if stopped:
    +                    print(f"  ⚠ Stopped running desktop app to free the build output (pid {', '.join(map(str, stopped))})")
                 build_result = subprocess.run([npm, "run", build_script], cwd=desktop_dir, env=env, check=False)
                 if build_result.returncode != 0 and not source_mode:
                     # A corrupt cached Electron zip makes `pack` fail with an ENOENT
    ```

    ```diff
    @@ -7706,10 +7788,16 @@ def cmd_gui(args: argparse.Namespace):
                         print("  ⚠ Desktop build failed; cleared cached Electron download and retrying once...")
                         for p in purged:
                             print(f"    - {p}")
    +                    # The purge can't remove a win-unpacked tree whose Hermes.exe
    +                    # is still locked by a running instance; stop it before retry.
    +                    _stop_desktop_processes_locking_build(desktop_dir)
                         build_result = subprocess.run([npm, "run", build_script], cwd=desktop_dir, env=env, check=False)
                 if build_result.returncode != 0:
                     print("✗ Desktop GUI build failed")
                     print(f"  Run manually:  cd apps/desktop && npm run {build_script}")
    +                if sys.platform == "win32":
    +                    print("  If this says \"Access is denied\" on Hermes.exe, close any")
    +                    print("  running Hermes desktop window and retry.")
                     sys.exit(build_result.returncode or 1)
                 packaged_executable = _desktop_packaged_executable(desktop_dir)
                 if not source_mode:
    ```
  - `tests/hermes_cli/test_gui_command.py`

    ```diff
    @@ -519,3 +519,78 @@ def test_gui_does_not_retry_when_purge_finds_nothing(tmp_path, monkeypatch, caps
         mock_purge.assert_called_once()
         assert mock_run.call_count == 1
         assert "Desktop GUI build failed" in capsys.readouterr().out
    +
    +
    +class _FakeProc:
    +    """Minimal psutil.Process stand-in for the lock-breaker tests."""
    +
    +    def __init__(self, pid: int, exe: str | None):
    +        self.pid = pid
    +        self.info = {"pid": pid, "exe": exe}
    +        self.terminated = False
    +        self.killed = False
    +
    +    def terminate(self):
    +        self.terminated = True
    +
    +    def kill(self):
    +        self.killed = True
    +
    +
    +def test_stop_desktop_build_lock_noop_off_windows(tmp_path, monkeypatch):
    +    """POSIX can unlink a running binary, so the helper is a no-op there."""
    +    desktop_dir = tmp_path / "apps" / "desktop"
    +    exe = desktop_dir / "release" / "linux-unpacked" / "hermes"
    +    exe.parent.mkdir(parents=True)
    +    exe.write_text("", encoding="utf-8")
    +    monkeypatch.setattr(cli_main.sys, "platform", "linux")
    +
    +    proc = _FakeProc(4321, str(exe))
    +    with patch("psutil.process_iter", return_value=[proc]) as it:
    +        assert cli_main._stop_desktop_processes_locking_build(desktop_dir) == []
    +    it.assert_not_called()
    +    assert proc.terminated is False
    +
    +
    +def test_stop_desktop_build_lock_terminates_only_release_procs(tmp_path, monkeypatch):
    +    desktop_dir = tmp_path / "apps" / "desktop"
    +    release = desktop_dir / "release" / "win-unpacked"
    +    release.mkdir(parents=True)
    +    locker_exe = release / "Hermes.exe"
    +    locker_exe.write_text("", encoding="utf-8")
    +    other_exe = tmp_path / "elsewhere" / "Hermes.exe"
    +    other_exe.parent.mkdir(parents=True)
    +    other_exe.write_text("", encoding="utf-8")
    +
    +    monkeypatch.setattr(cli_main.sys, "platform", "win32")
    +    monkeypatch.setattr(cli_main.os, "getpid", lambda: 999)
    +
    +    locker = _FakeProc(101, str(locker_exe))
    +    unrelated = _FakeProc(102, str(other_exe))
    +    selfish = _FakeProc(999, str(locker_exe))  # our own PID — never killed
    +    no_exe = _FakeProc(103, None)
    +
    +    captured = {}
    +
    +    def _wait(procs, timeout=None):
    +        captured["waited"] = list(procs)
    +        return procs, []
    +
    +    with patch("psutil.process_iter", return_value=[locker, unrelated, selfish, no_exe]), \
    +         patch("psutil.wait_procs", side_effect=_wait):
    +        stopped = cli_main._stop_desktop_processes_locking_build(desktop_dir)
    +
    +    assert stopped == [101]
    +    assert locker.terminated is True
    +    assert unrelated.terminated is False
    +    assert selfish.terminated is False
    +    assert captured["waited"] == [locker]
    +
    +
    +def test_stop_desktop_build_lock_no_release_dir(tmp_path, monkeypatch):
    +    desktop_dir = tmp_path / "apps" / "desktop"
    +    desktop_dir.mkdir(parents=True)
    +    monkeypatch.setattr(cli_main.sys, "platform", "win32")
    +    with patch("psutil.process_iter") as it:
    +        assert cli_main._stop_desktop_processes_locking_build(desktop_dir) == []
    +    it.assert_not_called()
    ```
- **#52044: fix(install): kill venv-resident gateway before recreating venv on Windows**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `scripts/install.ps1`, `scripts/release.py`, `tests/test_install_unmerged_index.py`
  - fix hunks:
  - `scripts/install.ps1`

    ```diff
    @@ -1562,16 +1562,54 @@ function Install-Venv {
         
         if (Test-Path "venv") {
             Write-Info "Virtual environment already exists, recreating..."
    -        # On Windows, native Python extensions (e.g. _bcrypt.pyd) are loaded as
    -        # DLLs by any running hermes process. Windows denies deletion of loaded
    -        # DLLs, so kill any hermes.exe tree before removing the venv.
    +        # On Windows, native Python extensions (e.g. _bcrypt.pyd, tornado's
    +        # speedups.pyd) are loaded as DLLs by any running hermes process.
    +        # Windows denies deletion of loaded DLLs, so every process running out
    +        # of this venv must be stopped before removing it -- otherwise
    +        # Remove-Item fails with "Access to the path '...' is denied" and the
    +        # whole install/update aborts at this stage.
             if ($env:OS -eq "Windows_NT") {
                 $myPid = $PID
                 Write-Info "Stopping any running hermes processes before recreating venv..."
    +            # The launcher CLI (hermes.exe) plus its child tree.
                 & taskkill /F /T /IM hermes.exe /FI "PID ne $myPid" 2>$null | Out-Null
    +            # taskkill /IM hermes.exe is NOT enough: the gateway/agent that a
    +            # scheduled task or watchdog autostarts runs as
    +            # `pythonw.exe -m hermes_cli.main gateway run` straight out of
    +            # venv\Scripts\, so its image name is python/pythonw, not hermes.exe.
    +            # That process holds the venv's .pyd files open and re-triggers the
    +            # access-denied failure. Stop anything whose executable lives under
    +            # this venv, matched by path prefix so the image name does not matter
    +            # and a global/system python outside the venv is never touched.
    +            #
    +            # The gateway autostart task registers with /RL LIMITED as the current
    +            # user (see hermes_cli/gateway_windows.py), so the installer always
    +            # runs at equal-or-higher integrity and can read its executable path.
    +            # Get-CimInstance is used over Get-Process because it returns a null
    +            # ExecutablePath for a process it cannot inspect (a different session)
    +            # instead of throwing, so an unreadable process is skipped rather than
    +            # aborting the whole sweep.
    +            $venvPrefix = [System.IO.Path]::GetFullPath((Join-Path $InstallDir "venv")).TrimEnd('\') + '\'
    +            try {
    +                Get-CimInstance Win32_Process -ErrorAction Stop |
    +                    Where-Object { $_.ProcessId -ne $myPid -and $_.ExecutablePath -and $_.ExecutablePath.StartsWith($venvPrefix, [System.StringComparison]::OrdinalIgnoreCase) } |
    +                    ForEach-Object {
    +                        Write-Info "  stopping PID $($_.ProcessId) ($($_.Name)) running from venv"
    +                        Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
    +                    }
    +            } catch {
    +                Write-Warn "Could not enumerate venv processes: $($_.Exception.Message)"
    +            }
                 Start-Sleep -Milliseconds 800
             }
    -        Remove-Item -Recurse -Force "venv"
    +        Remove-Item -Recurse -Force "venv" -ErrorAction SilentlyContinue
    +        # A killed process can take a moment to release its file handles, so a
    +        # first Remove-Item may still hit a locked .pyd. Retry once after a short
    +        # pause before giving up and letting the stage fail loudly.
    +        if (Test-Path "venv") {
    +            Start-Sleep -Seconds 2
    +            Remove-Item -Recurse -Force "venv"
    +        }
         }
         
         # uv creates the venv and pins the Python version in one step.  uv emits
    ```
  - `scripts/release.py`

    ```diff
    @@ -45,6 +45,7 @@
     
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
    +    "[redacted:email]": "Danamove",  # PR #46726 salvage (kill venv-resident pythonw gateway before recreating venv on Windows; #47036/#47557/#47910)
         "145739220+wgu9@users.noreply.github.com": "wgu9",  # PR #51468 salvage (WSL/no-systemd orphan gateway tracking, #51325)
         "165020384+uperLu@users.noreply.github.com": "uperLu",  # PR #50958 salvage (rename plugins/cron → plugins/cron_providers; #50872)
         "277269729+yusekiotacode@users.noreply.github.com": "yusekiotacode",  # PR #48706 salvage (anthropic OAuth login token endpoint → platform.claude.com; #45250/#49821)
    ```
  - `tests/test_install_unmerged_index.py`

    ```diff
    @@ -141,3 +141,37 @@ def test_install_sh_clears_unmerged_index_before_stash_source_order() -> None:
         idx_unmerged = text.index("ls-files --unmerged")
         idx_stash = text.index("stash push --include-untracked")
         assert idx_unmerged < idx_stash
    +
    +
    +def test_install_ps1_stops_venv_resident_processes_before_removing_venv() -> None:
    +    """The Windows venv-recreate path must stop every process running out of the
    +    old venv before deleting it.
    +
    +    A gateway autostarted by a scheduled task runs as
    +    ``venv\\Scripts\\pythonw.exe -m hermes_cli.main gateway run`` — image name
    +    ``pythonw``, not ``hermes.exe`` — so the ``taskkill /IM hermes.exe`` guard
    +    misses it, the loaded ``.pyd`` stays locked, and ``Remove-Item venv`` fails
    +    mid-recursion (issues #47036/#47557/#47910). The recreate branch must also
    +    sweep by venv path prefix, and that sweep must run before the delete.
    +    """
    +    text = INSTALL_PS1.read_text()
    +
    +    # The hermes.exe tree-kill is preserved (kills spawned child processes too).
    +    assert 'taskkill /F /T /IM hermes.exe' in text
    +
    +    # The venv path-prefix sweep exists. It must match by case-insensitive
    +    # StartsWith, NOT PowerShell -like: a venv path containing wildcard
    +    # metacharacters ('[', ']') — legal in a Windows user name — silently fails
    +    # to match under -like, reintroducing the exact miss this fix closes.
    +    idx_recreate = text.index("Virtual environment already exists, recreating")
    +    idx_sweep = text.index("StartsWith($venvPrefix", idx_recreate)
    +    assert "[System.StringComparison]::OrdinalIgnoreCase" in text[idx_sweep:idx_sweep + 200]
    +    assert 'ExecutablePath -like "$venvRoot' not in text, (
    +        "the -like wildcard match must not be used for venv path scoping"
    +    )
    +
    +    # The process sweep must run before the venv is removed, or it is a no-op.
    +    idx_remove = text.index('Remove-Item -Recurse -Force "venv"', idx_recreate)
    +    assert idx_sweep < idx_remove, (
    +        "venv-resident processes must be stopped before Remove-Item deletes the venv"
    +    )
    ```
- **#37790: fix(installer): reap Windows update locker processes**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#37731`
  - files: `apps/bootstrap-installer/src-tauri/src/update.rs`
- **#39487: fix(update): pause Windows gateway before venv refresh**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `78c11d99e35b111dd6198d8bc6aa1f7f6f168232`, `#45457`
- **#43381: fix(desktop): kill untracked hermes processes before update (#43268)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#46726: fix(install): kill venv-resident gateway before recreating venv on Windows**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52044`
- **#47569: fix(install): clear venv python locks on Windows**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#46726`, `#47557`, `#45120`
- **#47610: fix(installer): kill venv processes before removing venv dir (#47557)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#46726`, `#47557`, `#47569`
- **#47621: fix(cli): kill venv python processes before venv recreation on Windows**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `6638199c53f64f995ac02253456f719960ec7673`, `#52044`, `#47557`, `#46726`
- **#57852: fix(update): ignore Windows system venv holder noise**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `1f89f3102f`, `50ea329ab4`
  - files: `hermes_cli/main.py`, `tests/hermes_cli/test_update_venv_health.py`
- **#64386: feat(cli): add --force-kill flag for Windows update (#63300)**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `3bfb6fb474e8`, `#64386`, `#63300`, `#374151`
  - files: `hermes_cli/main.py`, `hermes_cli/subcommands/update.py`, `hermes_cli/update_cmd.py`, `tests/hermes_cli/test_update_concurrent_quarantine.py`
- **#68821: fix(update): hard-stop when Windows hermes.exe shim stays locked**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`; refs `#68760`

## Fix status

- Merged to main in PR #52044 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-24`.
- Subsystem: `apps/bootstrap-installer`
