#2110·MetaGPT

Terminal.run_command() hangs indefinitely on Linux/WSL2 — end-of-command marker never yielded from splitlines/tuple-unpack

Author: CyberSeppiCreated Jul 20, 2026Updated Sep 14, 2026
Labelsinactive

Summary

metagpt.tools.libs.terminal.Terminal.run_command() hangs indefinitely on Linux/WSL2 whenever the persistent shell's stdout ends with the end-of-command marker as the final line (i.e., always, under normal conditions).

The read loop in _read_and_process_output uses *lines, tmp = output.splitlines(True) after reading each byte, but when the buffer contains exactly one \n-terminated line, the tuple-unpacking idiom moves that last line into tmp instead of yielding it — so the marker check never fires and the loop awaits a next byte that will never come.

Because every Engineer2._think() call starts with await self.terminal.run_command("pwd"), this bug silently deadlocks the entire Engineer / DataAnalyst / RoleZero agent flow before any LLM call is made. The user sees "nothing happening" and eventually Ctrl-C's — no error message.

Steps to reproduce

Any Linux or WSL2 host, current main:

pip install -e .
python - <<'PY'
import asyncio
from metagpt.tools.libs.terminal import Terminal

async def main():
    t = Terminal()
    out = await asyncio.wait_for(t.run_command("pwd"), timeout=10)
    print("got:", repr(out))

asyncio.run(main())
PY

Expected: prints the workspace path in under a second. Actual: hangs 10s, asyncio.TimeoutError.

Running metagpt "Create a hello world CLI" exhibits the same hang right after the Team Leader delegates to Alex (Engineer) — logs stop at Alex(Engineer) observed: [...] and never advance.

Root cause

metagpt/tools/libs/terminal.py:_read_and_process_output, current main:

tmp = b""
while True:
    output = tmp + await self.process.stdout.read(1)
    if not output:
        continue
    *lines, tmp = output.splitlines(True)   # <-- BUG
    for line in lines:
        ...  # check for END_MARKER_VALUE

Minimal Python demonstration of the unpacking:

>>> b"\x18\x19\x1b\x18\n".splitlines(True)
[b'\x18\x19\x1b\x18\n']
>>> *lines, tmp = [b'\x18\x19\x1b\x18\n']
>>> lines
[]
>>> tmp
b'\x18\x19\x1b\x18\n'

splitlines(True) on a single \n-terminated buffer returns a 1-element list. The unpacking places that element into tmp and leaves lines empty, so the marker line is never checked. The next read(1) blocks forever because bash has emitted everything and is waiting on stdin.

The exact byte sequence that triggers it in practice: workspace path + \n + \x18\x19\x1b\x18\n (the END_MARKER_VALUE). Byte-by-byte accumulation yields the workspace line normally (a following \x18 pushes it out of tmp), but the final marker line is the last thing in the stream and stays stuck in tmp on the terminating \n.

Suggested fix

Treat a trailing-newline buffer as complete instead of stashing it back into tmp:

split = output.splitlines(True)
if split and split[-1].endswith((b"\n", b"\r")):
    lines = split
    tmp = b""
else:
    *lines, tmp = split

Locally patched with this and confirmed metagpt "Create a Python CLI dice simulator" runs end-to-end in ~2min instead of hanging forever.

Happy to open a PR if the maintainers agree with this approach.

Environment

  • MetaGPT: main @ 11cdf466d042aece04fc6cfd13b28e1a70341b1f
  • Python: 3.10.12
  • OS: Linux 6.6.87.2-microsoft-standard-WSL2 (Ubuntu on WSL2)
  • Shell: /usr/bin/bash 5.x
  • LLM backend: OpenAI-compatible local proxy (irrelevant — the hang is before any LLM call)

Impact

Any Linux user of Engineer2, DataAnalyst, or any RoleZero subclass that touches Terminal. Silent hang, no traceback, no timeout — the process just idles until killed. The bug is particularly bad because there is no error to grep for; the only symptom is that the agent stops working after _observe.

Source: FoundationAgents/MetaGPT