Fix namespace transport after background process exit - #588
Merged
Conversation
jdchawla29
marked this pull request as ready for review
August 16, 2026 00:00
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5bfc986. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Root cause
Namespace-host commands used asyncio subprocess streams as AsyncSSH redirect sources.
ProcessGroup.wait()intentionally returns when the process leader exits, while a background descendant may still hold those streams. HUD then closes the SSH channel. When the descendant later writes or closes its inherited stream, AsyncSSH's delayed stream task targets an already-closed channel and closes the shared internal SSH connection. All later workspace commands then fail without an exit status.Raw descriptors select AsyncSSH's pipe transport path, whose lifetime is owned by the SSH channel and safely closed when the channel exits. Models do not need to add
</dev/nullor rewrite daemon grammar to keep later workspace commands healthy.Validation
uv run --python 3.11 --with='.[dev]' pytest -q hud/environment/tests/test_workspace.py hud/utils/tests/test_process.py— 78 passeduv run --python 3.12 --with='.[dev]' pytest -q— 1028 passed, 9 deselecteduv run --python 3.12 --extra dev --extra train ty check --error-on-warninguv run --with='.[dev]' ruff format hud/environment/namespace.py hud/environment/tests/test_workspace.py --checkuv run --with='.[dev]' ruff check hud/environment/namespace.py hud/environment/tests/test_workspace.pyNote
High Risk
Changes core namespace-host process spawning and AsyncSSH I/O wiring; a bug could break all workspace commands or leak FDs, though regressions target the reported connection-death scenario.
Overview
Fixes workspace sessions dying after a persistent namespace command leaves background children holding inherited stdio.
namespace.pyno longer wires AsyncSSH to asyncio subprocess pipe objects. Non-TTY spawns useos.pipe()and hand the SSH channel raw read ends viaredirect_stdin/redirect_stdout/redirect_stderr(send_eof=False), so channel teardown owns transport lifetime even when the process leader exits early. PTY paths use the same redirect API with explicit dup/close handling. Spawn and wait paths now terminate the process group on errors or cancellation and close every FD infinally.test_workspace.pyadds_connected_namespace_hostto exercise the real namespace host without full bubblewrap/nsenter, plus regressions for background jobs (inherited EOF, late output, stdin detachment, full redirection) and stdin/stdout/stderr forwarding.Reviewed by Cursor Bugbot for commit 5bfc986. Bugbot is set up for automated code reviews on this repo. Configure here.