fix(code_execution): handle closed PTYs while reading output
Agent Zero Local committed
May 2, 2026 at 21:33 UTC
eecbb5ba344cbb9f780e6c0c14af1a4b16219144
2 files changed
+48
-11
plugins/_code_execution/helpers/shell_local.py
+8
-1
@@ -21,8 +21,15 @@ class LocalInteractiveSession:
21
22
async def close(self):
23
if self.session:
24
- await self.session.close()
24
+ session = self.session
25
self.session = None
26
+ try:
27
+ await session.close()
28
+ except Exception:
29
+ try:
30
+ session.kill()
31
+ except Exception:
32
+ pass
33
34
async def send_command(self, command: str):
35
if not self.session:
plugins/_code_execution/tools/code_execution_tool.py
+40
-10
@@ -1,4 +1,5 @@
1
import asyncio
2
+import errno
3
from dataclasses import dataclass
4
import re
5
import shlex
@@ -15,6 +16,17 @@ from plugins._code_execution.helpers.shell_local import LocalInteractiveSession
16
from plugins._code_execution.helpers.shell_ssh import SSHInteractiveSession
17
18
19
+def _is_closed_pty_error(exc: BaseException) -> bool:
20
+ if isinstance(exc, RuntimeError) and "TTYSpawn PTY is closed" in str(exc):
21
+ return True
22
+ if isinstance(exc, OSError) and exc.errno in (errno.EBADF, errno.EIO, errno.EINVAL):
23
+ return True
24
+ cause = getattr(exc, "__cause__", None)
25
+ if cause and cause is not exc:
26
+ return _is_closed_pty_error(cause)
27
+ return False
28
+
29
+
30
@dataclass
31
class ShellWrap:
32
id: int
@@ -194,11 +206,12 @@ class CodeExecution(Tool):
206
)
207
208
except Exception as e:
197
- PrintStyle.error(str(e))
198
- await self.prepare_state(cfg, reset=True, session=session)
199
- if i == 0:
209
+ if _is_closed_pty_error(e) and i == 0:
210
+ PrintStyle.warning(f"Terminal session {session} was closed; resetting and retrying once.")
211
+ await self.prepare_state(cfg, reset=True, session=session)
212
continue
201
- raise e
213
+ PrintStyle.error(str(e))
214
+ raise
215
216
def format_command_for_output(self, command: str):
217
short_cmd = command[:250]
@@ -244,9 +257,19 @@ class CodeExecution(Tool):
257
258
while True:
259
await asyncio.sleep(sleep_time)
247
- full_output, partial_output = await self.state.shells[session].session.read_output(
248
- timeout=1, reset_full_output=reset_full_output
249
- )
260
+ try:
261
+ full_output, partial_output = await self.state.shells[session].session.read_output(
262
+ timeout=1, reset_full_output=reset_full_output
263
+ )
264
+ except Exception as e:
265
+ if _is_closed_pty_error(e):
266
+ await self.prepare_state(cfg, reset=True, session=session)
267
+ self.mark_session_idle(session)
268
+ sysinfo = "Terminal session was closed and has been reset. Please run the command again."
269
+ response = self.agent.read_prompt("fw.code.info.md", info=sysinfo)
270
+ self.log.update(content=prefix + response)
271
+ return response
272
+ raise
273
reset_full_output = False # only reset once
274
275
await self.agent.handle_intervention()
@@ -363,9 +386,16 @@ class CodeExecution(Tool):
386
prompt_patterns = cfg["prompt_patterns"]
387
dialog_patterns = cfg["dialog_patterns"]
388
366
- full_output, _ = await self.state.shells[session].session.read_output(
367
- timeout=1, reset_full_output=reset_full_output
368
- )
389
+ try:
390
+ full_output, _ = await self.state.shells[session].session.read_output(
391
+ timeout=1, reset_full_output=reset_full_output
392
+ )
393
+ except Exception as e:
394
+ if _is_closed_pty_error(e):
395
+ await self.prepare_state(cfg, reset=True, session=session)
396
+ self.mark_session_idle(session)
397
+ return None
398
+ raise
399
truncated_output = self.fix_full_output(full_output)
400
self.set_progress(truncated_output)
401
heading = self.get_heading_from_output(truncated_output, 0)