Merge remote-tracking branch 'origin/main' into feat/opentui-native-engine

This commit is contained in:
alt-glitch 2026-06-15 15:59:30 +05:30
commit 946d3eaf95
534 changed files with 39187 additions and 6755 deletions

View file

@ -1171,6 +1171,20 @@ function Install-Repository {
# agent-created dirs (e.g. tinker-atropos/) survive too.
$statusOut = git -c windows.appendAtomically=false status --porcelain 2>$null
if (-not [string]::IsNullOrWhiteSpace(($statusOut -join "`n"))) {
# A previously interrupted update can leave the index with
# unmerged entries. In that state `git stash` aborts with
# "could not write index" and the following `git checkout`
# aborts with "you need to resolve your current index first"
# -- the GUI "git checkout main failed (exit 1)" install
# failure. Clear the conflict markers with `git reset` first:
# working-tree changes are kept (and stashed just below); only
# the index conflict state is dropped. Mirrors the `hermes
# update` path (#4735).
$unmergedOut = git -c windows.appendAtomically=false ls-files --unmerged 2>$null
if (-not [string]::IsNullOrWhiteSpace(($unmergedOut -join "`n"))) {
Write-Info "Clearing unmerged index entries from a previous conflict..."
git -c windows.appendAtomically=false reset -q 2>$null
}
$stashName = "hermes-install-autostash-" + (Get-Date -Format "yyyyMMdd-HHmmss")
Write-Info "Local changes detected, stashing before update..."
git -c windows.appendAtomically=false stash push --include-untracked -m "$stashName"
@ -1417,6 +1431,15 @@ 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.
if ($env:OS -eq "Windows_NT") {
$myPid = $PID
Write-Info "Stopping any running hermes processes before recreating venv..."
& taskkill /F /T /IM hermes.exe /FI "PID ne $myPid" 2>$null | Out-Null
Start-Sleep -Milliseconds 800
}
Remove-Item -Recurse -Force "venv"
}

View file

@ -1111,6 +1111,19 @@ clone_repo() {
local autostash_ref=""
if [ -n "$(git status --porcelain)" ]; then
# A previously interrupted update can leave the index with
# unmerged entries. In that state `git stash` aborts with
# "could not write index" and the later `git checkout` aborts
# with "you need to resolve your current index first", failing
# the whole install at the repository stage. Clear the conflict
# markers with `git reset` first -- this keeps working-tree
# changes (they're still stashed just below) and only drops the
# index-level conflict state. Mirrors the `hermes update` path
# (#4735).
if [ -n "$(git ls-files --unmerged)" ]; then
log_info "Clearing unmerged index entries from a previous conflict..."
git reset -q
fi
local stash_name
stash_name="hermes-install-autostash-$(date -u +%Y%m%d-%H%M%S)"
log_info "Local changes detected, stashing before update..."

View file

@ -45,7 +45,14 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"kenmege@yahoo.com": "Kenmege",
"dkobi16@gmail.com": "Diyoncrz18",
"arnaud@nolimitdevelopment.com": "ali-nld",
"sswdarius@gmail.com": "necoweb3",
"peterhao@Peters-MacBook-Air.local": "pinguarmy",
"adalsteinnhelgason@Aalsteinns-MacBook-Pro-3.local": "AIalliAI",
"adalsteinnhelgason@users.noreply.github.com": "AIalliAI",
"zhang.hz6666@gmail.com": "HaozheZhang6",
"barronlroth@gmail.com": "barronlroth",
"ondrej.drapalik@gmail.com": "OndrejDrapalik",
"tomasz.panek@gmail.com": "tomekpanek",
@ -75,7 +82,18 @@ AUTHOR_MAP = {
"harjoth.khara@gmail.com": "harjothkhara",
"129007007+HeLLGURD@users.noreply.github.com": "HeLLGURD",
"290859878+synapsesx@users.noreply.github.com": "synapsesx",
"157689911+itsflownium@users.noreply.github.com": "itsflownium",
"dirtyren@users.noreply.github.com": "dirtyren",
"35259607+zxcasongs@users.noreply.github.com": "zxcasongs",
"alfred@my-cloud.me": "alfred-smith-0",
"tangtaizhong792@gmail.com": "tangtaizong666",
"github@aldo.pw": "aldoeliacim",
"max@c60spaceship.com": "MaxFreedomPollard",
"achaljhawar03@gmail.com": "achaljhawar",
"claytonchew@ClaytonMacMiniM4.local": "claytonchew",
"hbentel@gmail.com": "hbentel",
"JustinBao@outlook.com": "justinbao19",
"kdunn926@gmail.com": "kdunn926",
"mvanhorn@MacBook-Pro.local": "mvanhorn",
"470766206@qq.com": "youjunxiaji",
"mharris@parallel.ai": "NormallyGaussian",
@ -345,6 +363,7 @@ AUTHOR_MAP = {
"rino.alpin@gmail.com": "kunci115", # PR #27098 salvage (thread-not-found retry)
"hayka-pacha@users.noreply.github.com": "hayka-pacha", # PR #25270 salvage (registry-aware mcp_ prefix strip)
"237601532+chromalinx@users.noreply.github.com": "chromalinx", # PR #27014 salvage (commands for groups+DM)
"chromalinx@users.noreply.github.com": "chromalinx", # PR #37026 salvage (SSL CA guard)
"booker1207@gmail.com": "booker1207", # PR #25132 salvage (gate profile bots by allowed topics)
"kiranvk2011@gmail.com": "kiranvk-2011", # PR #24815 salvage (image documents → vision)
"kosmonaut-t@centrum.cz": "rak135", # PR #25960 salvage (Windows /restart)
@ -486,6 +505,7 @@ AUTHOR_MAP = {
"20nik.nosov21@gmail.com": "nik1t7n",
"90299797+nik1t7n@users.noreply.github.com": "nik1t7n",
"suncokret@protonmail.com": "suncokret12",
"WompaJango@protonmail.com": "WompaJango",
"mio.imoto.ai@gmail.com": "mioimotoai-lgtm",
"aamirjawaid@microsoft.com": "heyitsaamir",
"johnnncenaaa77@gmail.com": "johnncenae",
@ -1518,6 +1538,8 @@ AUTHOR_MAP = {
"andreas@schwarz-ketsch.de": "Nea74", # PR #40022 co-author credit (same Windows ConPTY bridge design)
"chanhokyim@gmail.com": "joel611", # PR #33958 salvage (DISCORD_ALLOWED_ROLES role_authorized gateway flag)
"desg38@gmail.com": "dschnurbusch", # PR #42373 salvage (archive compressed conversation lineages)
"bsmith@bramarstrategicservices.com": "bcsmith528", # PR #20589 salvage (register_slack_action_handler plugin API)
"sunsky.lau@gmail.com": "liuhao1024", # PR #45494 salvage (claim session slot before auto-resume task; #45456)
}

View file

@ -73,6 +73,7 @@ exec env -i \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PYTHONHASHSEED=0 \
PYTHONDONTWRITEBYTECODE=1 \
${EXTRA_PYTHONPATH:+PYTHONPATH="$EXTRA_PYTHONPATH"} \
${EXTRA_PYTEST_PLUGINS:+PYTEST_PLUGINS="$EXTRA_PYTEST_PLUGINS"} \
"$PYTHON" "$SCRIPT_DIR/run_tests_parallel.py" "$@"

View file

@ -65,17 +65,14 @@ _DEFAULT_ROOTS = ["tests"]
# rebuild). The full pytest-shard runner can't
# host these because the session-scoped
# ``built_image`` fixture would do a 3-7min
# ``docker build`` inside a 180s per-test
# pytest-timeout cap (set by tests/docker/conftest.py),
# ``docker build``,
# so the build is guaranteed to die in fixture
# setup. The dedicated job sidesteps both costs.
_SKIP_PARTS = {"integration", "e2e", "docker"}
# Per-file wall-clock cap. Generous default — pytest-timeout still
# enforces per-test caps inside each subprocess; this is just an outer
# safety net so a single hung file can't stall the whole suite. Override
# Per-file wall-clock cap. Override
# via --file-timeout or HERMES_TEST_FILE_TIMEOUT.
_DEFAULT_FILE_TIMEOUT_SECONDS = 600.0 # 10 minutes
_DEFAULT_FILE_TIMEOUT_SECONDS = 140.0 # set by observing the slowest file at commit time was ~100s in CI and adding some leeway
# Duration cache: maps relative file paths to last-observed subprocess
# wall-clock seconds. Used by ``--slice`` to distribute files across
@ -246,27 +243,49 @@ def _kill_tree(proc: "subprocess.Popen", pgid: int | None = None) -> None:
pass
def _spawn_pytest_once(
cmd: List[str],
def _run_one_file(
file: Path,
pytest_args: List[str],
repo_root: Path,
file_timeout: float,
*,
timeout_note: str = "per-file timeout",
) -> Tuple[int, str]:
"""Run one ``pytest`` subprocess to completion and return ``(rc, output)``.
) -> Tuple[Path, int, str, dict[str, int], float]:
"""Run ``python -m pytest <file> <pytest_args>`` in a fresh subprocess.
Spawns the child in its own process group / session so a hung file and
its grandchildren (uvicorn servers, async runtimes, etc.) can be SIGKILL'd
as a tree on timeout rather than orphaning onto PID 1. Shared by the
primary per-file run and the exit-4 retry loop so the lifecycle/cleanup
logic lives in exactly one place.
Returns (file, returncode, captured_combined_output, summary_counts, subprocess_wall_seconds).
``summary_counts`` is the result of ``_parse_pytest_summary(output)``
pytest exit codes (https://docs.pytest.org/en/stable/reference/exit-codes.html):
0 = all tests passed
1 = some tests failed
2 = test execution interrupted
3 = internal error
4 = pytest CLI usage error
5 = no tests collected
We treat exit 5 as a pass: it just means every test in the file was
skipped or filtered by a marker (e.g. ``-m 'not integration'`` skips
files where every test is marked integration). That's intentional and
not a failure mode.
On per-file timeout (``file_timeout`` seconds) or any other exception
during ``communicate()``, we kill the whole process group / process
tree so grandchildren (uvicorn servers, async runtimes, etc.) do not
orphan onto PID 1. This outer timeout exists only to
bound a pathologically slow or hung file as a whole.
"""
cmd = [sys.executable, "-m", "pytest", str(file), *pytest_args]
subproc_start = time.monotonic()
# launch the pytest process
proc = subprocess.Popen(
cmd,
cwd=repo_root,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
# skipping writing bytecode because we're running a bunch of parallel python processes on the same code
env={**os.environ, 'PYTHONDONTWRITEBYTECODE': '1'},
# POSIX: place the child at the head of its own process group so
# _kill_tree can SIGKILL the group atomically.
# Windows: this maps to CREATE_NEW_PROCESS_GROUP in CPython 3.12+;
@ -296,7 +315,7 @@ def _spawn_pytest_once(
output = "(file timeout exceeded; output unavailable)"
rc = 124 # de facto convention for "killed by timeout".
output = (
f"({timeout_note}: {file_timeout:.0f}s exceeded; "
f"({file_timeout:.0f}s exceeded; "
f"process tree SIGKILL'd)\n{output}"
)
except BaseException:
@ -309,123 +328,7 @@ def _spawn_pytest_once(
# case it left grandchildren behind; already-dead is a no-op.
_kill_tree(proc, pgid=pgid)
return rc, output
# How many times to re-run a file that exits 4 ("file or directory not found")
# while the file demonstrably exists on disk. On loaded shared CI runners the
# planner can enumerate a file (tests counted via --collect-only) but the
# per-file subprocess fail to stat it moments later — and a SINGLE immediate
# retry can land in the same brief high-load window and fail again. We retry a
# few times with a short backoff so transient I/O pressure has time to settle.
_EXIT4_RETRY_ATTEMPTS = 3
_EXIT4_RETRY_BACKOFF_SECONDS = 0.5
def _file_present(file: Path, *, attempts: int = 3, delay: float = 0.2) -> bool:
"""Return True if ``file`` exists, re-checking a few times.
``Path.exists()`` itself issues a ``stat`` that can transiently fail under
the same load that makes pytest report "file or directory not found", so a
single negative check is not authoritative. Only conclude the file is
genuinely missing if it's absent across several spaced checks.
"""
for i in range(attempts):
if file.exists():
return True
if i < attempts - 1:
time.sleep(delay)
return False
def _run_one_file(
file: Path,
pytest_args: List[str],
repo_root: Path,
file_timeout: float,
) -> Tuple[Path, int, str, dict[str, int], float]:
"""Run ``python -m pytest <file> <pytest_args>`` in a fresh subprocess.
Returns (file, returncode, captured_combined_output, summary_counts, subprocess_wall_seconds).
``summary_counts`` is the result of ``_parse_pytest_summary(output)``
pytest exit codes (https://docs.pytest.org/en/stable/reference/exit-codes.html):
0 = all tests passed
1 = some tests failed
2 = test execution interrupted
3 = internal error
4 = pytest CLI usage error
5 = no tests collected
We treat exit 5 as a pass: it just means every test in the file was
skipped or filtered by a marker (e.g. ``-m 'not integration'`` skips
files where every test is marked integration). That's intentional and
not a failure mode.
On per-file timeout (``file_timeout`` seconds) or any other exception
during ``communicate()``, we kill the whole process group / process
tree so grandchildren (uvicorn servers, async runtimes, etc.) do not
orphan onto PID 1. The pytest-timeout plugin enforces per-test
timeouts inside the subprocess; this outer timeout exists only to
bound a pathologically slow or hung file as a whole.
"""
cmd = [sys.executable, "-m", "pytest", str(file), *pytest_args]
subproc_start = time.monotonic()
rc, output = _spawn_pytest_once(cmd, repo_root, file_timeout)
# pytest exit 4 = "file or directory not found" at exec time. On loaded
# shared CI runners we have seen the planner enumerate a file (its tests
# counted via --collect-only) but the per-file subprocess fail to stat it
# moments later — a transient the deterministic LPT slicer otherwise
# reproduces on every rerun (same file set → same shard). Re-run the file a
# few times with a short backoff so the I/O pressure has time to settle,
# but ONLY while the file demonstrably exists on disk. A single immediate
# retry (the old behaviour) could land in the same brief high-load window
# and fail again; a single Path.exists() check could itself be a flaky stat
# under that load, so we re-check existence across spaced attempts.
# We do NOT widen the exit-5 rule: exit 4 on a file that genuinely does not
# exist must still fail.
attempt = 0
while rc == 4 and attempt < _EXIT4_RETRY_ATTEMPTS and _file_present(file):
attempt += 1
time.sleep(_EXIT4_RETRY_BACKOFF_SECONDS * attempt)
rc, output = _spawn_pytest_once(
cmd, repo_root, file_timeout,
timeout_note=f"per-file timeout on exit-4 retry {attempt}",
)
if rc == 4:
# Exit-4 survived the retries (or the file was judged absent).
# Capture filesystem forensics so a CI-only "file not found" can
# be diagnosed from the log instead of guessed at: does the file
# exist NOW, what does the parent dir hold, and is the git tree
# clean? (June 2026: a PR-added test file repeatedly hit exit 4
# on one CI shard while passing locally — these lines exist so
# the next occurrence is attributable.)
forensics = [f"--- exit-4 forensics for {file} ---"]
try:
forensics.append(f"exists={file.exists()} retries_used={attempt}")
parent = file.parent
if parent.exists():
names = sorted(p.name for p in parent.iterdir())
sibling_hint = [n for n in names if file.stem[:12] in n]
forensics.append(
f"parent={parent} entries={len(names)} "
f"similar={sibling_hint[:5]}"
)
else:
forensics.append(f"parent={parent} MISSING")
git_st = subprocess.run(
["git", "status", "--porcelain"],
cwd=repo_root, capture_output=True, text=True, timeout=10,
)
dirty = git_st.stdout.strip().splitlines()
forensics.append(f"git_dirty_entries={len(dirty)}")
forensics.extend(f" {line}" for line in dirty[:10])
except Exception as exc: # noqa: BLE001 — forensics must never mask rc=4
forensics.append(f"(forensics error: {exc})")
output = output + "\n" + "\n".join(forensics)
output += "\n"
if rc == 5:
# No tests collected — every test in the file was filtered out.
@ -721,7 +624,7 @@ def main() -> int:
help=(
"Per-file wall-clock cap in seconds. On timeout, the pytest "
"subprocess and its full process tree are SIGKILL'd. "
"Default: 600 (10 min), env: HERMES_TEST_FILE_TIMEOUT."
f"Default: {_DEFAULT_FILE_TIMEOUT_SECONDS}s ({round(_DEFAULT_FILE_TIMEOUT_SECONDS/60)} min), env: HERMES_TEST_FILE_TIMEOUT."
),
)
parser.add_argument(