From 0798c1e159006e4a51bf27ce4a588c6759c7a366 Mon Sep 17 00:00:00 2001 From: ggandmee Date: Thu, 2 Jul 2026 18:42:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(tui=20v2):=20stop=20tool=20subprocesses=20i?= =?UTF-8?q?nheriting=20TTY=20stdin=20=E2=80=94=20multi-session=20key-eatin?= =?UTF-8?q?g=20on=20macOS/Linux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom ga tui2 with several concurrent sessions randomly swallows typed input — letters, digits, space and Enter vanish while agents are running tools. Single idle session is mostly unaffected. Environment Observed on macOS (Apple Silicon, zsh, Ghostty) with ga tui2 (frontends/tuiapp_v2.py, Textual). Not terminal-specific: any POSIX platform is affected. Windows never showed it — see below. Root cause code_run (ga.py) spawned tool subprocesses with stdout/stderr piped but stdin left untouched, so every bash/python child inherited the TUI's terminal as fd 0, in the same foreground process group as Textual. Any child that reads stdin (input(), interactive prompts, stdin-probing tools, readline tcsetattr on import) races Textual for the same keyboard bytes and can flip the tty out of raw mode. More sessions -> more concurrent children -> more collisions, hence 'multi-session' presentation. Windows was immune by accident: the existing creationflags=CREATE_NO_WINDOW branch detaches children from the console, so they can never touch its input buffer. The POSIX branch passed 0 — no isolation at all. This fix gives POSIX the same isolation Windows already had; behavior on Windows is unchanged. Fix Pass stdin=DEVNULL wherever no child legitimately needs the terminal: - ga.py code_run Popen (the main path every tool call goes through) - tuiapp_v2.py !shell inline command (30s window) - tuiapp_v2.py clipboard helper (only when no input= is fed) - checklist_helper.py _PK spawns (long-lived worker/master/bbs) Verification py_compile clean; code_run 'echo' unchanged; 'read -t 5 x' child now gets immediate EOF (~1s) instead of holding the tty for 5s. --- frontends/tuiapp_v2.py | 5 +++-- ga.py | 3 ++- memory/checklist_helper.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontends/tuiapp_v2.py b/frontends/tuiapp_v2.py index 60a3be7df..90354c386 100644 --- a/frontends/tuiapp_v2.py +++ b/frontends/tuiapp_v2.py @@ -1461,7 +1461,8 @@ def _sweep_stale_task_dirs() -> None: def _clipboard_run(cmd: list[str], input: bytes | None = None, timeout: float = 3.0) -> bytes | None: try: - r = subprocess.run(cmd, input=input, capture_output=True, timeout=timeout) + stdin = subprocess.DEVNULL if input is None else None + r = subprocess.run(cmd, input=input, stdin=stdin, capture_output=True, timeout=timeout) return r.stdout if r.returncode == 0 else None except (FileNotFoundError, subprocess.TimeoutExpired, OSError): return None @@ -5913,7 +5914,7 @@ def _run_shell(self, cmd: str) -> None: out = ''; rc = 0 try: r = subprocess.run( - shell_argv + [cmd], capture_output=True, + shell_argv + [cmd], stdin=subprocess.DEVNULL, capture_output=True, timeout=30, encoding='utf-8', errors='replace', ) out = (r.stdout or '') + (r.stderr or '') diff --git a/ga.py b/ga.py index d2b25aef4..37087570f 100644 --- a/ga.py +++ b/ga.py @@ -56,7 +56,8 @@ def stream_reader(proc, logs): try: process = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cmd, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, cwd=cwd, startupinfo=startupinfo, creationflags=0x08000000 if os.name == 'nt' else 0 ) diff --git a/memory/checklist_helper.py b/memory/checklist_helper.py index 19706dc17..d169c42ff 100644 --- a/memory/checklist_helper.py +++ b/memory/checklist_helper.py @@ -4,7 +4,7 @@ _R = Path(__file__).resolve().parent.parent _BBS, _MAIN = _R/"assets/agent_bbs.py", _R/"agentmain.py" _W_RE, _M_RE = _R/"reflect/agent_team_worker.py", _R/"reflect/checklist_master.py" -_PK = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} +_PK = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} if sys.platform == "win32": _PK["creationflags"] = 0x200 class CL: