Skip to content

Release v0.51.304 — Release JT (stage-p2a — un-held terminal reaper + opt-in Docker GPU) - #3757

Merged
nesquena-hermes merged 3 commits into
masterfrom
release/stage-p2a
Jun 7, 2026
Merged

nesquena-hermes merged 3 commits into
masterfrom
release/stage-p2a

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release v0.51.304 — Release JT (stage-p2a)

Un-held terminal-reaper fix + opt-in Docker GPU image. Both rebased onto fresh master (stale bases), fidelity-verified byte-identical to PR head.

Fixed

Added

Gates

  • Full suite: 8132 passed, 0 failed
  • Codex regression gate: SAFE TO SHIP (targeted check: terminal-PGID child reaped, unrelated-PGID child stays waitable)
  • Opus correctness gate: SAFE TO SHIP (audited group loop under set -e — all failure modes non-fatal, no-op for stock docker run)
  • Ruff forward gate: CLEAN
  • Revert-guard: origin/master is ancestor

Closes #3725, closes #3721.

nesquena-hermes and others added 3 commits June 7, 2026 00:42
…3725, #2577)

Embedded-terminal descendants reparented to the WebUI process could linger as
zombies. The reaper now calls os.waitpid(-terminal_pgid, WNOHANG) scoped to the
terminal's own process group (terminals spawn with start_new_session=True, so
proc.pid == pgid) rather than process-wide waitpid(-1), which would otherwise
reap unrelated WebUI subprocess children and silently coerce their exit codes to
0. Bounded by a 64-iteration limit and lock-guarded. Runs on reader cleanup and
terminal close.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
The default image stays CPU-only. A new INSTALL_GPU_LIBS=1 build arg installs
VA-API user-space libraries for users passing through host GPU devices, and
docker_init.bash preserves Docker --group-add supplemental groups (e.g. render/
video for /dev/dri) when dropping privileges to the runtime user. Default
(INSTALL_GPU_LIBS=0) is a no-op. Docs + regression test included.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ships two independent fixes rebased onto master: a scoped terminal-descendant reaper in api/terminal.py and an opt-in Docker GPU image path.

  • Terminal reaper fix: replaces the process-wide os.waitpid(-1, WNOHANG) with os.waitpid(-terminal_pgid, WNOHANG), scoping zombie reaping to the terminal's own process group (possible because terminals are spawned with start_new_session=True, making pid == pgid). Called from both _reader_loop and close_terminal, guarded by a threading lock. Four targeted tests cover PGID isolation, bounded iteration, error handling, and the close-terminal integration.
  • Optional GPU image: ARG INSTALL_GPU_LIBS=0 (default no-op) installs VA-API libraries when set to 1; docker_init.bash loops over Docker-provided supplemental GIDs and ensures hermeswebui inherits them before su drops privileges, with graceful fallbacks throughout. Docs and snapshot tests accompany both changes.

Confidence Score: 4/5

Safe to merge; the terminal reaper logic is well-scoped and well-tested, and the GPU/Docker path is purely opt-in with graceful fallbacks everywhere.

Both changes are narrow and self-contained. The PGID-scoped reaper correctly avoids the original cross-session clobber, and the Docker group loop suppresses every failure mode with || true. The only issues found are a dead guard in docker_init.bash, an over-broad threading lock in the reaper, and a hardcoded old-version sentinel in one test that would raise ValueError if that changelog entry is ever pruned.

docker_init.bash (dead guard) and tests/test_docker_gpu_runtime_docs.py (fragile changelog sentinel) deserve a quick look; api/terminal.py and the rest of the test suite look solid.

Important Files Changed

Filename Overview
api/terminal.py Adds _reap_terminal_descendants() — scoped waitpid(-pgid, WNOHANG) replacing the old process-wide form — called from both _reader_loop and close_terminal; guarded by a global lock (unnecessarily broad but correct).
docker_init.bash Adds a group-preservation loop that maps Docker --group-add GIDs to named groups and adds hermeswebui to each before privilege drop; contains one dead if [ -n "$group_name" ] guard after a continue.
Dockerfile Introduces opt-in ARG INSTALL_GPU_LIBS=0 block; default path is a no-op, GPU libs only installed on =1; Intel non-free driver has graceful apt-cache show fallback.
tests/test_terminal_zombie_reaper.py Linux-only tests covering the PGID-scoped reaper: fork/exec integration test, monkeypatched close_terminal flow, error-handling, and bounded-iteration tests — all logically sound.
tests/test_docker_gpu_runtime_docs.py Snapshot tests asserting Dockerfile, docker_init.bash, docs, and CHANGELOG content; the changelog test anchors its upper bound on a hardcoded older version string that will raise ValueError if that entry is ever pruned.
docs/docker.md Adds GPU runtime documentation covering Intel/AMD VA-API and NVIDIA passthrough; explicitly disclaims native verification and documents the supplemental-group preservation behavior.
CHANGELOG.md Adds v0.51.304 entry documenting the PGID-scoped reaper fix and the opt-in GPU image path.

Sequence Diagram

sequenceDiagram
    participant RT as _reader_loop (thread)
    participant CT as close_terminal (caller)
    participant R as _reap_terminal_descendants
    participant OS as os.waitpid

    Note over RT,OS: Shell exits naturally
    RT->>RT: proc.poll() → stores returncode
    RT->>R: _reap_terminal_descendants(proc.pid)
    R->>OS: waitpid(-pgid, WNOHANG) [scoped to terminal PGID]
    OS-->>R: (child_pid, status) or (0,0)
    R-->>RT: reaped count

    Note over CT,OS: close_terminal called concurrently
    CT->>CT: killpg(SIGHUP) + proc.wait(1.5s)
    CT->>R: _reap_terminal_descendants(proc.pid)
    Note over R: global lock serializes concurrent calls
    R->>OS: waitpid(-pgid, WNOHANG)
    OS-->>R: ECHILD / (0,0) — already reaped
    R-->>CT: 0
Loading

Reviews (1): Last reviewed commit: "docs(changelog): stamp v0.51.304 — Relea..." | Re-trigger Greptile

Comment thread docker_init.bash
Comment on lines +281 to +287
if [ -z "$group_name" ]; then
echo "!! WARNING: Could not create supplemental group for GID $gid; GPU device access may be unavailable"
continue
fi
if [ -n "$group_name" ]; then
usermod -a -G "$group_name" hermeswebui 2>/dev/null || echo "!! WARNING: Could not add hermeswebui to supplemental group $group_name ($gid)"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The if [ -n "$group_name" ] check on line 285 is unreachable dead code. The continue two lines above already guarantees that group_name is non-empty when execution reaches this point — any empty-group_name path has already bailed out. The guard adds noise and could mislead future readers into thinking there is a case where group_name is empty here.

Suggested change
if [ -z "$group_name" ]; then
echo "!! WARNING: Could not create supplemental group for GID $gid; GPU device access may be unavailable"
continue
fi
if [ -n "$group_name" ]; then
usermod -a -G "$group_name" hermeswebui 2>/dev/null || echo "!! WARNING: Could not add hermeswebui to supplemental group $group_name ($gid)"
fi
if [ -z "$group_name" ]; then
echo "!! WARNING: Could not create supplemental group for GID $gid; GPU device access may be unavailable"
continue
fi
usermod -a -G "$group_name" hermeswebui 2>/dev/null || echo "!! WARNING: Could not add hermeswebui to supplemental group $group_name ($gid)"

Comment on lines +90 to +92
def test_changelog_mentions_optional_gpu_runtime_path():
changelog = _repo_text("CHANGELOG.md")
unreleased = changelog[changelog.index("## [Unreleased]"):changelog.index("## [v0.51.293]")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The test anchors the upper bound of the changelog slice on the literal string "## [v0.51.293]". If changelog history is ever trimmed and that entry is removed, the test raises ValueError: substring not found rather than giving a useful assertion failure. A safer sentinel is the nearest preceding release entry.

Suggested change
def test_changelog_mentions_optional_gpu_runtime_path():
changelog = _repo_text("CHANGELOG.md")
unreleased = changelog[changelog.index("## [Unreleased]"):changelog.index("## [v0.51.293]")]
def test_changelog_mentions_optional_gpu_runtime_path():
changelog = _repo_text("CHANGELOG.md")
# Use the immediately preceding release as the lower-bound sentinel so the
# slice stays valid even if older entries are pruned from the changelog.
unreleased = changelog[changelog.index("## [Unreleased]"):changelog.index("## [v0.51.303]")]

Comment thread api/terminal.py
Comment on lines +92 to +93
_terminal_descendant_reaper_lock = threading.Lock()
_TERMINAL_DESCENDANT_REAPER_LIMIT = 64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Global lock over-serializes independent PGIDs

_terminal_descendant_reaper_lock is process-wide, so two concurrent calls for different terminal sessions block each other even though waitpid(-pgid_A, …) and waitpid(-pgid_B, …) are completely independent at the kernel level. The lock is only required to prevent two callers from racing on the same terminal's PGID. In practice the impact is negligible (each call exits after one ECHILD in the common case), but a per-session lock stored on TerminalSession would be more precise if this code ever needs to scale to many concurrent terminal closings.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@nesquena-hermes
nesquena-hermes merged commit 3a8a51e into master Jun 7, 2026
16 checks passed
@nesquena-hermes
nesquena-hermes deleted the release/stage-p2a branch June 7, 2026 01:04
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
… opt-in Docker GPU) (nesquena#3757)

* fix(terminal): reap reparented terminal descendants by process group (nesquena#3725, nesquena#2577)

Embedded-terminal descendants reparented to the WebUI process could linger as
zombies. The reaper now calls os.waitpid(-terminal_pgid, WNOHANG) scoped to the
terminal's own process group (terminals spawn with start_new_session=True, so
proc.pid == pgid) rather than process-wide waitpid(-1), which would otherwise
reap unrelated WebUI subprocess children and silently coerce their exit codes to
0. Bounded by a 64-iteration limit and lock-guarded. Runs on reader cleanup and
terminal close.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* docs(docker): add opt-in GPU runtime image path (nesquena#3721, nesquena#3243)

The default image stays CPU-only. A new INSTALL_GPU_LIBS=1 build arg installs
VA-API user-space libraries for users passing through host GPU devices, and
docker_init.bash preserves Docker --group-add supplemental groups (e.g. render/
video for /dev/dri) when dropping privileges to the runtime user. Default
(INSTALL_GPU_LIBS=0) is a no-op. Docs + regression test included.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* docs(changelog): stamp v0.51.304 — Release JT (stage-p2a nesquena#3725 nesquena#3721)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] api/terminal.py: Node child processes become zombies when Popen handle is not reaped (same class as #1912)

1 participant