Files
hermes-agent/pm/download_state.py
ethernet 1c8fae6180 fix(pm): preserve runtime and user state across failure paths
Keep downloads bound to their remote representation and publish through
atomic destination-local staging. Serialize shared partial ownership.

Keep explicit CA trust scoped to provider probes. Preserve checkpoint
history and edited files, validate all profile inputs before dependency
publication, and separate data removal from installed runtime ownership.

Exclude machine-specific PM state from portable transfers. Keep plugin
files and nested skill tools intact. Preserve native test isolation.

Focused native Windows receipts cover the individual repairs and their
integration. This commit does not claim a full-suite or release build.
2026-09-09 15:17:08 -04:00

54 lines
1.8 KiB
Python

"""Cross-process ownership of resumable partials and their GC boundary."""
from __future__ import annotations
from contextlib import contextmanager
import os
from pathlib import Path
import time
from hermes_cli.runtime_state import _lock
@contextmanager
def partial_lock(root: Path, key: str, *, cancelled=None, wait: bool = True):
# Lock inodes must survive release: removing one lets a new owner lock a
# different inode while an older waiter still holds the original handle.
locks = root / ".locks"
locks.mkdir(parents=True, exist_ok=True)
fd = os.open(locks / key, os.O_CREAT | os.O_RDWR, 0o600)
try:
while not _lock(fd, wait=False):
if not wait:
yield False
return
if cancelled is not None and cancelled():
yield False
return
time.sleep(0.05)
yield True
finally:
os.close(fd)
GC_GRACE_SECONDS = 6 * 60 * 60
def collect_partials(root: Path, *, grace_seconds: float = GC_GRACE_SECONDS) -> None:
"""Do not let a GC snapshot race a downloader acquiring ownership."""
if not root.is_dir():
return
keys = {path.stem for path in root.iterdir() if path.suffix in {".part", ".ranges"}}
now = time.time()
for key in sorted(keys):
with partial_lock(root, key, wait=False) as acquired:
if not acquired:
continue
pair = [root / f"{key}{suffix}" for suffix in (".part", ".ranges")]
try:
if any(path.exists() and grace_seconds > 0 and now - path.stat().st_mtime < grace_seconds for path in pair):
continue
for path in pair:
path.unlink(missing_ok=True)
except OSError:
continue # An inaccessible partial is not evidence it is unused.