fix(shell): drop gateway-wide waitpid(-1) zombie reaper

A global reaper can race asyncio's child watcher for just-exited
create_subprocess_exec children, causing Process returncode 255 instead
of the real command status. Keep only owned-PID reaps after wait/communicate
/kill paths in shell and exec_session.

Addresses review feedback on #4840.
This commit is contained in:
Eric Yang
2026-07-10 20:19:44 +08:00
committed by Xubin Ren
parent bda0c099ab
commit a1fbfd9f7b
-29
View File
@@ -201,7 +201,6 @@ app = typer.Typer(
)
console = Console()
_IS_WINDOWS = sys.platform == "win32"
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}
_REASONING_SENTENCE_ENDINGS = (".", "!", "?", "", "", "")
_REASONING_FLUSH_CHARS = 60
@@ -1696,29 +1695,6 @@ def _run_gateway(
else:
console.print("[yellow]✗[/yellow] Heartbeat: disabled")
async def _zombie_reaper() -> None:
"""Periodically reap zombie child processes.
asyncio's child-watcher *should* reap all children, but inside
Docker containers the pidfd / SIGCHLD mechanism can miss exits.
This task runs every 30 s and calls ``os.waitpid(-1, WNOHANG)``
in a loop to collect any zombies that slipped through.
"""
_INTERVAL = 30
while True:
await asyncio.sleep(_INTERVAL)
reaped = 0
while True:
try:
pid, _ = os.waitpid(-1, os.WNOHANG)
if pid == 0:
break # no more zombie children
reaped += 1
except ChildProcessError:
break # no child processes at all
if reaped:
logger.info("Zombie reaper: reaped {} defunct child process(es)", reaped)
async def _health_server(host: str, health_port: int):
"""Lightweight HTTP health endpoint on the gateway port."""
import json as _json
@@ -1844,11 +1820,6 @@ def _run_gateway(
name="nanobot-local-triggers",
),
]
if not _IS_WINDOWS:
tasks.append(asyncio.create_task(
_zombie_reaper(),
name="nanobot-zombie-reaper",
))
if health_server_enabled:
tasks.append(asyncio.create_task(
_health_server(config.gateway.host, port),