NousResearch/hermes-agent

Windows update blocked by locked venv files and processes

Cluster 26670 · apps/bootstrap-installer. Data as of

3 open items. Fix merged in #52044 on 24 Jun 2026. Check it covers your case.

The short answer

0open issues
3open PRs
30items grouped
Known?
Yes. 30 related items grouped here, 16 issues already closed.
Fix event
A fix was merged to main in PR #52044. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #26677, #42100, #52044. That is separate from the fix event.
Still open
0 issues and 3 PRs.

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.

Open issues0

No open issues.

Open PRs3

  • PR#37790fix(installer): reap Windows update locker processes
  • PR#57852fix(update): ignore Windows system venv holder noise
  • PR#64386feat(cli): add --force-kill flag for Windows update (#63300)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #26677 fix(update): quarantine hermes.exe vs concurrent Windows instance (#26670)

    merged to main

    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

    Show the diff
    • hermes_cli/main.py
      @@ -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
      @@ -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:
      @@ -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)
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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" },
      @@ -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]]
      @@ -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" },
      @@ -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]]
      @@ -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" },
      @@ -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
      @@ -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:
       
       ```
  • #42100 fix(desktop): stop running Hermes.exe locking win-unpacked before Windows pack

    merged to main

    files: hermes_cli/main.py, tests/hermes_cli/test_gui_command.py

    Show the diff
    • hermes_cli/main.py
      @@ -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.
       
      @@ -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
      @@ -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
      @@ -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

    files: scripts/install.ps1, scripts/release.py, tests/test_install_unmerged_index.py

    Show the diff
    • scripts/install.ps1
      @@ -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
      @@ -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)
           "[email protected]": "wgu9",  # PR #51468 salvage (WSL/no-systemd orphan gateway tracking, #51325)
           "[email protected]": "uperLu",  # PR #50958 salvage (rename plugins/cron → plugins/cron_providers; #50872)
           "[email protected]": "yusekiotacode",  # PR #48706 salvage (anthropic OAuth login token endpoint → platform.claude.com; #45250/#49821)
    • tests/test_install_unmerged_index.py
      @@ -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"
      +    )

8 PRs closed without merging

From the close comments: superseded by another PR 2 · done elsewhere 1 · duplicate 1 · merged via another change 1 · stale or abandoned 1 · other 2

  • #39037 fix(cli): detect venv python workers before update to prevent .pyd lock failures (#38789) · done elsewhere → b14d75f, 87ae4ae, #38789
  • #39487 fix(update): pause Windows gateway before venv refresh · other → 78c11d9, #45457
  • #43381 fix(desktop): kill untracked hermes processes before update (#43268) · other
  • #46726 fix(install): kill venv-resident gateway before recreating venv on Windows · superseded by another PR → #52044
  • #47569 fix(install): clear venv python locks on Windows · superseded by another PR → #46726, #47557, #45120
Show 3 more
  • #47610 fix(installer): kill venv processes before removing venv dir (#47557) · duplicate → #46726, #47557, #47569
  • #47621 fix(cli): kill venv python processes before venv recreation on Windows · merged via another change → 6638199, #52044, #47557, #46726
  • #68821 fix(update): hard-stop when Windows hermes.exe shim stays locked · stale or abandoned → #68760
Closed issues · 16
  • Closed#26670fix(update): hermes update on Windows fails to quarantine hermes.exe when another instance is running
  • Closed#37731Windows installer fails with "Access denied" — wipes venv while app + leaked child processes still hold files open
  • Closed#38789[Bug]: Windows hermes update fails when Gateway/Dashboard lock native .pyd dependencies
  • Closed#39431[Bug]: Auto-update fails on Windows — speedups.pyd access denied (venv bootstrap)
  • Closed#41178[Bug]: Windows Desktop self-update rebuild fails because running Hermes.exe locks release/win-unpacked files
  • Closed#43268[Bug] Windows Desktop auto-update fails every time: venv locked by running processes
  • Closed#47036Bootstrap fails to recreate venv when gateway process is running — hermes.exe locked on Windows
  • Closed#47557[Bug] Windows: scripts/install.ps1 venv recreation fails when pythonw.exe holds .pyd locks — only kills hermes.exe
  • Closed#47910Windows installer fails to recreate venv: pre-delete kill only targets hermes.exe, not the pythonw.exe gateway holding the lock
  • Closed#48100[Bug]: Windows installer fails with "Access Denied" on pythonw.exe during auto-update
  • Closed#49428Windows self-replacement deadlock causes hermes update to fail (Error 5 / Access Denied) — suggest detached updater
  • Closed#51015BUG: Windows pip upgrade fails with os error 32 (hermes.exe locked by self)
  • Closed#58387Windows update fails: locked .pyd files by running python/pythonw processes
  • Closed#58730Windows: hermes update fails persistently due to hermes.exe file lock (cannot overwrite running executable)
  • Closed#63300hermes update fails on Windows when headroom proxy/tui_gateway processes are running
  • Closed#68760hermes.exe locked during `hermes update` on Windows (WinError 32)
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
apps/bootstrap-installer
Merged PRs
#26677, #42100, #52044
Closed issues
16
Attempts
8 PRs closed without merging
Fix link checked
24 Jun 2026

For your agent

The same facts, typed.