fix(code_execution): recover from closed PTY sessions

Detect closed or exited local TTY sessions before writing, convert invalid PTY write errors into retryable session failures, and reset/retry the terminal session once after send/read failures.

Agent Zero Local committed May 2, 2026 at 13:34 UTC a0f0c2e8d2ca12f194fe50c553eda8b2824084fa
2 files changed +16 -7
plugins/_code_execution/helpers/tty_session.py
+12 -2
@@ -97,10 +97,20 @@ class TTYSession:
97 async def send(self, data: str | bytes):
98 if self._proc is None:
99 raise RuntimeError("TTYSpawn is not started")
100 + if self._pty_master is None and not _IS_WIN:
101 + raise RuntimeError("TTYSpawn PTY is closed")
102 + if getattr(self._proc, "returncode", None) is not None:
103 + raise RuntimeError("TTYSpawn process has exited")
104 if isinstance(data, str):
105 data = data.encode(self.encoding)
102 - self._proc.stdin.write(data) # type: ignore
103 - await self._proc.stdin.drain() # type: ignore
106 + try:
107 + self._proc.stdin.write(data) # type: ignore
108 + await self._proc.stdin.drain() # type: ignore
109 + except OSError as e:
110 + if e.errno in (errno.EBADF, errno.EIO, errno.EINVAL):
111 + self._pty_master = None
112 + raise RuntimeError("TTYSpawn PTY is closed") from e
113 + raise
114
115 async def sendline(self, line: str):
116 await self.send(line + "\n")
plugins/_code_execution/tools/code_execution_tool.py
+4 -5
@@ -194,12 +194,11 @@ class CodeExecution(Tool):
194 )
195
196 except Exception as e:
197 - if i == 1:
198 - PrintStyle.error(str(e))
199 - await self.prepare_state(cfg, reset=True, session=session)
197 + PrintStyle.error(str(e))
198 + await self.prepare_state(cfg, reset=True, session=session)
199 + if i == 0:
200 continue
201 - else:
202 - raise e
201 + raise e
202
203 def format_command_for_output(self, command: str):
204 short_cmd = command[:250]